T001–T006: Phase 1 setup (workspace, Biome, TS, Vitest, layer boundary enforcement)

This commit is contained in:
Lukas
2026-03-03 12:54:29 +01:00
parent ddb2b317d3
commit 7dd4abb12a
27 changed files with 2655 additions and 35 deletions

View 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.");
}
}