Files
gods-eye-view/scripts/module-analysis.mjs
T
German Khokhlov 306e3e9467 build: reach Navara only through import() and ship its worker wasm
The import-direction check now tells dynamic imports apart
(analyzeModule(source, { dynamic: true })) and rejects any static import
or re-export of src/globe/navara/** from outside that directory, so the
Navara engine can only be a lazily loaded chunk of the app.

Navara's prebuilt workers load their wasm by file name next to
themselves, which the bundler cannot see; a build-only Rollup plugin
copies the missing ones (draco decoder, font worker) into assets/ when
the bundle includes Navara, and adds nothing to a build without it.
2026-10-15 20:56:06 +04:00

91 lines
2.8 KiB
JavaScript

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. With `dynamic`, also list the import() specifiers apart.
*/
export function analyzeModule(source, { dynamic = false } = {}) {
const imports = [];
const dynamicImports = [];
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 === 'ImportExpression')
dynamicImports.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],
...(dynamic && { dynamicImports }),
};
}
/** Return static imports, literal dynamic imports and re-exports. */
export function moduleImports(source) {
return analyzeModule(source).imports;
}