Skip to content

Commit 2ba189b

Browse files
committed
loader: implement package maps
1 parent d080801 commit 2ba189b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+1817
-34
lines changed

doc/api/cli.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1218,6 +1218,27 @@ added:
12181218
12191219
Enable experimental support for the network inspection with Chrome DevTools.
12201220

1221+
### `--experimental-package-map=<path>`
1222+
1223+
<!-- YAML
1224+
added: REPLACEME
1225+
-->
1226+
1227+
> Stability: 1 - Experimental
1228+
1229+
Enable experimental package map resolution. The `path` argument specifies the
1230+
location of a JSON configuration file that defines package resolution mappings.
1231+
1232+
```bash
1233+
node --experimental-package-map=./package-map.json app.js
1234+
```
1235+
1236+
When enabled, bare specifier resolution consults the package map for resolution.
1237+
This allows explicit control over which packages can import which dependencies.
1238+
1239+
See [Package maps][] for details on the configuration file format and
1240+
resolution algorithm.
1241+
12211242
### `--experimental-print-required-tla`
12221243

12231244
<!-- YAML
@@ -3645,6 +3666,7 @@ one is included in the list below.
36453666
* `--experimental-json-modules`
36463667
* `--experimental-loader`
36473668
* `--experimental-modules`
3669+
* `--experimental-package-map`
36483670
* `--experimental-print-required-tla`
36493671
* `--experimental-quic`
36503672
* `--experimental-require-module`
@@ -4241,6 +4263,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
42414263
[Navigator API]: globals.md#navigator
42424264
[Node.js issue tracker]: https://github.com/nodejs/node/issues
42434265
[OSSL_PROVIDER-legacy]: https://www.openssl.org/docs/man3.0/man7/OSSL_PROVIDER-legacy.html
4266+
[Package maps]: packages.md#package-maps
42444267
[Permission Model]: permissions.md#permission-model
42454268
[REPL]: repl.md
42464269
[ScriptCoverage]: https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-ScriptCoverage

doc/api/errors.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2496,6 +2496,77 @@ A given value is out of the accepted range.
24962496
The `package.json` [`"imports"`][] field does not define the given internal
24972497
package specifier mapping.
24982498

2499+
<a id="ERR_PACKAGE_MAP_EXTERNAL_FILE"></a>
2500+
2501+
### `ERR_PACKAGE_MAP_EXTERNAL_FILE`
2502+
2503+
<!-- YAML
2504+
added: REPLACEME
2505+
-->
2506+
2507+
A module attempted to resolve a bare specifier using the [package map][], but
2508+
the importing file is not located within any package defined in the map.
2509+
2510+
```console
2511+
$ node --experimental-package-map=./package-map.json /tmp/script.js
2512+
Error [ERR_PACKAGE_MAP_EXTERNAL_FILE]: Cannot resolve "dep-a" from "/tmp/script.js": file is not within any package defined in /path/to/package-map.json
2513+
```
2514+
2515+
To fix this error, ensure the importing file is inside one of the package
2516+
directories listed in the package map, or add a new package entry whose `path`
2517+
covers the importing file.
2518+
2519+
<a id="ERR_PACKAGE_MAP_INVALID"></a>
2520+
2521+
### `ERR_PACKAGE_MAP_INVALID`
2522+
2523+
<!-- YAML
2524+
added: REPLACEME
2525+
-->
2526+
2527+
The [package map][] configuration file is invalid. This can occur when:
2528+
2529+
* The file does not exist at the specified path.
2530+
* The file contains invalid JSON.
2531+
* The file is missing the required `packages` object.
2532+
* A package entry is missing the required `path` field.
2533+
* Two package entries have the same `path` value.
2534+
2535+
```console
2536+
$ node --experimental-package-map=./missing.json app.js
2537+
Error [ERR_PACKAGE_MAP_INVALID]: Invalid package map at "./missing.json": file not found
2538+
```
2539+
2540+
<a id="ERR_PACKAGE_MAP_KEY_NOT_FOUND"></a>
2541+
2542+
### `ERR_PACKAGE_MAP_KEY_NOT_FOUND`
2543+
2544+
<!-- YAML
2545+
added: REPLACEME
2546+
-->
2547+
2548+
A package's `dependencies` object in the [package map][] references a package
2549+
key that is not defined in the `packages` object.
2550+
2551+
```json
2552+
{
2553+
"packages": {
2554+
"app": {
2555+
"path": "./app",
2556+
"dependencies": {
2557+
"foo": "nonexistent"
2558+
}
2559+
}
2560+
}
2561+
}
2562+
```
2563+
2564+
In this example, `"nonexistent"` is referenced as a dependency target but not
2565+
defined in `packages`, which will throw this error.
2566+
2567+
To fix this error, ensure all package keys referenced in `dependencies` values
2568+
are defined in the `packages` object.
2569+
24992570
<a id="ERR_PACKAGE_PATH_NOT_EXPORTED"></a>
25002571

