diff --git a/internal/generate-tree.mjs b/internal/generate-tree.mjs index 42ef981ec..c178a03e1 100644 --- a/internal/generate-tree.mjs +++ b/internal/generate-tree.mjs @@ -30,7 +30,7 @@ * Run standalone: bun generate-tree.mjs (expects vite build already ran) * Wired into: bun run build (runs after the per-bundle vite builds) */ -import { CDN, VENDORED } from '../library/constants/cdn.js' +import { CDN } from '../library/constants/cdn.js' import { isWebBuilt } from './plugin-targets.mjs' import { readFileSync, writeFileSync, readdirSync, existsSync, mkdirSync, statSync, @@ -1703,21 +1703,6 @@ function writeManifestAndHtml(root, bundles, tokenUrlExpr, bakedToken) { } mkdirSync(root, { recursive: true }) writeFileSync(join(root, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n') - // ★ THE MAP ENGINE, SAME-ORIGIN (2026-09-28: maps must load fast). Each pinned file in - // library/constants/cdn.js VENDORED is copied from node_modules into the tree, and a copy whose bytes do - // not hash to that entry's integrity pin stops the build: the page loads it with the same pin. - for (const v of VENDORED) { - const src = resolve(__dirname, '..', v.from) - const pin = Object.values(CDN).find((e) => e.url === v.cdn)?.integrity || '' - const bytes = readFileSync(src) - const got = 'sha384-' + createHash('sha384').update(bytes).digest('base64') - if (got !== pin) { - console.error(`generate-tree: ${v.from} does not match its pin (${pin.slice(0, 20)}…, got ${got.slice(0, 20)}…). Reinstall node_modules at the pinned version, or re-pin library/constants/cdn.js.`) - process.exit(1) - } - mkdirSync(dirname(join(root, v.path)), { recursive: true }) - writeFileSync(join(root, v.path), bytes) - } // ★ A CORS-READABLE COPY for the Research Gallery, byte-identical, written in the SAME step so the // two can never diverge. The live static layer sends Access-Control-Allow-Origin on paths inside a // folder of the tree but not on the root manifest (measured 2026-09-25), so a partner page's diff --git a/library/composables/useMapInstance.js b/library/composables/useMapInstance.js index 6cbce1156..e732ef0c5 100644 --- a/library/composables/useMapInstance.js +++ b/library/composables/useMapInstance.js @@ -99,8 +99,7 @@ const OPENFREEMAP_DARK_STYLE = 'https://tiles.openfreemap.org/styles/fiord' // 'midnight-glow' / … resolves per engine here; raw URLs pass through. import { resolveNamedStyle, applyStylePaint } from '../styles/registry.js' import { resolveMapProvider, explicitMapProvider } from './useMapProvider.js' -import { CDN, pinElement, vendoredUrl } from '../constants/cdn.js' -import { bundleTreeRoot } from '../utils/embedBase.js' +import { CDN, pinElement } from '../constants/cdn.js' import { installRtlTextPlugin } from '../maps/rtl-text.js' import { localizeBasemapLabels } from '../maps/localize-labels.js' import { tStatic } from '../i18n/index.js' @@ -139,26 +138,7 @@ export function engineGlobal(provider) { * inject undefined. */ export function engineStylesheetUrl(provider) { - const css = (ENGINES[provider] || ENGINES.mapbox).css || null - return (css && vendoredUrl(css, bundleTreeRoot())) || css -} - -// Load one engine file from its same-origin copy when this bundle was served from a built tree, falling -// back to the pinned CDN url if that copy is missing (a tree copied without its vendor/ folder). -function appendPinned(tag, url, fallbackUrl, attrs) { - return new Promise((resolve, reject) => { - const el = document.createElement(tag) - Object.assign(el, attrs) - if (tag === 'script') el.src = url; else el.href = url - pinElement(el, url) - el.addEventListener('load', () => resolve(el), { once: true }) - el.addEventListener('error', () => { - el.remove() - if (fallbackUrl && fallbackUrl !== url) appendPinned(tag, fallbackUrl, null, attrs).then(resolve, reject) - else reject(new Error(`[useMapInstance] Failed to load ${url}`)) - }, { once: true }) - document.head.appendChild(el) - }) + return (ENGINES[provider] || ENGINES.mapbox).css || null } /** @@ -171,14 +151,15 @@ export async function ensureEngineScript(provider) { if (engineGlobal(provider)) return // already loaded — no-op if (typeof document === 'undefined') return - const root = bundleTreeRoot() - const cssUrl = vendoredUrl(cfg.css, root) || cfg.css - const jsUrl = vendoredUrl(cfg.js, root) || cfg.js - if (!document.querySelector(`link[href="${cssUrl}"]`) && !document.querySelector(`link[href="${cfg.css}"]`)) { - appendPinned('link', cssUrl, cfg.css, { rel: 'stylesheet' }).catch(() => {}) + if (!document.querySelector(`link[href="${cfg.css}"]`)) { + const link = document.createElement('link') + link.rel = 'stylesheet' + link.href = cfg.css + pinElement(link, cfg.css) + document.head.appendChild(link) } - const existing = document.querySelector(`script[src="${jsUrl}"]`) || document.querySelector(`script[src="${cfg.js}"]`) + const existing = document.querySelector(`script[src="${cfg.js}"]`) if (existing) { if (engineGlobal(provider)) return await new Promise(resolve => { @@ -188,8 +169,14 @@ export async function ensureEngineScript(provider) { return } - await appendPinned('script', jsUrl, cfg.js, { async: false }).catch(() => { - throw new Error(`[useMapInstance] Failed to load ${provider} engine script`) + await new Promise((resolve, reject) => { + const s = document.createElement('script') + s.src = cfg.js + pinElement(s, cfg.js) + s.async = false + s.addEventListener('load', resolve, { once: true }) + s.addEventListener('error', () => reject(new Error(`[useMapInstance] Failed to load ${provider} engine script`)), { once: true }) + document.head.appendChild(s) }) } diff --git a/library/constants/cdn.js b/library/constants/cdn.js index 31e18b447..d8b2ec825 100644 --- a/library/constants/cdn.js +++ b/library/constants/cdn.js @@ -34,30 +34,9 @@ export const CDN = { d3: { url: 'https://cdn.jsdelivr.net/npm/d3@7.9.0/dist/d3.min.js', integrity: 'sha384-CjloA8y00+1SDAUkjs099PVfnY2KmDC2BZnws9kh8D/lX1s46w6EPhpXdqMfjK6i' }, } -/** - * SAME-ORIGIN COPIES (2026-09-28: maps must load fast). generate-tree copies each file below - * from node_modules into / and REFUSES a copy whose bytes do not match that entry's pin, so the - * one integrity hash guards both copies. A built page then loads the engine from the tree it already came - * from — no second origin to connect to, one cached copy for every bundle — and jsdelivr is the fallback. - * MapLibre only: Mapbox GL's licence ties it to Mapbox, whose servers a Mapbox map talks to anyway. - */ -export const VENDORED = [ - { cdn: CDN.maplibreGlJs.url, path: 'vendor/maplibre-gl@5.24.0/maplibre-gl.js', from: 'node_modules/maplibre-gl/dist/maplibre-gl.js' }, - { cdn: CDN.maplibreGlCss.url, path: 'vendor/maplibre-gl@5.24.0/maplibre-gl.css', from: 'node_modules/maplibre-gl/dist/maplibre-gl.css' }, -] - -/** The same-origin copy of a pinned CDN url under a built tree's root, or null (dev, or not vendored). */ -export function vendoredUrl(cdnUrl, treeRoot) { - const v = VENDORED.find((x) => x.cdn === cdnUrl) - return v && treeRoot ? String(treeRoot).replace(/\/+$/, '') + '/' + v.path : null -} - -/** Set integrity + crossorigin on a