Share > Embed: three codes for THIS plugin (URL, Iframe, JavaScript), from the tree it is served from (shareable pages P3b)
The Embed tab now offers the plugin's own page (URL), that page in an iframe (with the sizes), and its own bundle + element (JavaScript), each with one plain sentence and one Copy button. They embed the plugin whose Share was pressed, even nested in Plugin Studio, Deep Research or Posterverse. Where the codes point: a new build-config key shareBaseUrl, baked as __DOXA_SHARE_BASE__ and overridable per embed (profile-config.shareBaseUrl) or per page (window.MAP_SHARE_BASE_URL), https only (or a local test server). It is committed EMPTY: the codes then point at the tree the bundle is served from, so the stable site gives its own public URLs by construction. embedBaseUrl stays empty, so the manifest's snippets and a dropped tree are unchanged. The JavaScript code is the plugin's embed.script from that tree's manifest (library/utils/publicEmbed.js, fetched once when Embed opens, default credentials), made absolute, with integrity dropped from our unversioned bundle URL (a pinned hash would break every partner's embed at the next release). A plugin that tree does not carry gets a plain note instead of codes that would 404. Share this page stays first; the explanation and the 5 steps stay. English only (parity debt 19 per locale). The library index lists publicEmbed.js.
This commit is contained in:
parent
bcbaa816ae
commit
896775903b
10 changed files with 207 additions and 44 deletions
|
|
@ -280,6 +280,8 @@ and a change here can break other maps.
|
||||||
`import … from '@map/utils/peopleGroupName.js'`
|
`import … from '@map/utils/peopleGroupName.js'`
|
||||||
- **peopleGroupPoints** — peopleGroupPoints.js — SHARED (@map/utils/peopleGroupPoints.js).
|
- **peopleGroupPoints** — peopleGroupPoints.js — SHARED (@map/utils/peopleGroupPoints.js).
|
||||||
`import … from '@map/utils/peopleGroupPoints.js'`
|
`import … from '@map/utils/peopleGroupPoints.js'`
|
||||||
|
- **publicEmbed** — publicEmbed.js: the Share menu's Embed codes for THIS plugin, from the tree's PUBLIC address.
|
||||||
|
`import … from '@map/utils/publicEmbed.js'`
|
||||||
- **scrollport** — findScrollportAncestor — the nearest ancestor that actually scrolls.
|
- **scrollport** — findScrollportAncestor — the nearest ancestor that actually scrolls.
|
||||||
`import … from '@map/utils/scrollport.js'`
|
`import … from '@map/utils/scrollport.js'`
|
||||||
- **viewportScrim** — viewportScrim.js — make a position:fixed full-screen SCRIM actually cover the VIEWPORT,
|
- **viewportScrim** — viewportScrim.js — make a position:fixed full-screen SCRIM actually cover the VIEWPORT,
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,19 @@ function readGated() {
|
||||||
}
|
}
|
||||||
export const GATED_PROFILES = readGated()
|
export const GATED_PROFILES = readGated()
|
||||||
|
|
||||||
|
// The PUBLIC address of the tree for the Share menu's Embed tab (the config's shareBaseUrl): an https URL with no
|
||||||
|
// trailing slash, or '' (unset). vite.config.js bakes it as __DOXA_SHARE_BASE__; library/utils/embedBase.js reads it.
|
||||||
|
function readShareBase() {
|
||||||
|
let v = ''
|
||||||
|
try {
|
||||||
|
const cfg = JSON.parse(readFileSync(BUILD_CONFIG_FILE, 'utf8'))
|
||||||
|
v = typeof cfg.shareBaseUrl === 'string' ? cfg.shareBaseUrl.trim().replace(/\/+$/, '') : ''
|
||||||
|
} catch { return '' }
|
||||||
|
if (v && !/^https:\/\/[^\s"'<>]+$/i.test(v)) throw new Error(`[_tree-name] build config "shareBaseUrl" must be an https URL, got ${JSON.stringify(v)}`)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
export const SHARE_BASE = readShareBase()
|
||||||
|
|
||||||
// The name escaped for use inside a RegExp source (a '.' in a future name must not match any char).
|
// The name escaped for use inside a RegExp source (a '.' in a future name must not match any char).
|
||||||
export const TREE_NAME_RE = TREE_NAME.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
export const TREE_NAME_RE = TREE_NAME.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,8 @@ const DEBT = {
|
||||||
// 2026-10-03, the Share menu's 10 new share.json keys (shareable pages P3): localization is English-only for now.
|
// 2026-10-03, the Share menu's 10 new share.json keys (shareable pages P3): localization is English-only for now.
|
||||||
// tStatic falls back to the English text, so these locales show English until the translations arrive;
|
// tStatic falls back to the English text, so these locales show English until the translations arrive;
|
||||||
// lower each to 0 then, and empty the map again.
|
// lower each to 0 then, and empty the map again.
|
||||||
ar: 10, de: 10, es: 10, fr: 10, hi: 10, it: 10, pt: 10, ro: 10, ru: 10, zh: 10,
|
// + the Embed tab's 9 keys (URL / Iframe / JavaScript, P3b), the same English-only rule.
|
||||||
|
ar: 19, de: 19, es: 19, fr: 19, hi: 19, it: 19, pt: 19, ro: 19, ru: 19, zh: 19,
|
||||||
}
|
}
|
||||||
|
|
||||||
const fails = []
|
const fails = []
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,8 @@ import { tStatic } from '@map/i18n/index.js'
|
||||||
const T = (key, en) => tStatic(key) || en
|
const T = (key, en) => tStatic(key) || en
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useShadowStyles } from '../../composables/useShadowStyles.js'
|
import { useShadowStyles } from '../../composables/useShadowStyles.js'
|
||||||
import { resolveShareUrl } from '../../utils/embedBase.js'
|
import { resolveShareUrl, resolvePublicBase, resolveEmbedBase } from '../../utils/embedBase.js'
|
||||||
|
import { publicLeafUrl, publicScriptCode } from '../../utils/publicEmbed.js'
|
||||||
import { urlOwnerOf } from '../../composables/useViewAddress.js'
|
import { urlOwnerOf } from '../../composables/useViewAddress.js'
|
||||||
import MapControlButton from './MapControlButton.vue'
|
import MapControlButton from './MapControlButton.vue'
|
||||||
import MapControlMenu from './MapControlMenu.vue'
|
import MapControlMenu from './MapControlMenu.vue'
|
||||||
|
|
@ -357,6 +358,8 @@ const pageUrl = ref('') // "Share this page": read when the menu
|
||||||
const linkCopied = ref(false)
|
const linkCopied = ref(false)
|
||||||
const embedCopied = ref(false)
|
const embedCopied = ref(false)
|
||||||
const embedSize = ref('medium') // 'small' | 'medium' | 'large'
|
const embedSize = ref('medium') // 'small' | 'medium' | 'large'
|
||||||
|
const embedKind = ref('url') // 'url' | 'iframe' | 'script': the Embed tab's three codes
|
||||||
|
const scriptState = ref({ loading: false }) // the JavaScript code from the public manifest: { code } | { missing } | { failed }
|
||||||
|
|
||||||
const sizePresets = {
|
const sizePresets = {
|
||||||
small: { label: 'Small', height: 400 },
|
small: { label: 'Small', height: 400 },
|
||||||
|
|
@ -390,10 +393,35 @@ const shareUrl = computed(() => resolveShareUrl({
|
||||||
profileConfig: unref(profileConfig) || null,
|
profileConfig: unref(profileConfig) || null,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
/* ★ EMBED THIS PLUGIN, FROM THE PUBLIC SITE (shareable pages P3b). The Embed tab's three codes are for the plugin
|
||||||
|
whose Share was pressed (its own page; its own bundle + element), even nested in Plugin Studio, Deep Research or
|
||||||
|
Posterverse, and they point at the tree's PUBLIC address (resolvePublicBase: the build's shareBaseUrl), never a dev
|
||||||
|
server or the page's own host. With no public address configured, the tree this bundle loaded from stands in. */
|
||||||
|
const embedBase = computed(() => resolvePublicBase(unref(profileConfig) || null) || resolveEmbedBase(unref(profileConfig) || null))
|
||||||
|
const embedPathOf = () => props.embedPath || (unref(profileConfig) || {}).shareEmbedPath || ''
|
||||||
|
// URL: the plugin's own generated page on the public site; an absolute or root path keeps the old resolution.
|
||||||
|
const leafUrl = computed(() => publicLeafUrl(embedBase.value, embedPathOf()) || shareUrl.value)
|
||||||
const embedSnippet = computed(() => {
|
const embedSnippet = computed(() => {
|
||||||
const h = sizePresets[embedSize.value].height
|
const h = sizePresets[embedSize.value].height
|
||||||
return '<iframe src="' + shareUrl.value + '" width="100%" height="' + h + '" style="border:none; border-radius:12px;" allowfullscreen></iframe>'
|
return '<iframe src="' + leafUrl.value + '" width="100%" height="' + h + '" style="border:none; border-radius:12px;" allowfullscreen></iframe>'
|
||||||
})
|
})
|
||||||
|
// JavaScript: the plugin's embed.script from the public manifest, read once the Embed tab opens. "missing" means the
|
||||||
|
// public site does not carry this plugin yet, so the tab shows a plain note instead of codes that would 404.
|
||||||
|
function loadScriptCode() {
|
||||||
|
if (scriptState.value.code || scriptState.value.loading) return
|
||||||
|
scriptState.value = { loading: true }
|
||||||
|
publicScriptCode(embedBase.value, embedPathOf()).then((r) => { scriptState.value = r })
|
||||||
|
}
|
||||||
|
const embedText = computed(() => embedKind.value === 'url' ? leafUrl.value
|
||||||
|
: embedKind.value === 'iframe' ? embedSnippet.value : (scriptState.value.code || ''))
|
||||||
|
const EMBED_KINDS = ['url', 'iframe', 'script']
|
||||||
|
const kindLabel = (k) => ({ url: T('share.kind_url', 'URL'), iframe: T('share.kind_iframe', 'Iframe'), script: T('share.kind_script', 'JavaScript') })[k]
|
||||||
|
// One plain sentence per code, for a reader who has never embedded anything.
|
||||||
|
const kindHint = (k) => ({
|
||||||
|
url: T('share.url_hint', 'A link to open the map on its own page.'),
|
||||||
|
iframe: T('share.iframe_hint', 'Put the map in your page, in a frame.'),
|
||||||
|
script: T('share.script_hint', 'Load the map straight into your page, with no frame.'),
|
||||||
|
})[k]
|
||||||
|
|
||||||
/* ★ SHARE THIS PAGE. Where the page OWNS its address (useViewAddress: urlOwner 'app' on our own pages,
|
/* ★ SHARE THIS PAGE. Where the page OWNS its address (useViewAddress: urlOwner 'app' on our own pages,
|
||||||
'host' on DX Life), the address bar already names the exact view (the tab, and the filters), so the
|
'host' on DX Life), the address bar already names the exact view (the tab, and the filters), so the
|
||||||
|
|
@ -433,6 +461,7 @@ function toggle() {
|
||||||
pageUrl.value = pageLink()
|
pageUrl.value = pageLink()
|
||||||
linkCopied.value = false
|
linkCopied.value = false
|
||||||
embedCopied.value = false
|
embedCopied.value = false
|
||||||
|
embedKind.value = 'url'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -472,7 +501,7 @@ async function copyLink() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyEmbed() {
|
async function copyEmbed() {
|
||||||
if (await copyText(embedSnippet.value)) {
|
if (embedText.value && await copyText(embedText.value)) {
|
||||||
embedCopied.value = true
|
embedCopied.value = true
|
||||||
setTimeout(() => { embedCopied.value = false }, 2000)
|
setTimeout(() => { embedCopied.value = false }, 2000)
|
||||||
}
|
}
|
||||||
|
|
@ -578,7 +607,7 @@ function bindAnchor(el) {
|
||||||
<button
|
<button
|
||||||
class="share-tab" role="tab" data-share-tab="embed"
|
class="share-tab" role="tab" data-share-tab="embed"
|
||||||
:class="{ active: activeTab === 'embed' }" :aria-selected="String(activeTab === 'embed')"
|
:class="{ active: activeTab === 'embed' }" :aria-selected="String(activeTab === 'embed')"
|
||||||
@click="activeTab = 'embed'"
|
@click="activeTab = 'embed'; loadScriptCode()"
|
||||||
>{{ T('share.tab_embed', 'Embed') }}</button>
|
>{{ T('share.tab_embed', 'Embed') }}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -612,7 +641,7 @@ function bindAnchor(el) {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ===== Tab 2: Embed (for another website) ===== -->
|
<!-- ===== Tab 2: Embed (for another website): THIS plugin, from the public site ===== -->
|
||||||
<div v-if="activeTab === 'embed'" class="share-body">
|
<div v-if="activeTab === 'embed'" class="share-body">
|
||||||
<!-- What embedding is and why it helps, in plain words, before any code -->
|
<!-- What embedding is and why it helps, in plain words, before any code -->
|
||||||
<div class="share-embed-desc">
|
<div class="share-embed-desc">
|
||||||
|
|
@ -622,43 +651,57 @@ function bindAnchor(el) {
|
||||||
<ul class="share-steps">
|
<ul class="share-steps">
|
||||||
<li v-for="(step, i) in tm('buttons.shareSteps')" :key="i">{{ rt(step) }}</li>
|
<li v-for="(step, i) in tm('buttons.shareSteps')" :key="i">{{ rt(step) }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="share-embed-desc">
|
<!-- Not on the public site yet: one plain note instead of codes that would 404 -->
|
||||||
{{ T('share.embed_paste_hint', 'Paste this code into your website builder (WordPress, Squarespace, Wix, or any site that accepts HTML widgets).') }}
|
<div v-if="scriptState.missing" class="share-embed-desc" data-embed-note>
|
||||||
|
{{ T('share.not_public', "This map isn't on the public site yet. Its embed codes will appear here once it's released.") }}
|
||||||
</div>
|
</div>
|
||||||
|
<template v-else>
|
||||||
<!-- Size presets -->
|
<!-- Three ways, one at a time: a link, a frame, or straight into the page -->
|
||||||
<div class="share-size-pills">
|
<div class="share-size-pills" role="tablist">
|
||||||
|
<button
|
||||||
|
v-for="k in EMBED_KINDS" :key="k" type="button" role="tab"
|
||||||
|
class="share-size-pill" :data-embed-kind="k"
|
||||||
|
:class="{ active: embedKind === k }" :aria-selected="String(embedKind === k)"
|
||||||
|
@click="embedKind = k"
|
||||||
|
>{{ kindLabel(k) }}</button>
|
||||||
|
</div>
|
||||||
|
<div class="share-embed-desc">{{ kindHint(embedKind) }}</div>
|
||||||
|
<!-- Iframe: its height -->
|
||||||
|
<div v-if="embedKind === 'iframe'" class="share-size-pills">
|
||||||
|
<button
|
||||||
|
v-for="(preset, key) in sizePresets"
|
||||||
|
:key="key"
|
||||||
|
class="share-size-pill"
|
||||||
|
:class="{ active: embedSize === key }"
|
||||||
|
@click="embedSize = key"
|
||||||
|
>{{ sizeLabel(key) }}</button>
|
||||||
|
</div>
|
||||||
|
<!-- JavaScript: read from the public site when this tab opens -->
|
||||||
|
<div v-if="embedKind === 'script' && !scriptState.code" class="share-embed-desc">
|
||||||
|
{{ scriptState.failed ? T('share.public_failed', "Couldn't reach the public site just now. Try again in a moment.") : T('share.script_loading', 'Reading the public site…') }}
|
||||||
|
</div>
|
||||||
|
<div v-else class="share-code-box" data-embed-code>{{ embedText }}</div>
|
||||||
|
<!-- One Copy button -->
|
||||||
<button
|
<button
|
||||||
v-for="(preset, key) in sizePresets"
|
v-if="embedText"
|
||||||
:key="key"
|
class="share-secondary-btn"
|
||||||
class="share-size-pill"
|
:class="{ copied: embedCopied }"
|
||||||
:class="{ active: embedSize === key }"
|
@click="copyEmbed"
|
||||||
@click="embedSize = key"
|
>
|
||||||
>{{ sizeLabel(key) }}</button>
|
<svg v-if="!embedCopied" width="15" height="15" viewBox="0 0 24 24"
|
||||||
</div>
|
fill="none" stroke="currentColor" stroke-width="2"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round">
|
||||||
<!-- Code box -->
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
|
||||||
<div class="share-code-box">{{ embedSnippet }}</div>
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
||||||
|
</svg>
|
||||||
<!-- Copy Code button -->
|
<svg v-else width="15" height="15" viewBox="0 0 24 24"
|
||||||
<button
|
fill="none" stroke="currentColor" stroke-width="2.5"
|
||||||
class="share-secondary-btn"
|
stroke-linecap="round" stroke-linejoin="round">
|
||||||
:class="{ copied: embedCopied }"
|
<polyline points="20 6 9 17 4 12"/>
|
||||||
@click="copyEmbed"
|
</svg>
|
||||||
>
|
{{ embedCopied ? T('share.copied', 'Copied!') : (embedKind === 'url' ? T('share.copy_link', 'Copy link') : T('share.copy_code', 'Copy code')) }}
|
||||||
<svg v-if="!embedCopied" width="15" height="15" viewBox="0 0 24 24"
|
</button>
|
||||||
fill="none" stroke="currentColor" stroke-width="2"
|
</template>
|
||||||
stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
|
|
||||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
|
||||||
</svg>
|
|
||||||
<svg v-else width="15" height="15" viewBox="0 0 24 24"
|
|
||||||
fill="none" stroke="currentColor" stroke-width="2.5"
|
|
||||||
stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<polyline points="20 6 9 17 4 12"/>
|
|
||||||
</svg>
|
|
||||||
{{ embedCopied ? T('share.copied', 'Copied!') : T('share.copy_code', 'Copy code') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</MapControlMenu>
|
</MapControlMenu>
|
||||||
|
|
|
||||||
|
|
@ -11,5 +11,14 @@
|
||||||
"copied": "Copied!",
|
"copied": "Copied!",
|
||||||
"size_small": "Small",
|
"size_small": "Small",
|
||||||
"size_medium": "Medium",
|
"size_medium": "Medium",
|
||||||
"size_large": "Large"
|
"size_large": "Large",
|
||||||
|
"kind_url": "URL",
|
||||||
|
"kind_iframe": "Iframe",
|
||||||
|
"kind_script": "JavaScript",
|
||||||
|
"url_hint": "A link to open the map on its own page.",
|
||||||
|
"iframe_hint": "Put the map in your page, in a frame.",
|
||||||
|
"script_hint": "Load the map straight into your page, with no frame.",
|
||||||
|
"script_loading": "Reading the public site…",
|
||||||
|
"not_public": "This map isn't on the public site yet. Its embed codes will appear here once it's released.",
|
||||||
|
"public_failed": "Couldn't reach the public site just now. Try again in a moment."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -212,6 +212,24 @@ export function absoluteEmbedUrl(leaf, profileConfig = null) {
|
||||||
return rel
|
return rel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tree's PUBLIC address, for the Share menu's Embed tab (no trailing slash). Its URL, Iframe and JavaScript codes
|
||||||
|
* point here, so an embed follows the stable releases served there, never a dev server or the page's own host.
|
||||||
|
* Ladder: profileConfig.shareBaseUrl → window.MAP_SHARE_BASE_URL → the build's shareBaseUrl (vite.doxa-maps-build-
|
||||||
|
* config.json, baked as __DOXA_SHARE_BASE__) → '' (unset: the Embed tab keeps the tree the bundle was loaded from).
|
||||||
|
* @param {object|null} [profileConfig] the parsed profile-config (already unref'd)
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function resolvePublicBase(profileConfig) {
|
||||||
|
// https only (an http base would give mixed-content codes on an https partner page), plus a local test server
|
||||||
|
const ok = (v) => typeof v === 'string' && /^(https:\/\/|http:\/\/(127\.0\.0\.1|localhost)(:\d+)?\/)/i.test(v.trim())
|
||||||
|
const fromConfig = profileConfig?.shareBaseUrl
|
||||||
|
if (ok(fromConfig)) return stripSlashes(fromConfig)
|
||||||
|
if (typeof window !== 'undefined' && ok(window.MAP_SHARE_BASE_URL)) return stripSlashes(window.MAP_SHARE_BASE_URL)
|
||||||
|
// typeof-guarded: a build or a test that defines no __DOXA_SHARE_BASE__ reads '' instead of throwing
|
||||||
|
return stripSlashes(typeof __DOXA_SHARE_BASE__ === 'string' ? __DOXA_SHARE_BASE__ : '')
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveShareUrl({ shareUrl, embedPath, profileConfig } = {}) {
|
export function resolveShareUrl({ shareUrl, embedPath, profileConfig } = {}) {
|
||||||
const cfg = profileConfig || null
|
const cfg = profileConfig || null
|
||||||
const hasWindow = typeof window !== 'undefined'
|
const hasWindow = typeof window !== 'undefined'
|
||||||
|
|
|
||||||
73
library/utils/publicEmbed.js
Normal file
73
library/utils/publicEmbed.js
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
/**
|
||||||
|
* publicEmbed.js: the Share menu's Embed codes for THIS plugin, from the tree's PUBLIC address.
|
||||||
|
*
|
||||||
|
* Three codes, all for the plugin itself (its own leaf page, its own bundle + element), never for the app it is
|
||||||
|
* nested in (Plugin Studio, Deep Research, Posterverse):
|
||||||
|
* URL <base>/<bundle>/<profile>/index.html, the plugin's generated page;
|
||||||
|
* Iframe <iframe src="that URL">;
|
||||||
|
* JavaScript the plugin's own embed.script from the PUBLIC manifest (<base>/manifest.json), made absolute.
|
||||||
|
*
|
||||||
|
* ★ THE SCRIPT IS READ FROM THE PUBLIC MANIFEST, NOT BUILT HERE. The manifest's per-profile embed.script is the one
|
||||||
|
* source of truth for the element tag, its profile-config and the page globals a plugin needs; the PUBLIC copy is the
|
||||||
|
* release partners will load, so its tag and config match what they get. It is fetched once per base and cached
|
||||||
|
* (the public host serves it with Access-Control-Allow-Origin: *).
|
||||||
|
*
|
||||||
|
* ★ NO INTEGRITY ON OUR BUNDLE. The public bundle URL is not versioned and is rebuilt in place at each release, so a
|
||||||
|
* pinned integrity="sha384-…" would break every partner's embed at the next release. It is dropped from the copied
|
||||||
|
* code for scripts served from the base; a versioned third-party URL (the Mapbox preload) keeps its own. Versioned
|
||||||
|
* release URLs + SRI are a later improvement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const cache = new Map() // base → Promise<manifest|null>
|
||||||
|
|
||||||
|
/** The tree-relative leaf path of an embedPath ('./x/y/index.html' → 'x/y/index.html'); '' for absolute or root paths. */
|
||||||
|
export function treeRelative(embedPath) {
|
||||||
|
const p = String(embedPath || '').trim()
|
||||||
|
if (!p || /^https?:\/\//i.test(p) || p.startsWith('/')) return ''
|
||||||
|
return p.replace(/^\.\//, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The plugin's public page: <base>/<leaf>. '' when either is unknown. */
|
||||||
|
export const publicLeafUrl = (base, embedPath) => {
|
||||||
|
const rel = treeRelative(embedPath)
|
||||||
|
return base && rel ? base + '/' + rel : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadManifest(base) {
|
||||||
|
if (!cache.has(base)) {
|
||||||
|
// the default credentials ('same-origin'): nothing is sent to a cross-origin public host, and behind a login gate
|
||||||
|
// a same-origin base keeps its session
|
||||||
|
const p = fetch(base + '/manifest.json')
|
||||||
|
.then((r) => (r.ok ? r.json() : null))
|
||||||
|
.catch(() => { cache.delete(base); return undefined }) // a network failure may be retried later
|
||||||
|
cache.set(base, p)
|
||||||
|
}
|
||||||
|
return cache.get(base)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The plugin's JavaScript embed code from the PUBLIC manifest.
|
||||||
|
* Resolves { code } | { missing: true } (the public site does not carry this plugin yet) | { failed: true }.
|
||||||
|
*/
|
||||||
|
export async function publicScriptCode(base, embedPath) {
|
||||||
|
const rel = treeRelative(embedPath)
|
||||||
|
if (!base || !rel) return { missing: true }
|
||||||
|
const m = await loadManifest(base)
|
||||||
|
if (m === undefined) return { failed: true }
|
||||||
|
if (!m || !Array.isArray(m.bundles)) return { missing: true }
|
||||||
|
const norm = (u) => String(u || '').replace(/^\.\//, '')
|
||||||
|
const [bundle, profile] = rel.split('/')
|
||||||
|
let hit = null
|
||||||
|
for (const b of m.bundles) for (const pr of (b.profiles || [])) {
|
||||||
|
if (norm(pr.iframeUrl) === rel) hit = pr
|
||||||
|
else if (!hit && b.bundle === bundle && pr.profile === profile) hit = pr // an instance path: its profile's code
|
||||||
|
}
|
||||||
|
const raw = hit && hit.embed && hit.embed.script
|
||||||
|
if (!raw) return { missing: true }
|
||||||
|
const esc = base.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||||
|
const code = raw
|
||||||
|
.replace(/(\s(?:src|href)=")\.\//g, '$1' + base + '/')
|
||||||
|
.replace(/YOUR_TREE_URL/g, base)
|
||||||
|
.replace(new RegExp('(<script\\b[^>]*\\ssrc="' + esc + '/[^"]*")\\s+integrity="[^"]*"', 'g'), '$1')
|
||||||
|
return { code }
|
||||||
|
}
|
||||||
|
|
@ -77,7 +77,7 @@ const GLOBALS = [
|
||||||
'DataView', 'WeakRef', 'FinalizationRegistry', 'ClipboardItem', 'CSSStyleSheet', 'XMLSerializer',
|
'DataView', 'WeakRef', 'FinalizationRegistry', 'ClipboardItem', 'CSSStyleSheet', 'XMLSerializer',
|
||||||
// Build-time defines: vite REPLACES these before the browser ever sees them (vite.config.js
|
// Build-time defines: vite REPLACES these before the browser ever sees them (vite.config.js
|
||||||
// `define`), so they are bound by the bundler rather than by the file.
|
// `define`), so they are bound by the bundler rather than by the file.
|
||||||
'__DOXA_TREE_NAME__', '__DOXA_QA__',
|
'__DOXA_TREE_NAME__', '__DOXA_QA__', '__DOXA_SHARE_BASE__',
|
||||||
// Bound by the build too, but by a transform rather than `define`: with DX_SHIP_ONE_LOCALE the
|
// Bound by the build too, but by a transform rather than `define`: with DX_SHIP_ONE_LOCALE the
|
||||||
// i18nAreasPlugin prepends `const __DX_LOCALE_SRI__ = {…}` to library/i18n/index.js; without it the
|
// i18nAreasPlugin prepends `const __DX_LOCALE_SRI__ = {…}` to library/i18n/index.js; without it the
|
||||||
// name does not exist, which is why its one read sits behind `typeof … !== 'undefined'`.
|
// name does not exist, which is why its one read sits behind `typeof … !== 'undefined'`.
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ import { fileURLToPath } from 'node:url'
|
||||||
import { resolve, dirname, join } from 'node:path'
|
import { resolve, dirname, join } from 'node:path'
|
||||||
import { readdirSync, statSync, existsSync, readFileSync, writeFileSync } from 'node:fs'
|
import { readdirSync, statSync, existsSync, readFileSync, writeFileSync } from 'node:fs'
|
||||||
import { createHash } from 'node:crypto'
|
import { createHash } from 'node:crypto'
|
||||||
import { TREE_NAME, GATED_PROFILES } from './internal/_tree-name.mjs'
|
import { TREE_NAME, GATED_PROFILES, SHARE_BASE } from './internal/_tree-name.mjs'
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
// Ship only the ACTIVE locale: inline `en` (the fallbackLocale) and emit the other ten beside each
|
// Ship only the ACTIVE locale: inline `en` (the fallbackLocale) and emit the other ten beside each
|
||||||
|
|
@ -201,6 +201,8 @@ export default defineConfig(({ command, mode }) => {
|
||||||
// The output tree's name (vite.doxa-maps-build-config.json `name`, the same value OUT_ROOT uses),
|
// The output tree's name (vite.doxa-maps-build-config.json `name`, the same value OUT_ROOT uses),
|
||||||
// for the few runtime paths that must name the tree — so renaming the tree needs no source edit.
|
// for the few runtime paths that must name the tree — so renaming the tree needs no source edit.
|
||||||
__DOXA_TREE_NAME__: JSON.stringify(MFE_NAME),
|
__DOXA_TREE_NAME__: JSON.stringify(MFE_NAME),
|
||||||
|
// The tree's PUBLIC address for the Share menu's Embed tab (the config's shareBaseUrl), or '' when unset.
|
||||||
|
__DOXA_SHARE_BASE__: JSON.stringify(SHARE_BASE),
|
||||||
},
|
},
|
||||||
// Build only: shadow-DOM CSS is injected from useShadowStyles(`…`) literals that
|
// Build only: shadow-DOM CSS is injected from useShadowStyles(`…`) literals that
|
||||||
// ship verbatim, so their block comments are stripped before the Vue compiler
|
// ship verbatim, so their block comments are stripped before the Vue compiler
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
"name": "dx-maps",
|
"name": "dx-maps",
|
||||||
"paths": [],
|
"paths": [],
|
||||||
"embedBaseUrl": "",
|
"embedBaseUrl": "",
|
||||||
|
"//shareBaseUrl": "The PUBLIC address of this tree (https, no trailing slash) for the Share menu's Embed tab only: its URL, Iframe and JavaScript codes point there. Empty (the default, committed): the codes point at the tree the bundle is served from, so the stable site gives its own public URLs by construction. Set it at deploy time (or per page: window.MAP_SHARE_BASE_URL, or per embed: profile-config.shareBaseUrl) to send every code to one address. It does NOT touch the manifest's snippets (embedBaseUrl).",
|
||||||
|
"shareBaseUrl": "",
|
||||||
"assetsBaseUrl": "",
|
"assetsBaseUrl": "",
|
||||||
"tokenUrl": "",
|
"tokenUrl": "",
|
||||||
"token": "",
|
"token": "",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue