import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; import { dirname, join, relative, resolve } from "node:path"; export const codeQueueSourceSubdir = "src/components/microservices/code-queue/src"; const tsExtensions = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".json"] as const; export interface MissingRelativeImport { importer: string; specifier: string; expected: string[]; } export interface CodeQueueSourceImportPreflightResult { ok: boolean; guard: "code-queue-hostpath-source-imports"; root: string; sourceRoot: string; checkedFiles: number; checkedImports: number; missing: MissingRelativeImport[]; degradedReason: "none" | "source-root-missing" | "missing-relative-import-target"; message: string; } function normalizedRelative(from: string, to: string): string { return relative(from, to).split("\\").join("/"); } function walkTsFiles(dir: string): string[] { const result: string[] = []; for (const entry of readdirSync(dir, { withFileTypes: true })) { if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git" || entry.name === ".state") continue; const path = join(dir, entry.name); if (entry.isDirectory()) { result.push(...walkTsFiles(path)); continue; } if (entry.isFile() && /\.tsx?$/u.test(entry.name)) result.push(path); } return result.sort(); } function stripComments(text: string): string { return text .replace(/\/\*[\s\S]*?\*\//gu, "") .replace(/(^|[^:])\/\/.*$/gmu, "$1"); } function relativeImportSpecifiers(text: string): string[] { const cleaned = stripComments(text); const specifiers = new Set(); const patterns = [ /\b(?:import|export)\s+(?:type\s+)?(?:[\s\S]*?\s+from\s+)?["'](\.{1,2}\/[^"']+)["']/gu, /\bimport\s*\(\s*["'](\.{1,2}\/[^"']+)["']\s*\)/gu, ]; for (const pattern of patterns) { for (const match of cleaned.matchAll(pattern)) { const specifier = match[1]; if (specifier !== undefined) specifiers.add(specifier); } } return [...specifiers].sort(); } function importCandidates(importer: string, specifier: string): string[] { const base = resolve(dirname(importer), specifier); if (tsExtensions.some((extension) => base.endsWith(extension))) return [base]; return [ ...tsExtensions.map((extension) => `${base}${extension}`), ...tsExtensions.map((extension) => join(base, `index${extension}`)), ]; } function importTargetExists(candidates: string[]): boolean { return candidates.some((candidate) => { if (!existsSync(candidate)) return false; const stats = statSync(candidate); return stats.isFile(); }); } export function codeQueueSourceImportPreflight(rootDir: string): CodeQueueSourceImportPreflightResult { const root = resolve(rootDir); const sourceRoot = resolve(root, codeQueueSourceSubdir); if (!existsSync(sourceRoot) || !statSync(sourceRoot).isDirectory()) { return { ok: false, guard: "code-queue-hostpath-source-imports", root, sourceRoot, checkedFiles: 0, checkedImports: 0, missing: [], degradedReason: "source-root-missing", message: `Code Queue source root is missing: ${codeQueueSourceSubdir}`, }; } const files = walkTsFiles(sourceRoot); const missing: MissingRelativeImport[] = []; let checkedImports = 0; for (const file of files) { const text = readFileSync(file, "utf8"); for (const specifier of relativeImportSpecifiers(text)) { checkedImports += 1; const candidates = importCandidates(file, specifier); if (importTargetExists(candidates)) continue; missing.push({ importer: normalizedRelative(root, file), specifier, expected: candidates.map((candidate) => normalizedRelative(root, candidate)), }); } } return { ok: missing.length === 0, guard: "code-queue-hostpath-source-imports", root, sourceRoot, checkedFiles: files.length, checkedImports, missing, degradedReason: missing.length === 0 ? "none" : "missing-relative-import-target", message: missing.length === 0 ? `Code Queue hostPath source import preflight passed (${files.length} files, ${checkedImports} relative imports).` : `Code Queue hostPath source import preflight failed: ${missing.length} relative import target(s) are missing.`, }; } function optionValue(args: string[], name: string): string | null { const index = args.indexOf(name); if (index === -1) return null; const value = args[index + 1]; if (value === undefined || value.length === 0) throw new Error(`${name} requires a value`); return value; } export function runCodeQueueSourceGuardCli(args: string[]): CodeQueueSourceImportPreflightResult { if (args.includes("--help") || args.includes("-h")) { return { ok: true, guard: "code-queue-hostpath-source-imports", root: process.cwd(), sourceRoot: resolve(process.cwd(), codeQueueSourceSubdir), checkedFiles: 0, checkedImports: 0, missing: [], degradedReason: "none", message: "usage: bun scripts/code-queue-source-guard.ts --root ", }; } const root = optionValue(args, "--root") ?? process.cwd(); return codeQueueSourceImportPreflight(root); } export function emitCodeQueueSourceGuardCli(args: string[]): void { const result = runCodeQueueSourceGuardCli(args); process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); if (!result.ok) process.exitCode = 1; }