Assist_Design/scripts/check-domain-imports.mjs
barsa 3030d12138 Update Domain Import Practices and Enhance Documentation
- Added a new script to check domain imports, promoting better import hygiene across the codebase.
- Refactored multiple domain index files to remove unnecessary type re-exports, streamlining the module structure.
- Expanded documentation on import patterns and validation processes to provide clearer guidance for developers.
- Included an architecture diagram to illustrate the relationships between the Portal, BFF, and Domain packages.
2025-12-26 15:07:47 +09:00

147 lines
4.2 KiB
JavaScript

#!/usr/bin/env node
/**
* Domain Import Boundary Checker
*
* Validates:
* 1. No @customer-portal/domain (root) imports
* 2. No deep imports beyond module/providers
* 3. Portal has zero provider imports
*/
import fs from "node:fs/promises";
import path from "node:path";
const ROOT = process.cwd();
const APPS_DIR = path.join(ROOT, "apps");
const BFF_SRC_DIR = path.join(APPS_DIR, "bff", "src");
const PORTAL_SRC_DIR = path.join(APPS_DIR, "portal", "src");
const FILE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx"]);
const IGNORE_DIRS = new Set(["node_modules", "dist", ".next", ".turbo", ".cache"]);
function toPos(text, idx) {
// 1-based line/column
let line = 1;
let col = 1;
for (let i = 0; i < idx; i += 1) {
if (text.charCodeAt(i) === 10) {
line += 1;
col = 1;
} else {
col += 1;
}
}
return { line, col };
}
async function* walk(dir) {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const e of entries) {
const p = path.join(dir, e.name);
if (e.isDirectory()) {
if (IGNORE_DIRS.has(e.name) || e.name.startsWith(".")) continue;
yield* walk(p);
continue;
}
if (!e.isFile()) continue;
if (!FILE_EXTS.has(path.extname(e.name))) continue;
yield p;
}
}
function collectDomainImports(code) {
const results = [];
const patterns = [
{ kind: "from", re: /\bfrom\s+['"]([^'"]+)['"]/g },
{ kind: "import", re: /\bimport\s+['"]([^'"]+)['"]/g },
{ kind: "dynamicImport", re: /\bimport\(\s*['"]([^'"]+)['"]\s*\)/g },
{ kind: "require", re: /\brequire\(\s*['"]([^'"]+)['"]\s*\)/g },
];
for (const { kind, re } of patterns) {
for (const m of code.matchAll(re)) {
const spec = m[1];
if (!spec || !spec.startsWith("@customer-portal/domain")) continue;
const idx = typeof m.index === "number" ? m.index : 0;
results.push({ kind, spec, idx });
}
}
return results;
}
function validateSpecifier({ spec, isPortal }) {
if (spec === "@customer-portal/domain") {
return "Do not import @customer-portal/domain (root). Use @customer-portal/domain/<module> instead.";
}
if (spec.includes("/src/")) {
return "Import from @customer-portal/domain/<module> instead of internals.";
}
if (spec.startsWith("@customer-portal/domain/toolkit/")) {
return "Do not deep-import toolkit internals. Import from @customer-portal/domain/toolkit only.";
}
if (/^@customer-portal\/domain\/[^/]+\/providers\/.+/.test(spec)) {
return "Do not deep-import provider internals. Import from @customer-portal/domain/<module>/providers only.";
}
if (/^@customer-portal\/domain\/[^/]+\/providers$/.test(spec)) {
if (isPortal) {
return "Portal must not import provider adapters/types. Import normalized domain models from @customer-portal/domain/<module> instead.";
}
return null;
}
// Any 2+ segment import like @customer-portal/domain/a/b is illegal everywhere
// (except the explicit .../<module>/providers entrypoint handled above).
if (/^@customer-portal\/domain\/[^/]+\/[^/]+/.test(spec)) {
return "No deep @customer-portal/domain imports. Use @customer-portal/domain/<module> (or BFF-only: .../<module>/providers).";
}
return null;
}
async function main() {
const errors = [];
// Broad scan: both apps for root/deep imports
for (const baseDir of [BFF_SRC_DIR, PORTAL_SRC_DIR]) {
for await (const file of walk(baseDir)) {
const code = await fs.readFile(file, "utf8");
const isPortal = file.startsWith(PORTAL_SRC_DIR + path.sep);
for (const imp of collectDomainImports(code)) {
const message = validateSpecifier({ spec: imp.spec, isPortal });
if (!message) continue;
const pos = toPos(code, imp.idx);
errors.push({
file: path.relative(ROOT, file),
line: pos.line,
col: pos.col,
spec: imp.spec,
message,
});
}
}
}
if (errors.length > 0) {
console.error(`[domain] ERROR: illegal domain imports detected (${errors.length})`);
for (const e of errors) {
console.error(`[domain] ${e.file}:${e.line}:${e.col} ${e.spec}`);
console.error(` ${e.message}`);
}
process.exit(1);
}
console.log("[domain] OK: import contract checks passed.");
}
await main();