Files
pikasTech-unidesk/scripts/src/code-queue-source-guard.ts
T

162 lines
5.4 KiB
TypeScript

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<string>();
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 <repo-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;
}