import { createHash } from "node:crypto"; import { lstat, readdir, realpath } from "node:fs/promises"; import path from "node:path"; import { pathToFileURL } from "node:url"; import { gzipSync } from "node:zlib"; import { build, normalizePath, type Plugin, version as viteVersion, } from "vite"; const RECIPE_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; const SOURCE_EXTENSION = /\.(?:[cm]?[jt]s|[jt]sx)$/; const DECLARATION_FILE = /\.d\.[cm]?ts$/; type EmittedOutput = | Readonly<{ type: "chunk"; fileName: string; code: string; }> | Readonly<{ type: "asset"; fileName: string; source: string | Uint8Array; }>; export type OptionalRecipeBundleOutput = Readonly<{ fileName: string; bytes: number; gzipBytes: number; sha256: string; }>; export type OptionalRecipeBundleMeasurement = Readonly<{ recipeId: string; sourceRoots: readonly string[]; sourceFileCount: number; toolchain: Readonly<{ bundler: "vite"; viteVersion: string; mode: "production"; target: "es2022"; format: "es"; minifier: "esbuild"; treeshake: false; compression: "node-zlib-gzip"; }>; outputs: readonly OptionalRecipeBundleOutput[]; bytes: number; gzipBytes: number; bundleBudgetGzipBytes: number; remainingGzipBytes: number; sha256: string; passed: boolean; }>; /** * Builds an uncomposed reference runtime as a synthetic production consumer. * Every catalog-owned source module is exposed as an entry namespace and * tree-shaking is disabled so internal fail-closed paths remain in the budget. */ export async function measureOptionalRecipeBundle(input: Readonly<{ recipeId: string; sourceRoots: readonly string[]; bundleBudgetGzipBytes: number; workspaceRoot?: string; }>): Promise { const recipeId = validateRecipeId(input.recipeId); const bundleBudgetGzipBytes = positiveSafeInteger( input.bundleBudgetGzipBytes, "Optional recipe bundle budget", ); const workspaceRoot = await realpath( path.resolve(input.workspaceRoot ?? process.cwd()), ); const sourceRoots = validateSourceRoots(input.sourceRoots); const sourceFiles = await resolveSourceFiles( workspaceRoot, sourceRoots, ); const virtualEntry = `virtual:optional-reference-runtime-entry/${recipeId}`; const resolvedVirtualEntry = `\0${virtualEntry}`; const entrySource = sourceFiles .map( (sourceFile, index) => `export * as source${index} from ${JSON.stringify( viteSourceSpecifier(sourceFile), )};`, ) .join("\n") .concat("\n"); const preservePublicEntryPlugin = { name: "optional-reference-runtime-entry", enforce: "pre", resolveId(id) { return id === virtualEntry ? resolvedVirtualEntry : null; }, load(id) { return id === resolvedVirtualEntry ? entrySource : null; }, options(options) { return { ...options, preserveEntrySignatures: "strict", }; }, } satisfies Plugin; const buildResult = await build({ root: workspaceRoot, configFile: false, envFile: false, mode: "production", publicDir: false, clearScreen: false, logLevel: "silent", plugins: [preservePublicEntryPlugin], build: { target: "es2022", minify: "esbuild", sourcemap: false, write: false, emptyOutDir: false, copyPublicDir: false, cssCodeSplit: false, reportCompressedSize: false, rollupOptions: { input: virtualEntry, // Budget the complete selected runtime, including internal fail-closed // guards that a synthetic consumer cannot predict it will exercise. treeshake: false, output: { format: "es", entryFileNames: `${recipeId}.js`, chunkFileNames: `${recipeId}-chunk-[hash].js`, assetFileNames: `${recipeId}-asset-[name]-[hash][extname]`, }, }, }, }); const emitted = emittedOutputs(buildResult); if (emitted.length === 0) { throw new TypeError("Optional recipe bundle emitted no output."); } const outputs = emitted .map((output) => { const bytes = outputBytes(output); return Object.freeze({ fileName: output.fileName, bytes: bytes.byteLength, gzipBytes: gzipSync(bytes).byteLength, sha256: createHash("sha256").update(bytes).digest("hex"), }); }) .sort((left, right) => compareText(left.fileName, right.fileName)); const aggregateHash = createHash("sha256"); for (const output of outputs) { aggregateHash.update(output.fileName); aggregateHash.update("\0"); aggregateHash.update(output.sha256); aggregateHash.update("\0"); } const bytes = outputs.reduce( (total, output) => total + output.bytes, 0, ); const gzipBytes = outputs.reduce( (total, output) => total + output.gzipBytes, 0, ); return Object.freeze({ recipeId, sourceRoots: Object.freeze([...sourceRoots]), sourceFileCount: sourceFiles.length, toolchain: Object.freeze({ bundler: "vite" as const, viteVersion, mode: "production" as const, target: "es2022" as const, format: "es" as const, minifier: "esbuild" as const, treeshake: false as const, compression: "node-zlib-gzip" as const, }), outputs: Object.freeze(outputs), bytes, gzipBytes, bundleBudgetGzipBytes, remainingGzipBytes: bundleBudgetGzipBytes - gzipBytes, sha256: aggregateHash.digest("hex"), passed: gzipBytes <= bundleBudgetGzipBytes, }); } async function resolveSourceFiles( workspaceRoot: string, sourceRoots: readonly string[], ): Promise { const sourceBoundary = await realpath(path.join(workspaceRoot, "src")); const discovered: string[] = []; for (const sourceRoot of sourceRoots) { const target = path.resolve(workspaceRoot, sourceRoot); assertInsideSourceBoundary(target, sourceBoundary); const rootMetadata = await lstat(target); if (rootMetadata.isSymbolicLink()) { throw new TypeError("Optional recipe source root cannot be a symlink."); } assertInsideSourceBoundary(await realpath(target), sourceBoundary); if ( rootMetadata.isFile() && (!SOURCE_EXTENSION.test(target) || DECLARATION_FILE.test(target)) ) { throw new TypeError("Optional recipe source root is not executable source."); } discovered.push( ...(await collectExecutableSources(target, sourceBoundary)), ); } const unique = [...new Set(discovered)].sort((left, right) => compareText( normalizePath(path.relative(workspaceRoot, left)), normalizePath(path.relative(workspaceRoot, right)), ), ); if (unique.length === 0) { throw new TypeError("Optional recipe source roots contain no executable source."); } return Object.freeze(unique); } async function collectExecutableSources( target: string, sourceBoundary: string, ): Promise { const metadata = await lstat(target); if (metadata.isSymbolicLink()) { throw new TypeError("Optional recipe source cannot be a symlink."); } assertInsideSourceBoundary(target, sourceBoundary); if (metadata.isFile()) { return SOURCE_EXTENSION.test(target) && !DECLARATION_FILE.test(target) ? [target] : []; } if (!metadata.isDirectory()) return []; const entries = (await readdir(target, { withFileTypes: true })).sort( (left, right) => compareText(left.name, right.name), ); const groups = await Promise.all( entries.map((entry) => collectExecutableSources( path.join(target, entry.name), sourceBoundary, ), ), ); return groups.flat(); } function emittedOutputs(value: unknown): readonly EmittedOutput[] { const buildOutputs = Array.isArray(value) ? value : [value]; const emitted: EmittedOutput[] = []; for (const buildOutput of buildOutputs) { if (!isRecord(buildOutput) || !Array.isArray(buildOutput.output)) { throw new TypeError("Optional recipe bundle output is invalid."); } for (const output of buildOutput.output) { if (!isRecord(output)) { throw new TypeError("Optional recipe emitted output is invalid."); } if ( output.type === "chunk" && typeof output.fileName === "string" && typeof output.code === "string" ) { emitted.push({ type: "chunk", fileName: output.fileName, code: output.code, }); } else if ( output.type === "asset" && typeof output.fileName === "string" && (typeof output.source === "string" || output.source instanceof Uint8Array) ) { emitted.push({ type: "asset", fileName: output.fileName, source: output.source, }); } else { throw new TypeError("Optional recipe emitted output shape is invalid."); } } } return emitted; } function outputBytes(output: EmittedOutput): Buffer { if (output.type === "chunk") { return Buffer.from(output.code, "utf8"); } return Buffer.from(output.source); } function validateRecipeId(value: unknown): string { if (typeof value !== "string" || !RECIPE_ID.test(value)) { throw new TypeError("Optional recipe ID is invalid."); } return value; } function validateSourceRoots(value: unknown): readonly string[] { if ( !Array.isArray(value) || value.length === 0 || value.length > 32 || value.some( (sourceRoot) => typeof sourceRoot !== "string" || !sourceRoot.startsWith("src/") || sourceRoot.includes("\\") || sourceRoot .split("/") .some( (segment) => segment.length === 0 || segment === "." || segment === "..", ), ) || new Set(value).size !== value.length ) { throw new TypeError("Optional recipe source roots are invalid."); } return Object.freeze([...value].sort(compareText)); } function assertInsideSourceBoundary( target: string, sourceBoundary: string, ): void { const relative = path.relative(sourceBoundary, target); if ( relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative) ) { throw new TypeError("Optional recipe source escaped the source boundary."); } } function viteSourceSpecifier(sourceFile: string): string { return pathToFileURL(sourceFile).href; } function positiveSafeInteger(value: unknown, name: string): number { if (!Number.isSafeInteger(value) || (value as number) < 1) { throw new TypeError(`${name} is invalid.`); } return value as number; } function compareText(left: string, right: string): number { return left < right ? -1 : left > right ? 1 : 0; } function isRecord(value: unknown): value is Record { return Boolean(value) && typeof value === "object" && !Array.isArray(value); }