Files
Juan Denis d67e5d07d0 fix: complete audit follow-ups and add regression coverage
Centralize backup schedule calculations and shared request/form flows, encode query values, and move repository finalization into its service. Add browser CI coverage, repair stale test fixtures, and provide opt-in request and build measurement tools.
2026-09-05 14:41:15 -04:00

70 lines
3.7 KiB
JavaScript

// Deterministic byte census of a production dist. Gzip is per file, level 9;
// totals are storage/transfer estimates, never a page-load speed measurement.
import { readFileSync, readdirSync, statSync, mkdirSync, writeFileSync } from 'node:fs';
import { resolve, relative, dirname, extname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { gzipSync } from 'node:zlib';
import { createHash } from 'node:crypto';
export function linkedAssets(html) {
const result = new Set();
for (const tag of html.matchAll(/<(?:script|link)\b[^>]*>/gi)) {
const attributes = Object.fromEntries([...tag[0].matchAll(/([\w-]+)\s*=\s*["']([^"']*)["']/g)]
.map((match) => [match[1].toLowerCase(), match[2]]));
const asset = attributes.src || (['stylesheet', 'modulepreload'].includes(attributes.rel) ? attributes.href : null);
if (asset && !/^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(asset)) {
result.add(decodeURIComponent(new URL(asset, 'https://build.invalid/').pathname).slice(1));
}
}
return result;
}
export function measureBuild(dist) {
const root = resolve(dist);
const walk = (directory) => readdirSync(directory).flatMap((name) => {
const path = resolve(directory, name);
return statSync(path).isDirectory() ? walk(path) : [path];
});
const fingerprint = createHash('sha256');
const files = walk(root).filter((path) => ['.js', '.mjs', '.css'].includes(extname(path))).sort().map((path) => {
const bytes = readFileSync(path);
const name = relative(root, path).replaceAll('\\', '/');
fingerprint.update(name).update('\0').update(bytes).update('\0');
return { path: name, bytes: bytes.length, gzip_bytes: gzipSync(bytes, { level: 9 }).length };
});
const linked = linkedAssets(readFileSync(resolve(root, 'index.html'), 'utf8'));
const missing = [...linked].filter((path) => !files.some((file) => file.path === path));
if (missing.length) throw new Error(`HTML-linked code assets missing from build: ${missing.join(', ')}`);
const sum = (items) => ({
files: items.length,
bytes: items.reduce((total, file) => total + file.bytes, 0),
gzip_bytes: items.reduce((total, file) => total + file.gzip_bytes, 0),
});
return {
schema_version: 1,
measured_at_utc: new Date().toISOString(),
node_version: process.version,
measured_code_sha256: fingerprint.digest('hex'),
compression: 'gzip, level 9, each file separately; sizes are bytes',
scope: 'Built JS/MJS/CSS, including public vendor shims. Excludes HTML, fonts, images, maps and other non-code assets.',
html_linked_scope: 'Only script, stylesheet and modulepreload URLs in index.html; not a complete runtime network trace or every later lazy import.',
all_code: sum(files),
html_linked_code: sum(files.filter((file) => linked.has(file.path))),
largest_code_assets: [...files].sort((a, b) => b.bytes - a.bytes).slice(0, 12),
assets: files,
};
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const here = dirname(fileURLToPath(import.meta.url));
const args = process.argv.slice(2);
const value = (flag, fallback) => args.includes(flag) ? args[args.indexOf(flag) + 1] : fallback;
const report = measureBuild(value('--dist', resolve(here, '../dist')));
const output = value('--output', null);
if (output) {
mkdirSync(dirname(resolve(output)), { recursive: true });
writeFileSync(output, `${JSON.stringify(report, null, 2)}\n`);
}
console.log(JSON.stringify({ all_code: report.all_code, html_linked_code: report.html_linked_code }, null, 2));
}