mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 11:13:21 +00:00
* 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
184 lines
6.1 KiB
JavaScript
184 lines
6.1 KiB
JavaScript
import { readFileSync } from "node:fs";
|
|
import { isAbsolute, relative, resolve } from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
|
import { readImportSpecifiers } from "./import-specifiers.mjs";
|
|
|
|
const PURE_LAYERS = new Set(["domain", "contracts"]);
|
|
const MIGRATED_PROVIDER_ROOTS = new Set([
|
|
"sqlite",
|
|
"evm",
|
|
"go",
|
|
"il2cpp",
|
|
"model",
|
|
"gpu",
|
|
"python",
|
|
"rust",
|
|
"wasm",
|
|
"captures",
|
|
"signals",
|
|
"android",
|
|
"firmware",
|
|
"ghidra",
|
|
"hopper",
|
|
"ida",
|
|
"inspector",
|
|
"javascript",
|
|
]);
|
|
const CENTRALIZED_CONSTRUCTOR_OWNER =
|
|
/^src\/(?:frida\/FridaInstrumentationManager|cutter\/CutterBridge(?:Client|Service)|gdb\/GdbSessionManager|rizin\/RizinDebugSessionManager)\.(?:js|ts)$/u;
|
|
const repositoryPath = (root, path) =>
|
|
relative(root, path).replaceAll("\\", "/");
|
|
|
|
const packageImports = new Map();
|
|
|
|
/** Read the package map once per repository root. */
|
|
const readPackageImports = (root) => {
|
|
if (!packageImports.has(root)) {
|
|
try {
|
|
const manifest = JSON.parse(
|
|
readFileSync(resolve(root, "package.json"), "utf8"),
|
|
);
|
|
packageImports.set(root, manifest.imports ?? {});
|
|
} catch (cause) {
|
|
throw new Error(
|
|
`Cannot read package imports from ${resolve(root, "package.json")}: ${cause.message}`,
|
|
{ cause },
|
|
);
|
|
}
|
|
}
|
|
return packageImports.get(root);
|
|
};
|
|
|
|
/**
|
|
* The own source file a package-map specifier names.
|
|
*
|
|
* The map points at built output under `dist/`, so the same ownership rule is
|
|
* applied to the source file that produces it. Without this the gate silently
|
|
* accepts every `#`-prefixed import, which is the spelling the package map exists
|
|
* to encourage.
|
|
*/
|
|
const packageMapTarget = (root, specifier) => {
|
|
const target = readPackageImports(root)[specifier];
|
|
if (typeof target !== "string")
|
|
throw new Error(
|
|
`Cannot resolve package import ${JSON.stringify(specifier)}: this boundary check supports exact string mappings only; use a relative source import for conditional, array, pattern, or undeclared mappings.`,
|
|
);
|
|
const source = target.startsWith("./")
|
|
? repositoryPath(
|
|
root,
|
|
fileURLToPath(
|
|
new URL(target, pathToFileURL(resolve(root, "package.json"))),
|
|
),
|
|
)
|
|
.replace(/^dist\//u, "src/")
|
|
.replace(/\.(?:js|mjs|cjs)$/u, ".ts")
|
|
: undefined;
|
|
if (source === undefined || !source.startsWith("src/"))
|
|
throw new Error(
|
|
`Cannot resolve package import ${JSON.stringify(specifier)} to a source owner from ${JSON.stringify(target)}; use a relative source import for this boundary check.`,
|
|
);
|
|
return resolve(root, source);
|
|
};
|
|
|
|
const sourceTarget = (root, file, specifier) => {
|
|
if (specifier.startsWith("file:")) return fileURLToPath(specifier);
|
|
if (specifier.startsWith("#")) return packageMapTarget(root, specifier);
|
|
if (isAbsolute(specifier)) return resolve(specifier);
|
|
return specifier.startsWith(".")
|
|
? fileURLToPath(new URL(specifier, pathToFileURL(file)))
|
|
: undefined;
|
|
};
|
|
|
|
const failedBoundary = (file, target, typeOnly) => {
|
|
if (!file.startsWith("src/") || !target.startsWith("src/")) return undefined;
|
|
const layer = file.split("/")[1];
|
|
const targetLayer = target.split("/")[1];
|
|
if (
|
|
file.startsWith("src/process/capture/") &&
|
|
(["application", "composition", "server", "cli", "main"].includes(
|
|
targetLayer,
|
|
) ||
|
|
/^src\/(?:cli|main)\./u.test(target))
|
|
)
|
|
return "process-capture";
|
|
if (
|
|
file.startsWith("src/application/javascript/") &&
|
|
/^src\/artifacts\/(?:Asar|Directory)ArtifactReader\.(?:js|ts)$/u.test(
|
|
target,
|
|
)
|
|
)
|
|
return "provider-construction";
|
|
if (
|
|
(file.startsWith("src/artifacts/javascript/") ||
|
|
file.startsWith("src/artifacts/apple/") ||
|
|
file.startsWith("src/artifacts/inventory/") ||
|
|
file.startsWith("src/artifacts/extraction/") ||
|
|
/^src\/artifacts\/ArtifactHash\.(?:js|ts)$/u.test(file)) &&
|
|
(["application", "composition", "server", "cli", "main"].includes(
|
|
targetLayer,
|
|
) ||
|
|
/^src\/(?:cli|main)\./u.test(target))
|
|
)
|
|
return "artifact-acquisition";
|
|
if (PURE_LAYERS.has(layer)) {
|
|
if (
|
|
(/^src\/[^/]+\//u.test(target) && !PURE_LAYERS.has(targetLayer)) ||
|
|
/^src\/(?:cli|main)\./u.test(target)
|
|
)
|
|
return "pure-layer";
|
|
}
|
|
if (layer === "application" && targetLayer === "composition")
|
|
return "application-composition";
|
|
if (
|
|
(layer === "application" || layer === "server") &&
|
|
!typeOnly &&
|
|
CENTRALIZED_CONSTRUCTOR_OWNER.test(target)
|
|
)
|
|
return "provider-construction";
|
|
if (
|
|
(layer === "application" || layer === "server") &&
|
|
(MIGRATED_PROVIDER_ROOTS.has(targetLayer) ||
|
|
/^src\/(?:artifacts\/ArtifactProvider|dotnet\/ManagedStaticProvider|native\/NativeMacOSProvider)\.[^/]+$/u.test(
|
|
target,
|
|
) ||
|
|
/^src\/browser\/[^/]*Provider\.[^/]+$/u.test(target))
|
|
)
|
|
return "provider-construction";
|
|
return undefined;
|
|
};
|
|
|
|
/** Each own-source import of a file, resolved to its repository path. */
|
|
export const resolveModuleImports = (file, source, root) => {
|
|
const absoluteFile = resolve(root, file);
|
|
const importer = repositoryPath(root, absoluteFile);
|
|
const imports = [];
|
|
for (const { specifier, line, typeOnly } of readImportSpecifiers(source)) {
|
|
const resolved = sourceTarget(root, absoluteFile, specifier);
|
|
if (resolved === undefined) continue;
|
|
imports.push({
|
|
file: importer,
|
|
line,
|
|
specifier,
|
|
typeOnly,
|
|
target: repositoryPath(root, resolved),
|
|
});
|
|
}
|
|
return imports;
|
|
};
|
|
|
|
/** Check resolved source ownership, including type imports and reexports. */
|
|
export const inspectModuleBoundaries = (file, source, root) =>
|
|
inspectResolvedModuleBoundaries(resolveModuleImports(file, source, root));
|
|
|
|
/** Check already resolved imports, as returned by resolveModuleImports. */
|
|
export const inspectResolvedModuleBoundaries = (imports) => {
|
|
const violations = [];
|
|
for (const { file, line, specifier, typeOnly, target } of imports) {
|
|
const boundary = failedBoundary(file, target, typeOnly);
|
|
if (boundary !== undefined)
|
|
violations.push({ file, line, specifier, target, boundary });
|
|
}
|
|
return violations;
|
|
};
|