From 274ca0b33b6b0e78e9dd7ff5fc5a8f00b4484394 Mon Sep 17 00:00:00 2001 From: kwy404 Date: Sat, 26 Sep 2026 16:29:01 -0300 Subject: [PATCH 1/3] Fix exact "exports" and "imports" keys with no target falling back to a pattern matchSubpathFromExportsLike only tried subpath patterns when the exact key lookup returned null, so an exact key whose value is null, or a conditions object with no matching condition, fell through to a less specific pattern such as "./*". Node.js only tries patterns when there is no exact key, and reports these subpaths as not exported. Check whether the exact key exists instead of whether its target is null. --- .../src/__tests__/package-imports-test.js | 25 +++++++++++++++++++ .../src/utils/matchSubpathFromExportsLike.js | 5 ++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/metro-resolver/src/__tests__/package-imports-test.js b/packages/metro-resolver/src/__tests__/package-imports-test.js index d3b1050c52..a506904de7 100644 --- a/packages/metro-resolver/src/__tests__/package-imports-test.js +++ b/packages/metro-resolver/src/__tests__/package-imports-test.js @@ -84,6 +84,31 @@ describe('import subpath patterns resolution support', () => { filePath: p('/root/node_modules/test-pkg/src/features/foo.js.js'), }); }); + + test('exact subpath with a null or unmatched target does not fall back to a pattern', () => { + const logWarning = jest.fn(); + const context = { + ...createResolutionContext({ + [p('/root/node_modules/test-pkg/package.json')]: JSON.stringify({ + name: 'test-pkg', + imports: { + '#features/*': './src/features/*.js', + '#features/foo': null, + '#features/bar': {browser: './src/features/bar.web.js'}, + }, + }), + [p('/root/node_modules/test-pkg/src/index.js')]: '', + [p('/root/node_modules/test-pkg/src/features/foo.js')]: '', + [p('/root/node_modules/test-pkg/src/features/bar.js')]: '', + }), + originModulePath: p('/root/node_modules/test-pkg/src/index.js'), + unstable_logWarning: logWarning, + }; + + expect(() => Resolver.resolve(context, '#features/foo', null)).toThrow(); + expect(() => Resolver.resolve(context, '#features/bar', null)).toThrow(); + expect(logWarning).toHaveBeenCalledTimes(2); + }); }); describe('import subpath conditional imports resolution', () => { diff --git a/packages/metro-resolver/src/utils/matchSubpathFromExportsLike.js b/packages/metro-resolver/src/utils/matchSubpathFromExportsLike.js index 2caed7d08d..1c1c289efc 100644 --- a/packages/metro-resolver/src/utils/matchSubpathFromExportsLike.js +++ b/packages/metro-resolver/src/utils/matchSubpathFromExportsLike.js @@ -52,8 +52,9 @@ export function matchSubpathFromExportsLike( let target = exportsLikeMapAfterConditions.get(subpath); let patternMatch = null; - // Attempt to match after expanding any subpath pattern keys - if (target == null) { + // Attempt to match after expanding any subpath pattern keys, unless an exact + // key matched (a null target there hides the subpath, as in Node.js) + if (!exportsLikeMapAfterConditions.has(subpath)) { // Gather keys which are subpath patterns in descending order of specificity // For ordering, see `PATTERN_KEY_COMPARE` in: // https://nodejs.org/api/esm.html#resolution-algorithm-specification From 28bb300b9f9d47de1e76fcdfe93bfec9f75c36e1 Mon Sep 17 00:00:00 2001 From: Rob Hogan <2590098+robhogan@users.noreply.github.com> Date: Sun, 27 Sep 2026 08:18:46 +0100 Subject: [PATCH 2/3] Test exact "exports" keys with no target against a subpath pattern Covers the "exports" side of the fix: an exact key with a null target, and one with no matching condition, alongside a "./*" pattern. Both now warn and fall back to file-based resolution instead of resolving through the pattern. --- .../src/__tests__/package-exports-test.js | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/packages/metro-resolver/src/__tests__/package-exports-test.js b/packages/metro-resolver/src/__tests__/package-exports-test.js index e734bb7fc9..0b2c9960f5 100644 --- a/packages/metro-resolver/src/__tests__/package-exports-test.js +++ b/packages/metro-resolver/src/__tests__/package-exports-test.js @@ -655,6 +655,45 @@ describe('with package exports resolution enabled', () => { }); }); + test.each([ + ['null', 'internal'], + ['no matching condition', 'server'], + ])( + 'exact subpath with %s target does not fall back to a pattern', + (_, subpath) => { + const logWarning = jest.fn(); + const context = { + ...createResolutionContext({ + '/root/src/main.js': '', + '/root/node_modules/test-pkg/package.json': JSON.stringify({ + name: 'test-pkg', + exports: { + './*': './lib/*.js', + './internal': null, + './server': {browser: './server-browser.js'}, + }, + }), + '/root/node_modules/test-pkg/lib/internal.js': '', + '/root/node_modules/test-pkg/lib/server.js': '', + '/root/node_modules/test-pkg/internal.js': '', + '/root/node_modules/test-pkg/server.js': '', + }), + originModulePath: '/root/src/main.js', + unstable_enablePackageExports: true, + unstable_logWarning: logWarning, + }; + + expect(Resolver.resolve(context, `test-pkg/${subpath}`, null)).toEqual({ + type: 'sourceFile', + filePath: `/root/node_modules/test-pkg/${subpath}.js`, + }); + expect(logWarning).toHaveBeenCalledTimes(1); + expect(logWarning.mock.calls[0][0]).toContain( + `"/root/node_modules/test-pkg/${subpath}" which is listed in the "exports"`, + ); + }, + ); + describe('package encapsulation', () => { test('[nonstrict] should fall back to "browser" spec resolution and log inaccessible import warning', () => { const logWarning = jest.fn(); From bbdc4c3786faaf4b652356b10da5f46a5318b985 Mon Sep 17 00:00:00 2001 From: kwy404 Date: Sun, 27 Sep 2026 05:05:52 -0300 Subject: [PATCH 3/3] Cite the spec step and skip the extra lookup for missing keys --- .../src/utils/matchSubpathFromExportsLike.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/metro-resolver/src/utils/matchSubpathFromExportsLike.js b/packages/metro-resolver/src/utils/matchSubpathFromExportsLike.js index 1c1c289efc..17e24f35e8 100644 --- a/packages/metro-resolver/src/utils/matchSubpathFromExportsLike.js +++ b/packages/metro-resolver/src/utils/matchSubpathFromExportsLike.js @@ -49,12 +49,16 @@ export function matchSubpathFromExportsLike( createConfigError, ); - let target = exportsLikeMapAfterConditions.get(subpath); + let target = null; let patternMatch = null; - // Attempt to match after expanding any subpath pattern keys, unless an exact - // key matched (a null target there hides the subpath, as in Node.js) - if (!exportsLikeMapAfterConditions.has(subpath)) { + // Attempt to match subpath pattern keys only if there is no exact key. An + // exact key takes precedence even when its target is null. See step 2 of + // `PACKAGE_IMPORTS_EXPORTS_RESOLVE` in: + // https://nodejs.org/api/esm.html#resolution-algorithm-specification + if (exportsLikeMapAfterConditions.has(subpath)) { + target = exportsLikeMapAfterConditions.get(subpath); + } else { // Gather keys which are subpath patterns in descending order of specificity // For ordering, see `PATTERN_KEY_COMPARE` in: // https://nodejs.org/api/esm.html#resolution-algorithm-specification