import { promises as fs } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import ts from "typescript"; const baseTestSuffixes = [".test.ts", ".test-utils.ts", ".test-harness.ts", ".e2e-harness.ts"]; export function resolveRepoRoot(importMetaUrl) { return path.resolve(path.dirname(fileURLToPath(importMetaUrl)), "..", ".."); } export function resolveSourceRoots(repoRoot, relativeRoots) { return relativeRoots.map((root) => path.join(repoRoot, ...root.split("/").filter(Boolean))); } export function isTestLikeTypeScriptFile(filePath, options = {}) { const extraTestSuffixes = options.extraTestSuffixes ?? []; return [...baseTestSuffixes, ...extraTestSuffixes].some((suffix) => filePath.endsWith(suffix)); } export async function collectTypeScriptFiles(targetPath, options = {}) { const includeTests = options.includeTests ?? false; const extraTestSuffixes = options.extraTestSuffixes ?? []; const skipNodeModules = options.skipNodeModules ?? true; const ignoreMissing = options.ignoreMissing ?? false; let stat; try { stat = await fs.stat(targetPath); } catch (error) { if ( ignoreMissing && error && typeof error === "object" && "code" in error && error.code === "ENOENT" ) { return []; } throw error; } if (stat.isFile()) { if (!targetPath.endsWith(".ts")) { return []; } if (!includeTests && isTestLikeTypeScriptFile(targetPath, { extraTestSuffixes })) { return []; } return [targetPath]; } const entries = await fs.readdir(targetPath, { withFileTypes: true }); const out = []; for (const entry of entries) { const entryPath = path.join(targetPath, entry.name); if (entry.isDirectory()) { if (skipNodeModules && entry.name === "node_modules") { continue; } out.push(...(await collectTypeScriptFiles(entryPath, options))); continue; } if (!entry.isFile() || !entryPath.endsWith(".ts")) { continue; } if (!includeTests && isTestLikeTypeScriptFile(entryPath, { extraTestSuffixes })) { continue; } out.push(entryPath); } return out; } export async function collectTypeScriptFilesFromRoots(sourceRoots, options = {}) { return ( await Promise.all( sourceRoots.map( async (root) => await collectTypeScriptFiles(root, { ignoreMissing: true, ...options, }), ), ) ).flat(); } export async function collectFileViolations(params) { const files = await collectTypeScriptFilesFromRoots(params.sourceRoots, { extraTestSuffixes: params.extraTestSuffixes, }); const violations = []; for (const filePath of files) { if (params.skipFile?.(filePath)) { continue; } const content = await fs.readFile(filePath, "utf8"); const fileViolations = params.findViolations(content, filePath); for (const violation of fileViolations) { violations.push({ path: path.relative(params.repoRoot, filePath), ...violation, }); } } return violations; } export function toLine(sourceFile, node) { return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1; } export function getPropertyNameText(name) { if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) { return name.text; } return null; } export function unwrapExpression(expression) { let current = expression; while (true) { if (ts.isParenthesizedExpression(current)) { current = current.expression; continue; } if (ts.isAsExpression(current) || ts.isTypeAssertionExpression(current)) { current = current.expression; continue; } if (ts.isNonNullExpression(current)) { current = current.expression; continue; } return current; } } export function isDirectExecution(importMetaUrl) { const entry = process.argv[1]; if (!entry) { return false; } return path.resolve(entry) === fileURLToPath(importMetaUrl); } export function runAsScript(importMetaUrl, main) { if (!isDirectExecution(importMetaUrl)) { return; } main().catch((error) => { console.error(error); process.exit(1); }); }