build: guard the initial chunks by module id; main-chunk check in CI

Review of Task 0:
- build/initialChunkGuard.js, a Rollup plugin in the browser build, fails
  the build when an initial chunk (an entry and what it imports
  statically) holds a module from node_modules/three, @navaramap/* or
  postprocessing. Checked by forcing it on satellite.js, which the main
  chunk does hold: the build fails naming the chunk and module.
  navaraAssets.test now pins both build plugins.
- scripts/check-main-chunk.mjs is one script for both main-chunk checks:
  the composition (text markers in the initial chunks, sizes) and, when
  scripts/main-chunk-baseline.json exists, the phase-4 branch's gzip-9
  budget, with its exports (checkMainChunk, measureMainChunk) so the two
  branches merge into this file. The composition test moved to
  src/tooling/mainChunkComposition.test.mjs to leave phase 4's
  mainChunk.test.mjs its name.
- CI runs npm run check:main-chunk after each npm run build (unit,
  browser-gate and Windows jobs).
This commit is contained in:
German Khokhlov committed 2026-10-16 16:22:49 +04:00
1 parent f7f4e9a45c
commit 76851ee949
8 files changed
+289 -66

No files matched your search

+78
View File
@@ -0,0 +1,78 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
ENGINE_ONLY_MODULES,
initialChunkGuard,
initialChunkViolations,
} from '../../build/initialChunkGuard.js';
const chunk = (fields) => ({
type: 'chunk',
isEntry: false,
imports: [],
moduleIds: [],
...fields,
});
const bundle = () => ({
'assets/index-A.js': chunk({
isEntry: true,
imports: ['assets/vendor-B.js'],
moduleIds: ['/repo/src/main.js', '/repo/node_modules/cesium/index.js'],
}),
'assets/vendor-B.js': chunk({
moduleIds: ['/repo/node_modules/satellite.js/dist/index.js'],
}),
'assets/navara-engine-C.js': chunk({
isDynamicEntry: true,
moduleIds: [
'/repo/node_modules/three/build/three.module.js',
'/repo/node_modules/@navaramap/three/dist/index.js',
],
}),
'assets/style.css': { type: 'asset' },
});
test('lazy chunks may hold three and Navara; initial chunks are clean', () => {
assert.deepEqual(initialChunkViolations(bundle()), []);
assert.ok(ENGINE_ONLY_MODULES.test('/x/node_modules/three/src/a.js'));
assert.ok(
ENGINE_ONLY_MODULES.test('C:\\x\\node_modules\\@navaramap\\three\\a.js'),
);
assert.ok(!ENGINE_ONLY_MODULES.test('/x/node_modules/three-geojson/a.js'));
assert.ok(!ENGINE_ONLY_MODULES.test('/x/src/layers/threeD.js'));
});
test('three or Navara reached by a static import from the entry is reported', () => {
const broken = bundle();
broken['assets/vendor-B.js'].moduleIds.push(
'/repo/node_modules/three/build/three.core.js',
);
broken['assets/index-A.js'].moduleIds.push(
'/repo/node_modules/@navaramap/three-default-plugin/dist/index.js',
);
assert.deepEqual(initialChunkViolations(broken), [
{
chunk: 'assets/index-A.js',
module:
'/repo/node_modules/@navaramap/three-default-plugin/dist/index.js',
},
{
chunk: 'assets/vendor-B.js',
module: '/repo/node_modules/three/build/three.core.js',
},
]);
});
test('the plugin fails the build on a violation and passes a clean bundle', () => {
const plugin = initialChunkGuard({ log: () => {} });
const errors = [];
const context = { error: (message) => errors.push(message) };
plugin.generateBundle.call(context, {}, bundle());
assert.deepEqual(errors, []);
const broken = bundle();
broken['assets/index-A.js'].moduleIds.push('/r/node_modules/three/a.js');
plugin.generateBundle.call(context, {}, broken);
assert.equal(errors.length, 1);
assert.match(errors[0], /three.*index-A\.js/s);
});
@@ -84,3 +84,21 @@ test('three and Navara markers are reported, ordinary engine names are not', ()
]);
assert.ok(FORBIDDEN_MARKERS.length >= 2);
});
test('the script exports the gzip budget check (phase 4 shape) next to the composition check', async () => {
const { mkdirSync, mkdtempSync, writeFileSync } = await import('node:fs');
const { tmpdir } = await import('node:os');
const path = await import('node:path');
const { checkMainChunk, measureMainChunk } =
await import('../../scripts/check-main-chunk.mjs');
const baseline = { gzipBytes: 1000, allowanceBytes: 100 };
assert.equal(checkMainChunk(1100, baseline).ok, true);
assert.equal(checkMainChunk(1101, baseline).ok, false);
const dist = mkdtempSync(path.join(tmpdir(), 'gev-main-chunk-'));
mkdirSync(path.join(dist, 'assets'));
writeFileSync(path.join(dist, 'assets/index-small.js'), 'x');
writeFileSync(path.join(dist, 'assets/index-main.js'), 'x'.repeat(5000));
const measured = measureMainChunk(dist);
assert.equal(measured.name, 'index-main.js');
assert.ok(measured.gzipBytes > 0 && measured.gzipBytes < 5000);
});
+2 -2
View File
@@ -46,11 +46,11 @@ test('a build without Navara gets no Navara wasm', (t) => {
assert.deepEqual(emitted, []);
});
test('the browser build runs the Navara asset copy as a Rollup plugin', () => {
test('the browser build runs the Navara asset copy and the initial-chunk guard as Rollup plugins', () => {
const { build } = createBrowserViteConfig({});
assert.deepEqual(
build.rollupOptions.plugins.map(({ name }) => name),
['navara-worker-assets'],
['navara-worker-assets', 'gev-initial-chunk-guard'],
);
assert.equal(build.chunkSizeWarningLimit, 1500);
});