mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +00:00
build: the initial-chunk guard covers Navara's whole dependency closure
Review of Task 0: - build/navaraDependencies.js reads the dependency closure of @navaramap/three and @navaramap/three-default-plugin from node_modules (dependencies and peers) and drops what the app's other bundled dependencies reach too (Cesium is served as a script, so draco3d, which only Cesium shares, stays engine-only), plus three and postprocessing: 88 packages today (the @navaramap/* packages, @takram/*, @react-three/ fiber, @sparkjsdev/spark, astronomy-engine, d3*, draco3d, n8ao, three-stdlib, …). The browser build's initialChunkGuard fails on a module from any of them in an initial chunk. Tested on a fixture tree. - check-main-chunk's budget measures the entry the page loads (the same chunk the composition check reads), falling back to phase 4's largest index-*.js only for a dist without a page. Build clean: no Navara-only module in the initial chunks; main chunk 2,869,565 bytes (gzip 889,041).
This commit is contained in:
1 parent
ae52695b15
commit
d483e5897e
8 files changed
+209
-11
No files matched your search
@@ -5,7 +5,11 @@
|
|||||||
// (what Rollup put in each chunk), and fails the build otherwise;
|
// (what Rollup put in each chunk), and fails the build otherwise;
|
||||||
// scripts/check-main-chunk.mjs checks the built files' text as well.
|
// scripts/check-main-chunk.mjs checks the built files' text as well.
|
||||||
|
|
||||||
/** Module ids that only the Navara engine may load. */
|
/**
|
||||||
|
* Module ids that only the Navara engine may load, when no package list is
|
||||||
|
* given (the browser build passes the full dependency closure,
|
||||||
|
* navaraDependencies.js).
|
||||||
|
*/
|
||||||
export const ENGINE_ONLY_MODULES =
|
export const ENGINE_ONLY_MODULES =
|
||||||
/[\\/]node_modules[\\/](?:three|@navaramap[\\/][^\\/]+|postprocessing)[\\/]/;
|
/[\\/]node_modules[\\/](?:three|@navaramap[\\/][^\\/]+|postprocessing)[\\/]/;
|
||||||
|
|
||||||
@@ -32,21 +36,24 @@ export function initialChunkViolations(bundle, pattern = ENGINE_ONLY_MODULES) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The Rollup plugin: fails generateBundle when an initial chunk holds three or Navara. */
|
/** The Rollup plugin: fails generateBundle when an initial chunk holds three or Navara. */
|
||||||
export function initialChunkGuard({ log = console.log } = {}) {
|
export function initialChunkGuard({
|
||||||
|
log = console.log,
|
||||||
|
pattern = ENGINE_ONLY_MODULES,
|
||||||
|
} = {}) {
|
||||||
return {
|
return {
|
||||||
name: 'gev-initial-chunk-guard',
|
name: 'gev-initial-chunk-guard',
|
||||||
generateBundle(_options, bundle) {
|
generateBundle(_options, bundle) {
|
||||||
const violations = initialChunkViolations(bundle);
|
const violations = initialChunkViolations(bundle, pattern);
|
||||||
if (violations.length) {
|
if (violations.length) {
|
||||||
this.error(
|
this.error(
|
||||||
'three/Navara in the Cesium-mode initial chunks:\n' +
|
'Navara-only modules in the Cesium-mode initial chunks:\n' +
|
||||||
violations
|
violations
|
||||||
.map(({ chunk, module }) => ` ${chunk}: ${module}`)
|
.map(({ chunk, module }) => ` ${chunk}: ${module}`)
|
||||||
.join('\n'),
|
.join('\n'),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log('[initial-chunks] no three or Navara modules in the initial chunks');
|
log('[initial-chunks] no Navara-only modules in the initial chunks');
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
// The packages only the Navara engine brings: the dependency closure of
|
||||||
|
// @navaramap/three and @navaramap/three-default-plugin (dependencies and
|
||||||
|
// peer dependencies, read from node_modules), without the packages the
|
||||||
|
// app's other bundled dependencies reach too (Cesium is external), plus
|
||||||
|
// three and postprocessing, which
|
||||||
|
// the Cesium-mode page never loads statically either. The initial-chunk
|
||||||
|
// guard (initialChunkGuard.js) fails the build when one of them is in an
|
||||||
|
// initial chunk.
|
||||||
|
|
||||||
|
import { existsSync, readFileSync } from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
export const NAVARA_ROOTS = Object.freeze([
|
||||||
|
'@navaramap/three',
|
||||||
|
'@navaramap/three-default-plugin',
|
||||||
|
]);
|
||||||
|
const ALWAYS_ENGINE_ONLY = Object.freeze(['three', 'postprocessing']);
|
||||||
|
// Not bundled: vite-plugin-cesium serves Cesium as a script, so what only
|
||||||
|
// Cesium reaches (draco3d, …) is not in the app's bundled graph.
|
||||||
|
const EXTERNAL = Object.freeze(['cesium']);
|
||||||
|
|
||||||
|
function readManifest(root, name) {
|
||||||
|
const file = path.join(root, 'node_modules', name, 'package.json');
|
||||||
|
return existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Every package reachable from `names` through installed manifests. */
|
||||||
|
export function dependencyClosure(root, names) {
|
||||||
|
const seen = new Set();
|
||||||
|
const queue = [...names];
|
||||||
|
while (queue.length) {
|
||||||
|
const name = queue.pop();
|
||||||
|
if (seen.has(name)) continue;
|
||||||
|
const manifest = readManifest(root, name);
|
||||||
|
if (!manifest) continue;
|
||||||
|
seen.add(name);
|
||||||
|
for (const next of Object.keys({
|
||||||
|
...manifest.dependencies,
|
||||||
|
...manifest.peerDependencies,
|
||||||
|
}))
|
||||||
|
queue.push(next);
|
||||||
|
}
|
||||||
|
return seen;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The engine-only package names, sorted. */
|
||||||
|
export function navaraOnlyPackages(root) {
|
||||||
|
const app = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'));
|
||||||
|
const navara = dependencyClosure(root, NAVARA_ROOTS);
|
||||||
|
const appRoots = Object.keys(app.dependencies ?? {}).filter(
|
||||||
|
(name) =>
|
||||||
|
!navara.has(name) &&
|
||||||
|
!ALWAYS_ENGINE_ONLY.includes(name) &&
|
||||||
|
!EXTERNAL.includes(name),
|
||||||
|
);
|
||||||
|
const shared = dependencyClosure(root, appRoots);
|
||||||
|
return [
|
||||||
|
...new Set([
|
||||||
|
...[...navara].filter((name) => !shared.has(name)),
|
||||||
|
...ALWAYS_ENGINE_ONLY,
|
||||||
|
]),
|
||||||
|
].sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A module-id pattern matching node_modules/<package>/ for the names. */
|
||||||
|
export function packagePattern(names) {
|
||||||
|
const escaped = names.map((name) =>
|
||||||
|
name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace('/', '[\\\\/]'),
|
||||||
|
);
|
||||||
|
return new RegExp(
|
||||||
|
`[\\\\/]node_modules[\\\\/](?:${escaped.join('|')})[\\\\/]`,
|
||||||
|
);
|
||||||
|
}
|
||||||
+10
-1
@@ -1,3 +1,4 @@
|
|||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
import { applicationHtmlPlugin } from './application-html.js';
|
import { applicationHtmlPlugin } from './application-html.js';
|
||||||
import cesium from 'vite-plugin-cesium';
|
import cesium from 'vite-plugin-cesium';
|
||||||
import { DEFAULT_ALLOWED_HOSTS, hostCheckPlugin } from './allowedHosts.js';
|
import { DEFAULT_ALLOWED_HOSTS, hostCheckPlugin } from './allowedHosts.js';
|
||||||
@@ -5,6 +6,7 @@ import { embedFramingPlugin } from './embed-framing.js';
|
|||||||
import { panelBuildPlugin } from './panel.js';
|
import { panelBuildPlugin } from './panel.js';
|
||||||
import { navaraChunkFileNames, navaraWorkerAssets } from './navaraAssets.js';
|
import { navaraChunkFileNames, navaraWorkerAssets } from './navaraAssets.js';
|
||||||
import { initialChunkGuard } from './initialChunkGuard.js';
|
import { initialChunkGuard } from './initialChunkGuard.js';
|
||||||
|
import { navaraOnlyPackages, packagePattern } from './navaraDependencies.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Content-Security-Policy for every document the dev/preview server serves.
|
* Content-Security-Policy for every document the dev/preview server serves.
|
||||||
@@ -134,7 +136,14 @@ export function createBrowserViteConfig({
|
|||||||
// The lazy chunk with the Navara engine is named navara-engine.
|
// The lazy chunk with the Navara engine is named navara-engine.
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
// initialChunkGuard: no three/Navara module in the initial chunks.
|
// initialChunkGuard: no three/Navara module in the initial chunks.
|
||||||
plugins: [navaraWorkerAssets(), initialChunkGuard()],
|
plugins: [
|
||||||
|
navaraWorkerAssets(),
|
||||||
|
initialChunkGuard({
|
||||||
|
pattern: packagePattern(
|
||||||
|
navaraOnlyPackages(fileURLToPath(new URL('..', import.meta.url))),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
],
|
||||||
output: { chunkFileNames: navaraChunkFileNames },
|
output: { chunkFileNames: navaraChunkFileNames },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -42,8 +42,28 @@ export function checkMainChunk(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** gzip-9 size of the largest dist/assets/index-*.js, with its name. */
|
/**
|
||||||
export function measureMainChunk(distDir) {
|
* gzip-9 size of the main chunk, with its name: the entry the built page
|
||||||
|
* loads (`entry`, or found from dist/index.html or the Vite manifest), else
|
||||||
|
* (phase 4's rule, for a dist without a page) the largest
|
||||||
|
* dist/assets/index-*.js.
|
||||||
|
*/
|
||||||
|
export function measureMainChunk(distDir, { entry = null } = {}) {
|
||||||
|
let file = entry;
|
||||||
|
if (!file) {
|
||||||
|
try {
|
||||||
|
file = findEntry(distDir).entry;
|
||||||
|
} catch {
|
||||||
|
file = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (file) {
|
||||||
|
const bytes = fs.readFileSync(path.join(distDir, file));
|
||||||
|
return {
|
||||||
|
name: path.basename(file),
|
||||||
|
gzipBytes: gzipSync(bytes, { level: 9 }).length,
|
||||||
|
};
|
||||||
|
}
|
||||||
const assets = path.join(distDir, 'assets');
|
const assets = path.join(distDir, 'assets');
|
||||||
const candidates = fs
|
const candidates = fs
|
||||||
.readdirSync(assets)
|
.readdirSync(assets)
|
||||||
@@ -113,7 +133,7 @@ function main() {
|
|||||||
const baselinePath = path.join(ROOT, 'scripts/main-chunk-baseline.json');
|
const baselinePath = path.join(ROOT, 'scripts/main-chunk-baseline.json');
|
||||||
let budget = null;
|
let budget = null;
|
||||||
if (fs.existsSync(baselinePath)) {
|
if (fs.existsSync(baselinePath)) {
|
||||||
const measured = measureMainChunk(dist);
|
const measured = measureMainChunk(dist, { entry: composition.entry });
|
||||||
budget = {
|
budget = {
|
||||||
...measured,
|
...measured,
|
||||||
...checkMainChunk(
|
...checkMainChunk(
|
||||||
|
|||||||
@@ -1082,5 +1082,8 @@
|
|||||||
"src/globe/navara/picking.test.mjs",
|
"src/globe/navara/picking.test.mjs",
|
||||||
"src/data/selectionDispatcher.test.mjs",
|
"src/data/selectionDispatcher.test.mjs",
|
||||||
"src/globe/navara/cascadedLightCopy.test.mjs",
|
"src/globe/navara/cascadedLightCopy.test.mjs",
|
||||||
"src/tooling/w1Feeds.test.mjs"
|
"src/tooling/w1Feeds.test.mjs",
|
||||||
|
"src/tooling/navaraDependencies.test.mjs",
|
||||||
|
"build/navaraDependencies.js",
|
||||||
|
"build/initialChunkGuard.js"
|
||||||
]
|
]
|
||||||
@@ -74,5 +74,5 @@ test('the plugin fails the build on a violation and passes a clean bundle', () =
|
|||||||
broken['assets/index-A.js'].moduleIds.push('/r/node_modules/three/a.js');
|
broken['assets/index-A.js'].moduleIds.push('/r/node_modules/three/a.js');
|
||||||
plugin.generateBundle.call(context, {}, broken);
|
plugin.generateBundle.call(context, {}, broken);
|
||||||
assert.equal(errors.length, 1);
|
assert.equal(errors.length, 1);
|
||||||
assert.match(errors[0], /three.*index-A\.js/s);
|
assert.match(errors[0], /Navara-only.*index-A\.js.*three/s);
|
||||||
});
|
});
|
||||||
@@ -102,3 +102,20 @@ test('the script exports the gzip budget check (phase 4 shape) next to the compo
|
|||||||
assert.equal(measured.name, 'index-main.js');
|
assert.equal(measured.name, 'index-main.js');
|
||||||
assert.ok(measured.gzipBytes > 0 && measured.gzipBytes < 5000);
|
assert.ok(measured.gzipBytes > 0 && measured.gzipBytes < 5000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the budget measures the entry the page loads, not the largest index chunk', async () => {
|
||||||
|
const { mkdirSync, mkdtempSync, writeFileSync } = await import('node:fs');
|
||||||
|
const { tmpdir } = await import('node:os');
|
||||||
|
const path = await import('node:path');
|
||||||
|
const { measureMainChunk } =
|
||||||
|
await import('../../scripts/check-main-chunk.mjs');
|
||||||
|
const dist = mkdtempSync(path.join(tmpdir(), 'gev-main-entry-'));
|
||||||
|
mkdirSync(path.join(dist, 'assets'));
|
||||||
|
writeFileSync(path.join(dist, 'assets/index-entry.js'), 'x'.repeat(100));
|
||||||
|
writeFileSync(path.join(dist, 'assets/index-lazy.js'), 'y'.repeat(50000));
|
||||||
|
writeFileSync(
|
||||||
|
path.join(dist, 'index.html'),
|
||||||
|
'<script type="module" src="/assets/index-entry.js"></script>',
|
||||||
|
);
|
||||||
|
assert.equal(measureMainChunk(dist).name, 'index-entry.js');
|
||||||
|
});
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import test from 'node:test';
|
||||||
|
import {
|
||||||
|
navaraOnlyPackages,
|
||||||
|
packagePattern,
|
||||||
|
} from '../../build/navaraDependencies.js';
|
||||||
|
|
||||||
|
function tree(packages, app) {
|
||||||
|
const root = mkdtempSync(path.join(tmpdir(), 'gev-navara-deps-'));
|
||||||
|
writeFileSync(path.join(root, 'package.json'), JSON.stringify(app));
|
||||||
|
for (const [name, dependencies] of Object.entries(packages)) {
|
||||||
|
const dir = path.join(root, 'node_modules', name);
|
||||||
|
mkdirSync(dir, { recursive: true });
|
||||||
|
writeFileSync(
|
||||||
|
path.join(dir, 'package.json'),
|
||||||
|
JSON.stringify({ name, dependencies }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('engine-only packages: the Navara closure minus what the bundled app reaches', () => {
|
||||||
|
const root = tree(
|
||||||
|
{
|
||||||
|
'@navaramap/three': { 'd3-geo': '1', shared: '1', draco3d: '1' },
|
||||||
|
'@navaramap/three-default-plugin': { 'astronomy-engine': '1' },
|
||||||
|
'd3-geo': { 'd3-array': '1' },
|
||||||
|
'd3-array': {},
|
||||||
|
'astronomy-engine': {},
|
||||||
|
shared: {},
|
||||||
|
draco3d: {},
|
||||||
|
'mapillary-js': { shared: '1', three: '1' },
|
||||||
|
cesium: { draco3d: '1' },
|
||||||
|
three: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dependencies: {
|
||||||
|
'@navaramap/three': '1',
|
||||||
|
'@navaramap/three-default-plugin': '1',
|
||||||
|
'mapillary-js': '1',
|
||||||
|
cesium: '1',
|
||||||
|
three: '1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert.deepEqual(navaraOnlyPackages(root), [
|
||||||
|
'@navaramap/three',
|
||||||
|
'@navaramap/three-default-plugin',
|
||||||
|
'astronomy-engine',
|
||||||
|
'd3-array',
|
||||||
|
'd3-geo',
|
||||||
|
'draco3d', // only Cesium (external, not bundled) reaches it too
|
||||||
|
'postprocessing',
|
||||||
|
'three',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the pattern matches node_modules/<package>/ and nothing longer', () => {
|
||||||
|
const pattern = packagePattern(['three', '@navaramap/three', 'd3-geo']);
|
||||||
|
assert.ok(pattern.test('/r/node_modules/three/build/three.module.js'));
|
||||||
|
assert.ok(pattern.test('C:\\r\\node_modules\\@navaramap\\three\\dist\\i.js'));
|
||||||
|
assert.ok(pattern.test('/r/node_modules/d3-geo/src/index.js'));
|
||||||
|
assert.ok(!pattern.test('/r/node_modules/three-geojson/index.js'));
|
||||||
|
assert.ok(!pattern.test('/r/node_modules/d3-geo-projection/index.js'));
|
||||||
|
assert.ok(!pattern.test('/r/src/layers/threeD.js'));
|
||||||
|
});
|
||||||
Reference in new issue
Block a user