Skip to main content

Module Resolution

Module resolution is the process of translating module names to module paths at build time. For example, if your project contains the code:

// src/App.js
import {View} from 'react-native';
// ...

Metro needs to know where in your project to load the react-native module from. This will typically resolve to something like node_modules/react-native/index.js.

Likewise, if your project contains the (similar) code:

// src/App.js
import Comp from './Component';
// ...

Metro needs to understand that you are referring to, say, src/Component.js, and not another file named Component that may also exist elsewhere.

Metro implements a version of Node's module resolution algorithm, augmented with additional Metro-specific features.

These Metro-specific features include:

  • Haste: An opt-in mechanism for importing modules by their globally-unique name anywhere in the project, e.g. import Foo from 'Foo'.
  • Platform extensions: Used by React Native to allow developers to write platform-specific versions of their JavaScript modules.
  • Asset extensions and image resolutions: Used by React Native to automatically select the best version of an image asset based on the device's screen density at runtime.
  • Custom resolvers: Metro integrators can provide their own resolver implementations to override almost everything about how modules are resolved.

Resolution algorithm​

Given a resolution context context, a module name moduleName, and an optional platform identifier platform, Metro's resolver either returns one of the resolution types, or throws an error.

Resolution types​

Source file​

The request is resolved to some absolute path representing a physical file on disk.

Asset files​

The request is resolved to one or more absolute paths representing physical files on disk.

Empty module​

The request is resolved to a built-in empty module, namely the one specified in resolver.emptyModulePath.

Algorithm​

note

These are the rules that Metro's default resolver follows. Refer to metro-resolver's source code for more details.

  1. If a custom resolver is defined, call it and return the result.

  2. Otherwise, try to resolve moduleName as a relative or absolute path:

    1. If the path is relative, convert it to an absolute path by prepending the current directory (i.e. parent of context.originModulePath).

    2. If the path refers to an asset:

      1. Use context.resolveAsset to collect all asset variants.
      2. Return an asset resolution containing the collected asset paths.
    3. If the path refers to a file that exists after applying redirections, return it as a source file resolution.

    4. Try all platform and extension variants in sequence. Return a source file resolution for the first one that exists after applying redirections. For example, if platform is android and context.sourceExts is ['js', 'jsx'], try this sequence of potential file names:

      1. moduleName + '.android.js'
      2. moduleName + '.native.js' (if context.preferNativePlatform is true)
      3. moduleName + '.android.jsx'
      4. moduleName + '.native.jsx' (if context.preferNativePlatform is true)
    5. If a file named moduleName + '/package.json' exists:

      1. Get the package's entry path.
      2. Try to resolve the entry path as a file, after applying redirections and trying all platform and extension variants as described above.
      3. Try to resolve the entry path + '/index' as a file, after applying redirections and trying all platform and extension variants as described above.
      4. Throw an error if no resolution could be found.
    6. Try to resolve moduleName + '/index' as a file, after applying redirections and trying all platform and extension variants as described above.

  1. Apply redirections to moduleName. Skip the rest of this algorithm if this results in an empty module.

  2. If Haste resolutions are allowed:

    1. Try resolving moduleName as a Haste module. If found, return it as a source file resolution without applying redirections or trying any platform or extension variants.

    2. Try resolving moduleName as a Haste package, or a path relative to a Haste package. For example, if moduleName is 'a/b/c', try the following potential Haste package names:

      1. 'a/b/c', relative path ''
      2. 'a/b', relative path './c'
      3. 'a', with relative path './b/c'
    3. If resolved as a Haste package path, perform the algorithm for resolving a path (step 2 above). Throw an error if this resolution fails. For example, if the Haste package path for 'a/b' is foo/package.json, perform step 2 as if moduleName was foo/c.

  3. If context.disableHierarchicalLookup is not true:

    1. Try resolving moduleName under node_modules from the current directory (i.e. parent of context.originModulePath) up to the root directory.
    2. Perform the algorithm for resolving a path (step 2 above) for each candidate path.
  4. For each element nodeModulesPath of context.nodeModulesPaths:

    1. Try resolving moduleName under nodeModulesPath as if the latter was another node_modules directory (similar to step 5 above).
    2. Perform the algorithm for resolving a path (step 2 above) for each candidate path.
  5. If context.extraNodeModules is set:

    1. Split moduleName into a package name (including an optional scope) and relative path.
    2. Look up the package name in context.extraNodeModules. If found, construct a path by replacing the package name part of moduleName with the value found in context.extraNodeModules, and perform the algorithm for resolving a path (step 2 above).
  6. If no valid resolution has been found, throw a resolution failure error.

