mirror of
https://github.com/morluto/rea.git
synced 2026-10-11 21:39:55 +00:00
283 lines
9.1 KiB
TypeScript
283 lines
9.1 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
import { createRequire } from "node:module";
|
|
import { dirname, join, relative } from "node:path";
|
|
|
|
import { expect } from "vitest";
|
|
import { z } from "zod";
|
|
|
|
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
|
|
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { cliTest } from "../../support/cli/cliFixture.js";
|
|
|
|
const literalAliases = ["#/x", "#x%2Fy", "#x%2Ey", "#x\\y", "#x\0y"];
|
|
const fallbackAliases = {
|
|
"#tab-escape": ["./.\t./outside.cjs", "./config.cjs"],
|
|
"#newline-escape": ["./.%\n2e/outside.cjs", "./config.cjs"],
|
|
"#normalized-url": "./con\tfig.cjs",
|
|
"#trimmed-url": "./config.cjs \t",
|
|
"#url-suffix": "./config.cjs?selected=one#fragment",
|
|
"#inactive-array": {
|
|
node: [{ browser: "./browser.cjs" }],
|
|
default: "./config.cjs",
|
|
},
|
|
"#nested-inactive-array": {
|
|
node: [[{ browser: "./browser.cjs" }]],
|
|
default: "./config.cjs",
|
|
},
|
|
"#parent": ["../outside.cjs", "./config.cjs"],
|
|
"#absolute": { node: [["/outside.cjs"], "./config.cjs"] },
|
|
"#url": {
|
|
import: ["https://example.invalid/outside.cjs", "./config.cjs"],
|
|
require: ["file:///outside.cjs", "./config.cjs"],
|
|
},
|
|
};
|
|
const resolvedAliases = [...literalAliases, ...Object.keys(fallbackAliases)];
|
|
const refusedAliases = {
|
|
"#empty": {
|
|
target: [],
|
|
status: "external",
|
|
limitation: "an empty imports target array",
|
|
},
|
|
"#empty-active": {
|
|
target: { node: [] },
|
|
status: "external",
|
|
limitation: "an empty imports target array",
|
|
},
|
|
"#null-last": {
|
|
target: [[], null],
|
|
status: "external",
|
|
limitation: "an explicit null imports target",
|
|
},
|
|
"#empty-last": {
|
|
target: [null, []],
|
|
status: "external",
|
|
limitation: "an empty imports target array",
|
|
},
|
|
"#inactive": {
|
|
target: [{ browser: "./browser.cjs" }],
|
|
status: "external",
|
|
limitation: "it declares browser.",
|
|
},
|
|
"#nested-inactive": {
|
|
status: "external",
|
|
target: [
|
|
[{ browser: "./browser.cjs" }, { "rea-inactive": "./inactive.cjs" }],
|
|
],
|
|
limitation: "it declares browser, rea-inactive.",
|
|
},
|
|
"#escape": {
|
|
target: "./.\t./outside.cjs",
|
|
status: "rejected",
|
|
limitation: "escapes the package root",
|
|
},
|
|
"#encoded-separator": {
|
|
target: ["./con%2ffig.cjs", "./config.cjs"],
|
|
status: "rejected",
|
|
limitation: "separator bytes",
|
|
},
|
|
"#missing-url": {
|
|
target: ["./miss\ting.cjs", "./config.cjs"],
|
|
status: "not-found",
|
|
limitation: JSON.stringify("./miss\ting.cjs"),
|
|
},
|
|
};
|
|
const allAliases = [...resolvedAliases, ...Object.keys(refusedAliases)];
|
|
|
|
const createPackageImportAliasFixture = async () => {
|
|
const root = await createTestTempDirectory("rea-package-import-aliases-");
|
|
const mainSource = [
|
|
'const config = require("#config");',
|
|
'const nested = require("./node_modules/fixture/index.cjs");',
|
|
'const missing = require("#missing");',
|
|
...allAliases.map((specifier) => `require(${JSON.stringify(specifier)});`),
|
|
].join("\n");
|
|
const moduleSource = allAliases
|
|
.map((specifier) => `import ${JSON.stringify(specifier)};`)
|
|
.join("\n");
|
|
const nestedSource = [
|
|
'const config = require("#config");',
|
|
"module.exports = config;",
|
|
].join("\n");
|
|
const files = {
|
|
"package.json": JSON.stringify({
|
|
name: "app",
|
|
imports: {
|
|
"#config": "./config.cjs",
|
|
...Object.fromEntries(
|
|
literalAliases.map((specifier) => [specifier, "./config.cjs"]),
|
|
),
|
|
...fallbackAliases,
|
|
...Object.fromEntries(
|
|
Object.entries(refusedAliases).map(([specifier, { target }]) => [
|
|
specifier,
|
|
target,
|
|
]),
|
|
),
|
|
},
|
|
}),
|
|
"main.cjs": mainSource,
|
|
"main.mjs": moduleSource,
|
|
"config.cjs": "module.exports = 'app-config';",
|
|
"node_modules/fixture/package.json": JSON.stringify({
|
|
name: "fixture",
|
|
imports: { "#config": "./local.cjs" },
|
|
}),
|
|
"node_modules/fixture/index.cjs": nestedSource,
|
|
"node_modules/fixture/local.cjs": "module.exports = 'fixture-config';",
|
|
};
|
|
await Promise.all(
|
|
Object.entries(files).map(async ([path, content]) => {
|
|
const absolute = join(root, path);
|
|
await mkdir(dirname(absolute), { recursive: true });
|
|
await writeFile(absolute, content);
|
|
}),
|
|
);
|
|
return { root, mainSource, moduleSource, nestedSource };
|
|
};
|
|
|
|
cliTest.for(["CLI", "stdio MCP"] as const)(
|
|
"resolves exact package import aliases with package scope through %s",
|
|
async (surface, { cli }) => {
|
|
const { root, mainSource, moduleSource, nestedSource } =
|
|
await createPackageImportAliasFixture();
|
|
const nativeRequire = createRequire(join(root, "main.cjs"));
|
|
expect(
|
|
relative(root, nativeRequire.resolve("#config")).replaceAll("\\", "/"),
|
|
).toBe("config.cjs");
|
|
expect(
|
|
relative(
|
|
root,
|
|
createRequire(join(root, "node_modules/fixture/index.cjs")).resolve(
|
|
"#config",
|
|
),
|
|
).replaceAll("\\", "/"),
|
|
).toBe("node_modules/fixture/local.cjs");
|
|
expect(() => nativeRequire.resolve("#missing")).toThrow(
|
|
expect.objectContaining({ code: "ERR_PACKAGE_IMPORT_NOT_DEFINED" }),
|
|
);
|
|
|
|
let document: unknown;
|
|
if (surface === "CLI") {
|
|
const response = await cli.run({
|
|
arguments: ["analyze-javascript-application", root, "--format", "json"],
|
|
environment: { REA_LOG_LEVEL: "silent" },
|
|
});
|
|
expect(response.exitCode, response.stderr).toBe(0);
|
|
document = response.json;
|
|
} else {
|
|
const { call } = await connectLocalToolsMcp();
|
|
const response = await call("analyze_javascript_application", {
|
|
input_path: root,
|
|
format: "directory",
|
|
});
|
|
expect(response.isError, JSON.stringify(response)).not.toBe(true);
|
|
document = response.structuredContent;
|
|
}
|
|
const { graph } = z
|
|
.object({ normalized_result: javascriptApplicationAnalysisResultSchema })
|
|
.parse(document).normalized_result;
|
|
for (const specifier of resolvedAliases)
|
|
for (const [moduleKind, sourcePath, source] of [
|
|
["require", "main.cjs", mainSource],
|
|
["import", "main.mjs", moduleSource],
|
|
] as const)
|
|
expect(graph.edges).toContainEqual(
|
|
expect.objectContaining({
|
|
properties: expect.objectContaining({
|
|
module_link_kind: moduleKind,
|
|
specifier,
|
|
resolved_path: "config.cjs",
|
|
resolution_status: "resolved",
|
|
}),
|
|
evidence: sourceEvidence(sourcePath, source),
|
|
}),
|
|
);
|
|
const imports = graph.edges.filter(
|
|
({ properties }) => properties.module_link_kind === "require",
|
|
);
|
|
for (const [specifier, { status, limitation }] of Object.entries(
|
|
refusedAliases,
|
|
))
|
|
for (const [moduleKind, sourcePath, source] of [
|
|
["require", "main.cjs", mainSource],
|
|
["import", "main.mjs", moduleSource],
|
|
] as const)
|
|
expect(graph.edges).toContainEqual(
|
|
expect.objectContaining({
|
|
properties: expect.objectContaining({
|
|
module_link_kind: moduleKind,
|
|
specifier,
|
|
resolved_path: null,
|
|
resolution_status: status,
|
|
}),
|
|
evidence: sourceEvidence(sourcePath, source, limitation),
|
|
}),
|
|
);
|
|
expect(imports).toContainEqual(
|
|
expect.objectContaining({
|
|
properties: expect.objectContaining({
|
|
specifier: "#config",
|
|
resolved_path: "config.cjs",
|
|
resolution_status: "resolved",
|
|
}),
|
|
evidence: sourceEvidence("main.cjs", mainSource),
|
|
}),
|
|
);
|
|
expect(imports).toContainEqual(
|
|
expect.objectContaining({
|
|
properties: expect.objectContaining({
|
|
specifier: "#config",
|
|
resolved_path: "node_modules/fixture/local.cjs",
|
|
resolution_status: "resolved",
|
|
}),
|
|
evidence: sourceEvidence(
|
|
"node_modules/fixture/index.cjs",
|
|
nestedSource,
|
|
),
|
|
}),
|
|
);
|
|
expect(imports).toContainEqual(
|
|
expect.objectContaining({
|
|
properties: expect.objectContaining({
|
|
specifier: "#missing",
|
|
resolved_path: null,
|
|
resolution_status: "external",
|
|
}),
|
|
evidence: expect.objectContaining({
|
|
limitations: expect.arrayContaining([
|
|
expect.stringContaining("does not define package import alias"),
|
|
]),
|
|
}),
|
|
}),
|
|
);
|
|
},
|
|
);
|
|
|
|
const sourceEvidence = (
|
|
sourcePath: string,
|
|
source: string,
|
|
limitation?: string,
|
|
): unknown =>
|
|
expect.objectContaining({
|
|
artifact: expect.objectContaining({
|
|
available: true,
|
|
sha256: createHash("sha256").update(source).digest("hex"),
|
|
}),
|
|
location: expect.objectContaining({
|
|
available: true,
|
|
value: expect.objectContaining({
|
|
kind: "source-range",
|
|
source: sourcePath,
|
|
}),
|
|
}),
|
|
...(limitation === undefined
|
|
? {}
|
|
: {
|
|
limitations: expect.arrayContaining([
|
|
expect.stringContaining(limitation),
|
|
]),
|
|
}),
|
|
});
|