From f89aee1eacb183cf7ef92b5a17b93dd97dafa007 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Wed, 4 Nov 2026 17:15:27 +0400 Subject: [PATCH] Load the Cesium SDK and its widget styles only when a Cesium globe or a feature that needs it first runs index.html no longer loads /cesium/Cesium.js and widgets.css: the Cesium scene loads both before it builds its viewer. A cold Navara start requests nothing of the Cesium library again, and qa-strict-cold-chunks asserts that without the known-gap allowance. The strict harness puts an accessor in place of the Cesium global on every page and fails a scenario that reads it before Cesium.js has run; GEV_QA_CESIUM_TRAP=record lists every such read site instead. --- build/vite.js | 4 +- scripts/qa-strict-cold-chunks.mjs | 29 ++--------- scripts/qa-strict-harness.mjs | 83 +++++++++++++++++++++++++++++++ src/app/scene.js | 3 ++ 4 files changed, 93 insertions(+), 26 deletions(-) diff --git a/build/vite.js b/build/vite.js index 351139fd3..ac00a403a 100644 --- a/build/vite.js +++ b/build/vite.js @@ -186,7 +186,9 @@ export function createBrowserViteConfig({ apiResponseHeadersPlugin(), // Next, so no provider route answers a Host the server does not allow. hostCheckPlugin(), - cesium(), + // Keep the SDK asset serving/copying and external imports, but let the + // scene load the script and stylesheet only when Cesium is requested. + { ...cesium(), transformIndexHtml: undefined }, // Application views are Vue single-file components. vue(VUE_PLUGIN_OPTIONS), ...plugins, diff --git a/scripts/qa-strict-cold-chunks.mjs b/scripts/qa-strict-cold-chunks.mjs index ba5463dbc..37f79e157 100644 --- a/scripts/qa-strict-cold-chunks.mjs +++ b/scripts/qa-strict-cold-chunks.mjs @@ -7,13 +7,6 @@ // Control: after a pick to the other engine, its code is requested, so the // detection sees what it is looking for. // -// Known gap: the production build externalizes Cesium to the global its -// index.html script defines, and engine-neutral modules still read that -// global when they load, so a cold Navara start loads Cesium.js and its -// widget styles too. That row is reported as a known gap (its control is -// that the detection does see Cesium.js) instead of failing the run; any -// other Cesium request at a cold Navara start still fails it. -// // node scripts/qa-strict-cold-chunks.mjs http://127.0.0.1:4208 import assert from 'node:assert/strict'; @@ -46,10 +39,6 @@ const navaraCode = async (url) => { if (!pathname.endsWith('.js') || pathname.startsWith('/cesium/')) return []; return markersOf(url); }; -const KNOWN_COLD_NAVARA_CESIUM = new Set([ - '/cesium/Cesium.js', - '/cesium/Widgets/widgets.css', -]); const results = []; strict.extra.contracts = results; await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => { @@ -80,33 +69,23 @@ await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => { found.push(new URL(url).pathname); return [...new Set(found)]; }; - const loaded = await otherCode(before); - const known = - start === 'navara' - ? loaded.filter((path) => KNOWN_COLD_NAVARA_CESIUM.has(path)) - : []; const row = { start, otherEngine: other, - loadedAtColdStart: loaded.filter((path) => !known.includes(path)), - knownGap: known, + loadedAtColdStart: await otherCode(before), loadedAfterPick: (await otherCode(after)).length, }; results.push(row); console.log( - row.loadedAtColdStart.length - ? 'FAIL' - : row.knownGap.length - ? 'KNOWN GAP' - : 'PASS', + row.loadedAtColdStart.length ? 'FAIL' : 'PASS', JSON.stringify(row), ); await p.close(); } for (const row of results) assert.ok( - row.loadedAfterPick > 0 || row.knownGap.includes('/cesium/Cesium.js'), - `control: ${row.otherEngine} code is detected when it loads`, + row.loadedAfterPick > 0, + `control: picking ${row.otherEngine} loads its code`, ); assert.deepEqual( results.filter((row) => row.loadedAtColdStart.length), diff --git a/scripts/qa-strict-harness.mjs b/scripts/qa-strict-harness.mjs index ddb9dd711..072954641 100644 --- a/scripts/qa-strict-harness.mjs +++ b/scripts/qa-strict-harness.mjs @@ -281,6 +281,7 @@ export async function installStrictNetwork( // requests, and a live answer would still show up as a violation. if (type === 'page' || type === 'iframe') steps.push(intercept(session, type)); + if (type === 'page') steps.push(trapCesiumGlobal(session, ledger)); await Promise.all(steps); }; const attach = async (sessionId, info, waiting) => { @@ -317,6 +318,78 @@ export async function installStrictNetwork( return ledger; } +/** + * The production build reads every `cesium` import from the global that + * /cesium/Cesium.js defines, and the page loads that script only when a + * Cesium globe first starts. A read of the global before then throws in the + * app (a Navara page has no Cesium at all), wherever the app catches it, so + * each page gets an accessor in place of the global: reads before the script + * has assigned it are logged with their stack in `ledger.cesiumBeforeLoad` + * and answer `undefined`, as production would. + * + * GEV_QA_CESIUM_TRAP=record (a survey, not a check) answers such a read with + * the SDK instead, loaded synchronously on the first one, and logs every + * distinct read site until the page itself loads Cesium.js, so one run lists + * all the Navara paths that still need Cesium. + */ +async function trapCesiumGlobal(session, ledger) { + const binding = '__gevStrictCesiumRead'; + const record = process.env.GEV_QA_CESIUM_TRAP === 'record'; + session.on('Runtime.bindingCalled', (event) => { + if (event.name !== binding) return; + const stack = String(event.payload).slice(0, 1500); + const known = ledger.cesiumBeforeLoad.find((read) => read.stack === stack); + if (known) known.count++; + else if (ledger.cesiumBeforeLoad.length < (record ? 2000 : 50)) + ledger.cesiumBeforeLoad.push({ stack, count: 1 }); + }); + await session.send('Runtime.enable'); + await session.send('Runtime.addBinding', { name: binding }); + await session.send('Page.enable'); + await session.send('Page.addScriptToEvaluateOnNewDocument', { + source: `(() => { + if (window.top !== window) return; + const record = ${record}; + const seen = new Set(); + let sdk; + let pageLoaded = false; + const report = () => { + const limit = Error.stackTraceLimit; + Error.stackTraceLimit = 12; + const stack = String(new Error('Cesium read before it loaded').stack); + Error.stackTraceLimit = limit; + const site = stack.split('\\n').slice(2, 6).join('\\n'); + if (seen.has(site)) return; + seen.add(site); + window.${binding}?.(stack); + }; + Object.defineProperty(window, 'Cesium', { + configurable: true, + get() { + if (!record) { + if (sdk === undefined) report(); + return sdk; + } + if (!pageLoaded) { + pageLoaded = !!document.querySelector('script[src$="/Cesium.js"]'); + if (!pageLoaded) report(); + } + if (sdk === undefined) { + const request = new XMLHttpRequest(); + request.open('GET', '/cesium/Cesium.js', false); + request.send(); + (0, eval)(request.responseText); + } + return sdk; + }, + set(value) { + sdk = value; + }, + }); + })();`, + }); +} + /** A new, empty network ledger. */ export function networkLedger() { return { @@ -336,6 +409,7 @@ export function networkLedger() { }, violations: [], setupErrors: [], + cesiumBeforeLoad: [], }; } @@ -681,6 +755,7 @@ export function createStrict({ tileCache: ledger.tileCache, violations: ledger.violations, setupErrors: ledger.setupErrors, + cesiumBeforeLoad: ledger.cesiumBeforeLoad, fixtures, tileHosts: [...tileHosts], }; @@ -714,10 +789,13 @@ export function createStrict({ picks: picks.length, requests: ledger.requests, violations: ledger.violations.length, + cesiumBeforeLoad: ledger.cesiumBeforeLoad.length, }), ); for (const v of ledger.violations.slice(0, 20)) console.log('NETWORK VIOLATION', JSON.stringify(v)); + for (const read of ledger.cesiumBeforeLoad.slice(0, 10)) + console.log('CESIUM BEFORE LOAD', JSON.stringify(read)); if (failed) return; assert.equal( ledger.violations.length, @@ -725,6 +803,11 @@ export function createStrict({ 'network policy violations: ' + JSON.stringify(ledger.violations.slice(0, 5)), ); + assert.deepEqual( + ledger.cesiumBeforeLoad, + [], + 'the page read the Cesium global before Cesium.js loaded', + ); assert.deepEqual(missing, [], 'layers without an assertion'); assert.ok( picks.some((pick) => pick.via === 'pointer+keyboard'), diff --git a/src/app/scene.js b/src/app/scene.js index 5609d7790..1d6664720 100644 --- a/src/app/scene.js +++ b/src/app/scene.js @@ -17,6 +17,7 @@ import { import { describeError } from './errors.js'; import { createSceneEngine } from './globeEngine.js'; import { onLocaleChange, t } from '../i18n/index.js'; +import { loadCesium } from './loadCesium.js'; /** * Construct the application globe using the caller's local configuration. @@ -139,6 +140,8 @@ async function createRenderer({ } if (engineKind !== 'cesium') throw new TypeError(`Unknown globe engine: ${engineKind}`); + await loadCesium(); + signal.throwIfAborted(); loaderStatus.textContent = t('Configuring viewer...'); // Provider attribution stays visible, including clean-view and recording. const creditContainer = document.createElement('div');