Skip to content

Commit fbb108b

Browse files
arcanisaduh95
authored andcommitted
loader: implement package maps
Signed-off-by: Mael Nison <nison.mael@gmail.com> PR-URL: #62239 Reviewed-By: Aviv Keller <me@aviv.sh> Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent 1124c06 commit fbb108b

66 files changed

Lines changed: 1813 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
@@ -1306,6 +1306,27 @@ added:
13061306
13071307
Enable experimental support for the network inspection with Chrome DevTools.
13081308

1309+
### `--experimental-package-map=<path>`
1310+
1311+
<!-- YAML
1312+
added: REPLACEME
1313+
-->
1314+
1315+
> Stability: 1 - Experimental
1316+
1317+
Enable experimental package map resolution. The `path` argument specifies the
1318+
location of a JSON configuration file that defines package resolution mappings.
1319+
1320+
```bash
1321+
node --experimental-package-map=./package-map.json app.js
1322+
```
1323+
1324+
When enabled, bare specifier resolution consults the package map for resolution.
1325+
This allows explicit control over which packages can import which dependencies.
1326+
1327+
See [Package maps][] for details on the configuration file format and
1328+
resolution algorithm.
1329+
13091330
### `--experimental-print-required-tla`
13101331

13111332
<!-- YAML
@@ -3753,6 +3774,7 @@ one is included in the list below.
37533774
* `--experimental-json-modules`
37543775
* `--experimental-loader`
37553776
* `--experimental-modules`
3777+
* `--experimental-package-map`
37563778
* `--experimental-print-required-tla`
37573779
* `--experimental-quic`
37583780
* `--experimental-require-module`
@@ -4349,6 +4371,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
43494371
[Navigator API]: globals.md#navigator
43504372
[Node.js issue tracker]: https://github.com/nodejs/node/issues
43514373
[OSSL_PROVIDER-legacy]: https://www.openssl.org/docs/man3.0/man7/OSSL_PROVIDER-legacy.html
4374+
[Package maps]: packages.md#package-maps
43524375
[Permission Model]: permissions.md#permission-model
43534376
[REPL]: repl.md
43544377
[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
@@ -2537,6 +2537,77 @@ A given value is out of the accepted range.
25372537
The `package.json` [`"imports"`][] field does not define the given internal
25382538
package specifier mapping.
25392539

2540+
<a id="ERR_PACKAGE_MAP_EXTERNAL_FILE"></a>
2541+
2542+
### `ERR_PACKAGE_MAP_EXTERNAL_FILE`
2543+
2544+
<!-- YAML
2545+
added: REPLACEME
2546+
-->
2547+
2548+
A module attempted to resolve a bare specifier using the [package map][], but
2549+
the importing file is not located within any package defined in the map.
2550+
2551+
```console
2552+
$ node --experimental-package-map=./package-map.json /tmp/script.js
2553+
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
2554+
```
2555+
2556+
To fix this error, ensure the importing file is inside one of the package
2557+
directories listed in the package map, or add a new package entry whose `url`
2558+
covers the importing file.
2559+
2560+
<a id="ERR_PACKAGE_MAP_INVALID"></a>
2561+
2562+
### `ERR_PACKAGE_MAP_INVALID`
2563+
2564+
<!-- YAML
2565+
added: REPLACEME
2566+
-->
2567+
2568+
The [package map][] configuration file is invalid. This can occur when:
2569+
2570+
* The file does not exist at the specified path.
2571+
* The file contains invalid JSON.
2572+
* The file is missing the required `packages` object.
2573+
* A package entry is missing the required `url` field.
2574+
* Two package entries have the same `url` value.
2575+
2576+
```console
2577+
$ node --experimental-package-map=./missing.json app.js
2578+
Error [ERR_PACKAGE_MAP_INVALID]: Invalid package map at "./missing.json": file not found
2579+
```
2580+
2581+
<a id="ERR_PACKAGE_MAP_KEY_NOT_FOUND"></a>
2582+
2583+
### `ERR_PACKAGE_MAP_KEY_NOT_FOUND`
2584+
2585+
<!-- YAML
2586+
added: REPLACEME
2587+
-->
2588+
2589+
A package's `dependencies` object in the [package map][] references a package
2590+
key that is not defined in the `packages` object.
2591+
2592+
```json
2593+
{
2594+
"packages": {
2595+
"app": {
2596+
"url": "./app",
2597+
"dependencies": {
2598+
"foo": "nonexistent"
2599+
}
2600+
}
2601+
}
2602+
}
2603+
```
2604+
2605+
In this example, `"nonexistent"` is referenced as a dependency target but not
2606+
defined in `packages`, which will throw this error.
2607+
2608+
To fix this error, ensure all package keys referenced in `dependencies` values
2609+
are defined in the `packages` object.
2610+
25402611
<a id="ERR_PACKAGE_PATH_NOT_EXPORTED"></a>
25412612

25422613
### `ERR_PACKAGE_PATH_NOT_EXPORTED`
@@ -4559,6 +4630,7 @@ An error occurred trying to allocate memory. This should never happen.
45594630
[domains]: domain.md
45604631
[event emitter-based]: events.md#class-eventemitter
45614632
[file descriptors]: https://en.wikipedia.org/wiki/File_descriptor
4633+
[package map]: packages.md#package-maps
45624634
[relative URL]: https://url.spec.whatwg.org/#relative-url-string
45634635
[self-reference a package using its name]: packages.md#self-referencing-a-package-using-its-name
45644636
[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
@@ -941,6 +941,12 @@ The default loader has the following properties
941941
* Fails on unknown extensions for `file:` loading
942942
(supports only `.cjs`, `.js`, and `.mjs`)
943943
944+
When the [`--experimental-package-map`][] flag is enabled, bare specifier
945+
resolution first consults the package map configuration. If the importing
946+
module is within a mapped package and the specifier matches a declared
947+
dependency, the package map resolution takes precedence. See [Package maps][]
948+
for details.
949+
944950
### Resolution algorithm
945951
946952
The algorithm to load an ES module specifier is given through the
@@ -1304,12 +1310,14 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][].
13041310
[Loading ECMAScript modules using `require()`]: modules.md#loading-ecmascript-modules-using-require
13051311
[Module customization hooks]: module.md#customization-hooks
13061312
[Node.js Module Resolution And Loading Algorithm]: #resolution-algorithm-specification
1313+
[Package maps]: packages.md#package-maps
13071314
[Source Phase Imports]: https://github.com/tc39/proposal-source-phase-imports
13081315
[Terminology]: #terminology
13091316
[URL]: https://url.spec.whatwg.org/
13101317
[WebAssembly JS String Builtins Proposal]: https://github.com/WebAssembly/js-string-builtins
13111318
[`"exports"`]: packages.md#exports
13121319
[`"type"`]: packages.md#type
1320+
[`--experimental-package-map`]: cli.md#--experimental-package-mappath
13131321
[`--input-type`]: cli.md#--input-typetype
13141322
[`data:` URLs]: https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/data
13151323
[`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
@@ -352,8 +352,12 @@ require(X) from module at path Y
352352
4. If X begins with '#'
353353
a. LOAD_PACKAGE_IMPORTS(X, dirname(Y))
354354
5. LOAD_PACKAGE_SELF(X, dirname(Y))
355-
6. LOAD_NODE_MODULES(X, dirname(Y))
356-
7. THROW "not found"
355+
6. If a package map PACKAGE_MAP exists,
356+
a. Find the package ID for the package owning Y
357+
1. Let PARENT_PACKAGE_ID be FIND_PACKAGE_ID(dirname(Y), PACKAGE_MAP)
358+
b. LOAD_PACKAGE_MAP(X, PARENT_PACKAGE_ID, PACKAGE_MAP)
359+
7. LOAD_NODE_MODULES(X, dirname(Y))
360+
8. THROW "not found"
357361
358362
MAYBE_DETECT_AND_LOAD(X)
359363
1. If X parses as a CommonJS module, load X as a CommonJS module. STOP.
@@ -397,9 +401,11 @@ LOAD_AS_DIRECTORY(X)
397401
2. LOAD_INDEX(X)
398402
399403
LOAD_NODE_MODULES(X, START)
400-
1. let DIRS = NODE_MODULES_PATHS(START)
401-
2. for each DIR in DIRS:
402-
a. LOAD_PACKAGE_EXPORTS(X, DIR)
404+
1. Try to interpret X as a combination of NAME and SUBPATH where the name
405+
may have a @scope/ prefix and the subpath begins with a slash (`/`).
406+
2. let DIRS = NODE_MODULES_PATHS(START)
407+
3. for each DIR in DIRS:
408+
a. LOAD_PACKAGE_EXPORTS(SUBPATH, DIR/NAME)
403409
b. LOAD_AS_FILE(DIR/X)
404410
c. LOAD_AS_DIRECTORY(DIR/X)
405411
@@ -414,6 +420,25 @@ NODE_MODULES_PATHS(START)
414420
d. let I = I - 1
415421
5. return DIRS + GLOBAL_FOLDERS
416422
423+
FIND_PACKAGE_ID(PATH, PACKAGE_MAP)
424+
1. Find the PACKAGE_ID for the entry whose "path" is a parent directory of PATH
425+
2. If multiple entries are found, THROW "ambiguous resolution"
426+
3. If no entry was found, THROW "external file".
427+
4. return PACKAGE_ID
428+
429+
LOAD_PACKAGE_MAP(X, PARENT_PACKAGE_ID, PACKAGE_MAP)
430+
1. Try to interpret X as a combination of NAME and SUBPATH where the name
431+
may have a @scope/ prefix and the subpath begins with a slash (`/`).
432+
2. Find the package map entry for key PARENT_PACKAGE_ID
433+
3. Look up NAME in the entry's "dependencies" map.
434+
4. If NAME is not found, THROW "not found".
435+
5. Let TARGET be PACKAGE_MAP.packages[dependencies[name]]
436+
6. Let PACKAGE_PATH be the resolved path of TARGET.
437+
7. LOAD_PACKAGE_EXPORTS(SUBPATH, PACKAGE_PATH)
438+
8. LOAD_AS_FILE(PACKAGE_PATH/SUBPATH)
439+
9. LOAD_AS_DIRECTORY(PACKAGE_PATH/SUBPATH)
440+
10. THROW "not found"
441+
417442
LOAD_PACKAGE_IMPORTS(X, DIR)
418443
1. Find the closest package scope SCOPE to DIR.
419444
2. If no scope was found, return.
@@ -425,19 +450,15 @@ LOAD_PACKAGE_IMPORTS(X, DIR)
425450
CONDITIONS) defined in the ESM resolver.
426451
6. RESOLVE_ESM_MATCH(MATCH).
427452
428-
LOAD_PACKAGE_EXPORTS(X, DIR)
429-
1. Try to interpret X as a combination of NAME and SUBPATH where the name
430-
may have a @scope/ prefix and the subpath begins with a slash (`/`).
431-
2. If X does not match this pattern or DIR/NAME/package.json is not a file,
432-
return.
433-
3. Parse DIR/NAME/package.json, and look for "exports" field.
434-
4. If "exports" is null or undefined, return.
435-
5. If `--no-require-module` is not enabled
453+
LOAD_PACKAGE_EXPORTS(SUBPATH, PACKAGE_DIR)
454+
1. Parse PACKAGE_DIR/package.json, and look for "exports" field.
455+
2. If "exports" is null or undefined, return.
456+
3. If `--no-require-module` is not enabled
436457
a. let CONDITIONS = ["node", "require", "module-sync"]
437458
b. Else, let CONDITIONS = ["node", "require"]
438-
6. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(DIR/NAME), "." + SUBPATH,
459+
4. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(PACKAGE_DIR), "." + SUBPATH,
439460
`package.json` "exports", CONDITIONS) defined in the ESM resolver.
440-
7. RESOLVE_ESM_MATCH(MATCH)
461+
5. RESOLVE_ESM_MATCH(MATCH)
441462
442463
LOAD_PACKAGE_SELF(X, DIR)
443464
1. Find the closest package scope SCOPE to DIR.

0 commit comments

Comments
 (0)