Add the ratchet that keeps Cesium inside the renderer

Outside src/render/cesium/, only the 236 files on cesiumAllowList.json
may reach Cesium, and each must still do so, so porting a file removes
its entry. A file reaches Cesium by importing it or the Cesium adapter,
by name or by path, or by being handed the namespace as a parameter,
binding or property. Files added to the list since origin/main fail the
check, once the list is on main. The renderer contract, stub and
conformance suite must stay Cesium-free, and runtime code in a language
the check cannot read fails it.
This commit is contained in:
Sameh Khamis committed 2026-11-05 15:10:04 -08:00
1 parent 8d7f82f110
commit 03f2e2a9b9
3 files changed
+516

No files matched your search

+2
View File
@@ -672,6 +672,8 @@
"src/services/requests.js",
"src/app/scene.js",
"src/app/renderer.test.mjs",
"src/render/noCesiumOutsideRender.test.mjs",
"src/render/cesiumAllowList.json",
"src/geo/geo.test.mjs",
"src/geo/celestial.test.mjs",
"src/geo/quaternion.test.mjs",
+238
View File
@@ -0,0 +1,238 @@
[
"dev/conformance/main.js",
"src/annotations/annotationEngine.js",
"src/annotations/drawTool.js",
"src/annotations/resolver.js",
"src/annotations/screenAnnotationRenderer.js",
"src/annotations/worldAnnotationRenderer.js",
"src/app/embed.js",
"src/app/scene.js",
"src/app/viewer.js",
"src/camera.js",
"src/cameraGroundGuard.js",
"src/cameraVerbs.js",
"src/celestialRing.js",
"src/cockpitCloudEffects.js",
"src/cyberSonarGpu.js",
"src/cyberSonarScene.js",
"src/data/aircraftIconLayer.js",
"src/data/aircraftModelLayer.js",
"src/data/aircraftMotionRegime.js",
"src/data/bhoteKoshiEmbeddedMedia.js",
"src/data/bhoteKoshiEvent.js",
"src/data/bhoteKoshiLocator.js",
"src/data/cameraFraming.js",
"src/data/cctvGizmo.js",
"src/data/cctvViewshed.js",
"src/data/contactTrailRenderer.js",
"src/data/dataCredits.js",
"src/data/detection.js",
"src/data/focusDeemphasis.js",
"src/data/geoFileImport.js",
"src/data/geoImportPins.js",
"src/data/iconOrientation.js",
"src/data/iconSegment.js",
"src/data/iconSet.js",
"src/data/iconSetPrimitive.js",
"src/data/localGeojsonCore.js",
"src/data/modelRecord.js",
"src/data/modelVisualAnchor.js",
"src/data/motionModel.js",
"src/data/osmOverlays.js",
"src/data/trackedCamera.js",
"src/data/trailRenderer.js",
"src/hud.js",
"src/input/dispatcher.js",
"src/input/sensorPick.js",
"src/layers/airQuality/index.js",
"src/layers/alpr/index.js",
"src/layers/alpr/model.js",
"src/layers/alpr/overlay.js",
"src/layers/alpr/presentation.js",
"src/layers/alpr/visuals.js",
"src/layers/awareness/focus.js",
"src/layers/awareness/rendering.js",
"src/layers/awareness/state.js",
"src/layers/awareness/subject.js",
"src/layers/bikeshare/lifecycle.js",
"src/layers/bikeshare/policy.js",
"src/layers/bikeshare/rendering.js",
"src/layers/bikeshare/selection.js",
"src/layers/bikeshare/viewport.js",
"src/layers/cctv/cards.js",
"src/layers/cctv/geometry.js",
"src/layers/cctv/geometryQueue.js",
"src/layers/cctv/ground.js",
"src/layers/cctv/lifecycle.js",
"src/layers/cctv/model.js",
"src/layers/cctv/navigation.js",
"src/layers/cctv/policy.js",
"src/layers/cctv/projection.js",
"src/layers/cctv/rendering.js",
"src/layers/cctv/selection.js",
"src/layers/cctv/state.js",
"src/layers/cctv/testing.js",
"src/layers/cyclones/index.js",
"src/layers/cyclones/rendering.js",
"src/layers/directions/index.js",
"src/layers/earthquakes/index.js",
"src/layers/earthquakes/model.js",
"src/layers/firms/cards.js",
"src/layers/firms/lifecycle.js",
"src/layers/firms/model.js",
"src/layers/firms/policy.js",
"src/layers/firms/rendering.js",
"src/layers/firms/state.js",
"src/layers/firms/viewport.js",
"src/layers/flights/controller.js",
"src/layers/flights/enrichment.js",
"src/layers/flights/evidence.js",
"src/layers/flights/icons.js",
"src/layers/flights/lifecycle.js",
"src/layers/flights/motion.js",
"src/layers/flights/policy.js",
"src/layers/flights/queries.js",
"src/layers/flights/rendering.js",
"src/layers/flights/snapshotRenderer.js",
"src/layers/flights/state.js",
"src/layers/flights/testing.js",
"src/layers/flights/tracking.js",
"src/layers/ghostMaps/index.js",
"src/layers/gnss/index.js",
"src/layers/installations/controls.js",
"src/layers/installations/lifecycle.js",
"src/layers/installations/model.js",
"src/layers/installations/namedMarkers.js",
"src/layers/installations/rendering.js",
"src/layers/installations/state.js",
"src/layers/installations/viewport.js",
"src/layers/launches/camera.js",
"src/layers/launches/launchPad.js",
"src/layers/launches/lifecycle.js",
"src/layers/launches/model.js",
"src/layers/launches/orbitRendering.js",
"src/layers/launches/overlays.js",
"src/layers/launches/panel.js",
"src/layers/launches/paths.js",
"src/layers/launches/policy.js",
"src/layers/launches/policyHelpers.js",
"src/layers/launches/rendering.js",
"src/layers/launches/replay.js",
"src/layers/launches/selection.js",
"src/layers/launches/state.js",
"src/layers/localAdsb/index.js",
"src/layers/localAdsb/models.js",
"src/layers/localAdsb/motion.js",
"src/layers/military/lifecycle.js",
"src/layers/military/motion.js",
"src/layers/military/policy.js",
"src/layers/military/queries.js",
"src/layers/military/snapshotRenderer.js",
"src/layers/military/state.js",
"src/layers/military/testing.js",
"src/layers/military/tracking.js",
"src/layers/perimeters/index.js",
"src/layers/radio/lifecycle.js",
"src/layers/radio/navigation.js",
"src/layers/radio/queries.js",
"src/layers/radio/rendering.js",
"src/layers/radio/state.js",
"src/layers/recentImagery/rendering.js",
"src/layers/satellites/catalog.js",
"src/layers/satellites/controls.js",
"src/layers/satellites/ingestion.js",
"src/layers/satellites/lifecycle.js",
"src/layers/satellites/orbits.js",
"src/layers/satellites/policy.js",
"src/layers/satellites/records.js",
"src/layers/satellites/rendering.js",
"src/layers/satellites/state.js",
"src/layers/satellites/testing.js",
"src/layers/satellites/tracking.js",
"src/layers/streetLevel/cameraFraming.js",
"src/layers/streetLevel/credits.js",
"src/layers/streetLevel/groundClick.js",
"src/layers/streetLevel/index.js",
"src/layers/streetLevel/lineCoverage.js",
"src/layers/streetLevel/marker.js",
"src/layers/streetLevel/selection.js",
"src/layers/streetLevel/sequenceCones.js",
"src/layers/streetLevel/view.js",
"src/layers/submarineCables/ingestion.js",
"src/layers/submarineCables/interaction.js",
"src/layers/submarineCables/overlay.js",
"src/layers/submarineCables/policy.js",
"src/layers/submarineCables/rendering.js",
"src/layers/submarineCables/state.js",
"src/layers/submarineCables/surface.js",
"src/layers/traffic/animation.js",
"src/layers/traffic/flow.js",
"src/layers/traffic/footprint.js",
"src/layers/traffic/ingestion.js",
"src/layers/traffic/lifecycle.js",
"src/layers/traffic/model.js",
"src/layers/traffic/policy.js",
"src/layers/traffic/rendering.js",
"src/layers/traffic/retention.js",
"src/layers/traffic/state.js",
"src/layers/traffic/style.js",
"src/layers/traffic/surface.js",
"src/layers/traffic/timing.js",
"src/layers/traffic/viewport.js",
"src/layers/transit/height.js",
"src/layers/transit/lifecycle.js",
"src/layers/transit/rendering.js",
"src/layers/transit/selection.js",
"src/layers/transit/state.js",
"src/layers/transit/testing.js",
"src/layers/transit/trails.js",
"src/layers/transit/viewport.js",
"src/layers/vessels/cameraArea.js",
"src/layers/vessels/evidence.js",
"src/layers/vessels/queries.js",
"src/layers/vessels/rendering.js",
"src/layers/vessels/state.js",
"src/layers/vessels/tracking.js",
"src/layers/weather/index.js",
"src/layers/weather/rasterTiles.js",
"src/layers/weather/rendering.js",
"src/layers/weather/shellRendering.js",
"src/layers/wind/gpuRendering.js",
"src/layers/wind/index.js",
"src/layers/wind/inspection.js",
"src/layers/wind/inspectionMarker.js",
"src/layers/wind/relief.js",
"src/layers/wind/rendering.js",
"src/locations.js",
"src/maps/catalog.js",
"src/maps/controller.js",
"src/maps/credits.js",
"src/maps/google3d.js",
"src/maps/imagery.js",
"src/maps/terrain.js",
"src/orbit.js",
"src/overlays/worldOverlay.js",
"src/queries/heights.js",
"src/sceneCommandRelease.js",
"src/scenes/dataPacks/geometry.js",
"src/scenes/dataPacks/presentation.js",
"src/scenes/director.js",
"src/services/groundSnap.js",
"src/services/meshFloorSampler.js",
"src/sharelink.js",
"src/sky/starrySky.js",
"src/ui/applicationShell.js",
"src/ui/cameraOrientationControls.js",
"src/ui/cockpitCamera.js",
"src/ui/cockpitContext.js",
"src/ui/cockpitController.js",
"src/ui/flyToDataSource.js",
"src/ui/imageryBoxTool.js",
"src/ui/locationNavigation.js",
"src/ui/navigationController.js",
"src/ui/shareFollow.js",
"src/ui/visualEffects.js",
"src/voice/gevActions.js",
"src/voice/pointerPick.js",
"src/worldFocus.js"
]
+276
View File
@@ -0,0 +1,276 @@
/**
* The ratchet: outside `src/render/cesium/`, only the files on
* `cesiumAllowList.json` may reach Cesium, and every file on it must still
* do so. Porting a file to the renderer contract removes it from the list;
* nothing is ever added (checked against the list on `origin/main`).
*
* A file reaches Cesium when it:
* - imports `cesium` or `@cesium/*`, by name or by a path into
* `node_modules` (relative or Vite root-relative `/x`, any query ignored);
* - imports the Cesium adapter (`src/render/cesium/`);
* - is handed the namespace: a parameter or binding named `Cesium`/`cesium`
* however destructured, a property of that name (`deps.cesium`,
* `window.Cesium`), or member access on an unbound `Cesium`;
* - in a `.vue` file, does any of these in a script block, or names Cesium
* in `<script src>`.
*
* Not caught: a Cesium value (a colour, a constant) exported from a listed
* module and used elsewhere; each phase 2 port removes those along with its
* file. Any name `cesium` counts, so a config field of that name would
* over-report rather than slip through.
*
* Runtime code is every `.js`, `.mjs`, `.cjs` and `.vue` file under `src/`,
* `server/` and `dev/`, except tests and test support. */
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
import { parsers } from 'prettier/plugins/babel.mjs';
import { parse as parseSfc } from 'vue/compiler-sfc';
const root = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'../..',
);
const LIST = 'src/render/cesiumAllowList.json';
const allowList = JSON.parse(readFileSync(path.join(root, LIST), 'utf8'));
const SCOPES = ['src', 'server', 'dev'];
/** Not runtime code, though not named `.test`: imported by tests only. */
const TEST_ONLY = new Set([
'src/layers/recentImagery/testDoubles.mjs',
'src/layers/weather/shellFixture.mjs',
'src/overlays/worldOverlayAllocation.worker.mjs',
]);
const isTestSupport = (file) =>
/\.test\.[cm]?js$/.test(file) ||
file.startsWith('src/testSupport/') ||
file.startsWith('src/tooling/') ||
TEST_ONLY.has(file);
const isAdapter = (file) => file.startsWith('src/render/cesium/');
const git = (...args) =>
execFileSync('git', args, { cwd: root, encoding: 'utf8' });
const tracked = git(
'ls-files',
'-z',
'--cached',
'--others',
'--exclude-standard',
'--',
...SCOPES,
)
.split('\0')
.filter(Boolean);
const runtimeFiles = tracked.filter(
(file) =>
/\.(?:[cm]?js|vue)$/.test(file) && !isTestSupport(file) && !isAdapter(file),
);
/** Whether an import specifier from `file` reaches Cesium. */
function reachesCesium(file, specifier) {
const bare = specifier.replace(/[?#].*$/, '');
if (/^(?:cesium|@cesium\/[^/]+)(?:\/|$)/.test(bare)) return true;
if (!bare.startsWith('.') && !bare.startsWith('/')) return false;
// Vite reads `/x` from the project root, not the filesystem's.
const from = bare.startsWith('/')
? root
: path.join(root, path.dirname(file));
const target = path
.relative(root, path.join(from, bare))
.split(path.sep)
.join('/');
return (
isAdapter(target) ||
/^node_modules\/(?:cesium|@cesium\/[^/]+)(?:\/|$)/.test(target)
);
}
const NAMESPACE = new Set(['Cesium', 'cesium']);
/** The import specifiers of a module, and whether it takes the namespace. */
function scan(source) {
const imports = [];
const bound = new Set();
let takesNamespace = false;
let usesUnbound = false;
const keyName = (property) =>
property.computed
? property.key?.value
: (property.key?.name ?? property.key?.value);
/** Any name a binding pattern introduces, however nested or renamed. */
const bindName = (node) => {
if (!node) return;
switch (node.type) {
case 'Identifier':
if (NAMESPACE.has(node.name)) {
bound.add(node.name);
takesNamespace = true;
}
break;
case 'AssignmentPattern':
bindName(node.left);
break;
case 'RestElement':
bindName(node.argument);
break;
case 'ArrayPattern':
node.elements.forEach(bindName);
break;
case 'ObjectPattern':
for (const property of node.properties) {
if (property.type === 'RestElement') bindName(property.argument);
else {
if (NAMESPACE.has(keyName(property))) takesNamespace = true;
bindName(property.value);
}
}
break;
}
};
function walk(node) {
if (!node || typeof node !== 'object') return;
if (Array.isArray(node)) return node.forEach(walk);
// Every function form (declarations, arrows, object, class and private
// methods) carries its parameters as `params`.
if (Array.isArray(node.params)) node.params.forEach(bindName);
switch (node.type) {
case 'ImportDeclaration':
case 'ExportNamedDeclaration':
case 'ExportAllDeclaration':
case 'ImportExpression':
if (node.source?.type === 'StringLiteral')
imports.push(node.source.value);
else if (node.source)
throw new Error('Computed module imports are not allowed');
if (node.type === 'ImportDeclaration')
for (const s of node.specifiers) bound.add(s.local.name);
break;
case 'CallExpression':
if (node.callee?.name === 'require')
throw new Error('Runtime modules must use ES imports');
break;
case 'VariableDeclarator':
bindName(node.id);
break;
case 'CatchClause':
bindName(node.param);
break;
case 'AssignmentExpression':
if (node.left?.type?.endsWith('Pattern')) bindName(node.left);
break;
case 'MemberExpression':
case 'OptionalMemberExpression':
if (node.object?.type === 'Identifier' && node.object.name === 'Cesium')
usesUnbound = true;
// The namespace as a property: `deps.cesium`, `window.Cesium`,
// `globalThis['Cesium']`.
if (
NAMESPACE.has(
node.computed ? node.property?.value : node.property?.name,
)
)
takesNamespace = true;
break;
}
for (const [key, value] of Object.entries(node)) {
if (
['loc', 'comments', 'tokens', 'extra', 'leadingComments'].includes(key)
)
continue;
if (value && typeof value === 'object') walk(value);
}
}
walk(parsers.babel.parse(source));
// Member access on `Cesium` that nothing in the file binds is a global.
if (usesUnbound && !bound.has('Cesium')) takesNamespace = true;
return { imports, takesNamespace };
}
/** The scripts of a single-file component, read by Vue's own parser. */
function vueScript(file, source) {
const { descriptor, errors } = parseSfc(source, { filename: file });
if (errors.length) throw errors[0];
const blocks = [descriptor.script, descriptor.scriptSetup].filter(Boolean);
return {
code: blocks.map((block) => block.content).join('\n'),
external: blocks.map((block) => block.src).filter(Boolean),
};
}
function reachesCesiumFile(file) {
const source = readFileSync(path.join(root, file), 'utf8');
let code = source;
let external = [];
if (file.endsWith('.vue')) ({ code, external } = vueScript(file, source));
const { imports, takesNamespace } = scan(code);
return (
takesNamespace ||
[...imports, ...external].some((s) => reachesCesium(file, s))
);
}
test('only allow-listed files outside the Cesium adapter reach Cesium', () => {
const allowed = new Set(allowList);
const offenders = runtimeFiles.filter(
(file) => !allowed.has(file) && reachesCesiumFile(file),
);
assert.deepEqual(
offenders,
[],
'Reach the renderer through its contract (src/render/contract) instead.',
);
});
test('every allow-listed file still reaches Cesium', () => {
const present = new Set(runtimeFiles);
const stale = allowList.filter(
(file) => !present.has(file) || !reachesCesiumFile(file),
);
assert.deepEqual(stale, [], `Remove these from ${LIST}.`);
});
test('the allow-list only shrinks', (t) => {
// Any git failure fails the check: only the list's confirmed absence on
// origin/main (before it lands) skips it.
const base = git('merge-base', 'HEAD', 'origin/main').trim();
const atBase = git('ls-tree', '--name-only', base, '--', LIST).trim();
if (!atBase) {
t.skip(`${LIST} is not on origin/main yet`);
return;
}
const before = new Set(JSON.parse(git('show', `${base}:${LIST}`)));
const added = allowList.filter((file) => !before.has(file));
assert.deepEqual(
added,
[],
`Files added to ${LIST} since ${base} (a moved file needs its old entry removed).`,
);
});
test('the allow-list is sorted and has no duplicates', () => {
assert.deepEqual(allowList, [...new Set(allowList)].sort());
});
test('no runtime file is in a language the ratchet cannot read', () => {
const unread = tracked.filter(
(file) =>
/\.(?:[cm]?tsx?|jsx)$/.test(file) &&
!/\.d\.[cm]?ts$/.test(file) &&
!isTestSupport(file),
);
assert.deepEqual(unread, [], 'Teach the ratchet to read these first.');
});
test('the renderer contract, stub and conformance suite stay Cesium-free', () => {
const engineFree = tracked.filter(
(file) =>
/^src\/render\/(?:contract|stub|conformance)\//.test(file) &&
/\.[cm]?js$/.test(file) &&
!/\.test\.[cm]?js$/.test(file),
);
assert.ok(engineFree.length > 0);
assert.deepEqual(engineFree.filter(reachesCesiumFile), []);
});