fix: guard code queue hostpath source rollout
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user