Resolution context​

doesFileExist: string => boolean​

Returns true if the file with the given path exists, or false otherwise.

By default, Metro implements this by consulting an in-memory map of the filesystem that has been prepared in advance. This approach avoids disk I/O during module resolution.

isAssetFile: string => boolean​

Returns true if the given path represents an asset file, or false otherwise.

By default, Metro implements this by checking the file's extension against resolver.assetExts.

nodeModulesPaths: $ReadOnlyArray<string>​

A list of paths to check for modules after looking through all node_modules directories.

By default this is set to resolver.nodeModulesPaths

preferNativePlatform: boolean​

Whether to prefer .native.${ext} over .${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. In the default resolver, the resolution algorithm terminates with an empty module result if redirectModulePath returns false.

Metro uses this to implement the package.json browser field spec, particularly the ability to replace and ignore specific files.

The default implementation of this function respects resolver.resolverMainFields.

resolveAsset: (dirPath: string, assetName: string, extension: string) => ?$ReadOnlyArray<string>​

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 and uses the ${assetName}@${resolution}${extension} format for asset variant file names.

See also Static Image Resources in the React Native docs.

sourceExts: $ReadOnlyArray<string>​

The list of 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 fields in package.json that should be 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 file contents.

resolveHasteModule: string => ?string​

Resolves a Haste module name to an absolute path. Returns null if no such module exists.

The default implementation of this function uses metro-file-map's getModule method.

resolveHastePackage: string => ?string​

Resolves a Haste package name to an absolute package.json path. Returns null if no such package exists.

The default implementation of this function uses metro-file-map's getPackage method.

allowHaste: boolean​

true if Haste resolutions are allowed in the current context, false otherwise.

disableHierarchicalLookup: boolean​

If true, the resolver should not perform lookup in node_modules directories per the Node resolution algorithm. Defaults to resolver.disableHierarchicalLookup.

extraNodeModules: ?{[string]: string}​

A mapping of package names to directories that is consulted after the standard lookup through node_modules as well as any nodeModulesPaths.

originModulePath: string​

The path to the current module, e.g. the one containing the import we are currently resolving.

customResolverOptions: {[string]: mixed}​

Any custom options passed to the resolver. By default, Metro populates this based on URL parameters in the bundle request, e.g. http://localhost:8081/index.bundle?resolver.key=value becomes {key: 'value'}.

resolveRequest: CustomResolver​

A alternative resolver function to which the current request may be delegated. Defaults to resolver.resolveRequest.

Metro expects resolveRequest to have the following signature:

function resolveRequest(
context: ResolutionContext,
moduleName: string,
platform: string | null,
): Resolution {
// ...
}

type Resolution =
| {type: 'empty'}
| {type: 'sourceFile', filePath: string}
| {type: 'assetFiles', filePaths: $ReadOnlyArray<string>};

When calling the default resolver with a non-null resolveRequest function, it represents a custom resolver and will always be called, fully replacing the default resolution logic.

Inside a custom resolver, resolveRequest is set to the default resolver function, for easy chaining and customization.

Caching​

Resolver results may be cached under the following conditions:

  1. 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:
    1. A and B are in the same directory.
    2. The contents of customResolverOptions are equivalent ( = serialize to JSON the same) in both calls to the resolver.
  2. 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 may not return different resolutions for origin modules in the same directory under the same customResolverOptions.