55 lines
1.8 KiB
TypeScript
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)];
|
|
}
|