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", ]); const lifecycleRecipes = new Set([ "analytics-error-sink", "browser-permission", "client-workflow", "file-transfer", "generated-api", "multi-tab", "offline-indexeddb", "realtime", "service-worker-pwa", "web-worker", ]); /** @param {unknown} value */ function nonEmptyStrings(value) { return ( Array.isArray(value) && value.length > 0 && value.every((entry) => typeof entry === "string" && entry.trim().length > 0) ); } /** * @param {unknown} input * @param {Readonly>} packageDocument * @returns {string[]} */ export function validateRecipeCatalog(input, packageDocument) { const document = /** @type {Record} */ ( input && typeof input === "object" ? input : {} ); /** @type {string[]} */ const violations = []; 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 actualIds = document.recipes .map(/** @param {Record} recipe */ (recipe) => 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 document.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", ]) { if (typeof recipe[field] !== "string" || recipe[field].trim().length === 0) { violations.push(`${id}:MISSING_${field.toUpperCase()}`); } } for (const field of [ "forbiddenWhen", "failureKinds", "securityPrivacy", "removal", ]) { if (!nonEmptyStrings(recipe[field])) { violations.push(`${id}:MISSING_${field.toUpperCase()}`); } } if ( !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 = { .../** @type {Record} */ (packageDocument.dependencies ?? {}), .../** @type {Record} */ ( packageDocument.devDependencies ?? {} ), }; for (const pattern of document.vendorPackagePatterns ?? []) { const wildcard = String(pattern).endsWith("*"); const prefix = String(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; } /** @param {string} directory @returns {Promise} */ export async function sourceFiles(directory) { let entries; try { entries = await readdir(directory, { withFileTypes: true }); } catch (error) { if ( error && typeof error === "object" && "code" in error && error.code === "ENOENT" ) { return []; } throw error; } const groups = await Promise.all( entries.map((entry) => { const target = path.join(directory, entry.name); return entry.isDirectory() ? sourceFiles(target) : /\.(?:js|jsx|mjs|ts|tsx|mts)$/.test(entry.name) ? [target] : []; }), ); return groups.flat(); } /** * @param {string} root * @param {{scanProductionBoundary?: boolean}} [options] */ export async function scanOptionalRecipeSources( root, { scanProductionBoundary = true } = {}, ) { /** @type {Array<{ruleId: string; path: string}>} */ const violations = []; 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]); 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 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; } /** @param {string} distRoot */ export async function scanProductionBundle(distRoot) { /** @type {string[]} */ const violations = []; for (const file of await sourceFiles(distRoot)) { const content = await readFile(file, "utf8"); if (content.includes("frontend-optional-recipe-must-not-reach-production")) { violations.push(path.relative(process.cwd(), file)); } } return violations; }