Files
reverse-engineering-anything/scripts/verify-module-boundaries.mjs
T
morluto 8c052bdec5 test(architecture): ratchet top-level layer direction and mutual imports (#10584)
* test(architecture): ratchet top-level layer direction and mutual imports

Declare a layer for every top-level src/ directory and root source file
(domain, contracts, foundation, providers, application, composition,
adapters) in scripts/lib/module-layers.mjs. verify:module-boundaries now also
builds the production import graph between those units and compares it with
a counted baseline: 45 unit pairs import a higher layer (248 imports) and 23
unit pairs import each other. A new upward pair, more imports on a recorded
pair, a new mutual import, or an undeclared unit fails; shrinkage is
reported so the baseline can be lowered.

* fix(architecture): enforce layers for isolated units
2026-11-15 08:37:04 +08:00

114 lines
3.5 KiB
JavaScript

import { execFile } from "node:child_process";
import { readdir, readFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { promisify } from "node:util";
import {
inspectResolvedModuleBoundaries,
resolveModuleImports,
} from "./lib/module-boundaries.mjs";
import { compareLayerBaseline, layerGraphFacts } from "./lib/module-layers.mjs";
const execute = promisify(execFile);
const root = process.cwd();
const printLayerBaseline = process.argv.includes("--print-layer-baseline");
const selected = process.argv
.slice(2)
.filter((argument) => argument !== "--print-layer-baseline");
for (const file of selected)
if (!file.endsWith(".ts"))
throw new TypeError(`Expected an exact TypeScript source file: ${file}`);
const enumerateSourceFiles = async () => {
if (selected.length > 0) return selected;
let isGitRoot = false;
try {
const { stdout } = await execute("git", ["rev-parse", "--show-cdup"], {
cwd: root,
maxBuffer: 8 * 1024 * 1024,
});
isGitRoot = stdout.trim() === "";
} catch {
// Archives and source snapshots may not be in a Git worktree.
}
if (isGitRoot) {
try {
const { stdout } = await execute(
"git",
[
"ls-files",
"-z",
"--cached",
"--others",
"--exclude-standard",
"--",
"src",
],
{ cwd: root, maxBuffer: 8 * 1024 * 1024 },
);
return stdout.split("\0");
} catch {
// Fall back to disk when Git's index is unavailable or unreadable.
}
}
const entries = await readdir(resolve(root, "src"), { recursive: true });
return entries.map((entry) => join("src", entry));
};
const files = await enumerateSourceFiles();
let checked = 0;
const resolvedImports = [];
const checkedFiles = [];
for (const file of new Set(files.filter((path) => path.endsWith(".ts")))) {
let source;
try {
source = await readFile(resolve(root, file), "utf8");
} catch (cause) {
if (
selected.length === 0 &&
(cause.code === "ENOENT" || cause.code === "EISDIR")
)
continue;
throw cause;
}
checked += 1;
checkedFiles.push(file.replaceAll("\\", "/"));
try {
const imports = resolveModuleImports(file, source, root);
resolvedImports.push(...imports);
for (const violation of inspectResolvedModuleBoundaries(imports)) {
console.error(
`${violation.file}:${violation.line}: ${violation.boundary}: ${violation.specifier} resolves to ${violation.target}`,
);
process.exitCode = 1;
}
} catch (cause) {
console.error(`Could not inspect ${file}: ${cause.message}`);
process.exitCode = 1;
}
}
// Unit layering and cycles are whole-graph facts; a file selection cannot
// establish them.
if (selected.length === 0) {
const baselinePath = resolve(root, "scripts/lib/module-layers.baseline.json");
const baseline = JSON.parse(await readFile(baselinePath, "utf8"));
const facts = layerGraphFacts(resolvedImports, checkedFiles);
if (printLayerBaseline) {
console.log(
JSON.stringify({ upward: facts.upward, cycles: facts.cycles }, null, 2),
);
}
const { failures, lowerable } = compareLayerBaseline(facts, baseline);
for (const failure of failures) {
console.error(`module layers: ${failure}`);
process.exitCode = 1;
}
if (lowerable.length > 0)
console.log(
`Module layer baseline can be lowered in scripts/lib/module-layers.baseline.json:\n ${lowerable.join("\n ")}`,
);
}
if (process.exitCode !== 1)
console.log(`Checked resolved module boundaries in ${checked} source files.`);