Add a --write mode to the package boundary check

`node scripts/check-package-boundaries.mjs --write` regenerates each
group's module list in scripts/package-boundaries.json from what the
group's build reaches: entries that own nothing reached are removed, newly
reached modules are appended in sorted order, and the order of kept
entries stays. The check without the flag is unchanged.
This commit is contained in:
German Khokhlov committed 2026-11-10 12:56:36 +04:00
1 parent 60fb8fee07
commit f437c615a0
3 files changed
+293 -20

No files matched your search

+205 -4
View File
@@ -1,16 +1,29 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { execFileSync } from 'node:child_process';
import {
copyFile,
mkdtemp,
mkdir,
readFile,
writeFile,
rm,
} from 'node:fs/promises';
import path from 'node:path';
import { checkPackageBoundaries } from '../../scripts/check-package-boundaries.mjs';
import { fileURLToPath } from 'node:url';
import {
checkPackageBoundaries,
writePackageBoundaries,
} from '../../scripts/check-package-boundaries.mjs';
const repoRoot = fileURLToPath(new URL('../../', import.meta.url));
async function fixture(
t,
entry = 'export const value = 1;',
extraExports = {},
) {
const root = await mkdtemp(path.join(tmpdir(), 'gev-boundaries-'));
const root = await mkdtemp(path.join(repoRoot, 'tmp_package_boundaries_'));
t.after(() => rm(root, { recursive: true, force: true }));
await mkdir(path.join(root, 'scripts'));
await writeFile(
@@ -162,3 +175,191 @@ test('a module entry ending in a slash owns every file under that directory', as
/unowned module.*shared\.js/,
);
});
test('regeneration adds unowned modules, including unused and dynamic imports', async (t) => {
const root = await fixture(
t,
"import { unused } from './unused.js'; export const load = () => import('./lazy.js');",
);
await writeFile(path.join(root, 'unused.js'), 'export const unused = 1;');
await writeFile(path.join(root, 'lazy.js'), 'export const lazy = 2;');
assert.deepEqual(await writePackageBoundaries(root), [
{ name: 'feature', added: ['lazy.js', 'unused.js'], removed: [] },
]);
const manifest = JSON.parse(
await readFile(path.join(root, 'scripts/package-boundaries.json'), 'utf8'),
);
assert.deepEqual(manifest.feature.modules, [
'entry.js',
'lazy.js',
'unused.js',
]);
assert.deepEqual(await checkPackageBoundaries(root), [
{ name: 'feature', exports: 1, modules: 3 },
]);
});
test('regeneration removes unreached files without making the check reject stale entries', async (t) => {
const root = await fixture(t);
const file = path.join(root, 'scripts/package-boundaries.json');
const manifest = JSON.parse(await readFile(file, 'utf8'));
manifest.feature.modules.push('stale.js');
await writeFile(path.join(root, 'stale.js'), 'export const stale = 1;');
await writeFile(file, JSON.stringify(manifest));
assert.deepEqual(await checkPackageBoundaries(root), [
{ name: 'feature', exports: 1, modules: 1 },
]);
assert.deepEqual(await writePackageBoundaries(root), [
{ name: 'feature', added: [], removed: ['stale.js'] },
]);
assert.deepEqual(JSON.parse(await readFile(file, 'utf8')).feature.modules, [
'entry.js',
]);
});
test('regeneration removes deleted entries and adds an unowned export entry', async (t) => {
const root = await fixture(t);
const file = path.join(root, 'scripts/package-boundaries.json');
const manifest = JSON.parse(await readFile(file, 'utf8'));
manifest.feature.modules = ['deleted.js', 'deleted/'];
await writeFile(file, JSON.stringify(manifest));
assert.deepEqual(await writePackageBoundaries(root), [
{
name: 'feature',
added: ['entry.js'],
removed: ['deleted.js', 'deleted/'],
},
]);
assert.deepEqual(JSON.parse(await readFile(file, 'utf8')).feature.modules, [
'entry.js',
]);
});
test('regeneration keeps a directory only while it owns a reached module', async (t) => {
const root = await fixture(
t,
"export { value } from './shared/nested/value.js'; export { other } from './shared.js';",
);
await mkdir(path.join(root, 'shared/nested'), { recursive: true });
await writeFile(
path.join(root, 'shared/nested/value.js'),
'export const value = 1;',
);
await writeFile(path.join(root, 'shared.js'), 'export const other = 2;');
const file = path.join(root, 'scripts/package-boundaries.json');
const manifest = JSON.parse(await readFile(file, 'utf8'));
manifest.feature.modules.push('shared/');
await writeFile(file, JSON.stringify(manifest));
assert.deepEqual(await writePackageBoundaries(root), [
{ name: 'feature', added: ['shared.js'], removed: [] },
]);
assert.deepEqual(JSON.parse(await readFile(file, 'utf8')).feature.modules, [
'entry.js',
'shared/',
'shared.js',
]);
await writeFile(
path.join(root, 'entry.js'),
"export { other } from './shared.js';",
);
assert.deepEqual(await writePackageBoundaries(root), [
{ name: 'feature', added: [], removed: ['shared/'] },
]);
assert.deepEqual(JSON.parse(await readFile(file, 'utf8')).feature.modules, [
'entry.js',
'shared.js',
]);
});
test('regeneration preserves kept entry order and all other manifest fields', async (t) => {
const root = await fixture(
t,
"import './z.js'; import './b.js'; import './a.js'; export const value = 1;",
{ './build': { node: './build.js' } },
);
for (const name of ['z.js', 'b.js', 'a.js', 'build.js']) {
await writeFile(path.join(root, name), 'export const value = 1;');
}
const file = path.join(root, 'scripts/package-boundaries.json');
const manifest = {
feature: {
exports: ['./feature'],
modules: ['z.js', 'entry.js'],
external: [],
},
engineImportExceptions: [
{ file: 'entry.js', package: 'example', reason: 'Fixture exception.' },
],
build: {
runtime: 'node',
exports: ['./build'],
modules: ['build.js'],
external: [],
},
};
await writeFile(file, JSON.stringify(manifest));
assert.deepEqual(await writePackageBoundaries(root), [
{ name: 'feature', added: ['a.js', 'b.js'], removed: [] },
]);
manifest.feature.modules.push('a.js', 'b.js');
const updated = JSON.parse(await readFile(file, 'utf8'));
assert.deepEqual(updated, manifest);
assert.deepEqual(Object.keys(updated), Object.keys(manifest));
});
test('a failed regeneration leaves the manifest unchanged', async (t) => {
const root = await fixture(t, undefined, { './broken': './missing.js' });
const file = path.join(root, 'scripts/package-boundaries.json');
const manifest = JSON.parse(await readFile(file, 'utf8'));
manifest.feature.modules = [];
manifest.broken = { exports: ['./broken'], modules: [], external: [] };
const original = JSON.stringify(manifest);
await writeFile(file, original);
await assert.rejects(writePackageBoundaries(root), /missing\.js/);
assert.equal(await readFile(file, 'utf8'), original);
});
test('the CLI reports changes and a second --write leaves the manifest untouched', async (t) => {
const root = await fixture(t, "export { value } from './value.js';");
await writeFile(path.join(root, 'value.js'), 'export const value = 1;');
await writeFile(path.join(root, 'stale.js'), 'export const stale = 1;');
const file = path.join(root, 'scripts/package-boundaries.json');
const manifest = JSON.parse(await readFile(file, 'utf8'));
manifest.feature.modules.push('stale.js');
await writeFile(file, JSON.stringify(manifest));
await mkdir(path.join(root, 'build'));
for (const name of [
'scripts/check-package-boundaries.mjs',
'build/vueOptions.js',
]) {
await copyFile(path.join(repoRoot, name), path.join(root, name));
}
const run = (args) =>
execFileSync(
process.execPath,
['scripts/check-package-boundaries.mjs', ...args],
{
cwd: root,
encoding: 'utf8',
env: { ...process.env, GOOGLE_MAPS_API_KEY: '', CESIUM_ION_TOKEN: '' },
},
);
assert.equal(
run(['--write']),
'Updated feature: 1 added, 1 removed.\n + value.js\n - stale.js\n',
);
const first = await readFile(file, 'utf8');
assert.ok(first.includes('"exports": ["./feature"]'));
assert.ok(first.endsWith('\n'));
assert.equal(run(['--write']), '');
assert.equal(await readFile(file, 'utf8'), first);
assert.equal(run([]), 'Checked feature: 1 exports, 2 owned modules.\n');
let entry = "export { value } from './value.js';";
for (let i = 0; i < 21; i++) {
await writeFile(path.join(root, `extra${i}.js`), 'export const extra = 1;');
entry += `\nimport './extra${i}.js';`;
}
await writeFile(path.join(root, 'entry.js'), entry);
assert.equal(run(['--write']), 'Updated feature: 21 added, 0 removed.\n');
});