// fallow-ignore-file complexity import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; import { join, relative } from "node:path"; import { fileURLToPath } from "node:url"; import ts from "typescript"; const ROOT = join(import.meta.dirname, ".."); function at(sourceFile, node) { const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)); return `${sourceFile.fileName}:${position.line + 1}:${position.character + 1}`; } export function listDirectProcessTermination(source, filename = "source.ts") { const sourceFile = ts.createSourceFile(filename, source, ts.ScriptTarget.Latest, true); const issues = []; // AST traversal mirrors the small set of process-termination spellings. // fallow-ignore-next-line complexity function visit(node) { if ( ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "process" && (node.name.text === "exit" || node.name.text === "exitCode") ) { issues.push(`${at(sourceFile, node)} uses process.${node.name.text}`); } ts.forEachChild(node, visit); } visit(sourceFile); return issues; } // A bare close can wait out Puppeteer's protocol timeout; closeManagedBrowser is bounded. export function listUnmanagedTestBrowsers(source, filename = "source.test.ts") { const sourceFile = ts.createSourceFile(filename, source, ts.ScriptTarget.Latest, true); const issues = []; function visit(node) { if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) { const method = node.expression.name.text; const receiver = node.expression.expression.getText(sourceFile); if ( (method === "close" && /browser$/i.test(receiver)) || (method === "launch" && /^puppeteer(\.default)?$/.test(receiver)) ) { issues.push( `${at(sourceFile, node)} calls ${receiver}.${method}(); use launchManagedBrowser/closeManagedBrowser`, ); } } ts.forEachChild(node, visit); } visit(sourceFile); return issues; } // Each is the root of its own process: the CLI, the warm check process, and the check client in bin. const PROCESS_ROOTS = ["cli.ts", "warmCheck.ts", "warmCheckClient.ts"]; function listTypeScriptFiles(directory) { return readdirSync(directory).flatMap((name) => { const path = join(directory, name); if (statSync(path).isDirectory()) return listTypeScriptFiles(path); return path.endsWith(".ts") ? [path] : []; }); } export function checkCliProcessOwnership(root = ROOT) { const sourceRoot = join(root, "packages/cli/src"); if (!existsSync(sourceRoot)) return []; const roots = PROCESS_ROOTS.map((name) => join(sourceRoot, name)); const issues = (path, list) => list(readFileSync(path, "utf8"), relative(root, path)); return listTypeScriptFiles(sourceRoot).flatMap((path) => { if (path.endsWith(".test.ts")) return issues(path, listUnmanagedTestBrowsers); if (path.endsWith(".fixture.ts") || roots.includes(path)) return []; return issues(path, listDirectProcessTermination); }); } function main() { const issues = checkCliProcessOwnership(); if (issues.length > 0) { console.error("CLI process ownership violations:"); issues.forEach((issue) => console.error(`- ${issue}`)); process.exitCode = 1; return; } console.log( "CLI process ownership verified: only process roots terminate the process, and tests own their browsers.", ); } if (process.argv[1] === fileURLToPath(import.meta.url)) main();