Files
rea/tests/acceptance/applications/packageImportAliasesCli.test.ts
T

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),
]),
}),
});