From d9b3d073580ae980fcaabf8c4c4ec281ec3b90ce Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Fri, 28 Apr 2023 09:37:09 -0700 Subject: [PATCH] Update Resolver Algorithm docs to include Package Exports Reviewed By: motiz88 Differential Revision: D43443424 fbshipit-source-id: a61ca9c7179f6944737bdfd14ce2cfb88aae1308 --- docs/Resolution.md | 122 +++++++++++++++++++++++++++++---------------- 1 file changed, 80 insertions(+), 42 deletions(-) diff --git a/docs/Resolution.md b/docs/Resolution.md index e8111b0359..c7cee1477b 100644 --- a/docs/Resolution.md +++ b/docs/Resolution.md @@ -31,10 +31,9 @@ These Metro-specific features include: * **Asset extensions and image resolutions**: Used by [React Native](https://reactnative.dev/docs/images#static-image-resources) 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](#resolution-context) _context_, a module name _moduleName_, and an optional platform identifier _platform_, Metro's resolver either returns one of the [resolution types](#resolution-types), or throws an error. +Given a [resolution context](#resolution-context) _context_, a module name _moduleName_, and an optional platform identifier _platform_, Metro's resolver performs [**RESOLVE**](#resolve)(_context_, _moduleName_, _platform_), which either returns one of the [resolution types](#resolution-types), or throws an error. ### Resolution types @@ -58,65 +57,104 @@ These are the rules that Metro's default resolver follows. Refer to [`metro-reso ::: -1. If a [custom resolver](#resolverequest-customresolver) is defined, call it and return the result. +#### RESOLVE + +Parameters: (*context*, *moduleName*, *platform*) + +1. If a [custom resolver](#resolverequest-customresolver) is defined, then + 1. Return the result of the custom resolver. +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 + 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*). + 2. If resolved as a Haste package path, then + 1. 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`. +5. If [`context.disableHierarchicalLookup`](#disableHierarchicalLookup-boolean) is not `true`, then + 1. Try resolving _moduleName_ under `node_modules` from the current directory (i.e. parent of [`context.originModulePath`](#originmodulepath-string)) up to the root directory. + 2. Perform [**RESOLVE_PACKAGE**](#resolve_package)(*context*, *modulePath*, *platform*) for each candidate path. +6. For each element _nodeModulesPath_ of [`context.nodeModulesPaths`](#nodemodulespaths-readonlyarraystring): + 1. Try resolving _moduleName_ under _nodeModulesPath_ as if the latter was another `node_modules` directory (similar to step 5 above). + 2. Perform [**RESOLVE_PACKAGE**](#resolve_package)(*context*, *modulePath*, *platform*) for each candidate path. +7. If [`context.extraNodeModules`](#extranodemodules-string-string) is set: + 1. Split _moduleName_ into a package name (including an optional [scope](https://docs.npmjs.com/cli/v8/using-npm/scope)) and relative path. + 2. Look up the package name in [`context.extraNodeModules`](#extranodemodules-string-string). If found, then + 1. Construct a path _modulePath_ by replacing the package name part of _moduleName_ with the value found in [`context.extraNodeModules`](#extranodemodules-string-string) + 2. Return the result of [**RESOLVE_PACKAGE**](#resolve_package)(*context*, *modulePath*, *platform*). +8. If no valid resolution has been found, throw a resolution failure error. -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`](#originmodulepath-string)). - 2. If the path refers to an [asset](#assetexts-readonlysetstring): +#### RESOLVE_MODULE - 1. Use [`context.resolveAsset`](#resolveasset-dirpath-string-assetname-string-extension-string--readonlyarraystring) to collect all asset variants. - 2. Return an [asset resolution](#asset-files) containing the collected asset paths. +Parameters: (*context*, *moduleName*, *platform*) - 3. If the path refers to a file that [exists](#doesfileexist-string--boolean) after applying [redirections](#redirectmodulepath-string--string--false), return it as a [source file resolution](#source-file). - 4. 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. 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). +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): + 1. Let *mainModulePath* be the result of reading the package's entry path using [`context.mainFields`](#mainfields-readonlyarraystring). + 2. Return the result of [**RESOLVE_FILE**](#resolve_file)(*context*, *mainModulePath*, *platform*), or continue. + 3. Return the result of [**RESOLVE_FILE**](#resolve_file)(*context*, *mainModulePath* + `'/index'`, *platform*). + 4. Throw an error if no resolution could be found. - 1. _moduleName_ + `'.android.js'` - 2. _moduleName_ + `'.native.js'` (if [`context.preferNativePlatform`](#prefernativeplatform-boolean) is `true`) - 3. _moduleName_ + `'.android.jsx'` - 4. _moduleName_ + `'.native.jsx'` (if [`context.preferNativePlatform`](#prefernativeplatform-boolean) is `true`) +#### RESOLVE_PACKAGE - 5. If a file named _moduleName_ + `'/package.json'` [exists](#doesfileexist-string--boolean): +Parameters: (*context*, *moduleName*, *platform*) - 1. [Get the package's entry path](#getpackagemainpath-string--string). - 2. Try to resolve the entry path as a file, after applying [redirections](#redirectmodulepath-string--string--false) and trying all platform and extension variants as described above. - 3. Try to resolve the entry path + `'/index'` as a file, after applying [redirections](#redirectmodulepath-string--string--false) and trying all platform and extension variants as described above. - 4. Throw an error if no resolution could be found. +1. If `context.enablePackageExports` is enabled, and a containing `package.json` file contains the field `"exports"`, get result of [**RESOLVE_PACKAGE_EXPORTS**](#resolve_package-exports)(*context*, *packagePath*, *filePath*, *exportsField*, *platform*). + 1. If resolved path exists, return result. + 2. Else, log either a package configuration or package encapsulation warning. +2. Return the result of [**RESOLVE_MODULE**](#resolve_module)(*context*, *filePath*, *platform*). - 6. Try to resolve _moduleName_ + `'/index'` as a file, after applying [redirections](#redirectmodulepath-string--string--false) and trying all platform and extension variants as described above. +#### RESOLVE_PACKAGE_EXPORTS +Parameters: (*context*, *packagePath*, *filePath*, *exportsField*, *platform*) -3. Apply [redirections](#redirectmodulepath-string--string--false) to _moduleName_. Skip the rest of this algorithm if this results in an [empty module](#empty-module). +> Resolves a package subpath based on the [Package Entry Points spec](https://nodejs.org/docs/latest-v19.x/api/packages.html#package-entry-points) (the `"exports"` field), when [`resolver.unstable_enablePackageExports`](./configuration#unstable_enablepackageexports-experimental) is enabled. -4. If [Haste resolutions are allowed](#allowhaste-boolean): +1. Let *subpath* be the relative path from *packagePath* to *filePath*, or `'.'`. +2. If *exportsField* contains an invalid configuration or values, raise an `InvalidPackageConfigurationError`. +3. If *subpath* is not defined by *exportsField*, raise a `PackagePathNotExportedError`. +4. Let *target* be the result of matching *subpath* in *exportsField* after applying any [conditional exports](https://nodejs.org/docs/latest-v19.x/api/packages.html#conditional-exports) and/or substituting a [subpath pattern match](https://nodejs.org/docs/latest-v19.x/api/packages.html#subpath-patterns). + 1. Condition names will be asserted from the union of `context.unstable_conditionNames` and `context.unstable_conditionNamesByPlatform` for *platform*, in the order defined by *exportsField*. +5. If *target* refers to an [asset](#assetexts-readonlysetstring), then + 1. Return the result of [**RESOLVE_ASSET**](#resolve_asset)(*context*, *target*, *platform*). +6. Return *target* as a [source file resolution](#source-file) **without** applying redirections or trying any platform or extension variants. - 1. Try resolving _moduleName_ as a [Haste module](#resolvehastemodule-string--string). - If found, return it as a [source file resolution](#source-file) **without** applying redirections or trying any platform or extension variants. - 2. Try resolving _moduleName_ as a [Haste package](#resolvehastepackage-string--string), or a path *relative* to a Haste package. - For example, if _moduleName_ is `'a/b/c'`, try the following potential Haste package names: +#### RESOLVE_FILE - 1. `'a/b/c'`, relative path `''` - 2. `'a/b'`, relative path `'./c'` - 3. `'a'`, with relative path `'./b/c'` - 4. 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`. +Parameters: (*context*, *filePath*, *platform*) -5. If [`context.disableHierarchicalLookup`](#disableHierarchicalLookup-boolean) is not `true`: +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. _moduleName_ + `'.android.js'` + 2. _moduleName_ + `'.native.js'` (if [`context.preferNativePlatform`](#prefernativeplatform-boolean) is `true`) + 3. _moduleName_ + `'.android.jsx'` + 4. _moduleName_ + `'.native.jsx'` (if [`context.preferNativePlatform`](#prefernativeplatform-boolean) is `true`) - 1. Try resolving _moduleName_ under `node_modules` from the current directory (i.e. parent of [`context.originModulePath`](#originmodulepath-string)) up to the root directory. - 2. Perform the algorithm for resolving a path (step 2 above) for each candidate path. +#### RESOLVE_ASSET -6. For each element _nodeModulesPath_ of [`context.nodeModulesPaths`](#nodemodulespaths-readonlyarraystring): +Parameters: (*context*, *filePath*, *platform*) - 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. +1. Use [`context.resolveAsset`](#resolveasset-dirpath-string-assetname-string-extension-string--readonlyarraystring) to collect all asset variants. +2. Return an [asset resolution](#asset-files) containing the collected asset paths. -5. If [`context.extraNodeModules`](#extranodemodules-string-string) is set: +#### RESOLVE_HASTE - 1. Split _moduleName_ into a package name (including an optional [scope](https://docs.npmjs.com/cli/v8/using-npm/scope)) and relative path. - 2. Look up the package name in [`context.extraNodeModules`](#extranodemodules-string-string). If found, construct a path by replacing the package name part of _moduleName_ with the value found in [`context.extraNodeModules`](#extranodemodules-string-string), and perform the algorithm for resolving a path (step 2 above). +Parameters: (*context*, *moduleName*, *platform*) -6. If no valid resolution has been found, throw a resolution failure error. +1. Try resolving _moduleName_ as a [Haste module](#resolvehastemodule-string--string). + If found, then + 1. Return result as a [source file resolution](#source-file) **without** applying redirections or trying any platform or extension variants. +2. Try resolving _moduleName_ as a [Haste package](#resolvehastepackage-string--string), 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'` ### Resolution context