The map engine loads from the tree it was served from, with the CDN as fallback (unverified)
UNVERIFIED: carried into the handoff without a browser run on this base. MapLibre GL came from cdn.jsdelivr.net: a second origin to connect to before a map could start. generate-tree now copies the pinned maplibre-gl JS and CSS from node_modules into <tree>/vendor/maplibre-gl@5.24.0/ and stops the build if their bytes do not match the integrity pin. A built bundle loads the engine from its own tree with that same pin, one cached copy for every bundle, and falls back to jsdelivr if the copy is missing. Mapbox GL stays on api.mapbox.com.
This commit is contained in:
parent
8eb4e498fc
commit
fc7259e06d
3 changed files with 69 additions and 20 deletions
|
|
@ -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 } from '../library/constants/cdn.js'
|
||||
import { CDN, VENDORED } from '../library/constants/cdn.js'
|
||||
import { isWebBuilt } from './plugin-targets.mjs'
|
||||
import {
|
||||
readFileSync, writeFileSync, readdirSync, existsSync, mkdirSync, statSync,
|
||||
|
|
@ -1602,6 +1602,21 @@ 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 (the Driver, 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
|
||||
|
|
|
|||
|
|
@ -99,7 +99,8 @@ 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 } from '../constants/cdn.js'
|
||||
import { CDN, pinElement, vendoredUrl } from '../constants/cdn.js'
|
||||
import { bundleTreeRoot } from '../utils/embedBase.js'
|
||||
import { installRtlTextPlugin } from '../maps/rtl-text.js'
|
||||
import { localizeBasemapLabels } from '../maps/localize-labels.js'
|
||||
import { tStatic } from '../i18n/index.js'
|
||||
|
|
@ -138,7 +139,26 @@ export function engineGlobal(provider) {
|
|||
* inject undefined.
|
||||
*/
|
||||
export function engineStylesheetUrl(provider) {
|
||||
return (ENGINES[provider] || ENGINES.mapbox).css || null
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -151,15 +171,14 @@ export async function ensureEngineScript(provider) {
|
|||
if (engineGlobal(provider)) return // already loaded — no-op
|
||||
if (typeof document === 'undefined') return
|
||||
|
||||
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 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(() => {})
|
||||
}
|
||||
|
||||
const existing = document.querySelector(`script[src="${cfg.js}"]`)
|
||||
const existing = document.querySelector(`script[src="${jsUrl}"]`) || document.querySelector(`script[src="${cfg.js}"]`)
|
||||
if (existing) {
|
||||
if (engineGlobal(provider)) return
|
||||
await new Promise(resolve => {
|
||||
|
|
@ -169,14 +188,8 @@ export async function ensureEngineScript(provider) {
|
|||
return
|
||||
}
|
||||
|
||||
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)
|
||||
await appendPinned('script', jsUrl, cfg.js, { async: false }).catch(() => {
|
||||
throw new Error(`[useMapInstance] Failed to load ${provider} engine script`)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,9 +34,30 @@ export const CDN = {
|
|||
d3: { url: 'https://cdn.jsdelivr.net/npm/d3@7.9.0/dist/d3.min.js', integrity: 'sha384-CjloA8y00+1SDAUkjs099PVfnY2KmDC2BZnws9kh8D/lX1s46w6EPhpXdqMfjK6i' },
|
||||
}
|
||||
|
||||
/** Set integrity + crossorigin on a <script>/<link> for a registry entry (no-op for an unknown url). */
|
||||
/**
|
||||
* SAME-ORIGIN COPIES (2026-09-28: maps must load fast). generate-tree copies each file below
|
||||
* from node_modules into <tree>/<path> 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 <script>/<link> for a registry entry (no-op for an unknown url). A
|
||||
* same-origin copy carries its CDN entry's pin. */
|
||||
export function pinElement(el, url) {
|
||||
const e = Object.values(CDN).find((x) => x.url === url)
|
||||
const v = VENDORED.find((x) => String(url).endsWith('/' + x.path))
|
||||
const pinUrl = v ? v.cdn : url
|
||||
const e = Object.values(CDN).find((x) => x.url === pinUrl)
|
||||
if (!e) return el
|
||||
el.integrity = e.integrity
|
||||
el.crossOrigin = 'anonymous'
|
||||
|
|
|
|||
Loading…
Reference in a new issue