build: verify resolved module boundaries (#862)
This commit is contained in:
parent
3b92a2bced
commit
3427593daf
122
.oxlintrc.json
122
.oxlintrc.json
|
|
@ -34,97 +34,6 @@
|
|||
"no-var-requires": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"src/{contracts,domain,server}/**/*.ts",
|
||||
"src/application/**/*.ts"
|
||||
],
|
||||
"excludeFiles": ["src/application/runtime.ts"],
|
||||
"rules": {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"patterns": [
|
||||
{
|
||||
"group": [
|
||||
"**/ghidra/**",
|
||||
"**/hopper/**",
|
||||
"**/ida/**",
|
||||
"**/android/**",
|
||||
"**/firmware/**",
|
||||
"**/browser/*Provider.*",
|
||||
"**/javascript/**"
|
||||
],
|
||||
"message": "Provider imports belong behind typed ports or in production composition."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["src/{contracts,domain}/**/*.ts"],
|
||||
"rules": {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"patterns": [
|
||||
{
|
||||
"group": [
|
||||
"**/application/**",
|
||||
"**/server/**",
|
||||
"**/cli/**",
|
||||
"**/main/**",
|
||||
"**/cli.*",
|
||||
"**/main.*",
|
||||
"**/android/**",
|
||||
"**/artifacts/**",
|
||||
"**/browser/**",
|
||||
"**/dotnet/**",
|
||||
"**/firmware/**",
|
||||
"**/javascript/**",
|
||||
"**/ghidra/**",
|
||||
"**/hopper/**",
|
||||
"**/ida/**",
|
||||
"**/native/**",
|
||||
"**/process/**",
|
||||
"**/reference/**",
|
||||
"**/windows/**",
|
||||
"**/composition/**"
|
||||
],
|
||||
"message": "Domain and contracts depend inward; use typed ports for application, caller and provider boundaries."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["src/application/**/*.ts"],
|
||||
"excludeFiles": ["src/application/runtime.ts"],
|
||||
"rules": {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"patterns": [
|
||||
{
|
||||
"group": [
|
||||
"**/ghidra/**",
|
||||
"**/hopper/**",
|
||||
"**/ida/**",
|
||||
"**/android/**",
|
||||
"**/firmware/**",
|
||||
"**/browser/*Provider.*",
|
||||
"**/javascript/**",
|
||||
"**/composition/**"
|
||||
],
|
||||
"message": "Application workflows use typed ports; production construction belongs in composition."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["tests/**/*.ts", "src/**/*.test.ts", "src/**/*.fixture.ts"],
|
||||
"rules": {
|
||||
|
|
@ -139,37 +48,6 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["src/domain/**/*.{fixture,test}.ts"],
|
||||
"rules": {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"patterns": [
|
||||
{
|
||||
"group": [
|
||||
"**/application/**",
|
||||
"**/server/**",
|
||||
"**/process/**",
|
||||
"**/browser/**",
|
||||
"**/ghidra/**",
|
||||
"**/hopper/**",
|
||||
"**/ida/**",
|
||||
"**/replay/**",
|
||||
"**/cli.*",
|
||||
"**/main.*",
|
||||
"**/composition/**",
|
||||
"**/android/**",
|
||||
"**/firmware/**",
|
||||
"**/javascript/**"
|
||||
],
|
||||
"message": "Domain tests may import only their owning domain and inward dependencies."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["src/application/**/*.test.ts"],
|
||||
"rules": {
|
||||
|
|
|
|||
|
|
@ -60,6 +60,21 @@ provider implementations. Production composition is a deliberate outer boundary.
|
|||
Other existing adapter imports will migrate incrementally; do not widen a lint
|
||||
exception to conceal new protocol code inside application workflows.
|
||||
|
||||
`npm run verify:module-boundaries` parses repository-owned source imports,
|
||||
reexports, type imports and literal dynamic imports, then checks their resolved
|
||||
source layer. Nested names such as `domain/android` are domain code; `src/android`
|
||||
is the provider adapter. The check runs with lint, including cached static gates.
|
||||
It covers tracked and untracked non-ignored TypeScript source and skips cached
|
||||
paths removed during a move. Pass exact source paths for a focused check.
|
||||
|
||||
Pure source and its tests keep inward dependencies. Application/server provider
|
||||
construction guards cover the migrated deep, Android, firmware, JavaScript recovery and observation
|
||||
implementations. The existing binary runtime is an exact temporary composition
|
||||
exception; shared browser capture/export helpers remain admitted while their
|
||||
ownership is reviewed. Test lane restrictions still use Oxlint. Producer-dependent
|
||||
browser and managed comparison tests live in boundary lanes with their original
|
||||
fixtures and assertions. This is a development guard, not runtime authorization.
|
||||
|
||||
Select verification by the claim changed: schema/composition, package, real
|
||||
engine or host-native workflow. Record host/target prerequisites and source-owned
|
||||
fixtures. Run heavy lanes serially with bounded workers/heaps and reuse existing
|
||||
|
|
|
|||
|
|
@ -84,8 +84,8 @@
|
|||
"build:cached": "turbo run build",
|
||||
"format": "oxfmt .",
|
||||
"format:check": "oxfmt --check .",
|
||||
"lint": "oxlint",
|
||||
"lint:fix": "oxlint --fix",
|
||||
"lint": "oxlint && npm run verify:module-boundaries",
|
||||
"lint:fix": "oxlint --fix && npm run verify:module-boundaries",
|
||||
"knip": "knip",
|
||||
"jscpd": "jscpd src/ tests/",
|
||||
"scan:todos": "node scripts/scan-todos.mjs",
|
||||
|
|
@ -178,7 +178,8 @@
|
|||
"verify:ghidra:nativeaot": "npm run build:cached && node scripts/verify-real-ghidra-nativeaot.mjs",
|
||||
"verify:browser:scripts": "npm run build:cached && node scripts/verify-browser-scripts.mjs",
|
||||
"verify:test-discovery": "node scripts/verify-test-discovery.mjs",
|
||||
"verify:javascript:recovery": "node scripts/verify-javascript-recovery.mjs"
|
||||
"verify:javascript:recovery": "node scripts/verify-javascript-recovery.mjs",
|
||||
"verify:module-boundaries": "node scripts/verify-module-boundaries.mjs"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}": [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
/** One dependency crossing a settled source ownership boundary. */
|
||||
export interface ModuleBoundaryViolation {
|
||||
readonly file: string;
|
||||
readonly line: number;
|
||||
readonly specifier: string;
|
||||
readonly target: string;
|
||||
readonly boundary:
|
||||
| "pure-layer"
|
||||
| "application-composition"
|
||||
| "provider-construction";
|
||||
}
|
||||
|
||||
/** Check resolved source ownership, including type imports and reexports. */
|
||||
export function inspectModuleBoundaries(
|
||||
file: string,
|
||||
source: string,
|
||||
root: string,
|
||||
): readonly ModuleBoundaryViolation[];
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
import { parse } from "@babel/parser";
|
||||
import { traverseFast } from "@babel/types";
|
||||
import { isAbsolute, relative, resolve } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
const PURE_LAYERS = new Set(["domain", "contracts"]);
|
||||
const MIGRATED_PROVIDER_ROOTS = new Set([
|
||||
"android",
|
||||
"firmware",
|
||||
"ghidra",
|
||||
"hopper",
|
||||
"ida",
|
||||
"javascript",
|
||||
]);
|
||||
|
||||
const repositoryPath = (root, path) =>
|
||||
relative(root, path).replaceAll("\\", "/");
|
||||
|
||||
const literalValue = (node) => {
|
||||
if (node?.type === "StringLiteral") return node.value;
|
||||
if (node?.type === "TemplateLiteral" && node.expressions.length === 0)
|
||||
return node.quasis[0]?.value.cooked ?? undefined;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const importSpecifier = (node) => {
|
||||
switch (node.type) {
|
||||
case "ImportDeclaration":
|
||||
case "ExportNamedDeclaration":
|
||||
case "ExportAllDeclaration":
|
||||
case "ImportExpression":
|
||||
case "TSImportType":
|
||||
return literalValue(node.source);
|
||||
case "TSImportEqualsDeclaration":
|
||||
return node.moduleReference.type === "TSExternalModuleReference"
|
||||
? literalValue(node.moduleReference.expression)
|
||||
: undefined;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const sourceTarget = (file, specifier) => {
|
||||
if (specifier.startsWith("file:")) return fileURLToPath(specifier);
|
||||
if (isAbsolute(specifier)) return resolve(specifier);
|
||||
return specifier.startsWith(".")
|
||||
? fileURLToPath(new URL(specifier, pathToFileURL(file)))
|
||||
: undefined;
|
||||
};
|
||||
|
||||
const failedBoundary = (file, target) => {
|
||||
if (!file.startsWith("src/") || !target.startsWith("src/")) return undefined;
|
||||
const layer = file.split("/")[1];
|
||||
const targetLayer = target.split("/")[1];
|
||||
if (PURE_LAYERS.has(layer)) {
|
||||
if (
|
||||
(/^src\/[^/]+\//u.test(target) && !PURE_LAYERS.has(targetLayer)) ||
|
||||
/^src\/(?:cli|main)\./u.test(target)
|
||||
)
|
||||
return "pure-layer";
|
||||
}
|
||||
if (file === "src/application/runtime.ts") return undefined;
|
||||
if (layer === "application" && targetLayer === "composition")
|
||||
return "application-composition";
|
||||
if (
|
||||
(layer === "application" || layer === "server") &&
|
||||
(MIGRATED_PROVIDER_ROOTS.has(targetLayer) ||
|
||||
/^src\/browser\/[^/]*Provider\.[^/]+$/u.test(target))
|
||||
)
|
||||
return "provider-construction";
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Check resolved source ownership, including type imports and reexports. */
|
||||
export const inspectModuleBoundaries = (file, source, root) => {
|
||||
const absoluteFile = resolve(root, file);
|
||||
const importer = repositoryPath(root, absoluteFile);
|
||||
const tree = parse(source, {
|
||||
sourceType: "module",
|
||||
plugins: ["typescript"],
|
||||
});
|
||||
const violations = [];
|
||||
traverseFast(tree, (node) => {
|
||||
const specifier = importSpecifier(node);
|
||||
if (specifier === undefined) return;
|
||||
const resolved = sourceTarget(absoluteFile, specifier);
|
||||
if (resolved === undefined) return;
|
||||
const target = repositoryPath(root, resolved);
|
||||
const boundary = failedBoundary(importer, target);
|
||||
if (boundary !== undefined)
|
||||
violations.push({
|
||||
file: importer,
|
||||
line: node.loc?.start.line ?? 1,
|
||||
specifier,
|
||||
target,
|
||||
boundary,
|
||||
});
|
||||
});
|
||||
return violations;
|
||||
};
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import { execFile } from "node:child_process";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { inspectModuleBoundaries } from "./lib/module-boundaries.mjs";
|
||||
|
||||
const execute = promisify(execFile);
|
||||
const root = process.cwd();
|
||||
const selected = process.argv.slice(2);
|
||||
for (const file of selected)
|
||||
if (!file.endsWith(".ts"))
|
||||
throw new TypeError(`Expected an exact TypeScript source file: ${file}`);
|
||||
const files =
|
||||
selected.length > 0
|
||||
? selected
|
||||
: (
|
||||
await execute(
|
||||
"git",
|
||||
[
|
||||
"ls-files",
|
||||
"-z",
|
||||
"--cached",
|
||||
"--others",
|
||||
"--exclude-standard",
|
||||
"--",
|
||||
"src",
|
||||
],
|
||||
{ cwd: root, maxBuffer: 8 * 1024 * 1024 },
|
||||
)
|
||||
).stdout.split("\0");
|
||||
|
||||
let checked = 0;
|
||||
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") continue;
|
||||
throw cause;
|
||||
}
|
||||
checked += 1;
|
||||
try {
|
||||
for (const violation of inspectModuleBoundaries(file, source, root)) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
if (process.exitCode !== 1)
|
||||
console.log(`Checked resolved module boundaries in ${checked} source files.`);
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { listBrowserTargetsInputSchema } from "../domain/browserObservation.js";
|
||||
import { browserCaptureComparisonInputSchema } from "../domain/browserCaptureComparison.js";
|
||||
import { compareWebScreenshotsInputSchema } from "../domain/webScreenshot.js";
|
||||
import { BROWSER_TOOL_CONTRACTS } from "../contracts/browserToolContracts.js";
|
||||
import { CdpBrowserProvider } from "../browser/CdpBrowserProvider.js";
|
||||
import { listBrowserTargetsInputSchema } from "../../../src/domain/browserObservation.js";
|
||||
import { browserCaptureComparisonInputSchema } from "../../../src/domain/browserCaptureComparison.js";
|
||||
import { compareWebScreenshotsInputSchema } from "../../../src/domain/webScreenshot.js";
|
||||
import { BROWSER_TOOL_CONTRACTS } from "../../../src/contracts/browserToolContracts.js";
|
||||
import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
|
||||
import {
|
||||
compareWebCaptureEvidence,
|
||||
compareWebScreenshotEvidence,
|
||||
listBrowserTargets,
|
||||
} from "./BrowserObservationService.js";
|
||||
} from "../../../src/application/BrowserObservationService.js";
|
||||
|
||||
describe("browser observation service prerequisites", () => {
|
||||
it("reports a missing provider without requiring a permission grant", async () => {
|
||||
|
|
@ -5,12 +5,12 @@ import { describe, expect, it } from "vitest";
|
|||
import {
|
||||
compareManagedMembers,
|
||||
managedMemberComparisonResultSchema,
|
||||
} from "./managedMemberComparison.js";
|
||||
import { inspectManagedMembersBytes } from "../dotnet/ManagedMemberInspector.js";
|
||||
} from "../../../../src/domain/managedMemberComparison.js";
|
||||
import { inspectManagedMembersBytes } from "../../../../src/dotnet/ManagedMemberInspector.js";
|
||||
import {
|
||||
buildManagedPeFixture,
|
||||
managedPeFixtureTarget,
|
||||
} from "../dotnet/ManagedPe.fixture.js";
|
||||
} from "../../../../src/dotnet/ManagedPe.fixture.js";
|
||||
|
||||
describe("managed member comparison", () => {
|
||||
it("remaps renamed methods by exact CIL/signature without using names", () => {
|
||||
|
|
@ -2,12 +2,12 @@ import { createHash } from "node:crypto";
|
|||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { compareManagedMembers } from "./managedMemberComparison.js";
|
||||
import { inspectManagedMembersBytes } from "../dotnet/ManagedMemberInspector.js";
|
||||
import { compareManagedMembers } from "../../../../src/domain/managedMemberComparison.js";
|
||||
import { inspectManagedMembersBytes } from "../../../../src/dotnet/ManagedMemberInspector.js";
|
||||
import {
|
||||
buildManagedPeFixture,
|
||||
managedPeFixtureTarget,
|
||||
} from "../dotnet/ManagedPe.fixture.js";
|
||||
} from "../../../../src/dotnet/ManagedPe.fixture.js";
|
||||
|
||||
const inspect = (bytes: Buffer, path: string) => {
|
||||
const target = managedPeFixtureTarget(bytes, path);
|
||||
|
|
@ -1,10 +1,16 @@
|
|||
import { spawnSync } from "node:child_process";
|
||||
import { copyFile, mkdir, writeFile } from "node:fs/promises";
|
||||
import { execFile, spawnSync } from "node:child_process";
|
||||
import { copyFile, mkdir, rename, writeFile } from "node:fs/promises";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { inspectModuleBoundaries } from "../../scripts/lib/module-boundaries.mjs";
|
||||
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
||||
|
||||
const execute = promisify(execFile);
|
||||
const verifier = resolve("scripts/verify-module-boundaries.mjs");
|
||||
|
||||
describe("incremental module import boundaries", () => {
|
||||
it.each([
|
||||
["src/domain/probe.ts", "../application/Workflow.js", false],
|
||||
|
|
@ -32,16 +38,166 @@ describe("incremental module import boundaries", () => {
|
|||
["src/composition/probe.ts", "../ghidra/Provider.js", true],
|
||||
["src/composition/probe.ts", "../android/JadxProvider.js", true],
|
||||
["src/composition/probe.ts", "../firmware/FirmwareProvider.js", true],
|
||||
["src/domain/android/probe.ts", "./types.js", true],
|
||||
["src/domain/javascript/probe.ts", "./types.js", true],
|
||||
["src/domain/firmware/probe.ts", "../android/types.js", true],
|
||||
["src/domain/android/probe.ts", "../../android/JadxProvider.js", false],
|
||||
["src/application/android/probe.ts", "../../domain/android/types.js", true],
|
||||
[
|
||||
"src/application/android/probe.ts",
|
||||
"../../android/JadxProvider.js",
|
||||
false,
|
||||
],
|
||||
["src/application/runtime.ts", "../android/JadxProvider.js", true],
|
||||
[
|
||||
"src/application/android/runtime.ts",
|
||||
"../../android/JadxProvider.js",
|
||||
false,
|
||||
],
|
||||
[
|
||||
"src/application/probe.test.ts",
|
||||
"../browser/CdpBrowserProvider.js",
|
||||
false,
|
||||
],
|
||||
["src/domain/probe.test.ts", "../dotnet/ManagedMemberInspector.js", false],
|
||||
["src/server/probe.ts", "../composition/android.js", true],
|
||||
["src/application/probe.ts", "../browser/CdpCaptureValues.js", true],
|
||||
])("checks %s importing %s", async (file, dependency, allowed) => {
|
||||
const temporary = await createTestTempDirectory("rea-module-boundary-");
|
||||
const config = join(temporary, ".oxlintrc.json");
|
||||
await copyFile(resolve(".oxlintrc.json"), config);
|
||||
const fixture = join(temporary, file);
|
||||
await mkdir(dirname(fixture), { recursive: true });
|
||||
await writeFile(
|
||||
fixture,
|
||||
`import { value } from ${JSON.stringify(dependency)}; export { value };\n`,
|
||||
);
|
||||
const result = spawnSync(process.execPath, [verifier, file], {
|
||||
cwd: temporary,
|
||||
encoding: "utf8",
|
||||
});
|
||||
if (result.error !== undefined) throw result.error;
|
||||
expect(result.status).toBe(allowed ? 0 : 1);
|
||||
if (!allowed) expect(result.stderr).toContain("resolves to src/");
|
||||
});
|
||||
});
|
||||
|
||||
describe("module import syntax and source discovery", () => {
|
||||
it.each([
|
||||
'import { value } from "../android/Provider.js";',
|
||||
'import type { Value } from "../android/Provider.js";',
|
||||
'export { value } from "../android/Provider.js";',
|
||||
'export * from "../android/Provider.js";',
|
||||
'type Value = import("../android/Provider.js").Value;',
|
||||
'const loaded = import("../android/Provider.js");',
|
||||
"const loaded = import(`../android/Provider.js`);",
|
||||
'import provider = require("../android/Provider.js");',
|
||||
'import { value } from "../domain/../android/Provider.js";',
|
||||
'const loaded = import("../%61ndroid/Provider.js?view=one#fragment");',
|
||||
])("checks the resolved owner in %s", (source) => {
|
||||
expect(
|
||||
inspectModuleBoundaries("src/domain/probe.ts", source, process.cwd()),
|
||||
).toEqual([
|
||||
expect.objectContaining({
|
||||
target: "src/android/Provider.js",
|
||||
boundary: "pure-layer",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("ignores imports mentioned in comments and analyst fixture strings", () => {
|
||||
expect(
|
||||
inspectModuleBoundaries(
|
||||
"src/domain/probe.ts",
|
||||
'// import("../android/Provider.js");\nconst text = \'import("../android/Provider.js")\';',
|
||||
process.cwd(),
|
||||
),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it.each(["absolute", "file-url"])("resolves %s source imports", (kind) => {
|
||||
const target = resolve("src/android/Provider.js");
|
||||
const specifier = kind === "file-url" ? pathToFileURL(target).href : target;
|
||||
expect(
|
||||
inspectModuleBoundaries(
|
||||
"src/domain/probe.ts",
|
||||
`export * from ${JSON.stringify(specifier)};`,
|
||||
process.cwd(),
|
||||
),
|
||||
).toEqual([
|
||||
expect.objectContaining({
|
||||
target: "src/android/Provider.js",
|
||||
boundary: "pure-layer",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects unsupported selected arguments instead of checking zero files", () => {
|
||||
const result = spawnSync(process.execPath, [verifier, "--unknown"], {
|
||||
encoding: "utf8",
|
||||
});
|
||||
if (result.error !== undefined) throw result.error;
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stderr).toContain("Expected an exact TypeScript source file");
|
||||
});
|
||||
|
||||
it("reports malformed source with the selected file and parsing reason", async () => {
|
||||
const temporary = await createTestTempDirectory("rea-module-syntax-");
|
||||
await mkdir(join(temporary, "src", "domain"), { recursive: true });
|
||||
await writeFile(join(temporary, "src/domain/probe.ts"), "import {");
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[verifier, "src/domain/probe.ts"],
|
||||
{
|
||||
cwd: temporary,
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
if (result.error !== undefined) throw result.error;
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stderr).toContain("Could not inspect src/domain/probe.ts");
|
||||
expect(result.stderr).toContain("Unexpected token");
|
||||
});
|
||||
|
||||
it("checks untracked source files and ignores cached paths removed during a move", async () => {
|
||||
const temporary = await createTestTempDirectory("rea-module-discovery-");
|
||||
await mkdir(join(temporary, "src", "domain"), { recursive: true });
|
||||
await writeFile(join(temporary, "src/domain/old.ts"), "export const x=1;");
|
||||
await execute("git", ["init", "--quiet"], { cwd: temporary });
|
||||
await execute("git", ["add", "--", "src"], { cwd: temporary });
|
||||
await rename(
|
||||
join(temporary, "src/domain/old.ts"),
|
||||
join(temporary, "src/domain/new.ts"),
|
||||
);
|
||||
await writeFile(
|
||||
join(temporary, "src/domain/probe.ts"),
|
||||
'export * from "../android/Provider.js";',
|
||||
);
|
||||
const result = spawnSync(process.execPath, [verifier], {
|
||||
cwd: temporary,
|
||||
encoding: "utf8",
|
||||
});
|
||||
if (result.error !== undefined) throw result.error;
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stderr).toContain("src/domain/probe.ts:1: pure-layer");
|
||||
expect(result.stderr).not.toContain("old.ts");
|
||||
});
|
||||
});
|
||||
|
||||
describe("test lane import boundaries", () => {
|
||||
it.each([
|
||||
["src/application/probe.test.ts", "../server/createServer.js", false],
|
||||
["tests/composition/probe.test.ts", "node:child_process", false],
|
||||
["tests/composition/probe.test.ts", "node:net", false],
|
||||
["tests/composition/probe.test.ts", "playwright", false],
|
||||
["tests/composition/probe.test.ts", "../../src/domain/result.js", true],
|
||||
])("checks %s importing %s", async (file, dependency, allowed) => {
|
||||
const temporary = await createTestTempDirectory("rea-test-lane-");
|
||||
const config = join(temporary, ".oxlintrc.json");
|
||||
await copyFile(resolve(".oxlintrc.json"), config);
|
||||
await mkdir(dirname(join(temporary, file)), { recursive: true });
|
||||
await writeFile(
|
||||
join(temporary, file),
|
||||
`import { value } from ${JSON.stringify(dependency)}; export { value };\n`,
|
||||
);
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
|
|
|
|||
Loading…
Reference in New Issue