Move business logic out of App Router into module packages, add boundary validation scripts, and keep all routes as thin re-exports without changing URLs or API behavior. Co-authored-by: Cursor <cursoragent@cursor.com>
92 lines
3.1 KiB
JavaScript
92 lines
3.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/** Validate Accounting module import boundaries. */
|
|
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
import { join, relative, sep } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const ROOT = join(fileURLToPath(new URL(".", import.meta.url)), "..");
|
|
const EXT = new Set([".ts", ".tsx"]);
|
|
const importRegex = /from\s+["']([^"']+)["']/g;
|
|
|
|
const LEGACY = [
|
|
"@/components/accounting/",
|
|
"@/lib/accounting-api",
|
|
"@/lib/accounting-nav",
|
|
"@/lib/accounting-scoreboard",
|
|
];
|
|
|
|
const FORBIDDEN = [
|
|
"@/components/beauty/",
|
|
"@/components/healthcare/",
|
|
"@/modules/beauty/",
|
|
"@/src/modules/beauty/",
|
|
"@/src/modules/healthcare/",
|
|
"@/modules/hospitality/",
|
|
];
|
|
|
|
function walk(dir, files = []) {
|
|
for (const entry of readdirSync(dir)) {
|
|
if (entry === "node_modules" || entry === ".next") continue;
|
|
const full = join(dir, entry);
|
|
if (statSync(full).isDirectory()) walk(full, files);
|
|
else if (EXT.has(entry.slice(entry.lastIndexOf(".")))) files.push(full);
|
|
}
|
|
return files;
|
|
}
|
|
|
|
/** @type {Array<{file: string, importPath: string, reason: string}>} */
|
|
const violations = [];
|
|
|
|
for (const scope of ["app/accounting", "src/modules/accounting"]) {
|
|
const abs = join(ROOT, scope);
|
|
if (!statSync(abs, { throwIfNotFound: false })?.isDirectory()) continue;
|
|
for (const file of walk(abs)) {
|
|
const rel = relative(ROOT, file).split(sep).join("/");
|
|
const content = readFileSync(file, "utf8");
|
|
let m;
|
|
importRegex.lastIndex = 0;
|
|
while ((m = importRegex.exec(content)) !== null) {
|
|
const p = m[1];
|
|
for (const legacy of LEGACY) {
|
|
if (p.startsWith(legacy) || p === legacy) {
|
|
violations.push({ file: rel, importPath: p, reason: "legacy accounting path" });
|
|
}
|
|
}
|
|
for (const forbidden of FORBIDDEN) {
|
|
if (p.startsWith(forbidden)) {
|
|
violations.push({ file: rel, importPath: p, reason: "cross-module import" });
|
|
}
|
|
}
|
|
if (p.includes("../") && rel.startsWith("src/modules/accounting")) {
|
|
violations.push({ file: rel, importPath: p, reason: "relative import chain" });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const scope of ["src/shared", "components/ds"]) {
|
|
const abs = join(ROOT, scope);
|
|
if (!statSync(abs, { throwIfNotFound: false })?.isDirectory()) continue;
|
|
for (const file of walk(abs)) {
|
|
const rel = relative(ROOT, file).split(sep).join("/");
|
|
const content = readFileSync(file, "utf8");
|
|
let m;
|
|
importRegex.lastIndex = 0;
|
|
while ((m = importRegex.exec(content)) !== null) {
|
|
if (m[1].startsWith("@/src/modules/accounting/")) {
|
|
violations.push({ file: rel, importPath: m[1], reason: "shared imports accounting" });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log("Accounting Import Validation\n");
|
|
if (violations.length) {
|
|
console.error(`❌ ${violations.length} violation(s):`);
|
|
for (const v of violations) console.error(` ${v.file}: ${v.importPath} (${v.reason})`);
|
|
process.exit(1);
|
|
}
|
|
console.log("✅ Accounting imports use @/src/modules/accounting/*");
|
|
console.log("✅ No cross-module or relative import violations");
|
|
process.exit(0);
|