Files
clean-architecture-frontend…/scripts/lib/package-script-graph.ts
T

55 lines
1.8 KiB
TypeScript

const packageScriptInvocation = /\b(?:(?:corepack\s+)?pnpm(?:\s+--?[A-Za-z][A-Za-z-]*(?:=[^\s;&|]+)?)*(?:\s+run)?|npm\s+run)\s+([A-Za-z0-9:_-]+)/gu;
const pnpmNonScriptCommands = new Set(["dlx", "exec", "install"]);
export function validatePackageScriptGraph(
scripts: Readonly<Record<string, string>>,
entryScript: string,
): string[] {
const failures: string[] = [];
const visiting = new Set<string>();
const visited = new Set<string>();
const stack: string[] = [];
const visit = (scriptName: string): void => {
if (visiting.has(scriptName)) {
const start = stack.indexOf(scriptName);
failures.push(`package script cycle: ${[...stack.slice(start), scriptName].join(" -> ")}`);
return;
}
if (visited.has(scriptName)) return;
const command = scripts[scriptName];
if (command === undefined) {
failures.push(`package script missing: ${scriptName}`);
return;
}
visiting.add(scriptName);
stack.push(scriptName);
if (/\bscripts\/run-ci-gate(?:\.[cm]?[jt]s)?\b/u.test(command)) {
failures.push(`${scriptName} must not invoke the CI gate runner`);
}
if (/\bci:gate\b/u.test(command)) {
failures.push(`${scriptName} must not invoke ci:gate`);
}
packageScriptInvocation.lastIndex = 0;
const dependencies = Array.from(
command.matchAll(packageScriptInvocation),
(match) => match[1]!,
).filter((dependency) => !pnpmNonScriptCommands.has(dependency));
for (const dependency of dependencies) {
if (dependency !== "ci:gate") {
if (!(dependency in scripts)) {
failures.push(`package script missing: ${scriptName} -> ${dependency}`);
} else {
visit(dependency);
}
}
}
stack.pop();
visiting.delete(scriptName);
visited.add(scriptName);
};
visit(entryScript);
return [...new Set(failures)];
}