Module Resolution
Module resolution is how Metro turns the module name in an import or require() into a file on disk, at build time. For example, in an iOS bundle:
// src/App.js
import {View} from 'react-native'; // → typically node_modules/react-native/index.js
import Button from './Button'; // → e.g. src/Button.ios.js
In short:
- Relative paths (
'./Button','../utils') are resolved against the importing file's directory. If no file exists at that exact path, Metro tries platform-specific and extension variants in order:Button.ios.js,Button.native.js,Button.js, then.jsxvariants, and so on throughsourceExts. Extensions take priority over platforms: with the defaultsourceExts,Button.jswins overButton.ios.ts. - Package names (
'react-native','lodash/merge') are looked up innode_modulesdirectories, from the importing file's directory up to the root. Metro uses the package's"exports"field if present; otherwise it resolves the subpath as a file, finding the entry point via main fields (in React Native:"react-native","browser", then"main"). - Other specifiers, such as subpath imports (
'#foo'), Haste names and URL schemes, are covered in the algorithm.
This is a version of Node's module resolution algorithm. Metro adds Haste (importing modules by a globally unique name, e.g. import Foo from 'Foo'), platform-specific extensions, asset extensions and image resolutions (so React Native can pick the best image for the device's screen density at runtime), and custom resolvers that can override almost everything (resolver.resolveRequest).
Read on if you're writing a custom resolver, debugging an "Unable to resolve module" error, or working out why Metro picked a particular file.
Resolution algorithm
Given a resolution context context (whose fields mostly come from your resolver config), a module name moduleName, and an optional platform identifier platform, Metro's resolver performs RESOLVE(context, moduleName, platform), which either returns one of the resolution types or throws an error.
Resolution types
A successful resolution, including from a custom resolver, is one of:
Source file
An absolute path to a file on disk, e.g. a JavaScript or TypeScript file.
Asset files
One or more absolute paths to files on disk, e.g. icon.png and icon@2x.png for an image at different screen densities.
Empty module
The built-in empty module specified in resolver.emptyModulePath, e.g. when a package's "browser" field maps a module to false.
Algorithm
These are the rules Metro's default resolver follows. See metro-resolver's source code for more details.
RESOLVE
Parameters: (context, moduleName, platform)
- If a custom resolver is defined, then
- Return the result of the custom resolver.
- If moduleName is an absolute path, or equal to
'.'or'..', or begins with'./'or'../', then- Let absoluteModuleName be moduleName if it is an absolute path, otherwise moduleName joined to the current directory (the parent of
context.originModulePath). - Return RESOLVE_MODULE(context, absoluteModuleName, platform), or throw a resolution failure error.
- Let absoluteModuleName be moduleName if it is an absolute path, otherwise moduleName joined to the current directory (the parent of
- If moduleName begins with
'#'(a subpath import), then- Find the closest
package.jsontocontext.originModulePath. If there is none, or it has no"imports"field, throw an error. - Resolve moduleName against the
"imports"field, asserting the same condition names as RESOLVE_PACKAGE_EXPORTS. If resolved, return the result. E.g."imports": {"#utils/*": "./src/utils/*.js"}maps'#utils/format'to<package root>/src/utils/format.js. - Otherwise, log a warning and continue from step 5.
- Find the closest
- If moduleName parses as a URL, let scheme be its lowercased scheme (the prefix before
':'), e.g.nodefor'node:fs'andfoofor'Foo:bar', then- If
context.schemeResolvershas a resolver for scheme, return its result for (context, moduleName, platform), with context.resolveRequest set to the default resolver for chaining. - Otherwise, continue, but if no later step resolves moduleName, throw a scheme-specific error at step 11 instead of a generic resolution failure. (This deprecated fallback provides backwards compatibility for scheme-like specifiers via Haste or
extraNodeModules.) E.g. with no resolver fornode,'node:fs'fails withNo resolver is registered for the 'node:' URI scheme.
- If
- Apply BROWSER_SPEC_REDIRECTION to moduleName.
- If this is
false, return the empty module. - If the result is a relative path, return RESOLVE_MODULE for it relative to the redirecting
package.json's directory, or throw a resolution failure error. E.g."browser": {"crypto": "./shims/crypto.js"}in/app/package.jsonmaps'crypto'from/app/src/App.jsto/app/shims/crypto.js. - If the result is an absolute path, throw an
InvalidPackageConfigurationError.
- If this is
- If Haste resolutions are allowed, then
- Get the result of RESOLVE_HASTE(context, moduleName, platform).
- If resolved as a Haste package path, then
- Return RESOLVE_PACKAGE(context, packageDir + relativePath, platform), where packageDir is the Haste package's directory and relativePath the rest of moduleName after the package name. Throw an error if this fails.
E.g. if the Haste package path for
'@a/b'isfoo/package.json, resolve'@a/b/c'asfoo/c.
- Return RESOLVE_PACKAGE(context, packageDir + relativePath, platform), where packageDir is the Haste package's directory and relativePath the rest of moduleName after the package name. Throw an error if this fails.
E.g. if the Haste package path for
- If
context.unstable_enablePackageExportsis enabled, then- Get the result of PACKAGE_SELF_RESOLVE(context, moduleName, platform).
- If resolved, return result.
- If
context.disableHierarchicalLookupis nottrue, then- Try resolving moduleName under
node_modulesfrom the current directory (the parent ofcontext.originModulePath) up to the root directory. E.g.'lodash/merge'from/app/src/screens/Home.jstries/app/src/screens/node_modules/lodash/merge, then/app/src/node_modules/…,/app/node_modules/…and/node_modules/…. - Perform RESOLVE_PACKAGE(context, modulePath, platform) for each candidate path.
- Try resolving moduleName under
- For each element nodeModulesPath of
context.nodeModulesPaths:- Try resolving moduleName under nodeModulesPath as if it were another
node_modulesdirectory (similar to step 8). E.g.nodeModulesPaths: ['/monorepo/node_modules']tries/monorepo/node_modules/lodash/merge. - Perform RESOLVE_PACKAGE(context, modulePath, platform) for each candidate path.
- Try resolving moduleName under nodeModulesPath as if it were another
- If
context.extraNodeModulesis set:- Split moduleName into a package name (including an optional scope) and relative path.
- Look up the package name in
context.extraNodeModules. If found, then- Let modulePath be moduleName with its package name replaced by the value found. E.g.
extraNodeModules: {'shared': '/monorepo/shared'}maps'shared/utils'to/monorepo/shared/utils. - Return RESOLVE_PACKAGE(context, modulePath, platform).
- Let modulePath be moduleName with its package name replaced by the value found. E.g.
- If no valid resolution has been found, throw a resolution failure error — a scheme-specific error if step 4.2 applied, otherwise a generic one.
RESOLVE_MODULE
Parameters: (context, moduleName, platform)
Resolves an absolute path to a file or a directory's entry point. For example, on iOS, /app/src/Header resolves to the first that exists of (relative to /app/src):
Header.ios.js,Header.native.js,Header.js, … (step 2)Header/index.ios.js,Header/index.native.js, … ifHeader/has nopackage.json(step 4)Header/lib/main.ios.js, … ifHeader/package.jsonhas"main": "./lib/main"(step 5)
- Let filePath be the result of applying BROWSER_SPEC_REDIRECTION to moduleName (possibly a replacement subpath from a containing
package.json).- If this is
false, return the empty module.
- If this is
- Unless filePath ends with a path separator, return RESOLVE_FILE(context, filePath, platform), or continue.
- Otherwise, let dirPath be filePath. If dirPath is not an existing directory, throw an error.
- If a file dirPath +
'/package.json'does not exist, return RESOLVE_FILE(context, dirPath +'/index', platform), or throw an error. - Otherwise, resolve based on the
browserfield spec:- Let mainModulePath be the package's entry path, read using
context.mainFields. - Return RESOLVE_FILE(context, mainModulePath, platform), or continue.
- Return RESOLVE_FILE(context, mainModulePath +
'/index', platform). - Throw an error if no resolution could be found.
- Let mainModulePath be the package's entry path, read using
RESOLVE_PACKAGE
Parameters: (context, moduleName, platform)
Resolves an absolute path that may be inside an npm package, e.g. a candidate path under node_modules. Let filePath be moduleName, packagePath the directory of the closest containing package.json, and exportsField its "exports" field. E.g. for 'some-pkg/utils' from /app/src/App.js, RESOLVE step 8 passes filePath /app/node_modules/some-pkg/utils, so packagePath is /app/node_modules/some-pkg and the "exports" subpath is './utils'.
- If
context.unstable_enablePackageExportsis enabled and a containingpackage.jsonhas an"exports"field, get the result of RESOLVE_PACKAGE_EXPORTS(context, packagePath, filePath, exportsField, platform).- If resolved path exists, return result.
- Else, log either a package configuration or package encapsulation warning.
- Return RESOLVE_MODULE(context, filePath, platform).
PACKAGE_SELF_RESOLVE
Parameters: (context, moduleName, platform)
Lets code inside a package import that package by its own name via its "exports" field, matching Node's self-referencing behavior. E.g. if my-lib/package.json has "name": "my-lib" and "exports": {"./utils": "./src/utils.js"}, then 'my-lib/utils' from my-lib/src/index.js resolves to my-lib/src/utils.js, even if my-lib is not in any node_modules directory.
- Find the closest package to
context.originModulePath. If no package is found, return no resolution. - If the package does not declare both a
"name"field and an"exports"field, return no resolution. - Split moduleName into a package name (including an optional scope) and relative path.
- If the package name does not match the current package's
"name"field, return no resolution. - Get the result of RESOLVE_PACKAGE_EXPORTS(context, packagePath, filePath, exportsField, platform) against the current package's
"exports"field.- If resolved path exists, return result.
- Else, log either a package configuration or package encapsulation warning, then continue with normal resolution.
RESOLVE_PACKAGE_EXPORTS
Parameters: (context, packagePath, filePath, exportsField, platform)
Resolves a package subpath based on the Package Entry Points spec (the
"exports"field), whenresolver.unstable_enablePackageExportsis enabled.
- Let subpath be the relative path from packagePath to filePath, or
'.'. - If exportsField contains an invalid configuration or values, raise an
InvalidPackageConfigurationError. - If subpath is not defined by exportsField, raise a
PackagePathNotExportedError. - Let target be the result of matching subpath in exportsField after applying any conditional exports and/or substituting a subpath pattern match.
- Condition names will be asserted from the union of
'default','import'OR'require'according tocontext.isESMImport,context.unstable_conditionNamesandcontext.unstable_conditionsByPlatformfor platform, in the order defined by exportsField. E.g. for an iOSimportwith React Native's defaultunstable_conditionNames: ['react-native'], the asserted conditions are'default','import'and'react-native', so{"require": "./a.js", "react-native": "./b.js", "import": "./c.js"}resolves to./b.js, the first matching key.
- Condition names will be asserted from the union of
- If target refers to an asset, then
- Return RESOLVE_ASSET(context, target, platform).
- Return target as a source file resolution without applying redirections or trying any platform or extension variants.
RESOLVE_FILE
Parameters: (context, filePath, platform)
Applies platform-specific extensions and sourceExts to resolve a path to a file.
- If the path refers to an asset, then
- Return RESOLVE_ASSET(context, filePath, platform).
- Otherwise, if the path exists as a file, return a source file resolution for it.
- Otherwise:
- Try all platform and extension variants in sequence. Return a source file resolution for the first that exists after applying BROWSER_SPEC_REDIRECTION. For example, if platform is
androidandcontext.sourceExtsis['js', 'jsx'], try:- moduleName +
'.android.js' - moduleName +
'.native.js'(ifcontext.preferNativePlatformistrue) - moduleName +
'.js' - moduleName +
'.android.jsx' - moduleName +
'.native.jsx'(ifcontext.preferNativePlatformistrue) - moduleName +
'.jsx'
- moduleName +
- Try all platform and extension variants in sequence. Return a source file resolution for the first that exists after applying BROWSER_SPEC_REDIRECTION. For example, if platform is
RESOLVE_ASSET
Parameters: (context, filePath, platform)
- Use
context.resolveAssetto collect all asset variants (e.g. for different screen densities). - Return an asset resolution containing the collected asset paths.
RESOLVE_HASTE
Parameters: (context, moduleName, platform)
Looks up moduleName as a Haste module or package.
- If moduleName contains no
'/', try resolving it as a Haste module. If found, then- Return result as a source file resolution without applying redirections or trying any platform or extension variants.
- Split moduleName into a package name (including an optional scope) and relative path, and try resolving the package name as a Haste (global) package.
For example,
'a/b/c'tries package'a'with relative path'./b/c', and'@a/b/c'tries package'@a/b'with relative path'./c'.
BROWSER_SPEC_REDIRECTION
Parameters: (context, moduleName)
Applies browser field spec redirections (file to file, or module to empty module via false) from the closest package.json to the origin or target module. Metro reads them from every field in context.mainFields, not just "browser".
- Find the closest
package.jsonto moduleName, if moduleName is absolute, or tocontext.originModulePathotherwise, stopping at anynode_modules. Let packageScope be its containing directory. - If none is found, return moduleName.
- Define subpath:
- If moduleName begins with
'.', consider it relative tocontext.originModulePathand let subpath be the same path relative to packageScope, prefixed'./'. - Else if moduleName is absolute, let subpath be moduleName relative to packageScope, prefixed
'./'. - Else let subpath be moduleName.
- If moduleName begins with
- Merge the object values of each of
context.mainFieldsin thepackage.jsonat packageScope into a single map mainFieldsMap, with earlier fields taking precedence. Let expandedSubpath range over subpath, subpath +'.js'and subpath +'.json'.- For each expandedSubpath:
- If mainFieldsMap has the key expandedSubpath, let redirectedPath be its value, else continue.
- If moduleName is absolute or begins with
'.', and redirectedPath is a string:- If redirectedPath is an absolute path, return redirectedPath.
- Return packageScope joined with redirectedPath.
- Return redirectedPath.
- For each expandedSubpath:
- Return moduleName.
For example, if /app/node_modules/foo/package.json has "browser": {"./lib/server.js": "./lib/client.js", "fs": false}, then for imports from /app/node_modules/foo/lib/index.js:
'./server'has subpath'./lib/server', which matches the key'./lib/server.js', so it is redirected to/app/node_modules/foo/lib/client.js.'fs'has subpath'fs', which is mapped tofalse(the empty module).
Resolution context
The object passed to the resolver for each request, e.g. as the first argument to a custom resolver. Most fields come from your resolver config. Custom resolvers most often read originModulePath and customResolverOptions, and call resolveRequest to fall back to the default resolver.
assetExts: $ReadOnlySet<string>
The set of file extensions used to identify asset files. Defaults to resolver.assetExts.
dev: boolean
true for a development bundle, false otherwise.
doesFileExist: string => boolean Deprecated
Returns whether the file at the given path exists. The default implementation wraps fileSystemLookup; prefer using that directly.
fileSystemLookup: string => {exists: true, type: 'f'|'d', realPath: string} | {exists: false}
Added in v0.81.0
Returns information about the given absolute or project-relative path, following symlinks. A file "exists" only if Metro knows about it (see project root and watch folders), and a directory must be non-empty. The returned realPath is real and absolute; e.g. for a directory symlinked by pnpm, fileSystemLookup('/app/node_modules/foo') returns {exists: true, type: 'd', realPath: '/app/node_modules/.pnpm/foo@1.0.0/node_modules/foo'}. Missing paths return {exists: false}.
By default, Metro uses an in-memory file map prepared in advance, so resolution needs no disk I/O and real paths are almost free.
nodeModulesPaths: $ReadOnlyArray<string>
Paths to check for modules after looking through all node_modules directories. Defaults to resolver.nodeModulesPaths.
preferNativePlatform: boolean
If true, try .native.${ext} after .${platform}.${ext} and before .${ext}. Metro sets this to true, which is why it picks Foo.native.js when there is no Foo.ios.js (or other Foo.${platform}.js).
redirectModulePath: string => string | false Deprecated
Defaults to BROWSER_SPEC_REDIRECTION, which Metro's default resolver uses directly instead of calling this function. It is exposed for backwards-compatible use by custom resolvers, and will be removed in a future release.
resolveAsset: (dirPath: string, assetName: string, extension: string) => ?$ReadOnlyArray<string>
Returns all asset files in dirPath matching assetName and extension, or null if there are none. The default implementation considers ${assetName}${extension} and, for each of resolver.assetResolutions, ${assetName}@${resolution}x${extension}. For example, resolveAsset('/app/img', 'logo', '.png') may return ['/app/img/logo.png', '/app/img/logo@2x.png', '/app/img/logo@3x.png']. See also Static Image Resources.
sourceExts: $ReadOnlyArray<string>
File extensions to try, in order, when resolving a module path that does not exist on disk. Defaults to resolver.sourceExts.
mainFields: $ReadOnlyArray<string>
The ordered list of package.json fields read to resolve a package's main entry point (and any subpath file replacements) per the "browser" field spec. Defaults to resolver.resolverMainFields.
getPackage: string => ?PackageJson
Given the path to a package.json file, returns the parsed contents, or null if the file does not exist or is malformed.
getPackageForModule: (absoluteModulePath: string) => ?PackageForModule Deprecated
Given a candidate absolute module path, which need not exist, returns the closest package root (searching upwards, stopping at the nearest node_modules), its parsed package.json, and the package-relative path, or null if there's no package.json before the nearest node_modules. A path through a symlinked directory checks the link's target, then continues up from the link's parent; a symlinked file is scoped by where the link is. A missing path takes the scope of its deepest existing ancestor, or null if the first missing segment is node_modules. E.g. getPackageForModule('/app/node_modules/foo/lib/index.js') returns {rootPath: '/app/node_modules/foo', packageJson: {...}, packageRelativePath: 'lib/index.js'}.
isESMImport?: boolean
Whether the dependency was declared with an ESM import (import x from 'y' or await import('z')) rather than require(), the criteria Node.js uses to assert the "import" rather than the "require" condition.
When dependency is provided, this always equals dependency.data.isESMImport. Unlike dependency, it may be used to decide the resolution.
resolveHasteModule: string => ?string
Resolves a Haste module name to an absolute path, or null if no such module exists. The default implementation uses metro-file-map's getModule method.
resolveHastePackage: string => ?string
Resolves a Haste (global) package name to an absolute package.json path, or null if no such package exists. The default implementation uses metro-file-map's getPackage method and can be turned on or off using resolver.enableGlobalPackages.
allowHaste: boolean
true if Haste resolutions are allowed in the current context, false otherwise.
disableHierarchicalLookup: boolean
If true, the resolver does not look up modules in node_modules directories per the Node resolution algorithm. Defaults to resolver.disableHierarchicalLookup.
extraNodeModules: ?{[string]: string}
A mapping of package names to directories, consulted after the standard node_modules lookup and any nodeModulesPaths.
originModulePath: string
The path of the module containing the import or require() being resolved.
customResolverOptions: {[string]: mixed}
Any custom options passed to the resolver. By default, Metro populates this from URL parameters in the bundle request, e.g. http://localhost:8081/index.bundle?resolver.key=value becomes {key: 'value'}.
resolveRequest: CustomResolver
An alternative resolver function to which the current request may be delegated. Defaults to resolver.resolveRequest. Its signature is:
function resolveRequest(
context: ResolutionContext,
moduleName: string,
platform: string | null,
): Resolution {
// ...
}
type Resolution =
| {type: 'empty'}
| {type: 'sourceFile', filePath: string}
| {type: 'assetFiles', filePaths: $ReadOnlyArray<string>};
Returned paths (filePath and filePaths) must be absolute and real, such as the realPath returned by fileSystemLookup.
If resolveRequest is non-null when the default resolver is called, it is treated as a custom resolver that is always called and fully replaces the default logic (see RESOLVE step 1). Inside a custom resolver, resolveRequest is the default resolver, for easy chaining. For example, to resolve lodash as lodash-es and leave everything else to the default resolver:
// metro.config.js, in `resolver`
resolveRequest: (context, moduleName, platform) => {
if (moduleName === 'lodash') {
return context.resolveRequest(context, 'lodash-es', platform);
}
return context.resolveRequest(context, moduleName, platform);
},
Custom resolvers must follow the caching rules.
schemeResolvers: Readonly<{[scheme: string]: CustomResolver}>
Custom resolvers for specifiers prefixed with a URI scheme, keyed by scheme name (the part before the first ':', without the colon). Schemes are lowercased before lookup, so keys must be lowercase (both Foo: and foo: match 'foo'). Defaults to Metro's built-in scheme resolvers (currently metro:) merged with resolver.schemeResolvers, which take precedence.
A matching resolver is called with the full specifier (e.g. my-scheme:foo for key 'my-scheme') and can chain back into default resolution via context.resolveRequest, e.g. to resolve a relative path.
Relative (./, ../) and subpath (#…) imports are handled first and never treated as schemes. See RESOLVE step 4, and resolver.schemeResolvers for precedence relative to resolveRequest.
unstable_conditionNames: $ReadOnlyArray<string>
Additional condition names asserted when resolving the "exports" and "imports" fields. Defaults to resolver.unstable_conditionNames.
unstable_conditionsByPlatform: {[platform: string]: $ReadOnlyArray<string>}
Additional condition names asserted for a given platform when resolving the "exports" and "imports" fields. Defaults to resolver.unstable_conditionsByPlatform. For example, Metro's default {web: ['browser']} asserts 'browser' only when bundling for web.
unstable_enablePackageExports: boolean
Whether the "exports" field is used for resolution. Defaults to resolver.unstable_enablePackageExports.
dependency: ?Dependency
A descriptor of the dependency being resolved. It is for diagnostic purposes only and may not be used for semantic purposes (see Caching).
type Dependency = {
// The literal specifier, e.g. 'foo' for `require('foo')`.
name: string,
data: {
// A locally unique key for this dependency within the origin module.
key: string,
// Babel AST source locations of this dependency in the origin module. May be empty.
locs: $ReadOnlyArray<BabelSourceLocation>,
// E.g. 'async' for `import('foo')`, 'weak' for `require.resolveWeak('foo')`,
// and null for `import` and `require()`.
asyncType: 'async' | 'maybeSync' | 'prefetch' | 'weak' | null,
// Other properties are internal and may change.
...
},
};
Caching
Metro caches and reuses resolver results, which matters mainly for custom resolvers. Results may be cached under these conditions:
- For given origin module paths A and B and target module name M, the resolution for M may be reused if all of the following conditions hold:
- A and B are in the same directory.
- The contents of
devandcustomResolverOptionsare equivalent ( = serialize to JSON the same) in both calls to the resolver. - The
platformis the same in both calls. isESMImportis the same in both calls.
- Any cache of resolutions must be invalidated if any file in the project has changed.
Custom resolvers must adhere to these assumptions, e.g. they must not resolve './config' differently for src/App.js and src/App.test.js under the same customResolverOptions, because Metro may reuse either result for the other.