Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/scripts/verify-runtime-closures.mjs
T

221 lines
8.3 KiB
JavaScript

import fs from "node:fs";
import { builtinModules } from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { build as analyzeBuild } from "esbuild";
const BUILTIN_MODULES = new Set([...builtinModules, ...builtinModules.map((name) => `node:${name}`)]);
const OPAQUE_REQUIRE_PATTERN = /\brequireFromHere\(\s*["'`]([^"'`]+)["'`]\s*\)/gu;
// A dynamic import whose specifier is held in a local string variable is opaque to esbuild, so the two
// halves are matched separately and paired by position afterwards. Spanning both halves in one pattern
// (declaration, then a lazy `[\s\S]{0,500}?` gap, then the import) overflows the regex stack on a bundled
// server artifact, where a single generated line can run into the megabytes.
const OPAQUE_STRING_BINDING_PATTERN = /\b(?:const|let|var)\s+([$_A-Za-z][\w$]*)\s*=\s*["'`]([^"'`]+)["'`]\s*;/gu;
// The comment body uses an unrolled loop rather than a lazy span so it cannot backtrack across the file.
const OPAQUE_DYNAMIC_IMPORT_PATTERN = /\bimport\(\s*(?:\/\*[^*]*(?:\*(?!\/)[^*]*)*\*\/\s*)?([$_A-Za-z][\w$]*)\s*\)/gu;
/** How far after its declaration the variable may be imported and still count as the same binding. */
const OPAQUE_BINDING_MAX_DISTANCE = 500;
export const RUNTIME_CLOSURE_TARGETS = {
web: {
entryPoints: [
"build/server-runtime/server.js",
"apps/web/build/server/index.js",
"apps/web/build-portal/server/index.js",
"packages/db/dist/deploy-permission-catalog-sync.server.js",
],
manifestPath: "runtime/web/package.json",
},
api: {
entryPoints: ["apps/api/dist/server.js"],
manifestPath: "runtime/api/package.json",
},
worker: {
entryPoints: [
"apps/worker/dist/index.js",
"apps/worker/dist/health.js",
"apps/worker/dist/jobs/plugin-sandbox-worker.js",
"apps/worker/dist/jobs/file-preview-decoder-process.js",
],
manifestPath: "runtime/worker/package.json",
},
mcp: {
entryPoints: ["apps/mcp/dist/server.js"],
manifestPath: "runtime/mcp/package.json",
},
hocuspocus: {
entryPoints: ["apps/hocuspocus/dist/index.js"],
manifestPath: "runtime/hocuspocus/package.json",
},
"portal-domain-controller": {
entryPoints: ["apps/portal-domain-controller/dist/index.js"],
manifestPath: "runtime/portal-domain-controller/package.json",
},
};
export function packageNameFromSpecifier(specifier) {
if (!specifier || specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("#") || BUILTIN_MODULES.has(specifier)) {
return null;
}
const segments = specifier.split("/");
return specifier.startsWith("@") ? segments.slice(0, 2).join("/") : segments[0];
}
export async function collectRuntimePackageImports(rootDir, entryPoints) {
const packageNames = new Set();
const absoluteEntryPoints = entryPoints.map((entryPoint) => path.join(rootDir, entryPoint));
for (const [index, entryPoint] of absoluteEntryPoints.entries()) {
if (!fs.existsSync(entryPoint)) {
throw new Error(`missing runtime artifact entry point: ${entryPoints[index]}`);
}
}
const analysis = await analyzeBuild({
absWorkingDir: rootDir,
bundle: true,
entryPoints: absoluteEntryPoints,
format: "esm",
logLevel: "silent",
metafile: true,
outdir: path.join(rootDir, ".runtime-closure-analysis"),
packages: "external",
platform: "node",
write: false,
});
for (const input of Object.values(analysis.metafile.inputs)) {
for (const imported of input.imports) {
const packageName = packageNameFromSpecifier(imported.path);
if (imported.external && packageName) packageNames.add(packageName);
}
}
for (const inputPath of Object.keys(analysis.metafile.inputs)) {
const absoluteInputPath = path.isAbsolute(inputPath) ? inputPath : path.join(rootDir, inputPath);
if (!fs.existsSync(absoluteInputPath) || !/\.[cm]?js$/u.test(absoluteInputPath)) continue;
const source = fs.readFileSync(absoluteInputPath, "utf8");
for (const specifier of collectOpaquePackageSpecifiers(source)) {
const packageName = packageNameFromSpecifier(specifier);
if (packageName) packageNames.add(packageName);
}
}
return [...packageNames].sort();
}
export function collectOpaquePackageSpecifiers(source) {
const specifiers = [];
for (const match of source.matchAll(OPAQUE_REQUIRE_PATTERN)) {
if (match[1]) specifiers.push(match[1]);
}
// Declarations first, keyed by name and kept in source order, so each import can be paired with the
// nearest preceding one. Re-assigning the same name later shadows the earlier value, as it would at runtime.
const bindings = new Map();
for (const match of source.matchAll(OPAQUE_STRING_BINDING_PATTERN)) {
if (!match[1] || !match[2]) continue;
const declarations = bindings.get(match[1]) ?? [];
declarations.push({ specifier: match[2], endIndex: match.index + match[0].length });
bindings.set(match[1], declarations);
}
for (const match of source.matchAll(OPAQUE_DYNAMIC_IMPORT_PATTERN)) {
const declarations = bindings.get(match[1]);
if (!declarations) continue;
const declaration = lastDeclarationWithinReach(declarations, match.index);
if (declaration) specifiers.push(declaration.specifier);
}
return specifiers;
}
function lastDeclarationWithinReach(declarations, importIndex) {
for (let index = declarations.length - 1; index >= 0; index -= 1) {
const declaration = declarations[index];
if (declaration.endIndex > importIndex) continue;
return importIndex - declaration.endIndex <= OPAQUE_BINDING_MAX_DISTANCE ? declaration : null;
}
return null;
}
export async function verifyRuntimeClosure(rootDir, targetName, targetConfig = RUNTIME_CLOSURE_TARGETS[targetName]) {
if (!targetConfig) throw new Error(`unknown runtime closure target: ${targetName}`);
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, targetConfig.manifestPath), "utf8"));
const declaredDependencies = Object.keys(manifest.dependencies ?? {}).sort();
const importedPackages = await collectRuntimePackageImports(rootDir, targetConfig.entryPoints);
const missingDependencies = importedPackages.filter((packageName) => !declaredDependencies.includes(packageName));
return {
targetName,
manifestPath: targetConfig.manifestPath,
declaredDependencies,
importedPackages,
missingDependencies,
};
}
export function parseRuntimeSelection(args) {
const selected = [];
for (let index = 0; index < args.length; index += 1) {
const argument = args[index];
let targetName;
if (argument === "--") {
continue;
} else if (argument === "--runtime") {
targetName = args[index + 1];
index += 1;
} else if (argument.startsWith("--runtime=")) {
targetName = argument.slice("--runtime=".length);
} else {
throw new Error(`unknown runtime closure argument: ${argument}`);
}
if (!targetName) throw new Error("--runtime requires a target name");
if (!(targetName in RUNTIME_CLOSURE_TARGETS)) {
throw new Error(`unknown runtime closure target: ${targetName}`);
}
if (!selected.includes(targetName)) selected.push(targetName);
}
return selected.length > 0 ? selected : Object.keys(RUNTIME_CLOSURE_TARGETS);
}
export async function verifyRuntimeClosures(
rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."),
targetNames = Object.keys(RUNTIME_CLOSURE_TARGETS)
) {
return Promise.all(targetNames.map((targetName) => verifyRuntimeClosure(rootDir, targetName)));
}
async function runCli() {
let targetNames;
try {
targetNames = parseRuntimeSelection(process.argv.slice(2));
} catch (error) {
console.error(`FAIL ${(error instanceof Error && error.message) || String(error)}`);
process.exitCode = 1;
return;
}
const results = await verifyRuntimeClosures(undefined, targetNames);
let failed = false;
for (const result of results) {
if (result.missingDependencies.length > 0) {
failed = true;
console.error(`FAIL runtime=${result.targetName} missing=${result.missingDependencies.join(",")}`);
} else {
console.log(`PASS runtime=${result.targetName} imports=${result.importedPackages.length} dependencies=${result.declaredDependencies.length}`);
}
}
if (failed) process.exitCode = 1;
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await runCli();