247 lines
7.8 KiB
TypeScript
247 lines
7.8 KiB
TypeScript
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
|
|
import {
|
|
measureOptionalRecipeBundle,
|
|
type OptionalRecipeBundleMeasurement,
|
|
} from "./lib/optional-recipe-bundle.ts";
|
|
import {
|
|
scanOptionalRecipeSources,
|
|
scanProductionBundle,
|
|
validateRecipeCatalog,
|
|
} from "./lib/optional-recipes.ts";
|
|
|
|
type GateViolation = Readonly<{
|
|
ruleId: string;
|
|
path: string;
|
|
detail?: string;
|
|
}>;
|
|
|
|
type ReferenceRuntimeBundleInput = Readonly<{
|
|
recipeId: string;
|
|
sourceRoots: readonly string[];
|
|
bundleBudgetGzipBytes: number;
|
|
}>;
|
|
|
|
const argument = (name: string, fallback: string): string => {
|
|
const index = process.argv.indexOf(name);
|
|
return index === -1 ? fallback : (process.argv[index + 1] ?? fallback);
|
|
};
|
|
|
|
const catalogPath = argument(
|
|
"--catalog",
|
|
"config/recipes/frontend-capability-recipes.json",
|
|
);
|
|
const sourceRoot = argument("--source-root", "src");
|
|
const distRoot = argument("--dist-root", "dist");
|
|
const artifactPath = argument(
|
|
"--artifact",
|
|
"artifacts/quality/optional-recipes.json",
|
|
);
|
|
const requireDist = process.argv.includes("--require-dist");
|
|
|
|
const catalog = JSON.parse(
|
|
await readFile(catalogPath, "utf8"),
|
|
) as Record<string, unknown>;
|
|
const recipes = Array.isArray(catalog.recipes) ? catalog.recipes : [];
|
|
const packageDocument = JSON.parse(
|
|
await readFile("package.json", "utf8"),
|
|
) as Record<string, unknown>;
|
|
const catalogViolations = validateRecipeCatalog(catalog, packageDocument);
|
|
const runtimeSourceViolations = (
|
|
await Promise.all(
|
|
recipes.flatMap((recipe: unknown) => {
|
|
if (!recipe || typeof recipe !== "object") return [];
|
|
const row = recipe as Record<string, unknown>;
|
|
const runtime = row.referenceRuntime;
|
|
if (!runtime || typeof runtime !== "object") return [];
|
|
const sourceRoots = (runtime as Record<string, unknown>).sourceRoots;
|
|
if (!Array.isArray(sourceRoots)) return [];
|
|
return sourceRoots
|
|
.filter(
|
|
(sourceRoot): sourceRoot is string =>
|
|
typeof sourceRoot === "string",
|
|
)
|
|
.map(async (sourceRoot) => ({
|
|
sourceRoot,
|
|
exists: await stat(sourceRoot)
|
|
.then(() => true)
|
|
.catch(() => false),
|
|
}));
|
|
}),
|
|
)
|
|
).filter(({ exists }) => !exists);
|
|
const {
|
|
inputs: referenceRuntimeBundleInputs,
|
|
violations: referenceRuntimeBundleConfigurationViolations,
|
|
} = referenceRuntimeBundleInputsFrom(recipes);
|
|
const referenceRuntimeBundles: OptionalRecipeBundleMeasurement[] = [];
|
|
const referenceRuntimeBundleMeasurementViolations: GateViolation[] = [];
|
|
for (const bundleInput of referenceRuntimeBundleInputs) {
|
|
try {
|
|
const measurement = await measureOptionalRecipeBundle(bundleInput);
|
|
referenceRuntimeBundles.push(measurement);
|
|
if (!measurement.passed) {
|
|
referenceRuntimeBundleMeasurementViolations.push({
|
|
ruleId: "REFERENCE_RUNTIME_BUNDLE_BUDGET_EXCEEDED",
|
|
path: measurement.recipeId,
|
|
detail:
|
|
`${measurement.gzipBytes} > ` +
|
|
`${measurement.bundleBudgetGzipBytes} gzip bytes`,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
referenceRuntimeBundleMeasurementViolations.push({
|
|
ruleId: "REFERENCE_RUNTIME_BUNDLE_MEASUREMENT_FAILED",
|
|
path: bundleInput.recipeId,
|
|
detail: safeErrorSummary(error),
|
|
});
|
|
}
|
|
}
|
|
referenceRuntimeBundles.sort((left, right) =>
|
|
compareText(left.recipeId, right.recipeId),
|
|
);
|
|
const sourceViolations = await scanOptionalRecipeSources(sourceRoot);
|
|
const bundlePresent = await stat(`${distRoot}/.vite/manifest.json`)
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
const bundleViolations = await scanProductionBundle(distRoot);
|
|
const violations: GateViolation[] = [
|
|
...catalogViolations.map((ruleId) => ({ ruleId, path: catalogPath })),
|
|
...runtimeSourceViolations.map(({ sourceRoot }) => ({
|
|
ruleId: "REFERENCE_RUNTIME_SOURCE_MISSING",
|
|
path: sourceRoot,
|
|
})),
|
|
...sourceViolations,
|
|
...bundleViolations.map((path) => ({
|
|
ruleId: "UNSELECTED_RECIPE_IN_PRODUCTION_BUNDLE",
|
|
path,
|
|
})),
|
|
...(requireDist && !bundlePresent
|
|
? [{ ruleId: "PRODUCTION_BUNDLE_MISSING", path: distRoot }]
|
|
: []),
|
|
...referenceRuntimeBundleConfigurationViolations,
|
|
...referenceRuntimeBundleMeasurementViolations,
|
|
];
|
|
const report = {
|
|
schemaVersion: 1,
|
|
decisionId: "VD-10",
|
|
selectedCapabilities: [],
|
|
referenceRuntimes: recipes
|
|
.filter(
|
|
(recipe: unknown): recipe is Record<string, unknown> =>
|
|
Boolean(
|
|
recipe &&
|
|
typeof recipe === "object" &&
|
|
(recipe as Record<string, unknown>).referenceRuntime,
|
|
),
|
|
)
|
|
.map((recipe: Record<string, unknown>) => ({
|
|
id: recipe.id,
|
|
referenceRuntime: recipe.referenceRuntime,
|
|
})),
|
|
recipeCount: recipes.length,
|
|
productionRuntimeDependencies:
|
|
catalog.productionRuntimeDependencies ?? null,
|
|
referenceRuntimeBundleBudgets: referenceRuntimeBundles,
|
|
bundleStatus: bundlePresent
|
|
? bundleViolations.length === 0
|
|
? "PASS"
|
|
: "FAIL"
|
|
: "NOT_BUILT",
|
|
violations,
|
|
passed: violations.length === 0,
|
|
};
|
|
await mkdir("artifacts/quality", { recursive: true });
|
|
await writeFile(artifactPath, `${JSON.stringify(report, null, 2)}\n`);
|
|
|
|
if (violations.length > 0) {
|
|
process.stderr.write(
|
|
`Optional recipe contract failed:\n${violations
|
|
.map(
|
|
(violation) =>
|
|
`${violation.ruleId}: ${violation.path}` +
|
|
(violation.detail ? ` (${violation.detail})` : ""),
|
|
)
|
|
.join("\n")}\n`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
const referenceRuntimeBudgetStatus =
|
|
referenceRuntimeBundles.length > 0
|
|
? `, budgets=${referenceRuntimeBundles
|
|
.map(
|
|
(measurement) =>
|
|
`${measurement.recipeId}:${measurement.gzipBytes}/` +
|
|
measurement.bundleBudgetGzipBytes,
|
|
)
|
|
.join(",")} gzip bytes`
|
|
: "";
|
|
process.stdout.write(
|
|
`Optional recipes: PASS (${report.recipeCount} optional capabilities, ${report.referenceRuntimes.length} uncomposed reference runtimes, bundle=${report.bundleStatus}${referenceRuntimeBudgetStatus})\n`,
|
|
);
|
|
|
|
function referenceRuntimeBundleInputsFrom(
|
|
recipeRows: readonly unknown[],
|
|
): Readonly<{
|
|
inputs: readonly ReferenceRuntimeBundleInput[];
|
|
violations: readonly GateViolation[];
|
|
}> {
|
|
const inputs: ReferenceRuntimeBundleInput[] = [];
|
|
const violations: GateViolation[] = [];
|
|
for (const recipe of recipeRows) {
|
|
if (!recipe || typeof recipe !== "object") continue;
|
|
const row = recipe as Record<string, unknown>;
|
|
if (row.referenceRuntime === undefined) continue;
|
|
const recipeId =
|
|
typeof row.id === "string" ? row.id : "unknown-reference-runtime";
|
|
const runtime = row.referenceRuntime;
|
|
const sourceRoots =
|
|
runtime && typeof runtime === "object"
|
|
? (runtime as Record<string, unknown>).sourceRoots
|
|
: null;
|
|
if (
|
|
!Array.isArray(sourceRoots) ||
|
|
!sourceRoots.every(
|
|
(sourceRoot): sourceRoot is string =>
|
|
typeof sourceRoot === "string",
|
|
) ||
|
|
!Number.isSafeInteger(row.bundleBudgetGzipBytes) ||
|
|
(row.bundleBudgetGzipBytes as number) < 1
|
|
) {
|
|
violations.push({
|
|
ruleId: "REFERENCE_RUNTIME_BUNDLE_CONFIGURATION_INVALID",
|
|
path: recipeId,
|
|
});
|
|
continue;
|
|
}
|
|
inputs.push(
|
|
Object.freeze({
|
|
recipeId,
|
|
sourceRoots: Object.freeze([...sourceRoots]),
|
|
bundleBudgetGzipBytes: row.bundleBudgetGzipBytes as number,
|
|
}),
|
|
);
|
|
}
|
|
return Object.freeze({
|
|
inputs: Object.freeze(
|
|
inputs.sort((left, right) =>
|
|
compareText(left.recipeId, right.recipeId),
|
|
),
|
|
),
|
|
violations: Object.freeze(violations),
|
|
});
|
|
}
|
|
|
|
function safeErrorSummary(error: unknown): string {
|
|
const message =
|
|
error instanceof Error ? error.message : "unknown measurement failure";
|
|
return message
|
|
.split(/\r?\n/, 1)[0]
|
|
?.replaceAll(process.cwd(), ".")
|
|
.slice(0, 512) ?? "unknown measurement failure";
|
|
}
|
|
|
|
function compareText(left: string, right: string): number {
|
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
}
|