fix(javascript): honor module-sync package exports (#1361)

This commit is contained in:
Akshit Khokhani 2026-10-09 17:49:43 -05:00 committed by GitHub
parent a9f5dad5a8
commit d0e606930a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 58 additions and 2 deletions

View File

@ -307,6 +307,8 @@ such as `./web3:app.js` still names a local artifact.
Package `exports` fallback arrays are supported both at the top level and under
the root `"."` entry. The resolver selects targets in declared order using the
same conditional and invalid-entry handling as nested exports arrays.
For imports and requires, the active Node conditions include `module-sync`;
earlier active conditions retain precedence over later ones.
ESM relative module paths and selected package exports targets use URL suffix handling
and one percent-decoding pass. CommonJS relative paths and legacy package main

View File

@ -640,7 +640,8 @@ const exportTargets = (
* Node selects an exports target by walking the declared keys in order and
* taking the first whose condition is active for the calling resolver.
* "default" is always active, and "node" plus "node-addons" are active for the
* built-in resolver that owns installed-package imports and requires.
* built-in resolver that owns installed-package imports and requires. The
* supported Node runtimes also activate "module-sync" for both loaders.
*/
const packageExportConditions = (
moduleKind: ResolveArtifactPathInput["moduleKind"],
@ -648,6 +649,7 @@ const packageExportConditions = (
new Set([
"node",
"node-addons",
"module-sync",
...(moduleKind === undefined ? [] : [moduleKind]),
"default",
]);

View File

@ -103,6 +103,27 @@ const cases: readonly {
exports: { browser: "./../outside.cjs", default: "./actual.cjs" },
expected: "node_modules/fixture/actual.cjs",
},
{
name: "active module-sync rejects an invalid target instead of default",
exports: { "module-sync": "./../outside.cjs", default: "./actual.cjs" },
expected: null,
nodeError: "ERR_INVALID_PACKAGE_TARGET",
rejectedTarget: "./../outside.cjs",
},
{
name: "module-sync null blocks default",
exports: { "module-sync": null, default: "./actual.cjs" },
expected: null,
nodeError: "ERR_PACKAGE_PATH_NOT_EXPORTED",
},
{
name: "unmatched nested module-sync permits default",
exports: {
"module-sync": { browser: "./missing.cjs" },
default: "./actual.cjs",
},
expected: "node_modules/fixture/actual.cjs",
},
...["./bad%.cjs", "./bad%C3.cjs"].map((target) => ({
name: `malformed URL ${target} is selected before URL decoding`,
exports: [target, "./actual.cjs"],

View File

@ -202,6 +202,36 @@ it.each([
exports: { import: "./exported.mjs", require: "./actual.cjs" },
},
},
{
name: "import selects module-sync before default",
kind: "import",
metadata: {
exports: { "module-sync": "./sync.mjs", default: "./actual.cjs" },
},
},
{
name: "require selects nested module-sync before default",
kind: "require",
metadata: {
exports: {
".": { node: { "module-sync": "./sync.mjs", default: "./actual.cjs" } },
},
},
},
{
name: "an earlier import condition precedes module-sync",
kind: "import",
metadata: {
exports: { import: "./exported.mjs", "module-sync": "./sync.mjs" },
},
},
{
name: "an earlier default condition precedes module-sync",
kind: "require",
metadata: {
exports: { default: "./actual.cjs", "module-sync": "./sync.mjs" },
},
},
] as const)(
"matches the native Node loader when $name",
async ({ kind, metadata }) => {
@ -210,7 +240,7 @@ it.each([
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 value = require("fixture"); console.log(JSON.stringify({value: typeof value === "string" ? value : value.default, resolved: require.resolve("fixture")}));`;
const files = {
"package.json": JSON.stringify({
name: "app",
@ -226,6 +256,7 @@ it.each([
"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';",
"node_modules/fixture/sync.mjs": "export default 'sync.mjs';",
};
await Promise.all(
Object.entries(files).map(async ([path, text]) => {