Skip to content

Commit 32b8d54

Browse files
committed
Updates documentation
1 parent 1b2ad80 commit 32b8d54

2 files changed

Lines changed: 99 additions & 40 deletions

File tree

doc/api/modules.md

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -340,10 +340,6 @@ This feature can be detected by checking if
340340
To get the exact filename that will be loaded when `require()` is called, use
341341
the `require.resolve()` function.
342342

343-
When the [`--experimental-package-map`][] flag is enabled, bare specifier
344-
resolution first consults the package map before searching `node_modules`
345-
directories. See [Package maps][] for details.
346-
347343
Putting together all of the above, here is the high-level algorithm
348344
in pseudocode of what `require()` does:
349345

@@ -361,8 +357,12 @@ require(X) from module at path Y
361357
4. If X begins with '#'
362358
a. LOAD_PACKAGE_IMPORTS(X, dirname(Y))
363359
5. LOAD_PACKAGE_SELF(X, dirname(Y))
364-
6. LOAD_NODE_MODULES(X, dirname(Y))
365-
7. THROW "not found"
360+
6. If a package map PACKAGE_MAP exists,
361+
a. Find the package ID for the package owning Y
362+
1. Let PARENT_PACKAGE_ID be FIND_PACKAGE_ID(dirname(Y), PACKAGE_MAP)
363+
b. LOAD_PACKAGE_MAP(X, PARENT_PACKAGE_ID, PACKAGE_MAP)
364+
7. LOAD_NODE_MODULES(X, dirname(Y))
365+
8. THROW "not found"
366366
367367
MAYBE_DETECT_AND_LOAD(X)
368368
1. If X parses as a CommonJS module, load X as a CommonJS module. STOP.
@@ -407,9 +407,11 @@ LOAD_AS_DIRECTORY(X)
407407
2. LOAD_INDEX(X)
408408
409409
LOAD_NODE_MODULES(X, START)
410-
1. let DIRS = NODE_MODULES_PATHS(START)
411-
2. for each DIR in DIRS:
412-
a. LOAD_PACKAGE_EXPORTS(X, DIR)
410+
1. Try to interpret X as a combination of NAME and SUBPATH where the name
411+
may have a @scope/ prefix and the subpath begins with a slash (`/`).
412+
2. let DIRS = NODE_MODULES_PATHS(START)
413+
3. for each DIR in DIRS:
414+
a. LOAD_PACKAGE_EXPORTS(SUBPATH, DIR/NAME)
413415
b. LOAD_AS_FILE(DIR/X)
414416
c. LOAD_AS_DIRECTORY(DIR/X)
415417
@@ -424,6 +426,25 @@ NODE_MODULES_PATHS(START)
424426
d. let I = I - 1
425427
5. return DIRS + GLOBAL_FOLDERS
426428
429+
FIND_PACKAGE_ID(PATH, PACKAGE_MAP)
430+
1. Find the PACKAGE_ID for the entry whose "path" is a parent directory of PATH
431+
2. If multiple entries are found, THROW "ambiguous resolution"
432+
3. If no entry was found, THROW "external file".
433+
4. return PACKAGE_ID
434+
435+
LOAD_PACKAGE_MAP(X, PARENT_PACKAGE_ID, PACKAGE_MAP)
436+
1. Try to interpret X as a combination of NAME and SUBPATH where the name
437+
may have a @scope/ prefix and the subpath begins with a slash (`/`).
438+
2. Find the package map entry for key PARENT_PACKAGE_ID
439+
3. Look up NAME in the entry's "dependencies" map.
440+
4. If NAME is not found, THROW "not found".
441+
5. Let TARGET be PACKAGE_MAP.packages[dependencies[name]]
442+
6. Let PACKAGE_PATH be the resolved path of TARGET.
443+
7. LOAD_PACKAGE_EXPORTS(SUBPATH, PACKAGE_PATH)
444+
8. LOAD_AS_FILE(PACKAGE_PATH/SUBPATH)
445+
9. LOAD_AS_DIRECTORY(PACKAGE_PATH/SUBPATH)
446+
10. THROW "not found"
447+
427448
LOAD_PACKAGE_IMPORTS(X, DIR)
428449
1. Find the closest package scope SCOPE to DIR.
429450
2. If no scope was found, return.
@@ -435,19 +456,15 @@ LOAD_PACKAGE_IMPORTS(X, DIR)
435456
CONDITIONS) <a href="esm.md#resolver-algorithm-specification">defined in the ESM resolver</a>.
436457
6. RESOLVE_ESM_MATCH(MATCH).
437458
438-
LOAD_PACKAGE_EXPORTS(X, DIR)
439-
1. Try to interpret X as a combination of NAME and SUBPATH where the name
440-
may have a @scope/ prefix and the subpath begins with a slash (`/`).
441-
2. If X does not match this pattern or DIR/NAME/package.json is not a file,
442-
return.
443-
3. Parse DIR/NAME/package.json, and look for "exports" field.
444-
4. If "exports" is null or undefined, return.
445-
5. If `--no-require-module` is not enabled
459+
LOAD_PACKAGE_EXPORTS(SUBPATH, PACKAGE_DIR)
460+
1. Parse PACKAGE_DIR/package.json, and look for "exports" field.
461+
2. If "exports" is null or undefined, return.
462+
3. If `--no-require-module` is not enabled
446463
a. let CONDITIONS = ["node", "require", "module-sync"]
447464
b. Else, let CONDITIONS = ["node", "require"]
448-
6. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(DIR/NAME), "." + SUBPATH,
465+
4. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(PACKAGE_DIR), "." + SUBPATH,
449466
`package.json` "exports", CONDITIONS) <a href="esm.md#resolver-algorithm-specification">defined in the ESM resolver</a>.
450-
7. RESOLVE_ESM_MATCH(MATCH)
467+
5. RESOLVE_ESM_MATCH(MATCH)
451468
452469
LOAD_PACKAGE_SELF(X, DIR)
453470
1. Find the closest package scope SCOPE to DIR.

