fix: select Node legacy package entry fields for import and require (#1035)

fix(javascript): use Node legacy package entry fields

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
This commit is contained in:
Rudy Mizrahi Celekli 2026-10-08 08:47:01 -04:00 committed by GitHub
parent 68b9fa489b
commit 170600541b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 145 additions and 5 deletions

View File

@ -362,6 +362,35 @@ describe("contextual JavaScript package path resolution", () => {
});
});
describe("generic package entry heuristics", () => {
it.each([
['{"main":"actual.cjs","module":"bundler.mjs"}', "actual.cjs"],
['{"module":"bundler.mjs"}', "bundler.mjs"],
])(
"preserves generic entry heuristics without a module kind for %s",
(metadata, entry) => {
const files = fileMap([
file("app/consumer.js", "root"),
file("app/node_modules/fixture/package.json", "root", metadata),
file("app/node_modules/fixture/actual.cjs", "root"),
file("app/node_modules/fixture/bundler.mjs", "root"),
file("app/node_modules/fixture/index.js", "root"),
]);
expect(
resolve({
declaredPath: "fixture",
sourcePath: "app/consumer.js",
context: "module-specifier",
files,
}),
).toMatchObject({
resolution_status: "resolved",
resolved_path: `app/node_modules/fixture/${entry}`,
});
},
);
});
describe("directory package entrypoint precedence", () => {
it.each([
["require", '{"main":"actual.cjs"}', "actual.cjs"],

View File

@ -461,10 +461,12 @@ const packageEntry = (
? { ...exported, source: "exports" }
: exported;
}
const preferred =
moduleKind === "import"
? [Reflect.get(value, "module"), Reflect.get(value, "main")]
: [Reflect.get(value, "main"), Reflect.get(value, "module")];
// Node imports and requires ignore the bundler-only module field. Keep
// that fallback for callers without an explicit module-loading kind.
const preferred = [
Reflect.get(value, "main"),
...(moduleKind === undefined ? [Reflect.get(value, "module")] : []),
];
const entry = preferred.find((candidate) => candidate !== undefined);
if (entry === undefined) return { status: "missing" };
const legacy = packagePathValue(entry);

View File

@ -1,7 +1,11 @@
import { mkdir, writeFile } from "node:fs/promises";
import { execFile } from "node:child_process";
import { mkdir, realpath, writeFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { dirname, join, relative, sep } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { expect, it } from "vitest";
import { reconstructJavaScriptArtifact } from "../../../src/application/javascript/JavaScriptArtifactReconstruction.js";
@ -160,3 +164,108 @@ it.each(cases)(
expect(result.graph.coverage.status).toBe("complete");
},
);
const runNode = promisify(execFile);
it.each([
{
name: "import uses main before bundler module",
kind: "import",
metadata: { main: "actual.cjs", module: "bundler.mjs" },
},
{
name: "import ignores a module-only entry",
kind: "import",
metadata: { module: "bundler.mjs" },
},
{
name: "require ignores a module-only entry",
kind: "require",
metadata: { module: "bundler.mjs" },
},
{
name: "import preserves a main-only entry",
kind: "import",
metadata: { main: "actual.cjs" },
},
{
name: "require preserves main before bundler module",
kind: "require",
metadata: { main: "actual.cjs", module: "bundler.mjs" },
},
{
name: "import preserves declared exports before legacy entries",
kind: "import",
metadata: {
main: "actual.cjs",
module: "bundler.mjs",
exports: { import: "./exported.mjs", require: "./actual.cjs" },
},
},
] as const)(
"matches the native Node loader when $name",
async ({ kind, metadata }) => {
const root = await createTestTempDirectory("rea-node-package-fields-");
const entry = kind === "import" ? "main.mjs" : "main.cjs";
const source =
kind === "import"
? `import value from "fixture"; console.log(JSON.stringify({value, resolved: import.meta.resolve("fixture")}));`
: `const value = require("fixture"); console.log(JSON.stringify({value, resolved: require.resolve("fixture")}));`;
const files = {
"package.json": JSON.stringify({
name: "app",
main: entry,
type: kind === "import" ? "module" : "commonjs",
}),
[entry]: source,
"node_modules/fixture/package.json": JSON.stringify({
name: "fixture",
...metadata,
}),
"node_modules/fixture/actual.cjs": "module.exports = 'actual.cjs';",
"node_modules/fixture/index.js": "module.exports = 'index.js';",
"node_modules/fixture/bundler.mjs": "export default 'bundler.mjs';",
"node_modules/fixture/exported.mjs": "export default 'exported.mjs';",
};
await Promise.all(
Object.entries(files).map(async ([path, text]) => {
await mkdir(dirname(join(root, path)), { recursive: true });
await writeFile(join(root, path), text);
}),
);
const { stdout } = await runNode(process.execPath, [join(root, entry)], {
cwd: root,
});
const oracle: unknown = JSON.parse(stdout);
expect(oracle).toMatchObject({
value: expect.any(String),
resolved: expect.any(String),
});
if (typeof oracle !== "object" || oracle === null)
throw new Error("Node returned no package identity");
const resolved: unknown = Reflect.get(oracle, "resolved");
if (typeof resolved !== "string")
throw new Error("Node returned no resolved path");
const expected = relative(
await realpath(root),
kind === "import" ? fileURLToPath(resolved) : resolved,
)
.split(sep)
.join("/");
expect(Reflect.get(oracle, "value")).toBe(expected.split("/").at(-1));
const result = await reconstructJavaScriptArtifact({
input_path: root,
format: "directory",
});
const edge = result.graph.edges.find(
({ properties }) =>
properties.module_link_kind === kind &&
properties.specifier === "fixture",
);
expect(edge?.properties).toMatchObject({
resolution_status: "resolved",
resolved_path: expected,
});
expect(result.graph.coverage.status).toBe("complete");
},
);