25012572
### `ERR_PACKAGE_PATH_NOT_EXPORTED`
@@ -4470,6 +4541,7 @@ An error occurred trying to allocate memory. This should never happen.
44704541
[domains]: domain.md
44714542
[event emitter-based]: events.md#class-eventemitter
44724543
[file descriptors]: https://en.wikipedia.org/wiki/File_descriptor
4544+
[package map]: packages.md#package-maps
44734545
[relative URL]: https://url.spec.whatwg.org/#relative-url-string
44744546
[self-reference a package using its name]: packages.md#self-referencing-a-package-using-its-name
44754547
[special scheme]: https://url.spec.whatwg.org/#special-scheme

doc/api/esm.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,12 @@ The default loader has the following properties
943943
* Fails on unknown extensions for `file:` loading
944944
(supports only `.cjs`, `.js`, and `.mjs`)
945945
946+
When the [`--experimental-package-map`][] flag is enabled, bare specifier
947+
resolution first consults the package map configuration. If the importing
948+
module is within a mapped package and the specifier matches a declared
949+
dependency, the package map resolution takes precedence. See [Package maps][]
950+
for details.
951+
946952
### Resolution algorithm
947953
948954
The algorithm to load an ES module specifier is given through the
@@ -1306,12 +1312,14 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][].
13061312
[Loading ECMAScript modules using `require()`]: modules.md#loading-ecmascript-modules-using-require
13071313
[Module customization hooks]: module.md#customization-hooks
13081314
[Node.js Module Resolution And Loading Algorithm]: #resolution-algorithm-specification
1315+
[Package maps]: packages.md#package-maps
13091316
[Source Phase Imports]: https://github.com/tc39/proposal-source-phase-imports
13101317
[Terminology]: #terminology
13111318
[URL]: https://url.spec.whatwg.org/
13121319
[WebAssembly JS String Builtins Proposal]: https://github.com/WebAssembly/js-string-builtins
13131320
[`"exports"`]: packages.md#exports
13141321
[`"type"`]: packages.md#type
1322+
[`--experimental-package-map`]: cli.md#--experimental-package-mappath
13151323
[`--input-type`]: cli.md#--input-typetype
13161324
[`data:` URLs]: https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/data
13171325
[`export`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export

doc/api/modules.md

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -356,8 +356,12 @@ require(X) from module at path Y
356356
4. If X begins with '#'
357357
a. LOAD_PACKAGE_IMPORTS(X, dirname(Y))
358358
5. LOAD_PACKAGE_SELF(X, dirname(Y))
359-
6. LOAD_NODE_MODULES(X, dirname(Y))
360-
7. THROW "not found"
359+
6. If a package map PACKAGE_MAP exists,
360+
a. Find the package ID for the package owning Y
361+
1. Let PARENT_PACKAGE_ID be FIND_PACKAGE_ID(dirname(Y), PACKAGE_MAP)
362+
b. LOAD_PACKAGE_MAP(X, PARENT_PACKAGE_ID, PACKAGE_MAP)
363+
7. LOAD_NODE_MODULES(X, dirname(Y))
364+
8. THROW "not found"
361365
362366
MAYBE_DETECT_AND_LOAD(X)
363367
1. If X parses as a CommonJS module, load X as a CommonJS module. STOP.
@@ -402,9 +406,11 @@ LOAD_AS_DIRECTORY(X)
402406
2. LOAD_INDEX(X)
403407
404408
LOAD_NODE_MODULES(X, START)
405-
1. let DIRS = NODE_MODULES_PATHS(START)
406-
2. for each DIR in DIRS:
407-
a. LOAD_PACKAGE_EXPORTS(X, DIR)
409+
1. Try to interpret X as a combination of NAME and SUBPATH where the name
410+
may have a @scope/ prefix and the subpath begins with a slash (`/`).
411+
2. let DIRS = NODE_MODULES_PATHS(START)
412+
3. for each DIR in DIRS:
413+
a. LOAD_PACKAGE_EXPORTS(SUBPATH, DIR/NAME)
408414
b. LOAD_AS_FILE(DIR/X)
409415
c. LOAD_AS_DIRECTORY(DIR/X)
410416
@@ -419,6 +425,25 @@ NODE_MODULES_PATHS(START)
419425
d. let I = I - 1
420426
5. return DIRS + GLOBAL_FOLDERS
421427
428+
FIND_PACKAGE_ID(PATH, PACKAGE_MAP)
429+
1. Find the PACKAGE_ID for the entry whose "path" is a parent directory of PATH
430+
2. If multiple entries are found, THROW "ambiguous resolution"
431+
3. If no entry was found, THROW "external file".
432+
4. return PACKAGE_ID
433+
434+
LOAD_PACKAGE_MAP(X, PARENT_PACKAGE_ID, PACKAGE_MAP)
435+
1. Try to interpret X as a combination of NAME and SUBPATH where the name
436+
may have a @scope/ prefix and the subpath begins with a slash (`/`).
437+
2. Find the package map entry for key PARENT_PACKAGE_ID
438+
3. Look up NAME in the entry's "dependencies" map.
439+
4. If NAME is not found, THROW "not found".
440+
5. Let TARGET be PACKAGE_MAP.packages[dependencies[name]]
441+
6. Let PACKAGE_PATH be the resolved path of TARGET.
442+
7. LOAD_PACKAGE_EXPORTS(SUBPATH, PACKAGE_PATH)
443+
8. LOAD_AS_FILE(PACKAGE_PATH/SUBPATH)
444+
9. LOAD_AS_DIRECTORY(PACKAGE_PATH/SUBPATH)
445+
10. THROW "not found"
446+
422447
LOAD_PACKAGE_IMPORTS(X, DIR)
423448
1. Find the closest package scope SCOPE to DIR.
424449
2. If no scope was found, return.
@@ -430,19 +455,15 @@ LOAD_PACKAGE_IMPORTS(X, DIR)
430455
CONDITIONS) <a href="esm.md#resolver-algorithm-specification">defined in the ESM resolver</a>.
431456
6. RESOLVE_ESM_MATCH(MATCH).
432457
433-
LOAD_PACKAGE_EXPORTS(X, DIR)
434-
1. Try to interpret X as a combination of NAME and SUBPATH where the name
435-
may have a @scope/ prefix and the subpath begins with a slash (`/`).
436-
2. If X does not match this pattern or DIR/NAME/package.json is not a file,
437-
return.
438-
3. Parse DIR/NAME/package.json, and look for "exports" field.
439-
4. If "exports" is null or undefined, return.
440-
5. If `--no-require-module` is not enabled
458+
LOAD_PACKAGE_EXPORTS(SUBPATH, PACKAGE_DIR)
459+
1. Parse PACKAGE_DIR/package.json, and look for "exports" field.
460+
2. If "exports" is null or undefined, return.
461+
3. If `--no-require-module` is not enabled
441462
a. let CONDITIONS = ["node", "require", "module-sync"]
442463
b. Else, let CONDITIONS = ["node", "require"]
443-
6. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(DIR/NAME), "." + SUBPATH,
464+
4. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(PACKAGE_DIR), "." + SUBPATH,
444465
`package.json` "exports", CONDITIONS) <a href="esm.md#resolver-algorithm-specification">defined in the ESM resolver</a>.
445-
7. RESOLVE_ESM_MATCH(MATCH)
466+
5. RESOLVE_ESM_MATCH(MATCH)
446467
447468
LOAD_PACKAGE_SELF(X, DIR)
448469
1. Find the closest package scope SCOPE to DIR.

0 commit comments

Comments
 (0)