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(); 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..17e24f35e8 100644 --- a/packages/metro-resolver/src/utils/matchSubpathFromExportsLike.js +++ b/packages/metro-resolver/src/utils/matchSubpathFromExportsLike.js @@ -49,11 +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 - if (target == null) { + // 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