mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +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 { DEFAULT_ALLOWED_HOSTS, hostCheckPlugin } from './allowedHosts.js';
|
||||||
import { embedFramingPlugin } from './embed-framing.js';
|
import { embedFramingPlugin } from './embed-framing.js';
|
||||||
import { panelBuildPlugin } from './panel.js';
|
import { panelBuildPlugin } from './panel.js';
|
||||||
|
import { navaraWorkerAssets } from './navaraAssets.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Content-Security-Policy for every document the dev/preview server serves.
|
* Content-Security-Policy for every document the dev/preview server serves.
|
||||||
@@ -125,6 +126,11 @@ export function createBrowserViteConfig({
|
|||||||
mapillaryToken ?? '',
|
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
|
helpers, nor import an engine implementation; the `src/globe/cesium/**` graph
|
||||||
reaches no Navara/three package or `src/globe/navara/**` module, and the
|
reaches no Navara/three package or `src/globe/navara/**` module, and the
|
||||||
`src/globe/navara/**` graph no Cesium package or `src/globe/cesium/**` module,
|
`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,
|
Realtime protocol implementation. Negative fixtures cover indirect helpers,
|
||||||
self-package imports, symlinks and unreachable files.
|
self-package imports, symlinks and unreachable files.
|
||||||
|
|
||||||
|
|||||||
@@ -97,7 +97,9 @@ export function checkImportDirections(root) {
|
|||||||
}
|
}
|
||||||
let analysis;
|
let analysis;
|
||||||
try {
|
try {
|
||||||
analysis = analyzeModule(readFileSync(resolved, 'utf8'));
|
analysis = analyzeModule(readFileSync(resolved, 'utf8'), {
|
||||||
|
dynamic: true,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
report(file, error.message);
|
report(file, error.message);
|
||||||
return null;
|
return null;
|
||||||
@@ -141,6 +143,18 @@ export function checkImportDirections(root) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
record.edges.push(to);
|
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/'))
|
if (file.startsWith('src/') && to.startsWith('server/'))
|
||||||
report(file, `Browser imports server: ${to}`);
|
report(file, `Browser imports server: ${to}`);
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -15,9 +15,13 @@ const browserGlobals = new Set([
|
|||||||
'getComputedStyle',
|
'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 imports = [];
|
||||||
|
const dynamicImports = [];
|
||||||
const browser = new Set();
|
const browser = new Set();
|
||||||
function walk(node, parent, field) {
|
function walk(node, parent, field) {
|
||||||
if (!node || typeof node !== 'object') return;
|
if (!node || typeof node !== 'object') return;
|
||||||
@@ -35,6 +39,8 @@ export function analyzeModule(source) {
|
|||||||
'Computed module imports are not allowed in runtime code',
|
'Computed module imports are not allowed in runtime code',
|
||||||
);
|
);
|
||||||
imports.push(node.source.value);
|
imports.push(node.source.value);
|
||||||
|
if (node.type === 'ImportExpression')
|
||||||
|
dynamicImports.push(node.source.value);
|
||||||
}
|
}
|
||||||
if (node.type === 'CallExpression' && node.callee?.name === 'require')
|
if (node.type === 'CallExpression' && node.callee?.name === 'require')
|
||||||
throw new Error('Runtime modules must use ES imports');
|
throw new Error('Runtime modules must use ES imports');
|
||||||
@@ -71,7 +77,11 @@ export function analyzeModule(source) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
walk(parsers.babel.parse(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. */
|
/** Return static imports, literal dynamic imports and re-exports. */
|
||||||
|
|||||||
@@ -33,7 +33,8 @@
|
|||||||
"build/application-html.js",
|
"build/application-html.js",
|
||||||
"build/allowedHosts.js",
|
"build/allowedHosts.js",
|
||||||
"build/embed-framing.js",
|
"build/embed-framing.js",
|
||||||
"build/panel.js"
|
"build/panel.js",
|
||||||
|
"build/navaraAssets.js"
|
||||||
],
|
],
|
||||||
"external": ["vite-plugin-cesium"]
|
"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) => {
|
test('geo modules may import each other, and its tests may import cesium', (t) => {
|
||||||
const { root, write } = fixture(t);
|
const { root, write } = fixture(t);
|
||||||
write('src/geo/math.js', 'export const half = (value) => value / 2;');
|
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