doxa-maps/library/utils/embedBase.js
swift-river542 9cefd6faa4 Plugin gallery: one generator for the dev server and the build
- internal/playground → internal/plugin-gallery; playground-smoke → plugin-gallery-smoke;
  generate-spa → generate-gallery (scripts: gallery, smoke:gallery; smoke:playground stays as alias)
- generate-gallery exports renderGallery(model, { mode }) — the dev plugin serves the same page
  at / from the discovered registry (dev mode imports the dev entries, ladder popovers on);
  the build writes public/index.html from the manifest (byte-identical to before)
- Docs and probes use the gallery vocabulary
2026-09-16 20:04:18 -05:00

113 lines
5.6 KiB
JavaScript

/**
* embedBase.js — Where THIS bundle's built tree is served from, resolved at runtime.
*
* WHY: the built tree (<root>/<bundle>/<bundle>.js + <bundle>/<profile>/index.html …) is
* dropped onto many hosts — the dev plugin gallery, doxa.life under /js/doxa-maps-build/,
* any CDN subpath. Share links + embed snippets must point INTO that tree wherever it
* lands, so profiles declare their embed page TREE-RELATIVE (`<bundle>/<profile>/index.html`
* or `<bundle>/<profile>/<instance>/index.html`) and this module supplies the tree root.
* A hardcoded `/js/doxa-maps/…` 404s the moment the tree is served anywhere else.
*
* Tree root — derived from the bundle's OWN script URL. Rollup rewrites `import.meta.url`
* in the IIFE build to `document.currentScript.src` (captured at script start, with a
* `document.baseURI` fallback), so in a built bundle it is `<treeRoot>/<bundle>/<bundle>.js`
* and the root is two path segments up. It is trusted ONLY when the last two segments look
* like `<name>/<name>.js` (the layout generate-tree.mjs emits). In dev (ESM) it is this
* module's own URL (`…/library/utils/embedBase.js`) and is rejected — the plugin gallery passes
* a whole `shareUrl` instead.
*
* Embed base ladder — resolveEmbedBase(profileConfig):
* 1. profileConfig.embedBaseUrl — host-supplied per embed (`profile-config` JSON)
* 2. window.MAP_EMBED_BASE_URL — host-supplied page global, set before the IIFE loads
* 3. bundleTreeRoot() — derived from the script URL (a dropped tree works anywhere)
* 4. '' — none known; a tree-relative path resolves against the page
*
* Share URL ladder — resolveShareUrl({ shareUrl, embedPath, profileConfig }):
* 1. shareUrl | profileConfig.shareUrl — a WHOLE url from the host wins (plugin gallery deep link)
* 2. embedPath | profileConfig.shareEmbedPath
* absolute http(s) → passes through
* leading '/' → legacy root-relative: location.origin + path
* otherwise (tree-relative) → embedBase + '/' + path when a base is known,
* else resolved against document.baseURI
* 3. location.origin + location.pathname — the page itself ("Share shares THE MAP")
*
* Host usage (inject before the IIFE loads):
* <script>window.MAP_EMBED_BASE_URL = 'https://cdn.example.org/maps/doxa-maps';</script>
* <script src="https://cdn.example.org/maps/doxa-maps/doxa-simple-map/doxa-simple-map.js"></script>
*
* Pure JS, SSR-safe: every window/document access is guarded.
*/
// Evaluated ONCE at module init — inside the IIFE that is script start, while
// document.currentScript still points at the bundle's own <script> tag.
const SCRIPT_URL = import.meta.url
const stripSlashes = (s) => String(s || '').trim().replace(/\/+$/, '')
/**
* The served root of the built tree, derived from the bundle's own script URL.
* Returns null when that URL does not look like a built bundle (`…/<name>/<name>.js`),
* e.g. the dev ESM module URL — never guess from an untrusted layout.
* @returns {string|null} e.g. "https://doxa.life/js/doxa-maps-build/doxa-maps"
*/
export function bundleTreeRoot() {
try {
const href = String(SCRIPT_URL || '').split(/[?#]/)[0]
// …/<dir>/<file>.js — the dir must equal the file's basename (generate-tree layout).
const m = href.match(/^(.*)\/([^/]+)\/([^/]+)\.js$/)
if (!m || m[2] !== m[3]) return null
return m[1] || null
} catch {
return null
}
}
/**
* The base URL tree-relative embed pages are joined to (no trailing slash).
* Ladder: profileConfig.embedBaseUrl → window.MAP_EMBED_BASE_URL → bundleTreeRoot() → ''.
* @param {object|null} [profileConfig] the parsed profile-config (already unref'd)
* @returns {string}
*/
export function resolveEmbedBase(profileConfig) {
const fromConfig = profileConfig?.embedBaseUrl
if (typeof fromConfig === 'string' && fromConfig.trim()) return stripSlashes(fromConfig)
if (typeof window !== 'undefined') {
const fromHost = window.MAP_EMBED_BASE_URL
if (typeof fromHost === 'string' && fromHost.trim()) return stripSlashes(fromHost)
}
return stripSlashes(bundleTreeRoot() || '')
}
/**
* The URL the Share button shows, copies and iframes. See the header for the ladder.
* @param {object} [opts]
* @param {string} [opts.shareUrl] explicit whole url (beats everything)
* @param {string} [opts.embedPath] the profile's embed page — tree-relative, root-relative or absolute
* @param {object|null} [opts.profileConfig] the parsed profile-config (already unref'd)
* @returns {string}
*/
export function resolveShareUrl({ shareUrl, embedPath, profileConfig } = {}) {
const cfg = profileConfig || null
const hasWindow = typeof window !== 'undefined'
// (1) a whole url supplied by the host — the plugin gallery's own deep link, a CMS page, …
const whole = shareUrl || cfg?.shareUrl
if (typeof whole === 'string' && whole.trim()) return whole.trim()
// (2) the profile's embed page
const path = String(embedPath || cfg?.shareEmbedPath || '').trim()
if (path) {
if (/^https?:\/\//i.test(path)) return path
if (path.startsWith('/')) return hasWindow ? window.location.origin + path : path
const base = resolveEmbedBase(cfg)
const rel = path.replace(/^\.\//, '')
if (base) return base + '/' + rel
if (typeof document !== 'undefined') {
try { return new URL(rel, document.baseURI).href } catch { /* fall through */ }
}
return rel
}
// (3) the page itself — each map builds to a self-contained page.
return hasWindow ? window.location.origin + window.location.pathname : ''
}