diff --git a/docs/Resolution.md b/docs/Resolution.md index 2959dea86e..9cea0eb5be 100644 --- a/docs/Resolution.md +++ b/docs/Resolution.md @@ -66,7 +66,7 @@ Parameters: (*context*, *moduleName*, *platform*) 2. Otherwise, attempt to resolve *moduleName* as a path 1. Let *absoluteModuleName* be the result of prepending the current directory (i.e. parent of [`context.originModulePath`](#originmodulepath-string)) with *moduleName*. 2. Return the result of [**RESOLVE_MODULE**](#resolve_module)(*context*, *absoluteModuleName*, *platform*), or continue. -3. Apply [redirections](#redirectmodulepath-string--string--false) to *moduleName*. If this results in an [empty module](#empty-module), then +3. Apply [**BROWSER_SPEC_REDIRECTION**](#browser_spec_redirection) to *moduleName*. If this is `false`: 1. Return the empty module. 4. If [Haste resolutions are allowed](#allowhaste-boolean), then 1. Get the result of [**RESOLVE_HASTE**](#resolve_haste)(*context*, *moduleName*, *platform*). @@ -90,7 +90,7 @@ Parameters: (*context*, *moduleName*, *platform*) Parameters: (*context*, *moduleName*, *platform*) -1. Let *filePath* be the result of applying [redirections](#redirectmodulepath-string--string--false) to *moduleName*. This may locate a replacement subpath from a containing `package.json` file based on the [`browser` field spec](https://github.com/defunctzombie/package-browser-field-spec). +1. Let *filePath* be the result of applying [**BROWSER_SPEC_REDIRECTION**](#browser_spec_redirection) to *moduleName*. This may locate a replacement subpath from a containing `package.json` file based on the [`browser` field spec](https://github.com/defunctzombie/package-browser-field-spec). 2. Return the result of [**RESOLVE_FILE**](#resolve_file)(*context*, *filePath*, *platform*), or continue. 3. Otherwise, let *dirPath* be the directory path of *filePath*. 4. If a file *dirPath* + `'package.json'` exists, resolve based on the [`browser` field spec](https://github.com/defunctzombie/package-browser-field-spec): @@ -130,7 +130,7 @@ Parameters: (*context*, *filePath*, *platform*) 1. If the path refers to an [asset](#assetexts-readonlysetstring), then 1. Return the result of [**RESOLVE_ASSET**](#resolve_asset)(*context*, *filePath*, *platform*). 2. Otherwise, if the path [exists](#doesfileexist-string--boolean), then - 1. Try all platform and extension variants in sequence. Return a [source file resolution](#source-file) for the first one that [exists](#doesfileexist-string--boolean) after applying [redirections](#redirectmodulepath-string--string--false). For example, if _platform_ is `android` and [`context.sourceExts`](#sourceexts-readonlyarraystring) is `['js', 'jsx']`, try this sequence of potential file names: + 1. Try all platform and extension variants in sequence. Return a [source file resolution](#source-file) for the first one that [exists](#doesfileexist-string--boolean) after applying [**BROWSER_SPEC_REDIRECTION**](#browser_spec_redirection). For example, if _platform_ is `android` and [`context.sourceExts`](#sourceexts-readonlyarraystring) is `['js', 'jsx']`, try this sequence of potential file names: 1. _moduleName_ + `'.android.js'` 2. _moduleName_ + `'.native.js'` (if [`context.preferNativePlatform`](#prefernativeplatform-boolean) is `true`) 3. _moduleName_ + `'.js'` @@ -213,14 +213,12 @@ By default this is set to [`resolver.nodeModulesPaths`](./Configuration.md#nodem If `true`, try `.native.${ext}` before `.${ext}` and after `.${platform}.${ext}` during resolution. Metro sets this to `true`. -#### `redirectModulePath: string => string | false` - -Rewrites a module path, or returns `false` to redirect to the special [empty module](#empty-module). In the default resolver, the resolution algorithm terminates with an [empty module result](#empty-module) if `redirectModulePath` returns `false`. - -Metro uses this to implement the `package.json` [`browser` field spec](https://github.com/defunctzombie/package-browser-field-spec), particularly the ability to [replace](https://github.com/defunctzombie/package-browser-field-spec#replace-specific-files---advanced) and [ignore](https://github.com/defunctzombie/package-browser-field-spec#ignore-a-module) specific files. +#### `redirectModulePath: string => string | false`
Deprecated
The default implementation of this function is specified by [**BROWSER_SPEC_REDIRECTION**](#browser_spec_redirection). +Metro's default resolver does not call this function, instead using the [**BROWSER_SPEC_REDIRECTION**](#browser_spec_redirection) implementation directly. It is exposed here for backwards-compatible use by custom resolvers, but is considered deprecated and will be removed in a future release. + #### `resolveAsset: (dirPath: string, assetName: string, extension: string) => ?$ReadOnlyArray` Given a directory path, the base asset name and an extension, returns a list of all the asset file names that match the given base name in that directory, or `null` if no such files are found. The default implementation considers each of [`resolver.assetResolutions`](./Configuration.md#assetresolutions) and uses the `${assetName}@${resolution}${extension}` format for asset variant file names. diff --git a/packages/metro-resolver/src/PackageResolve.js b/packages/metro-resolver/src/PackageResolve.js index f42d63b1ae..730ef0c42f 100644 --- a/packages/metro-resolver/src/PackageResolve.js +++ b/packages/metro-resolver/src/PackageResolve.js @@ -69,8 +69,6 @@ export function getPackageEntryPoint( * * Implements legacy (non-exports) package resolution behaviour based on the * ["browser" field spec](https://github.com/defunctzombie/package-browser-field-spec). - * - * This is the default implementation of `context.redirectModulePath`. */ export function redirectModulePath( context: $ReadOnly<{ diff --git a/packages/metro-resolver/src/__tests__/index-test.js b/packages/metro-resolver/src/__tests__/index-test.js index b9dd6f9fa0..ea5c98e4ab 100644 --- a/packages/metro-resolver/src/__tests__/index-test.js +++ b/packages/metro-resolver/src/__tests__/index-test.js @@ -15,8 +15,7 @@ import type {ResolutionContext} from '../index'; import {createResolutionContext} from './utils'; -const FailedToResolvePathError = require('../errors/FailedToResolvePathError'); -const Resolver = require('../index'); +let Resolver = require('../index'); const fileMap = { '/root/project/foo.js': '', @@ -107,7 +106,7 @@ test('resolves a relative path in another folder', () => { test('does not resolve a relative path ending in a slash as a file', () => { expect(() => Resolver.resolve(CONTEXT, './bar/', null)).toThrow( - new FailedToResolvePathError({ + new Resolver.FailedToResolvePathError({ file: null, dir: { type: 'sourceFile', @@ -137,7 +136,7 @@ test('fails to resolve a relative path', () => { Resolver.resolve(CONTEXT, './apple', null); throw new Error('should not reach'); } catch (error) { - if (!(error instanceof FailedToResolvePathError)) { + if (!(error instanceof Resolver.FailedToResolvePathError)) { throw error; } expect(error.candidates).toEqual({ @@ -209,7 +208,10 @@ test('does not resolve to additional `node_modules` if `nodeModulesPaths` is not }); test('uses `nodeModulesPaths` to find additional node_modules not in the direct path', () => { - const context = {...CONTEXT, nodeModulesPaths: ['/other-root/node_modules']}; + const context = { + ...CONTEXT, + nodeModulesPaths: ['/other-root/node_modules'], + }; expect(Resolver.resolve(context, 'banana', null)).toEqual({ type: 'sourceFile', filePath: '/other-root/node_modules/banana/main.js', @@ -339,12 +341,29 @@ test('throws a descriptive error when a file inside a Haste package cannot be re }); describe('redirectModulePath', () => { - const redirectModulePath = jest.fn(); - const context = {...CONTEXT, redirectModulePath}; + const mockRedirectModulePath = jest.fn(); + const context = CONTEXT; beforeEach(() => { - redirectModulePath.mockReset(); - redirectModulePath.mockImplementation(filePath => false); + mockRedirectModulePath.mockReset(); + mockRedirectModulePath.mockImplementation(filePath => false); + + jest.resetModules(); + jest.mock('../PackageResolve', () => { + return { + ...jest.requireActual('../PackageResolve'), + redirectModulePath: (_ctx, specifier) => + mockRedirectModulePath(specifier), + }; + }); + + Resolver = require('../index'); + }); + + afterEach(() => { + jest.unmock('../PackageResolve'); + jest.resetModules(); + Resolver = require('../index'); }); test('is used for relative path requests', () => { @@ -353,8 +372,8 @@ describe('redirectModulePath', () => { "type": "empty", } `); - expect(redirectModulePath).toBeCalledTimes(1); - expect(redirectModulePath).toBeCalledWith('/root/project/bar'); + expect(mockRedirectModulePath).toBeCalledTimes(1); + expect(mockRedirectModulePath).toBeCalledWith('/root/project/bar'); }); test('is used for absolute path requests', () => { @@ -363,8 +382,8 @@ describe('redirectModulePath', () => { "type": "empty", } `); - expect(redirectModulePath).toBeCalledTimes(1); - expect(redirectModulePath).toBeCalledWith('/bar'); + expect(mockRedirectModulePath).toBeCalledTimes(1); + expect(mockRedirectModulePath).toBeCalledWith('/bar'); }); test('is used for non-Haste package requests', () => { @@ -374,12 +393,12 @@ describe('redirectModulePath', () => { "type": "empty", } `); - expect(redirectModulePath).toBeCalledTimes(1); - expect(redirectModulePath).toBeCalledWith('does-not-exist'); + expect(mockRedirectModulePath).toBeCalledTimes(1); + expect(mockRedirectModulePath).toBeCalledWith('does-not-exist'); }); - test('can be used to redirect to an arbitrary relative module', () => { - redirectModulePath + test('can redirect to an arbitrary relative module', () => { + mockRedirectModulePath .mockImplementationOnce(filePath => '../smth/beep') .mockImplementation(filePath => filePath); expect(Resolver.resolve(context, 'does-not-exist', null)) @@ -389,7 +408,7 @@ describe('redirectModulePath', () => { "type": "sourceFile", } `); - expect(redirectModulePath.mock.calls).toMatchInlineSnapshot(` + expect(mockRedirectModulePath.mock.calls).toMatchInlineSnapshot(` Array [ Array [ "does-not-exist", @@ -405,7 +424,7 @@ describe('redirectModulePath', () => { }); test("is called for source extension candidates that don't exist on disk", () => { - redirectModulePath.mockImplementation(filePath => + mockRedirectModulePath.mockImplementation(filePath => filePath.replace('.another-fake-ext', '.js'), ); expect( @@ -420,7 +439,7 @@ describe('redirectModulePath', () => { "type": "sourceFile", } `); - expect(redirectModulePath.mock.calls).toMatchInlineSnapshot(` + expect(mockRedirectModulePath.mock.calls).toMatchInlineSnapshot(` Array [ Array [ "/root/smth/beep", @@ -436,7 +455,7 @@ describe('redirectModulePath', () => { }); test('can resolve to empty from a candidate with an added source extension', () => { - redirectModulePath.mockImplementation(filePath => + mockRedirectModulePath.mockImplementation(filePath => filePath.endsWith('.fake-ext') ? false : filePath, ); expect( @@ -450,7 +469,7 @@ describe('redirectModulePath', () => { "type": "empty", } `); - expect(redirectModulePath.mock.calls).toMatchInlineSnapshot(` + expect(mockRedirectModulePath.mock.calls).toMatchInlineSnapshot(` Array [ Array [ "/root/smth/beep", @@ -463,14 +482,14 @@ describe('redirectModulePath', () => { }); test('is not called redundantly for a candidate that does exist on disk', () => { - redirectModulePath.mockImplementation(filePath => filePath); + mockRedirectModulePath.mockImplementation(filePath => filePath); expect(Resolver.resolve(context, './bar', null)).toMatchInlineSnapshot(` Object { "filePath": "/root/project/bar.js", "type": "sourceFile", } `); - expect(redirectModulePath.mock.calls).toMatchInlineSnapshot(` + expect(mockRedirectModulePath.mock.calls).toMatchInlineSnapshot(` Array [ Array [ "/root/project/bar", diff --git a/packages/metro-resolver/src/resolve.js b/packages/metro-resolver/src/resolve.js index 0604ccd105..485df78fb6 100644 --- a/packages/metro-resolver/src/resolve.js +++ b/packages/metro-resolver/src/resolve.js @@ -26,7 +26,7 @@ import InvalidPackageConfigurationError from './errors/InvalidPackageConfigurati import InvalidPackageError from './errors/InvalidPackageError'; import PackagePathNotExportedError from './errors/PackagePathNotExportedError'; import {resolvePackageTargetFromExports} from './PackageExportsResolve'; -import {getPackageEntryPoint} from './PackageResolve'; +import {getPackageEntryPoint, redirectModulePath} from './PackageResolve'; import resolveAsset from './resolveAsset'; import isAssetFile from './utils/isAssetFile'; import path from 'path'; @@ -57,7 +57,7 @@ function resolve( return result.resolution; } - const realModuleName = context.redirectModulePath(moduleName); + const realModuleName = redirectModulePath(context, moduleName); // exclude if (realModuleName === false) { @@ -155,7 +155,7 @@ function resolve( .filter(Boolean) .concat(extraPaths); for (let i = 0; i < allDirPaths.length; ++i) { - const candidate = context.redirectModulePath(allDirPaths[i]); + const candidate = redirectModulePath(context, allDirPaths[i]); if (candidate === false) { return {type: 'empty'}; @@ -224,7 +224,7 @@ function resolveModulePath( ? toModuleName : toModuleName.replaceAll('/', '\\') : path.join(path.dirname(context.originModulePath), toModuleName); - const redirectedPath = context.redirectModulePath(modulePath); + const redirectedPath = redirectModulePath(context, modulePath); if (redirectedPath === false) { return resolvedAs({type: 'empty'}); } @@ -565,7 +565,7 @@ function resolveSourceFileForExt( const filePath = `${context.filePathPrefix}${extension}`; const redirectedPath = // Any redirections for the bare path have already happened - extension !== '' ? context.redirectModulePath(filePath) : filePath; + extension !== '' ? redirectModulePath(context, filePath) : filePath; if (redirectedPath === false) { return {type: 'empty'}; }