- Adjusted ESLint configuration to improve clarity in import rules, specifically preventing deep imports from the domain. - Removed unnecessary blank lines in `check-domain-imports.mjs`, `check-exports.mjs`, and `codemod-domain-imports.mjs` scripts for better code cleanliness and maintainability. - Enhanced readability of the import validation message in ESLint configuration.
145 lines
4.2 KiB
JavaScript
145 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();
|