mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
refactor: enforce import directions and expose source factories
This commit is contained in:
1 parent
704e0fcc51
commit
cc960b0dbe
41 files changed
+1080
-417
No files matched your search
@@ -0,0 +1,254 @@
|
||||
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);
|
||||
const renderer = (file) =>
|
||||
/^src\/(?:ui|app|standalone|overlays)\//.test(file) ||
|
||||
/^src\/layers\/[^/]+\/(?:index|rendering|snapshotRenderer|overlay|presentation|cards|controls)\.js$/.test(
|
||||
file,
|
||||
);
|
||||
// Two compatibility composition entries intentionally select standalone defaults.
|
||||
const compatibilityEdge = (from, to) =>
|
||||
(from === 'server/providers/local.js' &&
|
||||
to === 'server/standalone/key-setup.js') ||
|
||||
(from === 'src/ui.js' && to === 'src/standalone/catalog.js');
|
||||
const portableExport = (key) =>
|
||||
key.startsWith('./sources/') ||
|
||||
/\/source$/.test(key) ||
|
||||
/^\.\/layers\/(?:flights|military|vessels)\/(?:records|ingestion)$/.test(
|
||||
key,
|
||||
) ||
|
||||
['./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'));
|
||||
} 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 (
|
||||
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);
|
||||
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',
|
||||
);
|
||||
if (
|
||||
file === 'src/app/constructCatalog.js' &&
|
||||
to === 'src/data/localGeojson.js'
|
||||
)
|
||||
report(
|
||||
file,
|
||||
'Catalog must use the services owner without compatibility layer construction',
|
||||
);
|
||||
}
|
||||
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 [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)
|
||||
)
|
||||
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);
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -659,5 +659,14 @@
|
||||
"src/voice/sessionCommands.js",
|
||||
"src/voice/realtimeSession.js",
|
||||
"src/voice/realtimeEvents.js",
|
||||
"src/voice/session.test.mjs"
|
||||
"src/voice/session.test.mjs",
|
||||
"src/layers/alpr/records.js",
|
||||
"src/layers/earthquakes/records.js",
|
||||
"src/sources/sourceSlot.js",
|
||||
"src/data/feedState.js",
|
||||
"src/app/localGeojsonServices.js",
|
||||
"src/layers/cctv/sourcePolicy.js",
|
||||
"scripts/module-analysis.mjs",
|
||||
"scripts/check-import-directions.mjs",
|
||||
"src/tooling/importDirections.test.mjs"
|
||||
]
|
||||
@@ -0,0 +1,80 @@
|
||||
import { parsers } from 'prettier/plugins/babel.mjs';
|
||||
|
||||
const browserGlobals = new Set([
|
||||
'window',
|
||||
'document',
|
||||
'navigator',
|
||||
'HTMLElement',
|
||||
'HTMLCanvasElement',
|
||||
'Image',
|
||||
'Audio',
|
||||
'localStorage',
|
||||
'sessionStorage',
|
||||
'requestAnimationFrame',
|
||||
'cancelAnimationFrame',
|
||||
'getComputedStyle',
|
||||
]);
|
||||
|
||||
/** Inspect literal module dependencies and browser-platform references without executing code. */
|
||||
export function analyzeModule(source) {
|
||||
const imports = [];
|
||||
const browser = new Set();
|
||||
function walk(node, parent, field) {
|
||||
if (!node || typeof node !== 'object') return;
|
||||
if (
|
||||
[
|
||||
'ImportDeclaration',
|
||||
'ExportNamedDeclaration',
|
||||
'ExportAllDeclaration',
|
||||
'ImportExpression',
|
||||
].includes(node.type) &&
|
||||
node.source
|
||||
) {
|
||||
if (node.source.type !== 'StringLiteral')
|
||||
throw new Error(
|
||||
'Computed module imports are not allowed in runtime code',
|
||||
);
|
||||
imports.push(node.source.value);
|
||||
}
|
||||
if (node.type === 'CallExpression' && node.callee?.name === 'require')
|
||||
throw new Error('Runtime modules must use ES imports');
|
||||
if (node.type === 'Identifier' && browserGlobals.has(node.name)) {
|
||||
const property =
|
||||
(parent?.type === 'MemberExpression' ||
|
||||
parent?.type === 'OptionalMemberExpression') &&
|
||||
field === 'property' &&
|
||||
!parent.computed;
|
||||
const key =
|
||||
['ObjectProperty', 'ObjectMethod', 'ClassMethod'].includes(
|
||||
parent?.type,
|
||||
) &&
|
||||
field === 'key' &&
|
||||
!parent.computed;
|
||||
const declaration =
|
||||
field === 'id' ||
|
||||
field === 'params' ||
|
||||
parent?.type?.startsWith('Import');
|
||||
if (!property && !key && !declaration) browser.add(node.name);
|
||||
}
|
||||
if (
|
||||
['MemberExpression', 'OptionalMemberExpression'].includes(node.type) &&
|
||||
['globalThis', 'self'].includes(node.object?.name)
|
||||
) {
|
||||
const name = node.computed ? node.property?.value : node.property?.name;
|
||||
if (browserGlobals.has(name)) browser.add(name);
|
||||
}
|
||||
for (const [key, value] of Object.entries(node)) {
|
||||
if (['loc', 'comments', 'tokens', 'extra'].includes(key)) continue;
|
||||
if (Array.isArray(value))
|
||||
value.forEach((child) => walk(child, node, key));
|
||||
else if (value && typeof value === 'object') walk(value, node, key);
|
||||
}
|
||||
}
|
||||
walk(parsers.babel.parse(source));
|
||||
return { imports, browser: [...browser] };
|
||||
}
|
||||
|
||||
/** Return static imports, literal dynamic imports and re-exports. */
|
||||
export function moduleImports(source) {
|
||||
return analyzeModule(source).imports;
|
||||
}
|
||||
@@ -268,7 +268,7 @@
|
||||
"server/providers/common/source-root.js",
|
||||
"server/standalone/key-setup.js",
|
||||
"src/keySetupCore.mjs",
|
||||
"src/keySetupHardening.mjs"
|
||||
"server/standalone/key-setup-hardening.mjs"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
@@ -358,7 +358,8 @@
|
||||
"src/ui/clearLayersControl.js",
|
||||
"src/loadingFeedback.js",
|
||||
"src/data/installationFeedback.js",
|
||||
"src/keySetupCore.mjs"
|
||||
"src/keySetupCore.mjs",
|
||||
"src/data/feedState.js"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
@@ -552,7 +553,8 @@
|
||||
"src/ui/visualInput.js",
|
||||
"src/ui/visualPresets.js",
|
||||
"src/ui/visualSettings.js",
|
||||
"src/worldFocus.js"
|
||||
"src/worldFocus.js",
|
||||
"src/data/feedState.js"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
},
|
||||
@@ -735,7 +737,8 @@
|
||||
"modules": [
|
||||
"src/layers/earthquakes/index.js",
|
||||
"src/layers/earthquakes/model.js",
|
||||
"src/layers/earthquakes/source.js"
|
||||
"src/layers/earthquakes/source.js",
|
||||
"src/layers/earthquakes/records.js"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
},
|
||||
@@ -940,7 +943,8 @@
|
||||
"src/layers/cctv/testing.js",
|
||||
"src/overlays/worldOverlayTokens.js",
|
||||
"src/data/cctvFootprint.js",
|
||||
"src/data/inputOwnership.js"
|
||||
"src/data/inputOwnership.js",
|
||||
"src/layers/cctv/sourcePolicy.js"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
},
|
||||
@@ -954,7 +958,8 @@
|
||||
"src/layers/alpr/visuals.js",
|
||||
"src/layers/alpr/overlay.js",
|
||||
"src/layers/alpr/source.js",
|
||||
"src/data/inputOwnership.js"
|
||||
"src/data/inputOwnership.js",
|
||||
"src/layers/alpr/records.js"
|
||||
],
|
||||
"external": ["cesium"]
|
||||
},
|
||||
@@ -1126,7 +1131,7 @@
|
||||
"src/app/layers/traffic.js",
|
||||
"src/app/operations.js",
|
||||
"src/app/scene.js",
|
||||
"src/app/sourceSlot.js",
|
||||
"src/sources/sourceSlot.js",
|
||||
"src/app/sources.js",
|
||||
"src/app/startupChrome.js",
|
||||
"src/app/stateChannel.js",
|
||||
@@ -1608,7 +1613,12 @@
|
||||
"src/voice/session.js",
|
||||
"src/voice/sessionCommands.js",
|
||||
"src/voice/realtimeSession.js",
|
||||
"src/voice/realtimeEvents.js"
|
||||
"src/voice/realtimeEvents.js",
|
||||
"src/app/localGeojsonServices.js",
|
||||
"src/data/feedState.js",
|
||||
"src/layers/alpr/records.js",
|
||||
"src/layers/earthquakes/records.js",
|
||||
"src/layers/cctv/sourcePolicy.js"
|
||||
],
|
||||
"external": [
|
||||
"@mapbox/vector-tile",
|
||||
@@ -1647,7 +1657,7 @@
|
||||
"src/app/layers/satellites.js",
|
||||
"src/app/layers/submarineCables.js",
|
||||
"src/app/layers/traffic.js",
|
||||
"src/app/sourceSlot.js",
|
||||
"src/sources/sourceSlot.js",
|
||||
"src/cctvFocusRequest.js",
|
||||
"src/celestialRing.js",
|
||||
"src/cockpitMath.js",
|
||||
@@ -1956,7 +1966,11 @@
|
||||
"src/app/layers/directions.js",
|
||||
"src/search/coordinateGeocoder.js",
|
||||
"src/search/coordinateParser.js",
|
||||
"src/search/presetGeocoder.js"
|
||||
"src/search/presetGeocoder.js",
|
||||
"src/app/localGeojsonServices.js",
|
||||
"src/layers/alpr/records.js",
|
||||
"src/layers/earthquakes/records.js",
|
||||
"src/layers/cctv/sourcePolicy.js"
|
||||
],
|
||||
"external": [
|
||||
"cesium",
|
||||
@@ -1973,7 +1987,7 @@
|
||||
"src/annotations/annotationResolver.js",
|
||||
"src/annotations/resolver.js",
|
||||
"src/app/operations.js",
|
||||
"src/app/sourceSlot.js",
|
||||
"src/sources/sourceSlot.js",
|
||||
"src/app/surfaceServices.js",
|
||||
"src/data/dataCredits.js",
|
||||
"src/data/geoid.js",
|
||||
@@ -2166,5 +2180,67 @@
|
||||
"src/voice/control.js"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
"alpr-source": {
|
||||
"exports": ["./layers/alpr/source"],
|
||||
"modules": [
|
||||
"src/layers/alpr/policy.js",
|
||||
"src/layers/alpr/records.js",
|
||||
"src/layers/alpr/source.js"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
"bikeshare-source": {
|
||||
"exports": ["./layers/bikeshare/source"],
|
||||
"modules": ["src/layers/bikeshare/source.js"],
|
||||
"external": []
|
||||
},
|
||||
"cctv-source": {
|
||||
"exports": ["./layers/cctv/source"],
|
||||
"modules": ["src/layers/cctv/source.js", "src/layers/cctv/sourcePolicy.js"],
|
||||
"external": []
|
||||
},
|
||||
"earthquakes-source": {
|
||||
"exports": ["./layers/earthquakes/source"],
|
||||
"modules": [
|
||||
"src/layers/earthquakes/records.js",
|
||||
"src/layers/earthquakes/source.js"
|
||||
],
|
||||
"external": []
|
||||
},
|
||||
"firms-source": {
|
||||
"exports": ["./layers/firms/source"],
|
||||
"modules": ["src/layers/firms/source.js"],
|
||||
"external": []
|
||||
},
|
||||
"installations-source": {
|
||||
"exports": ["./layers/installations/source"],
|
||||
"modules": ["src/layers/installations/source.js"],
|
||||
"external": []
|
||||
},
|
||||
"launches-source": {
|
||||
"exports": ["./layers/launches/source"],
|
||||
"modules": ["src/layers/launches/source.js"],
|
||||
"external": []
|
||||
},
|
||||
"radio-source": {
|
||||
"exports": ["./layers/radio/source"],
|
||||
"modules": ["src/layers/radio/policy.js", "src/layers/radio/source.js"],
|
||||
"external": []
|
||||
},
|
||||
"satellites-source": {
|
||||
"exports": ["./layers/satellites/source"],
|
||||
"modules": ["src/layers/satellites/source.js"],
|
||||
"external": []
|
||||
},
|
||||
"traffic-source-factory": {
|
||||
"exports": ["./layers/traffic/source"],
|
||||
"modules": [
|
||||
"src/data/tomtomTiles.js",
|
||||
"src/layers/traffic/flowDecode.js",
|
||||
"src/layers/traffic/flowSource.js",
|
||||
"src/layers/traffic/source.js"
|
||||
],
|
||||
"external": ["@mapbox/vector-tile", "pbf"]
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user