doc/api/packages.md

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -981,7 +981,7 @@ node --experimental-package-map=./package-map.json app.js
981981
### Configuration file format
982982

983983
The package map configuration file is a JSON file with a `packages` object.
984-
Each key in `packages` is a unique identifier for a package entry:
984+
Each key in `packages` is called a package ID and is a unique identifier for a package entry:
985985

986986
```json
987987
{
@@ -1008,9 +1008,10 @@ Each key in `packages` is a unique identifier for a package entry:
10081008

10091009
Each package entry has the following fields:
10101010

1011-
* `path` {string} **Required.** Relative path from the configuration file to
1012-
the package directory. Each path must be unique across all packages in the
1013-
map; duplicate paths will throw an [`ERR_PACKAGE_MAP_INVALID`][] error.
1011+
* `path` {string} **Required.** Absolute or relative path from the configuration
1012+
file to the package directory. Multiple packages are allowed to share the same
1013+
path; consumers must key module instances by both package and package IDs to
1014+
differentiate them.
10141015
* `dependencies` {Object} An object mapping bare specifiers to package keys.
10151016
Each key is the import name used in source code, and each value is the
10161017
corresponding package key in the `packages` object. Defaults to an empty
@@ -1020,29 +1021,18 @@ Each package entry has the following fields:
10201021

10211022
When a bare specifier is encountered:
10221023

1023-
1. Node.js determines which package contains the importing file by checking
1024-
if the file path is within any package's `path`.
1025-
2. If the importing file is not within any mapped package, an
1024+
1. Node.js determines which package performs the resolution request.
1025+
* If possible the package ID for the importer file should be provided to the resolution algorithm.
1026+
* Failing that, the resolution will check if the file path is within any package's `path`.
1027+
2. If no package ID is provided and the importing file is not within any mapped package, an
10261028
[`ERR_PACKAGE_MAP_EXTERNAL_FILE`][] error is thrown.
10271029
3. Node.js looks up the specifier's package name in the importing package's
10281030
`dependencies` object to find the corresponding package key.
10291031
4. If found, the specifier resolves to the target package's `path`.
10301032
5. If the specifier is not in `dependencies`, a
10311033
`MODULE_NOT_FOUND` error is thrown.
10321034

1033-
### Subpath resolution
1034-
1035-
Package maps support importing subpaths. Given the configuration above:
1036-
1037-
```js
1038-
// In packages/app/index.js
1039-
import { helper } from '@myorg/utils'; // Resolves to ./packages/utils
1040-
import { format } from '@myorg/utils/format'; // Resolves to ./packages/utils/format
1041-
```
1042-
1043-
The subpath portion of the specifier is preserved and appended to the resolved
1044-
package path. The target package's `package.json` [`"exports"`][] field is
1045-
then used to resolve the final file path.
1035+
More details can be found in the [resolution algorithm pseudo-code][].
10461036

10471037
### Multiple package versions
10481038

@@ -1078,6 +1068,58 @@ can map the same specifier to different targets:
10781068
Both `app` and `legacy` can `import 'component'`, but they resolve to
10791069
different paths based on their declared dependencies.
10801070

1071+
### Multiple packages for the same path
1072+
1073+
To address complex hoisting situations, multiple packages may share the same
1074+
path, which introduces ambiguity when determining which package an import
1075+
originates from:
1076+
1077+
```json
1078+
{
1079+
"packages": {
1080+
"app-old": {
1081+
"path": "./app-old",
1082+
"dependencies": {
1083+
"lib": "lib-old"
1084+
}
1085+
},
1086+
"app-new": {
1087+
"path": "./app-new",
1088+
"dependencies": {
1089+
"lib": "lib-new"
1090+
}
1091+
},
1092+
"lib-old": {
1093+
"path": "./lib",
1094+
"dependencies": {
1095+
"react": "react-15"
1096+
}
1097+
},
1098+
"lib-new": {
1099+
"path": "./lib",
1100+
"dependencies": {
1101+
"react": "react-18"
1102+
}
1103+
}
1104+
}
1105+
}
1106+
```
1107+
1108+
In the example above both `lib-old` and `lib-new` use the same `./lib` folder to
1109+
store their sources, the only difference being in which version of `react` they'll
1110+
access when performing require calls.
1111+
1112+
Because multiple package entries share the same path, resolving a bare specifier
1113+
from a file within that path is ambiguous unless the originating package ID is
1114+
known. If the package ID cannot be determined (for example, because the caller
1115+
did not propagate it from a previous resolution), Node.js will throw an error
1116+
rather than guess.
1117+
1118+
To support this pattern, implementers must key module instances by package ID
1119+
and propagate it from each resolution result to subsequent resolution requests.
1120+
This ensures that when `lib` requires `react`, the runtime knows whether the
1121+
request comes from `lib-old` or `lib-new` and can select the correct dependency.
1122+
10811123
### CommonJS and ES modules
10821124

10831125
Package maps work with both CommonJS (`require()`) and ES modules (`import`).
@@ -1343,7 +1385,6 @@ This field defines [subpath imports][] for the current package.
13431385
[`--experimental-package-map`]: cli.md#--experimental-package-mappath
13441386
[`--no-addons` flag]: cli.md#--no-addons
13451387
[`ERR_PACKAGE_MAP_EXTERNAL_FILE`]: errors.md#err_package_map_external_file
1346-
[`ERR_PACKAGE_MAP_INVALID`]: errors.md#err_package_map_invalid
13471388
[`ERR_PACKAGE_PATH_NOT_EXPORTED`]: errors.md#err_package_path_not_exported
13481389
[`ERR_UNKNOWN_FILE_EXTENSION`]: errors.md#err_unknown_file_extension
13491390
[`package.json`]: #nodejs-packagejson-field-definitions
@@ -1354,6 +1395,7 @@ This field defines [subpath imports][] for the current package.
13541395
[load ECMAScript modules from CommonJS modules]: modules.md#loading-ecmascript-modules-using-require
13551396
[merve]: https://github.com/anonrig/merve
13561397
[packages folder mapping]: https://github.com/WICG/import-maps#packages-via-trailing-slashes
1398+
[resolution algorithm pseudo-code]: modules.md#all-together
13571399
[self-reference]: #self-referencing-a-package-using-its-name
13581400
[subpath exports]: #subpath-exports
13591401
[subpath imports]: #subpath-imports

0 commit comments

Comments
 (0)