Skip to content

Commit 9914aab

Browse files
committed
loader: implement package maps
1 parent 2962ccf commit 9914aab

56 files changed

Lines changed: 1822 additions & 34 deletions

Some content is hidden

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

doc/api/cli.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1267,6 +1267,27 @@ added:
12671267
12681268
Enable experimental support for the network inspection with Chrome DevTools.
12691269

1270+
### `--experimental-package-map=<path>`
1271+
1272+
<!-- YAML
1273+
added: REPLACEME
1274+
-->
1275+
1276+
> Stability: 1 - Experimental
1277+
1278+
Enable experimental package map resolution. The `path` argument specifies the
1279+
location of a JSON configuration file that defines package resolution mappings.
1280+
1281+
```bash
1282+
node --experimental-package-map=./package-map.json app.js
1283+
```
1284+
1285+
When enabled, bare specifier resolution consults the package map for resolution.
1286+
This allows explicit control over which packages can import which dependencies.
1287+
1288+
See [Package maps][] for details on the configuration file format and
1289+
resolution algorithm.
1290+
12701291
### `--experimental-print-required-tla`
12711292

12721293
<!-- YAML
@@ -3702,6 +3723,7 @@ one is included in the list below.
37023723
* `--experimental-json-modules`
37033724
* `--experimental-loader`
37043725
* `--experimental-modules`
3726+
* `--experimental-package-map`
37053727
* `--experimental-print-required-tla`
37063728
* `--experimental-quic`
37073729
* `--experimental-require-module`
@@ -4298,6 +4320,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
42984320
[Navigator API]: globals.md#navigator
42994321
[Node.js issue tracker]: https://github.com/nodejs/node/issues
43004322
[OSSL_PROVIDER-legacy]: https://www.openssl.org/docs/man3.0/man7/OSSL_PROVIDER-legacy.html
4323+
[Package maps]: packages.md#package-maps
43014324
[Permission Model]: permissions.md#permission-model
43024325
[REPL]: repl.md
43034326
[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
@@ -2514,6 +2514,77 @@ A given value is out of the accepted range.
25142514
The `package.json` [`"imports"`][] field does not define the given internal
25152515
package specifier mapping.
25162516

2517+
<a id="ERR_PACKAGE_MAP_EXTERNAL_FILE"></a>
2518+
2519+
### `ERR_PACKAGE_MAP_EXTERNAL_FILE`
2520+
2521+
<!-- YAML
2522+
added: REPLACEME
2523+
-->
2524+
2525+
A module attempted to resolve a bare specifier using the [package map][], but
2526+
the importing file is not located within any package defined in the map.
2527+
2528+
```console
2529+
$ node --experimental-package-map=./package-map.json /tmp/script.js
2530+
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
2531+
```
2532+
2533+
To fix this error, ensure the importing file is inside one of the package
2534+
directories listed in the package map, or add a new package entry whose `path`
2535+
covers the importing file.
2536+
2537+
<a id="ERR_PACKAGE_MAP_INVALID"></a>
2538+
2539+
### `ERR_PACKAGE_MAP_INVALID`
2540+
2541+
<!-- YAML
2542+
added: REPLACEME
2543+
-->
2544+
2545+
The [package map][] configuration file is invalid. This can occur when:
2546+
2547+
* The file does not exist at the specified path.
2548+
* The file contains invalid JSON.
2549+
* The file is missing the required `packages` object.
2550+
* A package entry is missing the required `path` field.
2551+
* Two package entries have the same `path` value.
2552+
2553+
```console
2554+
$ node --experimental-package-map=./missing.json app.js
2555+
Error [ERR_PACKAGE_MAP_INVALID]: Invalid package map at "./missing.json": file not found
2556+
```
2557+
2558+
<a id="ERR_PACKAGE_MAP_KEY_NOT_FOUND"></a>
2559+
2560+
### `ERR_PACKAGE_MAP_KEY_NOT_FOUND`
2561+
2562+
<!-- YAML
2563+
added: REPLACEME
2564+
-->
2565+
2566+
A package's `dependencies` object in the [package map][] references a package
2567+
key that is not defined in the `packages` object.
2568+
2569+
```json
2570+
{
2571+
"packages": {
2572+
"app": {
2573+
"path": "./app",
2574+
"dependencies": {
2575+
"foo": "nonexistent"
2576+
}
2577+
}
2578+
}
2579+
}
2580+
```
2581+
2582+
In this example, `"nonexistent"` is referenced as a dependency target but not
2583+
defined in `packages`, which will throw this error.
2584+
2585+
To fix this error, ensure all package keys referenced in `dependencies` values
2586+
are defined in the `packages` object.
2587+
25172588
<a id="ERR_PACKAGE_PATH_NOT_EXPORTED"></a>
25182589

25192590
### `ERR_PACKAGE_PATH_NOT_EXPORTED`
@@ -4501,6 +4572,7 @@ An error occurred trying to allocate memory. This should never happen.
45014572
[domains]: domain.md
45024573
[event emitter-based]: events.md#class-eventemitter
45034574
[file descriptors]: https://en.wikipedia.org/wiki/File_descriptor
4575+
[package map]: packages.md#package-maps
45044576
[relative URL]: https://url.spec.whatwg.org/#relative-url-string
45054577
[self-reference a package using its name]: packages.md#self-referencing-a-package-using-its-name
45064578
[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
@@ -357,8 +357,12 @@ require(X) from module at path Y
357357
4. If X begins with '#'
358358
a. LOAD_PACKAGE_IMPORTS(X, dirname(Y))
359359
5. LOAD_PACKAGE_SELF(X, dirname(Y))
360-
6. LOAD_NODE_MODULES(X, dirname(Y))
361-
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"
362366
363367
MAYBE_DETECT_AND_LOAD(X)
364368
1. If X parses as a CommonJS module, load X as a CommonJS module. STOP.
@@ -403,9 +407,11 @@ LOAD_AS_DIRECTORY(X)
403407
2. LOAD_INDEX(X)
404408
405409
LOAD_NODE_MODULES(X, START)
406-
1. let DIRS = NODE_MODULES_PATHS(START)
407-
2. for each DIR in DIRS:
408-
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)
409415
b. LOAD_AS_FILE(DIR/X)
410416
c. LOAD_AS_DIRECTORY(DIR/X)
411417
@@ -420,6 +426,25 @@ NODE_MODULES_PATHS(START)
420426
d. let I = I - 1
421427
5. return DIRS + GLOBAL_FOLDERS
422428
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+
423448
LOAD_PACKAGE_IMPORTS(X, DIR)
424449
1. Find the closest package scope SCOPE to DIR.
425450
2. If no scope was found, return.
@@ -431,19 +456,15 @@ LOAD_PACKAGE_IMPORTS(X, DIR)
431456
CONDITIONS) <a href="esm.md#resolver-algorithm-specification">defined in the ESM resolver</a>.
432457
6. RESOLVE_ESM_MATCH(MATCH).
433458
434-
LOAD_PACKAGE_EXPORTS(X, DIR)
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. If X does not match this pattern or DIR/NAME/package.json is not a file,
438-
return.
439-
3. Parse DIR/NAME/package.json, and look for "exports" field.
440-
4. If "exports" is null or undefined, return.
441-
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
442463
a. let CONDITIONS = ["node", "require", "module-sync"]
443464
b. Else, let CONDITIONS = ["node", "require"]
444-
6. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(DIR/NAME), "." + SUBPATH,
465+
4. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(PACKAGE_DIR), "." + SUBPATH,
445466
`package.json` "exports", CONDITIONS) <a href="esm.md#resolver-algorithm-specification">defined in the ESM resolver</a>.
446-
7. RESOLVE_ESM_MATCH(MATCH)
467+
5. RESOLVE_ESM_MATCH(MATCH)
447468
448469
LOAD_PACKAGE_SELF(X, DIR)
449470
1. Find the closest package scope SCOPE to DIR.

0 commit comments

Comments
 (0)