#!/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);