TorbatYar/frontend/scripts/validate-beauty-imports.mjs
Mortezakoohjani 6f4a484051 Migrate Beauty, Healthcare, and Accounting frontend to modular src/modules architecture.
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>
2026-07-26 22:28:27 +03:30

89 lines
3.1 KiB
JavaScript

#!/usr/bin/env node
/** Validate Beauty module import boundaries after migration. */
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_BEAUTY = [
"@/components/beauty/",
"@/lib/beauty-business-api",
"@/lib/beauty-portals",
"@/hooks/useBeautyLookups",
];
const FORBIDDEN_IN_BEAUTY = [
"@/components/accounting/",
"@/components/healthcare/",
"@/modules/accounting/",
"@/modules/healthcare/",
"@/src/modules/healthcare/",
];
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/beauty", "modules/beauty"]) {
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_BEAUTY) {
if (p.startsWith(legacy) || p === legacy.replace(/\/$/, "")) {
violations.push({ file: rel, importPath: p, reason: "legacy beauty path" });
}
}
for (const forbidden of FORBIDDEN_IN_BEAUTY) {
if (p.startsWith(forbidden)) {
violations.push({ file: rel, importPath: p, reason: "cross-module import" });
}
}
}
}
}
// Shared/platform must not import modules/beauty except deprecated shims in lib/hooks
for (const scope of ["components/ds", "shared"]) {
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("@/modules/beauty/")) {
violations.push({ file: rel, importPath: m[1], reason: "shared imports beauty module" });
}
}
}
}
console.log("Beauty 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("✅ All beauty imports use @/modules/beauty/* paths");
console.log("✅ No cross-module imports in beauty scope");
process.exit(0);