import assert from 'node:assert/strict';
import { execFileSync, spawnSync } from 'node:child_process';
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { after, before, test } from 'node:test';
import {
DEFAULT_DIST_DIR,
INITIAL_BYTES_COUNTER,
extractInitialResources,
extractStaticImports,
formatReport,
measureInitialBytes,
parseArgs,
scanLiveTags,
toJourneySummary,
} from './measure-initial-bytes.mjs';
const scriptPath = fileURLToPath(
new URL('./measure-initial-bytes.mjs', import.meta.url)
);
/** Mirrors the shape the Angular application builder emits for apps/web. */
const BUILT_INDEX_HTML = `
`;
const BUILT_FILES = {
'assets/app-config.js': 65,
'styles-VDU4SQ5F.css': 311539,
'chunk-B6uziQ1i.js': 1566,
'chunk-Cn2Agfvf.js': 529624,
'polyfills-EBB6HFCX.js': 35876,
'main-EI6PCDGR.js': 1131437,
};
let workDir;
async function writeDist(
name,
{ indexHtml = BUILT_INDEX_HTML, files = BUILT_FILES } = {}
) {
const distDir = path.join(workDir, name);
await mkdir(distDir, { recursive: true });
if (indexHtml !== null) {
await writeFile(path.join(distDir, 'index.html'), indexHtml);
}
for (const [file, bytes] of Object.entries(files)) {
const target = path.join(distDir, file);
await mkdir(path.dirname(target), { recursive: true });
await writeFile(target, 'x'.repeat(bytes));
}
return distDir;
}
let GRAPH_FILES = {};
/** Writes files with real contents, for tests that read module imports. */
async function workDirWith(name, files) {
GRAPH_FILES = files;
const distDir = path.join(workDir, name);
await mkdir(distDir, { recursive: true });
for (const [file, contents] of Object.entries(files)) {
await writeFile(path.join(distDir, file), contents);
}
return distDir;
}
before(async () => {
workDir = await mkdtemp(path.join(os.tmpdir(), 'measure-initial-bytes-'));
});
after(async () => {
await rm(workDir, { recursive: true, force: true });
});
test('extracts scripts, stylesheets and modulepreload chunks in document order', () => {
assert.deepEqual(
extractInitialResources(BUILT_INDEX_HTML).map(({ path, kind }) => ({
path,
kind,
})),
[
{ path: 'assets/app-config.js', kind: 'script' },
{ path: 'styles-VDU4SQ5F.css', kind: 'stylesheet' },
{ path: 'chunk-B6uziQ1i.js', kind: 'modulepreload' },
{ path: 'chunk-Cn2Agfvf.js', kind: 'modulepreload' },
{ path: 'polyfills-EBB6HFCX.js', kind: 'script' },
{ path: 'main-EI6PCDGR.js', kind: 'script' },
]
);
});
test('ignores manifest, icon and external references', () => {
const html = `
`;
assert.deepEqual(extractInitialResources(html), [
{ path: 'main.js', url: 'main.js', kind: 'script' },
]);
});
test('deduplicates by request URL, ignores fragments and normalizes relative URLs', () => {
const html = `
`;
assert.deepEqual(extractInitialResources(html), [
{ path: 'chunk-a.js', url: 'chunk-a.js', kind: 'modulepreload' },
{ path: 'chunk-a.js', url: 'chunk-a.js?v=2', kind: 'modulepreload' },
{ path: 'chunk-b.js', url: 'chunk-b.js', kind: 'modulepreload' },
{ path: 'main.js', url: 'main.js', kind: 'script' },
]);
});
test('ignores commented-out tags and tag-like text inside inline scripts and styles', () => {
const html = `
`;
assert.deepEqual(
extractInitialResources(html).map((resource) => resource.url),
['assets/app-config.js', 'main.js']
);
assert.deepEqual(
scanLiveTags('').map((t) => t.tag),
['script', 'link']
);
// '', so
// the script after it is live, exactly as the HTML tokenizer sees it.
assert.deepEqual(
extractInitialResources(
'-- -->'
).map((resource) => resource.url),
['x.js']
);
});
test('a comment opener inside a script body does not swallow later live tags', () => {
const html = ``;
assert.deepEqual(
extractInitialResources(html).map((resource) => resource.url),
['main.js', 'chunk.js']
);
const reverse = ``;
assert.deepEqual(
extractInitialResources(reverse).map((resource) => resource.url),
['live.js']
);
// Unterminated raw text swallows the rest, as it does in a browser.
assert.deepEqual(
extractInitialResources(' {
const lookalike = ``;
assert.deepEqual(
extractInitialResources(lookalike).map((resource) => resource.url),
['real.js']
);
const fallback = ``;
assert.deepEqual(
extractInitialResources(fallback).map((resource) => resource.url),
['app.js']
);
assert.deepEqual(
extractInitialResources(
''
).map((r) => r.url),
['y.js']
);
});
test('template contents are inert and character references are decoded', () => {
const html = ``;
assert.deepEqual(
extractInitialResources(html).map((resource) => resource.url),
['chunk.js?a=1&b=2', 'main.js']
);
});
test('SVG script elements are not HTML scripts, HTML inside foreignObject is', () => {
const html = ``;
assert.deepEqual(
extractInitialResources(html).map((resource) => resource.url),
['html-in-svg.js', 'main.js']
);
});
test('counts a file once per distinct request URL', async () => {
const distDir = await writeDist('cache-busted', {
indexHtml: ``,
files: { 'chunk-a.js': 100 },
});
const measurement = await measureInitialBytes({ distDir });
assert.equal(measurement.resources.length, 2);
assert.equal(measurement.totals.modulepreload, 200);
});
test('sums index.html and every referenced file into the counter', async () => {
const distDir = await writeDist('built');
const measurement = await measureInitialBytes({ distDir });
const indexBytes = Buffer.byteLength(BUILT_INDEX_HTML);
const script = 65 + 35876 + 1131437;
const stylesheet = 311539;
const modulepreload = 1566 + 529624;
assert.deepEqual(measurement.indexHtml, {
path: 'index.html',
bytes: indexBytes,
});
assert.equal(measurement.resources.length, 6);
assert.deepEqual(measurement.totals, {
indexHtml: indexBytes,
script,
stylesheet,
modulepreload,
staticImport: 0,
initialBytes: indexBytes + script + stylesheet + modulepreload,
});
assert.deepEqual(measurement.counters, {
[INITIAL_BYTES_COUNTER]:
indexBytes + script + stylesheet + modulepreload,
});
});
test('extracts static import and re-export specifiers, not dynamic imports', () => {
const source =
'import{a as b}from"./chunk-A.js";import"./chunk-B.js";' +
'export{c}from"./sub/chunk-C.js";export*from"../chunk-D.js";' +
'const l=()=>import("./chunk-LAZY.js");import"@angular/core";' +
"const t=\"from './not-a-file.css'\";import\n { e } from './chunk-E.mjs';";
assert.deepEqual(extractStaticImports(source).sort(), [
'../chunk-D.js',
'./chunk-A.js',
'./chunk-B.js',
'./chunk-E.mjs',
'./sub/chunk-C.js',
]);
});
test('counts chunks that only a static import names, beyond the preload hints', async () => {
// Angular emits at most ten modulepreload hints; main imports the rest.
const distDir = await workDirWith('import-graph', {
'index.html':
'',
'main.js':
'import"./chunk-hinted.js";import{x}from"./chunk-unhinted.js";const lazy=()=>import("./chunk-lazy.js");',
'chunk-hinted.js': 'export const h=1;',
'chunk-unhinted.js': 'import"./chunk-deep.js";export const x=1;',
'chunk-deep.js': 'export const d=1;',
'chunk-lazy.js': 'export const never=1;',
});
const measurement = await measureInitialBytes({ distDir });
const counted = Object.fromEntries(
measurement.resources.map((resource) => [resource.path, resource.kind])
);
assert.deepEqual(counted, {
'chunk-hinted.js': 'modulepreload',
'main.js': 'script',
'chunk-unhinted.js': 'staticImport',
'chunk-deep.js': 'staticImport',
});
const size = (file) => Buffer.byteLength(GRAPH_FILES[file]);
assert.equal(
measurement.totals.staticImport,
size('chunk-unhinted.js') + size('chunk-deep.js')
);
});
test('import-like text in strings, templates and comments never counts', async () => {
const distDir = await workDirWith('import-lookalikes', {
'index.html': '',
'main.js': [
'const a=\'import "./chunk-string.js"\';',
'const b=`export{x}from"./chunk-template.js"`;',
'/* import"./chunk-comment.js" */',
'// import { y } from "./chunk-line-comment.js";',
'export const main=1;',
].join('\n'),
});
const measurement = await measureInitialBytes({ distDir });
assert.equal(measurement.totals.staticImport, 0);
assert.deepEqual(
measurement.resources.map((resource) => resource.path),
['main.js']
);
});
test('counts an import once per request URL, like index.html references', async () => {
// The preload and the import name the same file under different URLs,
// so the browser fetches two modules.
const distDir = await workDirWith('url-dedupe', {
'index.html':
'',
'main.js': 'import"./chunk-a.js";import"./chunk-a.js";',
'chunk-a.js': 'export const a=1;',
});
const measurement = await measureInitialBytes({ distDir });
assert.deepEqual(
measurement.resources.map(({ url, kind }) => [url, kind]),
[
['chunk-a.js?v=1', 'modulepreload'],
['main.js', 'script'],
['chunk-a.js', 'staticImport'],
]
);
assert.equal(
measurement.totals.staticImport,
Buffer.byteLength(GRAPH_FILES['chunk-a.js'])
);
});
test('fails when a startup module imports a file the build did not emit', async () => {
const distDir = await workDirWith('broken-import', {
'index.html': '',
'main.js': 'import"./chunk-gone.js";',
});
await assert.rejects(
measureInitialBytes({ distDir }),
/import files that are not in .*broken-import: chunk-gone\.js \(imported by main\.js\)/
);
});
test('fails when index.html is missing instead of reporting zero bytes', async () => {
const distDir = await writeDist('no-index', { indexHtml: null, files: {} });
await assert.rejects(
measureInitialBytes({ distDir }),
/No index\.html under .*no-index.*pnpm nx build web/
);
});
test('fails and names every referenced file that is missing from the build', async () => {
const dropped = ['chunk-Cn2Agfvf.js', 'main-EI6PCDGR.js'];
const files = Object.fromEntries(
Object.entries(BUILT_FILES).filter(([file]) => !dropped.includes(file))
);
const distDir = await writeDist('missing-chunk', { files });
await assert.rejects(
measureInitialBytes({ distDir }),
/not in .*missing-chunk: chunk-Cn2Agfvf\.js, main-EI6PCDGR\.js/
);
});
test('journey summary carries the counter under the launch journey', async () => {
const distDir = await writeDist('summary');
const measurement = await measureInitialBytes({ distDir });
const summary = toJourneySummary(measurement, {
measuredAt: new Date('2026-09-26T00:00:00.000Z'),
});
assert.deepEqual(summary, {
version: 1,
measuredAt: '2026-09-26T00:00:00.000Z',
journeys: {
launch: {
counters: {
[INITIAL_BYTES_COUNTER]: measurement.totals.initialBytes,
},
},
},
});
});
test('report lists the largest files first and ends with the counter', async () => {
const distDir = await writeDist('report');
const report = formatReport(await measureInitialBytes({ distDir }));
const lines = report.split('\n');
const mainLine = lines.findIndex((line) =>
line.startsWith('main-EI6PCDGR.js')
);
const configLine = lines.findIndex((line) =>
line.startsWith('assets/app-config.js')
);
assert.ok(mainLine > 0 && mainLine < configLine);
assert.match(
lines.at(-1),
/^renderer\.initialBytes = [\d,]+ bytes \(6 files \+ index\.html\)$/
);
});
test('parses CLI arguments and rejects unknown ones', () => {
assert.deepEqual(parseArgs([]), {
distDir: DEFAULT_DIST_DIR,
json: false,
summary: null,
});
assert.deepEqual(
parseArgs(['--', '--dist', 'out', '--json', '--summary=s.json']),
{
distDir: 'out',
json: true,
summary: 's.json',
}
);
assert.deepEqual(
parseArgs(['--dist=out/web', '--summary', 'dist/s.json']).distDir,
'out/web'
);
assert.throws(
() => parseArgs(['--verbose']),
/Unknown argument: --verbose/
);
assert.throws(() => parseArgs(['--dist']), /Missing value for --dist/);
});
test('CLI writes the journey summary and exits 0 on a complete build', async () => {
const distDir = await writeDist('cli');
const summaryPath = path.join(workDir, 'out', 'journey-summary.json');
const stdout = execFileSync(
process.execPath,
[scriptPath, '--dist', distDir, '--summary', summaryPath],
{ encoding: 'utf8' }
);
assert.match(stdout, /renderer\.initialBytes = [\d,]+ bytes/);
const summary = JSON.parse(await readFile(summaryPath, 'utf8'));
assert.equal(
summary.journeys.launch.counters[INITIAL_BYTES_COUNTER],
Buffer.byteLength(BUILT_INDEX_HTML) +
Object.values(BUILT_FILES).reduce((sum, bytes) => sum + bytes, 0)
);
});
test('CLI exits 1 with a readable message when the build is missing', () => {
const result = spawnSync(
process.execPath,
[scriptPath, '--dist', path.join(workDir, 'does-not-exist')],
{ encoding: 'utf8' }
);
assert.equal(result.status, 1);
assert.match(result.stderr, /measure-initial-bytes: No index\.html under/);
});