T001–T006: Phase 1 setup (workspace, Biome, TS, Vitest, layer boundary enforcement)
This commit is contained in:
109
scripts/check-layer-boundaries.mjs
Normal file
109
scripts/check-layer-boundaries.mjs
Normal file
@@ -0,0 +1,109 @@
|
||||
import { readdirSync, readFileSync } from "node:fs";
|
||||
import { join, relative } from "node:path";
|
||||
|
||||
const ROOT = new URL("..", import.meta.url).pathname.replace(/\/$/, "");
|
||||
|
||||
const FORBIDDEN = {
|
||||
"packages/domain/src": [
|
||||
"@initiative/application",
|
||||
"apps/",
|
||||
"react",
|
||||
"react-dom",
|
||||
"vite",
|
||||
],
|
||||
"packages/application/src": ["apps/", "react", "react-dom", "vite"],
|
||||
};
|
||||
|
||||
/** Directories to skip when collecting files */
|
||||
const SKIP_DIRS = new Set(["__tests__", "node_modules"]);
|
||||
|
||||
/** @param {string} dir */
|
||||
function collectTsFiles(dir) {
|
||||
/** @type {string[]} */
|
||||
const results = [];
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (!SKIP_DIRS.has(entry.name)) {
|
||||
results.push(...collectTsFiles(full));
|
||||
}
|
||||
} else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) {
|
||||
results.push(full);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an import path matches a forbidden module.
|
||||
* "vite" should match "vite" and "vite/foo" but not "vitest".
|
||||
* @param {string} importPath
|
||||
* @param {string} forbidden
|
||||
*/
|
||||
function matchesForbidden(importPath, forbidden) {
|
||||
if (forbidden.endsWith("/")) {
|
||||
return importPath.startsWith(forbidden);
|
||||
}
|
||||
return importPath === forbidden || importPath.startsWith(`${forbidden}/`);
|
||||
}
|
||||
|
||||
/** @returns {{ file: string, line: number, importPath: string, forbidden: string }[]} */
|
||||
export function checkLayerBoundaries() {
|
||||
/** @type {{ file: string, line: number, importPath: string, forbidden: string }[]} */
|
||||
const violations = [];
|
||||
|
||||
for (const [srcDir, forbidden] of Object.entries(FORBIDDEN)) {
|
||||
const absDir = join(ROOT, srcDir);
|
||||
let files;
|
||||
try {
|
||||
files = collectTsFiles(absDir);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const content = readFileSync(file, "utf-8");
|
||||
const lines = content.split("\n");
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const match = line.match(
|
||||
/(?:import|from)\s+["']([^"']+)["']|require\s*\(\s*["']([^"']+)["']\s*\)/,
|
||||
);
|
||||
if (!match) continue;
|
||||
const importPath = match[1] || match[2];
|
||||
for (const f of forbidden) {
|
||||
if (matchesForbidden(importPath, f)) {
|
||||
violations.push({
|
||||
file: relative(ROOT, file),
|
||||
line: i + 1,
|
||||
importPath,
|
||||
forbidden: f,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
// Run as CLI if invoked directly
|
||||
if (
|
||||
process.argv[1] &&
|
||||
(process.argv[1].endsWith("check-layer-boundaries.mjs") ||
|
||||
process.argv[1] === new URL(import.meta.url).pathname)
|
||||
) {
|
||||
const violations = checkLayerBoundaries();
|
||||
if (violations.length > 0) {
|
||||
console.error("Layer boundary violations found:");
|
||||
for (const v of violations) {
|
||||
console.error(
|
||||
` ${v.file}:${v.line} — imports "${v.importPath}" (forbidden: ${v.forbidden})`,
|
||||
);
|
||||
}
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log("No layer boundary violations found.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user