chore: initialize from frontend template 4dc033c
This commit is contained in:
@@ -0,0 +1,373 @@
|
||||
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<OptionalRecipeBundleMeasurement> {
|
||||
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<readonly string[]> {
|
||||
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<string[]> {
|
||||
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<string, unknown> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
Reference in New Issue
Block a user