442 lines
14 KiB
TypeScript
442 lines
14 KiB
TypeScript
import { readFile, readdir } from "node:fs/promises";
|
|
import path from "node:path";
|
|
|
|
export const REQUIRED_RECIPE_IDS = Object.freeze([
|
|
"analytics-error-sink",
|
|
"browser-permission",
|
|
"client-workflow",
|
|
"feature-flag",
|
|
"file-transfer",
|
|
"generated-api",
|
|
"large-data-ui",
|
|
"multi-tab",
|
|
"offline-indexeddb",
|
|
"realtime",
|
|
"service-worker-pwa",
|
|
"web-worker",
|
|
] as const);
|
|
|
|
const lifecycleRecipes: ReadonlySet<string> = new Set([
|
|
"analytics-error-sink",
|
|
"browser-permission",
|
|
"client-workflow",
|
|
"file-transfer",
|
|
"generated-api",
|
|
"multi-tab",
|
|
"offline-indexeddb",
|
|
"realtime",
|
|
"service-worker-pwa",
|
|
"web-worker",
|
|
]);
|
|
|
|
type Document = Readonly<Record<string, unknown>>;
|
|
export type OptionalRecipeSourceViolation = Readonly<{
|
|
ruleId: string;
|
|
path: string;
|
|
}>;
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
}
|
|
|
|
function recordValue(value: unknown): Record<string, unknown> {
|
|
return isRecord(value) ? value : {};
|
|
}
|
|
|
|
function recordRows(value: unknown): Record<string, unknown>[] {
|
|
return Array.isArray(value) ? value.filter(isRecord) : [];
|
|
}
|
|
|
|
function nonEmptyStrings(value: unknown): value is string[] {
|
|
return (
|
|
Array.isArray(value) &&
|
|
value.length > 0 &&
|
|
value.every(
|
|
(entry): entry is string =>
|
|
typeof entry === "string" && entry.trim().length > 0,
|
|
)
|
|
);
|
|
}
|
|
|
|
function packageVersions(value: unknown): Record<string, string> {
|
|
return Object.fromEntries(
|
|
Object.entries(recordValue(value)).filter(
|
|
(entry): entry is [string, string] => typeof entry[1] === "string",
|
|
),
|
|
);
|
|
}
|
|
|
|
export function validateRecipeCatalog(
|
|
input: unknown,
|
|
packageDocument: Document,
|
|
): string[] {
|
|
const document = recordValue(input);
|
|
const violations: string[] = [];
|
|
if (document.schemaVersion !== 1) violations.push("CATALOG_SCHEMA_VERSION");
|
|
if (document.decisionId !== "VD-10") violations.push("CATALOG_DECISION");
|
|
if (document.defaultStatus !== "NOT_INSTALLED") {
|
|
violations.push("CATALOG_DEFAULT_MUST_BE_NOT_INSTALLED");
|
|
}
|
|
if (
|
|
!Array.isArray(document.productionRuntimeDependencies) ||
|
|
document.productionRuntimeDependencies.length > 0
|
|
) {
|
|
violations.push("UNSELECTED_RUNTIME_DEPENDENCY");
|
|
}
|
|
if (!nonEmptyStrings(document.vendorPackagePatterns)) {
|
|
violations.push("VENDOR_PATTERN_CATALOG");
|
|
}
|
|
if (!Array.isArray(document.recipes)) {
|
|
return [...violations, "RECIPE_CATALOG_MISSING"];
|
|
}
|
|
|
|
const recipes = recordRows(document.recipes);
|
|
const actualIds = recipes.map((recipe) => String(recipe.id ?? "")).sort();
|
|
if (JSON.stringify(actualIds) !== JSON.stringify(REQUIRED_RECIPE_IDS)) {
|
|
violations.push("RECIPE_ID_SET");
|
|
}
|
|
if (new Set(actualIds).size !== actualIds.length) {
|
|
violations.push("RECIPE_ID_DUPLICATE");
|
|
}
|
|
|
|
for (const recipe of recipes) {
|
|
const id = typeof recipe.id === "string" ? recipe.id : "unknown";
|
|
if (recipe.status !== "RECIPE_AVAILABLE") {
|
|
violations.push(`${id}:STATUS_MUST_NOT_CLAIM_INSTALLED`);
|
|
}
|
|
for (const field of [
|
|
"trigger",
|
|
"boundary",
|
|
"port",
|
|
"fake",
|
|
"owner",
|
|
"fallback",
|
|
"serverStatePolicy",
|
|
] as const) {
|
|
const value = recipe[field];
|
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
violations.push(`${id}:MISSING_${field.toUpperCase()}`);
|
|
}
|
|
}
|
|
for (const field of [
|
|
"forbiddenWhen",
|
|
"failureKinds",
|
|
"securityPrivacy",
|
|
"removal",
|
|
] as const) {
|
|
if (!nonEmptyStrings(recipe[field])) {
|
|
violations.push(`${id}:MISSING_${field.toUpperCase()}`);
|
|
}
|
|
}
|
|
if (
|
|
typeof recipe.bundleBudgetGzipBytes !== "number" ||
|
|
!Number.isInteger(recipe.bundleBudgetGzipBytes) ||
|
|
recipe.bundleBudgetGzipBytes < 1
|
|
) {
|
|
violations.push(`${id}:INVALID_BUNDLE_BUDGET`);
|
|
}
|
|
if (recipe.owner === "frontend-platform") {
|
|
violations.push(`${id}:PROJECT_OWNER_NOT_ASSIGNED`);
|
|
}
|
|
if (lifecycleRecipes.has(id) && !nonEmptyStrings(recipe.lifecycleMethods)) {
|
|
violations.push(`${id}:CLEANUP_CONTRACT_MISSING`);
|
|
}
|
|
if (
|
|
id === "client-workflow" &&
|
|
recipe.serverStatePolicy !== "reference-only"
|
|
) {
|
|
violations.push(`${id}:SERVER_STATE_DUPLICATION_POLICY`);
|
|
}
|
|
}
|
|
|
|
const dependencies = {
|
|
...packageVersions(packageDocument.dependencies),
|
|
...packageVersions(packageDocument.devDependencies),
|
|
};
|
|
const packageScripts = packageVersions(packageDocument.scripts);
|
|
for (const recipe of recipes) {
|
|
if (recipe.referenceRuntime === undefined) continue;
|
|
const runtime = recordValue(recipe.referenceRuntime);
|
|
const id = typeof recipe.id === "string" ? recipe.id : "unknown";
|
|
if (
|
|
runtime.status !== "AVAILABLE_NOT_COMPOSED" ||
|
|
runtime.productionComposition !== false
|
|
) {
|
|
violations.push(`${id}:REFERENCE_RUNTIME_COMPOSITION`);
|
|
}
|
|
if (!nonEmptyStrings(runtime.sourceRoots)) {
|
|
violations.push(`${id}:REFERENCE_RUNTIME_SOURCE_ROOTS`);
|
|
} else if (
|
|
runtime.sourceRoots.some(
|
|
(sourceRoot) =>
|
|
!sourceRoot.startsWith("src/") ||
|
|
sourceRoot.includes("\\") ||
|
|
sourceRoot.split("/").includes(".."),
|
|
)
|
|
) {
|
|
violations.push(`${id}:REFERENCE_RUNTIME_SOURCE_BOUNDARY`);
|
|
}
|
|
if (!nonEmptyStrings(runtime.coveredCapabilities)) {
|
|
violations.push(`${id}:REFERENCE_RUNTIME_CAPABILITIES`);
|
|
}
|
|
if (!nonEmptyStrings(runtime.conformanceScripts)) {
|
|
violations.push(`${id}:REFERENCE_RUNTIME_CONFORMANCE`);
|
|
} else {
|
|
for (const script of runtime.conformanceScripts) {
|
|
if (!(script in packageScripts)) {
|
|
violations.push(`${id}:UNKNOWN_CONFORMANCE_SCRIPT:${script}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
const vendorPatterns = Array.isArray(document.vendorPackagePatterns)
|
|
? document.vendorPackagePatterns.filter(
|
|
(entry): entry is string => typeof entry === "string",
|
|
)
|
|
: [];
|
|
for (const pattern of vendorPatterns) {
|
|
const wildcard = pattern.endsWith("*");
|
|
const prefix = pattern.replace(/\/?\*$/, "");
|
|
if (
|
|
Object.keys(dependencies).some(
|
|
(dependency) =>
|
|
dependency === prefix ||
|
|
dependency.startsWith(`${prefix}/`) ||
|
|
(wildcard && dependency.startsWith(prefix)),
|
|
)
|
|
) {
|
|
violations.push(`UNSELECTED_VENDOR_INSTALLED:${prefix}`);
|
|
}
|
|
}
|
|
return violations;
|
|
}
|
|
|
|
export async function sourceFiles(directory: string): Promise<string[]> {
|
|
let entries;
|
|
try {
|
|
entries = await readdir(directory, { withFileTypes: true });
|
|
} catch (error: unknown) {
|
|
if (
|
|
isRecord(error) &&
|
|
"code" in error &&
|
|
error.code === "ENOENT"
|
|
) {
|
|
return [];
|
|
}
|
|
throw error;
|
|
}
|
|
const groups: string[][] = await Promise.all(
|
|
entries.map((entry) => {
|
|
const target = path.join(directory, entry.name);
|
|
return entry.isDirectory()
|
|
? sourceFiles(target)
|
|
: /\.(?:[cm]?[jt]s|[jt]sx)$/.test(entry.name)
|
|
? [target]
|
|
: [];
|
|
}),
|
|
);
|
|
return groups.flat();
|
|
}
|
|
|
|
export async function scanOptionalRecipeSources(
|
|
root: string,
|
|
{ scanProductionBoundary = true }: Readonly<{
|
|
scanProductionBoundary?: boolean;
|
|
}> = {},
|
|
): Promise<OptionalRecipeSourceViolation[]> {
|
|
const violations: OptionalRecipeSourceViolation[] = [];
|
|
for (const file of await sourceFiles(root)) {
|
|
const relative = path.relative(process.cwd(), file).replaceAll("\\", "/");
|
|
const relativeToRoot = path.relative(root, file).replaceAll("\\", "/");
|
|
const content = await readFile(file, "utf8");
|
|
const imports = [
|
|
...content.matchAll(/(?:from\s*|import\s*\(\s*)["']([^"']+)["']/g),
|
|
]
|
|
.map((match) => match[1])
|
|
.filter((specifier): specifier is string => specifier !== undefined);
|
|
|
|
if (
|
|
scanProductionBoundary &&
|
|
(relativeToRoot.startsWith("src/") ||
|
|
(path.basename(path.resolve(root)) === "src" &&
|
|
!relativeToRoot.startsWith(".."))) &&
|
|
imports.some((specifier) =>
|
|
/(?:^|\/)recipes\/frontend-capabilities(?:\/|$)/.test(specifier),
|
|
)
|
|
) {
|
|
violations.push({ ruleId: "PRODUCTION_IMPORTS_RECIPE", path: relative });
|
|
}
|
|
|
|
const productionRelative =
|
|
path.basename(path.resolve(root)) === "src"
|
|
? `src/${relativeToRoot}`
|
|
: relativeToRoot;
|
|
const isCompositionSource =
|
|
/(?:^|\/)src\/bootstrap\//.test(productionRelative) ||
|
|
/(?:^|\/)src\/features\/installed-feature-(?:adapters|runtimes)\./.test(
|
|
productionRelative,
|
|
);
|
|
if (
|
|
scanProductionBoundary &&
|
|
isCompositionSource &&
|
|
(imports.some((specifier) =>
|
|
/(?:^|\/)adapters\/(?:browser-file-storage|browser-files|browser-transfer|cache-storage|storage\/(?:indexeddb|opfs))(?:\/|$)/.test(
|
|
specifier,
|
|
),
|
|
) ||
|
|
/["'][^"'\r\n]*(?:^|\/)adapters\/(?:browser-file-storage|browser-files|browser-transfer|cache-storage|storage\/(?:indexeddb|opfs))(?:\/|["'])/u.test(
|
|
content,
|
|
))
|
|
) {
|
|
violations.push({
|
|
ruleId: "REFERENCE_RUNTIME_COMPOSED_WITHOUT_SELECTION",
|
|
path: relative,
|
|
});
|
|
}
|
|
|
|
const localVendorAdapter =
|
|
relative.includes("recipes/") && relative.includes("/adapters/");
|
|
if (
|
|
!localVendorAdapter &&
|
|
imports.some((specifier) =>
|
|
/^(?:@launchdarkly\/|@sentry\/|@opentelemetry\/|@openapitools\/openapi-generator-cli$|@reduxjs\/toolkit$|@tanstack\/react-virtual$|@uppy\/|firebase(?:\/|$)|idb$|react-window$|redux(?:\/|$)|socket\.io-client$|tus-js-client$|workbox-window$|xstate$|zustand$)/.test(
|
|
specifier,
|
|
),
|
|
)
|
|
) {
|
|
violations.push({ ruleId: "VENDOR_IMPORT_OUTSIDE_ADAPTER", path: relative });
|
|
}
|
|
|
|
if (
|
|
/localStorage\s*\.\s*(?:setItem|getItem)\s*\([^)]*(?:credential|password|secret|token)/is.test(
|
|
content,
|
|
) ||
|
|
/searchParams\s*\.\s*set\s*\(\s*["'](?:credential|password|secret|token)/is.test(
|
|
content,
|
|
) ||
|
|
/(?:record|track|emit)\s*\(\s*\{[\s\S]{0,400}(?:credential|password|secret|token)\s*:/i.test(
|
|
content,
|
|
)
|
|
) {
|
|
violations.push({ ruleId: "CREDENTIAL_LEAK_PATH", path: relative });
|
|
}
|
|
|
|
if (
|
|
/(?:createStore|configureStore|create\s*\()\s*\([\s\S]{0,600}(?:apiResponse|queryData|serverState)\s*:/i.test(
|
|
content,
|
|
)
|
|
) {
|
|
violations.push({
|
|
ruleId: "CLIENT_STORE_DUPLICATES_SERVER_STATE",
|
|
path: relative,
|
|
});
|
|
}
|
|
}
|
|
return violations;
|
|
}
|
|
|
|
export async function scanProductionBundle(
|
|
distRoot: string,
|
|
): Promise<string[]> {
|
|
const violations: string[] = [];
|
|
const forbiddenRuntimeMarkers = [
|
|
"frontend-optional-recipe-must-not-reach-production",
|
|
"Browser file runtime hard limits are invalid.",
|
|
"Object URL allocation failed",
|
|
"Storage pressure policy is invalid.",
|
|
"IndexedDB runtime configuration is invalid.",
|
|
"Invalid IndexedDB schema migration.",
|
|
"OPFS runtime policy is invalid.",
|
|
"OPFS operation failed.",
|
|
"Public Cache Storage policy is invalid.",
|
|
"Public cache validation failed.",
|
|
"Presigned capability vault limit is invalid.",
|
|
"Resumable upload policy is invalid.",
|
|
"Image CDN policy registry is invalid.",
|
|
] as const;
|
|
for (const file of await sourceFiles(distRoot)) {
|
|
const content = await readFile(file, "utf8");
|
|
if (forbiddenRuntimeMarkers.some((marker) => content.includes(marker))) {
|
|
violations.push(path.relative(process.cwd(), file));
|
|
}
|
|
}
|
|
|
|
const viteManifestPath = path.join(distRoot, ".vite/manifest.json");
|
|
const emittedModuleInventoryPath = path.join(
|
|
distRoot,
|
|
".vite/module-inventory.json",
|
|
);
|
|
const moduleInventoryCandidates = [
|
|
emittedModuleInventoryPath,
|
|
...(path.resolve(distRoot) === path.resolve("dist")
|
|
? ["artifacts/quality/vite-module-inventory.json"]
|
|
: []),
|
|
];
|
|
const viteManifestExists = await readFile(viteManifestPath, "utf8")
|
|
.then(() => true)
|
|
.catch((error: unknown) => {
|
|
if (isRecord(error) && error.code === "ENOENT") return false;
|
|
throw error;
|
|
});
|
|
if (!viteManifestExists) return [...new Set(violations)];
|
|
|
|
let inventory: unknown;
|
|
let moduleInventoryPath = emittedModuleInventoryPath;
|
|
for (const candidate of moduleInventoryCandidates) {
|
|
try {
|
|
inventory = JSON.parse(await readFile(candidate, "utf8"));
|
|
moduleInventoryPath = candidate;
|
|
break;
|
|
} catch {
|
|
// A generated build may move the inventory out of the deploy directory.
|
|
}
|
|
}
|
|
if (inventory === undefined) {
|
|
violations.push(
|
|
path.relative(process.cwd(), moduleInventoryPath),
|
|
);
|
|
return [...new Set(violations)];
|
|
}
|
|
const inventoryDocument = recordValue(inventory);
|
|
const chunks = recordRows(inventoryDocument.chunks);
|
|
if (
|
|
inventoryDocument.schemaVersion !== 1 ||
|
|
!Array.isArray(inventoryDocument.chunks) ||
|
|
chunks.length !== inventoryDocument.chunks.length
|
|
) {
|
|
violations.push(path.relative(process.cwd(), moduleInventoryPath));
|
|
return [...new Set(violations)];
|
|
}
|
|
|
|
const forbiddenSourcePrefixes = [
|
|
"src/application/ports/browser-file-storage/",
|
|
"src/application/ports/browser-transfer/",
|
|
"src/adapters/browser-file-storage/",
|
|
"src/adapters/browser-files/",
|
|
"src/adapters/browser-transfer/",
|
|
"src/adapters/cache-storage/",
|
|
"src/adapters/storage/indexeddb/",
|
|
"src/adapters/storage/opfs/",
|
|
] as const;
|
|
for (const chunk of chunks) {
|
|
if (
|
|
typeof chunk.fileName !== "string" ||
|
|
!Array.isArray(chunk.modules) ||
|
|
chunk.modules.some((moduleId) => typeof moduleId !== "string")
|
|
) {
|
|
violations.push(path.relative(process.cwd(), moduleInventoryPath));
|
|
continue;
|
|
}
|
|
for (const moduleId of chunk.modules as string[]) {
|
|
if (
|
|
forbiddenSourcePrefixes.some((prefix) =>
|
|
moduleId.startsWith(prefix),
|
|
)
|
|
) {
|
|
violations.push(`${chunk.fileName}:${moduleId}`);
|
|
}
|
|
}
|
|
}
|
|
return [...new Set(violations)];
|
|
}
|