94 lines
2.9 KiB
TypeScript
94 lines
2.9 KiB
TypeScript
import { readdirSync, readFileSync } from "node:fs";
|
|
import { dirname, relative, resolve, sep } from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
const housekeepingRoot = resolve(process.cwd(), "src/features/housekeeping");
|
|
|
|
function sourceFiles(directory: string): string[] {
|
|
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
const path = resolve(directory, entry.name);
|
|
if (entry.isDirectory()) return sourceFiles(path);
|
|
if (!/\.(?:ts|tsx)$/u.test(entry.name) || /\.test\./u.test(entry.name)) {
|
|
return [];
|
|
}
|
|
return [path];
|
|
});
|
|
}
|
|
|
|
function moduleSpecifiers(source: string): string[] {
|
|
return [
|
|
...source.matchAll(
|
|
/(?:from\s+|import\s*\(|import\s+|require\s*\()\s*["']([^"']+)["']/gu,
|
|
),
|
|
].flatMap((match) => (match[1] ? [match[1]] : []));
|
|
}
|
|
|
|
function resolveHousekeepingImport(
|
|
file: string,
|
|
specifier: string,
|
|
): string | null {
|
|
if (specifier.startsWith(".")) {
|
|
return resolve(dirname(file), specifier).replaceAll("\\", "/");
|
|
}
|
|
const prefix = "@/features/housekeeping/";
|
|
if (specifier.startsWith(prefix)) {
|
|
return resolve(housekeepingRoot, specifier.slice(prefix.length)).replaceAll(
|
|
"\\",
|
|
"/",
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function displayPath(path: string): string {
|
|
return relative(process.cwd(), path).split(sep).join("/");
|
|
}
|
|
|
|
describe("Housekeeping source boundaries", () => {
|
|
it("keeps foundation independent from domain internals, database, and actions", () => {
|
|
const violations = sourceFiles(resolve(housekeepingRoot, "foundation"))
|
|
.flatMap((file) =>
|
|
moduleSpecifiers(readFileSync(file, "utf8")).flatMap((specifier) => {
|
|
const resolved = resolveHousekeepingImport(file, specifier);
|
|
const importsDomain =
|
|
resolved?.includes("/features/housekeeping/domains/") === true;
|
|
const importsInfrastructure =
|
|
specifier === "@/db" ||
|
|
specifier.startsWith("@/db/") ||
|
|
specifier === "@/actions" ||
|
|
specifier.startsWith("@/actions/");
|
|
return importsDomain || importsInfrastructure
|
|
? [`${displayPath(file)} -> ${specifier}`]
|
|
: [];
|
|
}),
|
|
)
|
|
.sort();
|
|
|
|
expect(violations).toEqual([]);
|
|
});
|
|
|
|
it("prevents one domain from importing another domain's internals", () => {
|
|
const domainsRoot = resolve(housekeepingRoot, "domains");
|
|
const violations = sourceFiles(domainsRoot)
|
|
.flatMap((file) => {
|
|
const owner = relative(domainsRoot, file).split(sep)[0];
|
|
return moduleSpecifiers(readFileSync(file, "utf8")).flatMap(
|
|
(specifier) => {
|
|
const resolved = resolveHousekeepingImport(file, specifier);
|
|
if (!resolved) return [];
|
|
const marker = "/features/housekeeping/domains/";
|
|
const offset = resolved.indexOf(marker);
|
|
if (offset < 0) return [];
|
|
const target = resolved.slice(offset + marker.length).split("/")[0];
|
|
return target && target !== owner
|
|
? [`${displayPath(file)} -> ${specifier}`]
|
|
: [];
|
|
},
|
|
);
|
|
})
|
|
.sort();
|
|
|
|
expect(violations).toEqual([]);
|
|
});
|
|
});
|