mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
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.
This commit is contained in:
1 parent
72fec292d4
commit
306e3e9467
8 files changed
+179
-7
No files matched your search
@@ -0,0 +1,45 @@
|
||||
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
/** Where @navaramap/three keeps its prebuilt worker assets, or null. */
|
||||
function defaultAssetsDir() {
|
||||
try {
|
||||
const entry = fileURLToPath(import.meta.resolve('@navaramap/three'));
|
||||
return path.join(path.dirname(entry), 'assets');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navara's prebuilt workers (@navaramap/three/dist/assets) load their wasm by
|
||||
* file name next to themselves (`new URL('x-hash.wasm', self.location)`), a
|
||||
* reference the bundler cannot see. When a build includes Navara (the
|
||||
* ?engine=navara chunk), copy every such wasm the bundle lacks into
|
||||
* `assets/` under its own name. A build without Navara gets none.
|
||||
* @param {{ assetsDir?: string | null }} [options]
|
||||
*/
|
||||
export function navaraWorkerAssets({ assetsDir } = {}) {
|
||||
return {
|
||||
name: 'navara-worker-assets',
|
||||
generateBundle(_options, bundle) {
|
||||
const usesNavara = Object.values(bundle).some(
|
||||
(file) => file.type === 'chunk' && file.code.includes('navara_wasm'),
|
||||
);
|
||||
if (!usesNavara) return;
|
||||
const dir = assetsDir === undefined ? defaultAssetsDir() : assetsDir;
|
||||
if (!dir || !existsSync(dir)) return;
|
||||
for (const name of readdirSync(dir).sort()) {
|
||||
if (!name.endsWith('.wasm')) continue;
|
||||
const fileName = `assets/${name}`;
|
||||
if (bundle[fileName]) continue;
|
||||
this.emitFile({
|
||||
type: 'asset',
|
||||
fileName,
|
||||
source: readFileSync(path.join(dir, name)),
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
+7
-1
@@ -3,6 +3,7 @@ import cesium from 'vite-plugin-cesium';
|
||||
import { DEFAULT_ALLOWED_HOSTS, hostCheckPlugin } from './allowedHosts.js';
|
||||
import { embedFramingPlugin } from './embed-framing.js';
|
||||
import { panelBuildPlugin } from './panel.js';
|
||||
import { navaraWorkerAssets } from './navaraAssets.js';
|
||||
|
||||
/**
|
||||
* Content-Security-Policy for every document the dev/preview server serves.
|
||||
@@ -125,6 +126,11 @@ export function createBrowserViteConfig({
|
||||
mapillaryToken ?? '',
|
||||
),
|
||||
},
|
||||
build: { chunkSizeWarningLimit: 1500 },
|
||||
build: {
|
||||
chunkSizeWarningLimit: 1500,
|
||||
// Build only: the wasm Navara's workers load by name (the Navara
|
||||
// engine is a separate chunk loaded on ?engine=navara).
|
||||
rollupOptions: { plugins: [navaraWorkerAssets()] },
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -70,7 +70,9 @@ architectural check, not a JavaScript sandbox. The globe contract
|
||||
helpers, nor import an engine implementation; the `src/globe/cesium/**` graph
|
||||
reaches no Navara/three package or `src/globe/navara/**` module, and the
|
||||
`src/globe/navara/**` graph no Cesium package or `src/globe/cesium/**` module,
|
||||
through helpers too. Common voice controls cannot depend on a
|
||||
through helpers too. Outside its own directory, `src/globe/navara/**` is
|
||||
reached only through a literal `import()` (the app's `?engine=navara` mode), so
|
||||
Navara stays out of the Cesium-mode bundle. Common voice controls cannot depend on a
|
||||
Realtime protocol implementation. Negative fixtures cover indirect helpers,
|
||||
self-package imports, symlinks and unreachable files.
|
||||
|
||||
|
||||
@@ -97,7 +97,9 @@ export function checkImportDirections(root) {
|
||||
}
|
||||
let analysis;
|
||||
try {
|
||||
analysis = analyzeModule(readFileSync(resolved, 'utf8'));
|
||||
analysis = analyzeModule(readFileSync(resolved, 'utf8'), {
|
||||
dynamic: true,
|
||||
});
|
||||
} catch (error) {
|
||||
report(file, error.message);
|
||||
return null;
|
||||
@@ -141,6 +143,18 @@ export function checkImportDirections(root) {
|
||||
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}`,
|
||||
);
|
||||
if (file.startsWith('src/') && to.startsWith('server/'))
|
||||
report(file, `Browser imports server: ${to}`);
|
||||
if (
|
||||
|
||||
@@ -15,9 +15,13 @@ const browserGlobals = new Set([
|
||||
'getComputedStyle',
|
||||
]);
|
||||
|
||||
/** Inspect literal module dependencies and browser-platform references without executing code. */
|
||||
export function analyzeModule(source) {
|
||||
/**
|
||||
* 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;
|
||||
@@ -35,6 +39,8 @@ export function analyzeModule(source) {
|
||||
'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');
|
||||
@@ -71,7 +77,11 @@ export function analyzeModule(source) {
|
||||
}
|
||||
}
|
||||
walk(parsers.babel.parse(source));
|
||||
return { imports, browser: [...browser] };
|
||||
return {
|
||||
imports,
|
||||
browser: [...browser],
|
||||
...(dynamic && { dynamicImports }),
|
||||
};
|
||||
}
|
||||
|
||||
/** Return static imports, literal dynamic imports and re-exports. */
|
||||
|
||||
@@ -33,7 +33,8 @@
|
||||
"build/application-html.js",
|
||||
"build/allowedHosts.js",
|
||||
"build/embed-framing.js",
|
||||
"build/panel.js"
|
||||
"build/panel.js",
|
||||
"build/navaraAssets.js"
|
||||
],
|
||||
"external": ["vite-plugin-cesium"]
|
||||
},
|
||||
|
||||
@@ -241,6 +241,44 @@ for (const [name, files, pattern] of [
|
||||
});
|
||||
}
|
||||
|
||||
test('module analysis can tell dynamic imports from static ones', () => {
|
||||
assert.deepEqual(
|
||||
analyzeModule(
|
||||
"import a from 'one'; export * from 'two'; import('three'); const x = () => import('four');",
|
||||
{ dynamic: true },
|
||||
),
|
||||
{
|
||||
imports: ['one', 'two', 'three', 'four'],
|
||||
browser: [],
|
||||
dynamicImports: ['three', 'four'],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('the app reaches the Navara engine only through a dynamic import', (t) => {
|
||||
const { root, write } = fixture(t);
|
||||
write('src/globe/navara/index.js', "import './engine.js';");
|
||||
write('src/globe/navara/engine.js', '');
|
||||
write(
|
||||
'src/app/scene.js',
|
||||
"export const load = () => import('../globe/navara/index.js');",
|
||||
);
|
||||
assert.doesNotThrow(() => checkImportDirections(root));
|
||||
write('src/app/scene.js', "import '../globe/navara/index.js';");
|
||||
assert.throws(
|
||||
() => checkImportDirections(root),
|
||||
/src\/app\/scene\.js: The Navara engine loads only through a dynamic import\(\): src\/globe\/navara\/index\.js/,
|
||||
);
|
||||
write(
|
||||
'src/app/scene.js',
|
||||
"export { createNavaraGlobe } from '../globe/navara/index.js';",
|
||||
);
|
||||
assert.throws(
|
||||
() => checkImportDirections(root),
|
||||
/only through a dynamic import/,
|
||||
);
|
||||
});
|
||||
|
||||
test('geo modules may import each other, and its tests may import cesium', (t) => {
|
||||
const { root, write } = fixture(t);
|
||||
write('src/geo/math.js', 'export const half = (value) => value / 2;');
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { navaraWorkerAssets } from '../../build/navaraAssets.js';
|
||||
import { createBrowserViteConfig } from '../../build/vite.js';
|
||||
|
||||
test('the build copies the wasm files Navara workers load by name next to them', (t) => {
|
||||
const dir = mkdtempSync(path.join(os.tmpdir(), 'gev-navara-assets-'));
|
||||
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
||||
writeFileSync(path.join(dir, 'draco_decoder-AAAA.wasm'), 'draco');
|
||||
writeFileSync(path.join(dir, 'navara_wasm_worker_bg-BBBB.wasm'), 'worker');
|
||||
writeFileSync(path.join(dir, 'fontWorker-CCCC.js'), 'not wasm');
|
||||
const plugin = navaraWorkerAssets({ assetsDir: dir });
|
||||
const emitted = [];
|
||||
const bundle = {
|
||||
// Rollup already emitted this one under the same name.
|
||||
'assets/navara_wasm_worker_bg-BBBB.wasm': { type: 'asset' },
|
||||
// A Navara worker chunk shows the build includes Navara.
|
||||
'assets/index-DDDD.js': { type: 'chunk', code: 'navara_wasm_worker_bg' },
|
||||
};
|
||||
plugin.generateBundle.call(
|
||||
{ emitFile: (file) => emitted.push(file) },
|
||||
{},
|
||||
bundle,
|
||||
);
|
||||
assert.deepEqual(
|
||||
emitted.map(({ fileName, type }) => [fileName, type]),
|
||||
[['assets/draco_decoder-AAAA.wasm', 'asset']],
|
||||
);
|
||||
assert.equal(String(emitted[0].source), 'draco');
|
||||
});
|
||||
|
||||
test('a build without Navara gets no Navara wasm', (t) => {
|
||||
const dir = mkdtempSync(path.join(os.tmpdir(), 'gev-navara-assets-'));
|
||||
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
||||
writeFileSync(path.join(dir, 'draco_decoder-AAAA.wasm'), 'draco');
|
||||
const plugin = navaraWorkerAssets({ assetsDir: dir });
|
||||
const emitted = [];
|
||||
plugin.generateBundle.call(
|
||||
{ emitFile: (file) => emitted.push(file) },
|
||||
{},
|
||||
{ 'assets/index-EEEE.js': { type: 'chunk', code: 'cesium only' } },
|
||||
);
|
||||
assert.deepEqual(emitted, []);
|
||||
});
|
||||
|
||||
test('the browser build runs the Navara asset copy as a Rollup plugin', () => {
|
||||
const { build } = createBrowserViteConfig({});
|
||||
assert.deepEqual(
|
||||
build.rollupOptions.plugins.map(({ name }) => name),
|
||||
['navara-worker-assets'],
|
||||
);
|
||||
assert.equal(build.chunkSizeWarningLimit, 1500);
|
||||
});
|
||||
Reference in new issue
Block a user