diff --git a/packages/metro-resolver/src/__tests__/browser-spec-test.js b/packages/metro-resolver/src/__tests__/browser-spec-test.js index c3122002ee..9b4f326008 100644 --- a/packages/metro-resolver/src/__tests__/browser-spec-test.js +++ b/packages/metro-resolver/src/__tests__/browser-spec-test.js @@ -99,4 +99,61 @@ describe('browser field spec', () => { }); }); }); + + describe('replace specific files', () => { + test('should resolve a bare-specifier redirect relative to the origin package root, not its containing directory', () => { + // Per the browser spec, paths in the `browser` map are relative to the + // package.json file location. When the origin module lives in a + // subdirectory of its package (here `lib/nested/`), the redirect target + // must still resolve against the package root. + const packageJson = { + name: 'origin-pkg', + main: 'lib/nested/index.js', + browser: { + 'foo-pkg': './shims/foo.js', + }, + }; + const context = { + ...createResolutionContext({ + '/root/node_modules/origin-pkg/package.json': + JSON.stringify(packageJson), + '/root/node_modules/origin-pkg/lib/nested/index.js': '', + '/root/node_modules/origin-pkg/shims/foo.js': '', + }), + originModulePath: '/root/node_modules/origin-pkg/lib/nested/index.js', + mainFields: ['browser', 'main'], + }; + + expect(Resolver.resolve(context, 'foo-pkg', null)).toEqual({ + type: 'sourceFile', + filePath: '/root/node_modules/origin-pkg/shims/foo.js', + }); + }); + + test('should resolve a bare-specifier redirect for an origin outside of `node_modules`', () => { + // Project-level package.json — there is no enclosing `node_modules` + // segment, so the old heuristic of slicing after `node_modules/` would + // misbehave. The redirect must resolve against the package root. + const context = { + ...createResolutionContext({ + '/root/project/package.json': JSON.stringify({ + name: 'project', + main: 'src/index.js', + browser: { + 'foo-pkg': './shims/foo.js', + }, + }), + '/root/project/src/index.js': '', + '/root/project/shims/foo.js': '', + }), + originModulePath: '/root/project/src/index.js', + mainFields: ['browser', 'main'], + }; + + expect(Resolver.resolve(context, 'foo-pkg', null)).toEqual({ + type: 'sourceFile', + filePath: '/root/project/shims/foo.js', + }); + }); + }); }); diff --git a/packages/metro-resolver/src/resolve.js b/packages/metro-resolver/src/resolve.js index 7f1fc583f4..ab8d9cb6cd 100644 --- a/packages/metro-resolver/src/resolve.js +++ b/packages/metro-resolver/src/resolve.js @@ -135,22 +135,16 @@ export default function resolve( // If the specifier was redirected to a relative path if ( maybeRedirectedSpecifier != null && + closestPackageToOrigin != null && // Implied by maybeRedirectedSpecifier != null isRelativeImport(maybeRedirectedSpecifier) ) { - // TODO: (robhogan) This isn't right - per browser spec: "All paths for - // browser fields are relative to the package.json file location". The - // *closest* package.json is the relevant one for browser spec, regardless - // of the closest node_modules. - - // derive absolute path /.../node_modules/originModuleDir/specifier - const fromModuleParentIdx = - originModulePath.lastIndexOf('node_modules' + path.sep) + 13; - const originModuleDir = originModulePath.slice( - 0, - originModulePath.indexOf(path.sep, fromModuleParentIdx), + // Per the "browser" spec: "All paths for browser fields are relative to + // the package.json file location". `closestPackageToOrigin` is the package + // that provided the redirect, so join relative paths to its `rootPath`. + const absPath = path.resolve( + closestPackageToOrigin.rootPath, + maybeRedirectedSpecifier, ); - - const absPath = path.join(originModuleDir, maybeRedirectedSpecifier); const result = resolveModulePath(context, absPath, platform); if (result.type === 'failed') { throw new FailedToResolvePathError(result.candidates);