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.
This commit is contained in:
German Khokhlov committed 2026-11-04 17:15:27 +04:00
1 parent b8305de14b
commit f89aee1eac
4 files changed
+93 -26

No files matched your search

+3 -1
View File
@@ -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,
+4 -25
View File
@@ -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),
+83
View File
@@ -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'),
+3
View File
@@ -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');