mirror of
https://github.com/morluto/rea
synced 2026-10-27 22:45:19 -04:00
0e19db9fce
* fix(javascript): resolve exact package export subpaths Bare module specifiers with subpaths now resolve against the selected package's exports map instead of staying external. An exact subpath key selects its target through the existing condition, array, and null semantics, and the selected target resolves as one exact file with no extension or index fallback. A declared exports map encapsulates every other subpath lookup, and packages without an exports map keep legacy file and directory-index resolution. Package-private import aliases remain external pending a separate increment (#1365). Verification: npm run check:fast; focused and full src/application/javascript source tests on Windows; verify:test-discovery; docs:check. Four pre-existing failures in JavaScriptFileUrlPaths and JavaScriptModuleSpecifierPaths reproduce on unmodified main on Windows because their fixtures write file names containing '?'. * fix(javascript): unify package path resolution --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
161 lines
6.1 KiB
TypeScript
161 lines
6.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";
|
|
|
|
cliTest.for(["CLI", "stdio MCP"] as const)(
|
|
"resolves package subpaths with source provenance through %s",
|
|
async (surface, { cli }) => {
|
|
const root = await createTestTempDirectory("rea-package-subpaths-");
|
|
const source = [
|
|
'const nearest = require("fixture/parser");',
|
|
'const nested = require("nested/parser");',
|
|
'const parser = require("exported/parser");',
|
|
'const directory = require("exported/parser/");',
|
|
'const hidden = require("exported/hidden");',
|
|
'const missing = require("exported/missing");',
|
|
'new BrowserWindow({webPreferences:{preload:"exported/parser"}});',
|
|
].join("\n");
|
|
const files = {
|
|
"src/main.cjs": source,
|
|
"src/node_modules/fixture/parser.js": "module.exports = 1;",
|
|
"node_modules/fixture/package.json": "{}",
|
|
"node_modules/fixture/parser.js": "module.exports = 2;",
|
|
"node_modules/nested/package.json": "{}",
|
|
"node_modules/nested/parser/package.json": JSON.stringify({
|
|
main: "./entry.js",
|
|
exports: "./decoy.js",
|
|
}),
|
|
"node_modules/nested/parser/entry.js": "module.exports = 3;",
|
|
"node_modules/nested/parser/index.js": "module.exports = 4;",
|
|
"node_modules/nested/parser/decoy.js": "module.exports = 5;",
|
|
"node_modules/exported/package.json": JSON.stringify({
|
|
exports: {
|
|
"./parser": "./parser.cjs",
|
|
"./parser/": "./parser.cjs",
|
|
"./missing": "./missing.cjs",
|
|
},
|
|
}),
|
|
"node_modules/exported/parser.cjs": "module.exports = 6;",
|
|
"node_modules/exported/hidden.js": "module.exports = 7;",
|
|
"node_modules/exported/missing/index.js": "module.exports = 8;",
|
|
};
|
|
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);
|
|
}),
|
|
);
|
|
|
|
const nativeRequire = createRequire(join(root, "src/main.cjs"));
|
|
const expectedTargets = {
|
|
"fixture/parser": "src/node_modules/fixture/parser.js",
|
|
"nested/parser": "node_modules/nested/parser/entry.js",
|
|
"exported/parser": "node_modules/exported/parser.cjs",
|
|
};
|
|
for (const [specifier, target] of Object.entries(expectedTargets))
|
|
expect(
|
|
relative(root, nativeRequire.resolve(specifier)).replaceAll("\\", "/"),
|
|
).toBe(target);
|
|
for (const specifier of ["exported/parser/", "exported/hidden"])
|
|
expect(() => nativeRequire.resolve(specifier)).toThrow(
|
|
expect.objectContaining({ code: "ERR_PACKAGE_PATH_NOT_EXPORTED" }),
|
|
);
|
|
expect(() => nativeRequire.resolve("exported/missing")).toThrow(
|
|
expect.objectContaining({ code: "MODULE_NOT_FOUND" }),
|
|
);
|
|
|
|
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;
|
|
const imports = graph.edges.filter(
|
|
({ properties }) => properties.module_link_kind === "require",
|
|
);
|
|
const sourceDigest = createHash("sha256").update(source).digest("hex");
|
|
for (const [specifier, target] of Object.entries(expectedTargets))
|
|
expect(imports).toContainEqual(
|
|
expect.objectContaining({
|
|
properties: expect.objectContaining({
|
|
specifier,
|
|
resolved_path: target,
|
|
resolution_status: "resolved",
|
|
}),
|
|
evidence: expect.objectContaining({
|
|
artifact: expect.objectContaining({
|
|
available: true,
|
|
sha256: sourceDigest,
|
|
}),
|
|
location: expect.objectContaining({
|
|
available: true,
|
|
value: expect.objectContaining({
|
|
kind: "source-range",
|
|
source: "src/main.cjs",
|
|
}),
|
|
}),
|
|
}),
|
|
}),
|
|
);
|
|
for (const [specifier, status] of [
|
|
["exported/parser/", "external"],
|
|
["exported/hidden", "external"],
|
|
["exported/missing", "not-found"],
|
|
])
|
|
expect(imports).toContainEqual(
|
|
expect.objectContaining({
|
|
properties: expect.objectContaining({
|
|
specifier,
|
|
resolved_path: null,
|
|
resolution_status: status,
|
|
}),
|
|
}),
|
|
);
|
|
const preloads = graph.nodes.filter(
|
|
({ kind }) => kind === "electron-preload",
|
|
);
|
|
expect(preloads).toHaveLength(1);
|
|
expect(preloads[0]?.identity).toMatchObject({
|
|
key: "electron-preload:module-specifier:node_modules/exported/parser.cjs",
|
|
});
|
|
for (const observation of preloads[0]?.observations ?? [])
|
|
expect(observation).toMatchObject({
|
|
properties: {
|
|
declared_path: "exported/parser",
|
|
resolution_context: "module-specifier",
|
|
resolved_path: "node_modules/exported/parser.cjs",
|
|
},
|
|
evidence: {
|
|
location: {
|
|
available: true,
|
|
value: { source: "src/main.cjs", start: { line: 7 } },
|
|
},
|
|
},
|
|
});
|
|
},
|
|
);
|