mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 07:42:23 +00:00
src/app/layers/renderers.js now holds LAYER_MANIFEST, one entry per layer in directory order: a display (the same render/display.js on every engine, with the contract primitives it needs and, for CCTV and Global Context, a Navara backend loaded by import()), a layer that draws itself through the contract, or per-engine renderers for the layers not on a display yet (weather, wind, Bhote Koshi, sea marks, ski pistes). A layer is available where the engine has the primitives it needs. The Cesium renderer table, CESIUM_BUILTIN, the registry and the Navara mode's map of about 35 loaders go; the application loads a kind's renderers before it builds the catalog. The tools attach through one tree over the engine, in Cesium's order. Each engine kind adds its own tools from one backend that reads its extension once: the Cesium backend holds the viewer-only tools, the Navara backend the annotation renderer, draw surface and file import drawing. Coupling: engine.extensions reads fall from 12 to 8, engine-name literal lines from 73 to 66.
417 lines
15 KiB
JavaScript
417 lines
15 KiB
JavaScript
import { execFileSync } from 'node:child_process';
|
|
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
import { builtinModules } from 'node:module';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { analyzeModule } from './module-analysis.mjs';
|
|
|
|
const builtins = new Set(
|
|
builtinModules.flatMap((name) => [name, `node:${name}`]),
|
|
);
|
|
const code = /\.[mc]?js$/;
|
|
const entry = (file) =>
|
|
file === 'src/main.js' || file.startsWith('src/standalone/');
|
|
const tests = (file) =>
|
|
file.endsWith('.test.mjs') ||
|
|
file.startsWith('src/testSupport/') ||
|
|
file.startsWith('src/tooling/') ||
|
|
file === 'src/overlays/worldOverlayAllocation.worker.mjs';
|
|
const source = (file) =>
|
|
file.startsWith('src/sources/') ||
|
|
/^src\/layers\/[^/]+\/(?:source|bundledSource|flowSource)\.js$/.test(file);
|
|
// Engine-neutral geometry ported from Cesium; every runtime file is a portable root.
|
|
const geo = (file) => file.startsWith('src/geo/');
|
|
// The app clock (src/time): plain numbers, no engine, no DOM.
|
|
const time = (file) => file.startsWith('src/time/');
|
|
// The engine-neutral globe contract and its test support; engines live below it.
|
|
const globeContract = (file) =>
|
|
/^src\/globe\/[^/]+$/.test(file) || file.startsWith('src/globe/testing/');
|
|
const globeEngine = (file) => /^src\/globe\/(?:cesium|navara)\//.test(file);
|
|
const cesiumPackage = /^(?:cesium(?:\/|$)|@cesium\/)/;
|
|
// Navara renders through three.js and its postprocessing peer.
|
|
const navaraPackage = /^(?:(?:three|postprocessing)(?:\/|$)|@navaramap\/)/;
|
|
// Layers built on layer renderers keep each engine in their renderers:
|
|
// cesium only in render/cesium*.js, three and Navara only in
|
|
// render/navara*.js. The list grows with each layer moved onto renderers.
|
|
const RENDERED_LAYER_DIRS = [
|
|
'src/layers/streetLevel/',
|
|
'src/layers/traffic/',
|
|
'src/layers/directions/',
|
|
'src/layers/infrastructure/',
|
|
'src/layers/installations/',
|
|
'src/layers/awareness/',
|
|
'src/layers/droneAirspace/',
|
|
'src/layers/cyclones/',
|
|
'src/layers/ghostMaps/',
|
|
'src/layers/submarineCables/',
|
|
'src/layers/alpr/',
|
|
'src/layers/cctv/',
|
|
'src/layers/transit/',
|
|
'src/layers/airQuality/',
|
|
'src/layers/gnss/',
|
|
'src/layers/perimeters/',
|
|
'src/layers/radio/',
|
|
'src/layers/bikeshare/',
|
|
'src/layers/earthquakes/',
|
|
'src/layers/firms/',
|
|
'src/layers/flights/',
|
|
'src/layers/military/',
|
|
'src/layers/localAdsb/',
|
|
'src/layers/vessels/',
|
|
'src/layers/satellites/',
|
|
'src/layers/launches/',
|
|
'src/layers/aircraft/',
|
|
'src/layers/shared/',
|
|
];
|
|
const W1_SHARED_FILES = new Set(
|
|
[
|
|
'trackedCamera',
|
|
'trailRenderer',
|
|
'contactTrailRenderer',
|
|
'focusDeemphasis',
|
|
'spriteOrder',
|
|
'iconOrientation',
|
|
'modelVisualAnchor',
|
|
'trackingClickGesture',
|
|
].map((name) => `src/data/${name}.js`),
|
|
);
|
|
const renderedLayer = (file) =>
|
|
W1_SHARED_FILES.has(file) ||
|
|
file.startsWith('src/data/render/') ||
|
|
file === 'src/celestialRing.js' ||
|
|
file.startsWith('src/celestialRing/') ||
|
|
RENDERED_LAYER_DIRS.some((dir) => file.startsWith(dir));
|
|
const cesiumLayerRenderer = (file) =>
|
|
/\/render\/cesium(?:[^/]*|\/[^/]+)\.js$/.test(file);
|
|
// Navara renderers outside a layer's render/ directory: like those, they
|
|
// reach the app only through the Navara mode's import().
|
|
const NAVARA_SCENE_RENDERERS = new Set(['src/scenes/dataPacks/navara.js']);
|
|
// The modules that load Navara renderers, each through import().
|
|
const NAVARA_RENDERER_LOADERS = new Set([
|
|
'src/app/navaraMode.js',
|
|
'src/app/layers/renderers.js',
|
|
]);
|
|
const navaraLayerRenderer = (file) =>
|
|
/\/render\/navara[^/]*\.js$/.test(file) || NAVARA_SCENE_RENDERERS.has(file);
|
|
const renderer = (file) =>
|
|
/^src\/(?:ui|app|standalone|overlays)\//.test(file) ||
|
|
/^src\/layers\/[^/]+\/(?:index|rendering|snapshotRenderer|overlay|presentation|cards|controls)\.js$/.test(
|
|
file,
|
|
);
|
|
// One compatibility composition entry intentionally selects standalone defaults.
|
|
const compatibilityEdge = (from, to) =>
|
|
from === 'server/providers/local.js' &&
|
|
to === 'server/standalone/key-setup.js';
|
|
const portableExport = (key) =>
|
|
key.startsWith('./sources/') ||
|
|
/\/source$/.test(key) ||
|
|
/^\.\/layers\/(?:flights|military|vessels)\/(?:records|ingestion)$/.test(
|
|
key,
|
|
) ||
|
|
[
|
|
'./director',
|
|
'./tools',
|
|
'./tools/mcp',
|
|
'./tools/services',
|
|
'./view',
|
|
'./voice/action-schemas',
|
|
'./voice/session',
|
|
'./data/lifecycle',
|
|
].includes(key);
|
|
|
|
/** Check every runtime file, plus transitive portable/source graphs, independently of bundler reachability. */
|
|
export function checkImportDirections(root) {
|
|
root = realpathSync(root);
|
|
const pkg = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
const files = [
|
|
...new Set(
|
|
execFileSync(
|
|
'git',
|
|
[
|
|
'ls-files',
|
|
'-z',
|
|
'--cached',
|
|
'--others',
|
|
'--exclude-standard',
|
|
'--',
|
|
'src',
|
|
'server',
|
|
],
|
|
{ cwd: root, encoding: 'utf8' },
|
|
).split('\0'),
|
|
),
|
|
].filter(
|
|
(file) =>
|
|
code.test(file) && !tests(file) && existsSync(path.join(root, file)),
|
|
);
|
|
const errors = new Set();
|
|
const modules = new Map();
|
|
const relative = (file) =>
|
|
path.relative(root, file).split(path.sep).join('/');
|
|
const report = (file, message) => errors.add(`${file}: ${message}`);
|
|
function load(file) {
|
|
if (modules.has(file)) return modules.get(file);
|
|
const resolved = realpathSync(path.join(root, file));
|
|
if (relative(resolved) !== file) {
|
|
report(
|
|
file,
|
|
'Runtime modules must not alias other owners through symlinks',
|
|
);
|
|
return null;
|
|
}
|
|
let analysis;
|
|
try {
|
|
analysis = analyzeModule(readFileSync(resolved, 'utf8'), {
|
|
dynamic: true,
|
|
});
|
|
} catch (error) {
|
|
report(file, error.message);
|
|
return null;
|
|
}
|
|
const record = { ...analysis, edges: [], external: [] };
|
|
modules.set(file, record);
|
|
for (const specifier of analysis.imports) {
|
|
let target;
|
|
if (specifier.startsWith('.'))
|
|
target = path.resolve(path.dirname(resolved), specifier.split('?')[0]);
|
|
else if (specifier === pkg.name || specifier.startsWith(`${pkg.name}/`)) {
|
|
const key =
|
|
specifier === pkg.name ? '.' : '.' + specifier.slice(pkg.name.length);
|
|
const exported = pkg.exports?.[key];
|
|
if (typeof exported !== 'string') {
|
|
report(file, `Unsupported self import: ${specifier}`);
|
|
continue;
|
|
}
|
|
target = path.resolve(root, exported);
|
|
} else {
|
|
record.external.push(specifier);
|
|
if (renderedLayer(file) && !tests(file)) {
|
|
if (cesiumPackage.test(specifier) && !cesiumLayerRenderer(file))
|
|
report(
|
|
file,
|
|
`cesium only in the layer's render/cesium*.js: ${specifier}`,
|
|
);
|
|
if (navaraPackage.test(specifier) && !navaraLayerRenderer(file))
|
|
report(
|
|
file,
|
|
`three and Navara only in the layer's render/navara*.js: ${specifier}`,
|
|
);
|
|
}
|
|
if (
|
|
file.startsWith('src/') &&
|
|
(builtins.has(specifier) || specifier.startsWith('node:'))
|
|
)
|
|
report(file, `Node builtin in browser module: ${specifier}`);
|
|
if (
|
|
specifier.startsWith('/') ||
|
|
(/^[a-z]+:/i.test(specifier) && !specifier.startsWith('node:'))
|
|
)
|
|
report(file, `Use repository or package imports: ${specifier}`);
|
|
continue;
|
|
}
|
|
if (!existsSync(target)) {
|
|
report(file, `Missing import: ${specifier}`);
|
|
continue;
|
|
}
|
|
const to = relative(realpathSync(target));
|
|
if (to.startsWith('../') || path.isAbsolute(to)) {
|
|
report(file, `Import escapes repository: ${specifier}`);
|
|
continue;
|
|
}
|
|
record.edges.push(to);
|
|
// Navara stays out of the Cesium-mode bundle: outside its own directory
|
|
// it is reached only through import() (src/app/scene.js), which the
|
|
// bundler splits into a chunk loaded on ?engine=navara.
|
|
if (
|
|
to.startsWith('src/globe/navara/') &&
|
|
!file.startsWith('src/globe/navara/') &&
|
|
!analysis.dynamicImports.includes(specifier)
|
|
)
|
|
report(
|
|
file,
|
|
`The Navara engine loads only through a dynamic import(): ${to}`,
|
|
);
|
|
// Like the engine, a layer's Navara renderer reaches the app only
|
|
// through an import() of the Navara mode (src/app/navaraMode.js) or
|
|
// of the layer manifest (src/app/layers/renderers.js).
|
|
if (
|
|
navaraLayerRenderer(to) &&
|
|
!navaraLayerRenderer(file) &&
|
|
!(
|
|
NAVARA_RENDERER_LOADERS.has(file) &&
|
|
analysis.dynamicImports.includes(specifier)
|
|
)
|
|
)
|
|
report(
|
|
file,
|
|
`A Navara layer renderer loads only through the Navara mode's dynamic import(): ${to}`,
|
|
);
|
|
if (file.startsWith('src/') && to.startsWith('server/'))
|
|
report(file, `Browser imports server: ${to}`);
|
|
if (
|
|
!compatibilityEdge(file, to) &&
|
|
file.startsWith('server/providers/') &&
|
|
(to.startsWith('server/standalone/') || renderer(to))
|
|
)
|
|
report(file, `Provider imports application/rendering: ${to}`);
|
|
if (
|
|
!compatibilityEdge(file, to) &&
|
|
file.startsWith('src/') &&
|
|
!entry(file) &&
|
|
entry(to)
|
|
)
|
|
report(file, `Reusable module imports standalone setup: ${to}`);
|
|
if (source(file) && renderer(to))
|
|
report(file, `Source imports rendering/application: ${to}`);
|
|
if (file === 'src/voice/gevActions.js' && to === 'src/data/manager.js')
|
|
report(
|
|
file,
|
|
'Actions must consume feed state without the manager facade',
|
|
);
|
|
}
|
|
return record;
|
|
}
|
|
for (const file of files) load(file);
|
|
// A helper outside src must not smuggle Node or server code into a browser graph.
|
|
const browserSeen = new Set();
|
|
function browser(file) {
|
|
if (browserSeen.has(file) || !code.test(file)) return;
|
|
browserSeen.add(file);
|
|
const info = load(file);
|
|
if (!info) return;
|
|
for (const specifier of info.external) {
|
|
if (builtins.has(specifier) || specifier.startsWith('node:'))
|
|
report(file, `Browser graph reaches Node builtin: ${specifier}`);
|
|
}
|
|
for (const to of info.edges) {
|
|
if (to.startsWith('server/') || tests(to))
|
|
report(file, `Browser graph reaches non-runtime owner: ${to}`);
|
|
else browser(to);
|
|
}
|
|
}
|
|
for (const file of files.filter((file) => file.startsWith('src/')))
|
|
browser(file);
|
|
const roots = new Map(files.filter(source).map((file) => [file, 'source']));
|
|
for (const file of files.filter(geo)) roots.set(file, 'geo');
|
|
for (const file of files.filter(time)) roots.set(file, 'time');
|
|
for (const [key, value] of Object.entries(pkg.exports || {})) {
|
|
if (portableExport(key) && typeof value === 'string')
|
|
roots.set(value.replace(/^\.\//, ''), key);
|
|
}
|
|
roots.set('src/data/feedState.js', 'feed state');
|
|
for (const [start, label] of roots) {
|
|
if (!existsSync(path.join(root, start))) {
|
|
report(start, `Missing portable entry: ${label}`);
|
|
continue;
|
|
}
|
|
const seen = new Set();
|
|
function visit(file) {
|
|
if (seen.has(file) || !code.test(file)) return;
|
|
seen.add(file);
|
|
const info = load(file);
|
|
if (!info) return;
|
|
if (info.browser.length)
|
|
report(
|
|
file,
|
|
`${label} reaches browser globals: ${info.browser.join(', ')}`,
|
|
);
|
|
for (const specifier of info.external) {
|
|
if (
|
|
builtins.has(specifier) ||
|
|
specifier.startsWith('node:') ||
|
|
/^(?:cesium|vite)(?:\/|$)/.test(specifier) ||
|
|
// Geo must not reach Cesium through its workspace packages either.
|
|
(label === 'geo' && /^@cesium\//.test(specifier)) ||
|
|
// Neither engine reaches the app clock.
|
|
(label === 'time' &&
|
|
(cesiumPackage.test(specifier) || navaraPackage.test(specifier)))
|
|
)
|
|
report(
|
|
file,
|
|
`${label} reaches platform/rendering dependency: ${specifier}`,
|
|
);
|
|
}
|
|
for (const to of info.edges) {
|
|
if (renderer(to) || to.startsWith('server/'))
|
|
report(file, `${label} reaches application/rendering: ${to}`);
|
|
else visit(to);
|
|
}
|
|
}
|
|
visit(start);
|
|
}
|
|
// The globe contract reaches no engine, directly or through helpers; each
|
|
// engine implementation imports only its own engine's packages.
|
|
const globeSeen = new Set();
|
|
function globe(file) {
|
|
if (globeSeen.has(file) || !code.test(file)) return;
|
|
globeSeen.add(file);
|
|
const info = load(file);
|
|
if (!info) return;
|
|
for (const specifier of info.external) {
|
|
if (cesiumPackage.test(specifier) || navaraPackage.test(specifier))
|
|
report(file, `globe contract reaches engine dependency: ${specifier}`);
|
|
}
|
|
for (const to of info.edges) {
|
|
if (globeEngine(to))
|
|
report(file, `globe contract imports engine implementation: ${to}`);
|
|
else globe(to);
|
|
}
|
|
}
|
|
for (const file of files.filter(globeContract)) globe(file);
|
|
// Each engine graph stays on its own engine, through helpers too.
|
|
for (const start of files.filter(globeEngine)) {
|
|
const navara = start.startsWith('src/globe/navara/');
|
|
const label = navara ? 'Navara globe engine' : 'Cesium globe engine';
|
|
const foreignPackage = navara ? cesiumPackage : navaraPackage;
|
|
const foreignEngine = navara ? 'src/globe/cesium/' : 'src/globe/navara/';
|
|
const seen = new Set();
|
|
const visit = (file) => {
|
|
if (seen.has(file) || !code.test(file)) return;
|
|
seen.add(file);
|
|
const info = load(file);
|
|
if (!info) return;
|
|
for (const specifier of info.external)
|
|
if (foreignPackage.test(specifier))
|
|
report(file, `${label} reaches ${specifier}`);
|
|
for (const to of info.edges) {
|
|
if (to.startsWith(foreignEngine))
|
|
report(file, `${label} reaches ${to}`);
|
|
else visit(to);
|
|
}
|
|
};
|
|
visit(start);
|
|
}
|
|
// Common voice controls may use their DOM view, but never a protocol implementation.
|
|
const voiceSeen = new Set();
|
|
function voice(file) {
|
|
if (voiceSeen.has(file)) return;
|
|
voiceSeen.add(file);
|
|
const info = load(file);
|
|
for (const to of info?.edges || []) {
|
|
if (/^src\/voice\/realtime/.test(to))
|
|
report(file, `Common voice controls import protocol: ${to}`);
|
|
else if (code.test(to)) voice(to);
|
|
}
|
|
}
|
|
if (existsSync(path.join(root, 'src/voice/sessionCommands.js')))
|
|
voice('src/voice/sessionCommands.js');
|
|
if (errors.size) throw new Error([...errors].join('\n'));
|
|
return { modules: files.length, portableEntries: roots.size };
|
|
}
|
|
|
|
if (
|
|
process.argv[1] &&
|
|
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
) {
|
|
try {
|
|
console.log(
|
|
JSON.stringify(
|
|
checkImportDirections(fileURLToPath(new URL('../', import.meta.url))),
|
|
),
|
|
);
|
|
} catch (error) {
|
|
console.error(error.message);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|