Skip to content

Commit f6a4015

Browse files
committed
loader: implement package maps
1 parent 66a687f commit f6a4015

Some content is hidden

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

45 files changed

+1436
-18
lines changed

doc/api/cli.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1219,6 +1219,28 @@ added:
12191219
12201220
Enable experimental support for the network inspection with Chrome DevTools.
12211221

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

12241246
<!-- YAML
@@ -3604,6 +3626,7 @@ one is included in the list below.
36043626
* `--experimental-json-modules`
36053627
* `--experimental-loader`
36063628
* `--experimental-modules`
3629+
* `--experimental-package-map`
36073630
* `--experimental-print-required-tla`
36083631
* `--experimental-quic`
36093632
* `--experimental-require-module`
@@ -4197,6 +4220,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
41974220
[Navigator API]: globals.md#navigator
41984221
[Node.js issue tracker]: https://github.com/nodejs/node/issues
41994222
[OSSL_PROVIDER-legacy]: https://www.openssl.org/docs/man3.0/man7/OSSL_PROVIDER-legacy.html
4223+
[Package maps]: packages.md#package-maps
42004224
[Permission Model]: permissions.md#permission-model
42014225
[REPL]: repl.md
42024226
[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_ACCESS_DENIED"></a>
2500+
2501+
### `ERR_PACKAGE_MAP_ACCESS_DENIED`
2502+
2503+
<!-- YAML
2504+
added: REPLACEME
2505+
-->
2506+
2507+
A package attempted to import another package that exists in the [package map][]
2508+
but is not listed in its `dependencies` array.
2509+
2510+
```js
2511+
// package-map.json declares "app" with dependencies: ["utils"]
2512+
// but "app" tries to import "secret-lib" which exists in the map
2513+
2514+
// In app/index.js
2515+
import secret from 'secret-lib'; // Throws ERR_PACKAGE_MAP_ACCESS_DENIED
2516+
```
2517+
2518+
To fix this error, add the required package to the importing package's
2519+
`dependencies` array in the package map configuration file.
2520+
2521+
<a id="ERR_PACKAGE_MAP_INVALID"></a>
2522+
2523+
### `ERR_PACKAGE_MAP_INVALID`
2524+
2525+
<!-- YAML
2526+
added: REPLACEME
2527+
-->
2528+
2529+
The [package map][] configuration file is invalid. This can occur when:
2530+
2531+
* The file does not exist at the specified path.
2532+
* The file contains invalid JSON.
2533+
* The file is missing the required `packages` object.
2534+
* A package entry is missing the required `path` field.
2535+
2536+
```console
2537+
$ node --experimental-package-map=./missing.json app.js
2538+
Error [ERR_PACKAGE_MAP_INVALID]: Invalid package map at "./missing.json": file not found
2539+
```
2540+
2541+
<a id="ERR_PACKAGE_MAP_KEY_NOT_FOUND"></a>
2542+
2543+
### `ERR_PACKAGE_MAP_KEY_NOT_FOUND`
2544+
2545+
<!-- YAML
2546+
added: REPLACEME
2547+
-->
2548+
2549+
A package's `dependencies` array in the [package map][] references a key that
2550+
is not defined in the `packages` object.
2551+
2552+
```json
2553+
{
2554+
"packages": {
2555+
"app": {
2556+
"name": "app",
2557+
"path": "./app",
2558+
"dependencies": ["nonexistent"]
2559+
}
2560+
}
2561+
}
2562+
```
2563+
2564+
In this example, `"nonexistent"` is referenced in `dependencies` but not
2565+
defined in `packages`, which will throw this error.
2566+
2567+
To fix this error, ensure all keys referenced in `dependencies` arrays are
2568+
defined in the `packages` object.
2569+
24992570
<a id="ERR_PACKAGE_PATH_NOT_EXPORTED"></a>
25002571

25012572
### `ERR_PACKAGE_PATH_NOT_EXPORTED`
@@ -4433,6 +4504,7 @@ An error occurred trying to allocate memory. This should never happen.
44334504
[`new URL(input)`]: url.md#new-urlinput-base
44344505
[`new URLPattern(input)`]: url.md#new-urlpatternstring-baseurl-options
44354506
[`new URLSearchParams(iterable)`]: url.md#new-urlsearchparamsiterable
4507+
[package map]: packages.md#package-maps
44364508
[`package.json`]: packages.md#nodejs-packagejson-field-definitions
44374509
[`postMessage()`]: worker_threads.md#portpostmessagevalue-transferlist
44384510
[`postMessageToThread()`]: worker_threads.md#worker_threadspostmessagetothreadthreadid-value-transferlist-timeout

doc/api/esm.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -934,6 +934,12 @@ The default loader has the following properties
934934
* Fails on unknown extensions for `file:` loading
935935
(supports only `.cjs`, `.js`, and `.mjs`)
936936
937+
When the [`--experimental-package-map`][] flag is enabled, bare specifier
938+
resolution first consults the package map configuration. If the importing
939+
module is within a mapped package and the specifier matches a declared
940+
dependency, the package map resolution takes precedence. See [Package maps][]
941+
for details.
942+
937943
### Resolution algorithm
938944
939945
The algorithm to load an ES module specifier is given through the
@@ -1303,6 +1309,7 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][].
13031309
[WebAssembly JS String Builtins Proposal]: https://github.com/WebAssembly/js-string-builtins
13041310
[`"exports"`]: packages.md#exports
13051311
[`"type"`]: packages.md#type
1312+
[`--experimental-package-map`]: cli.md#--experimental-package-mappath
13061313
[`--input-type`]: cli.md#--input-typetype
13071314
[`data:` URLs]: https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/data
13081315
[`export`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export
@@ -1324,6 +1331,7 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][].
13241331
[custom https loader]: module.md#import-from-https
13251332
[import.meta.resolve]: #importmetaresolvespecifier
13261333
[merve]: https://github.com/anonrig/merve/tree/v1.0.0
1334+
[Package maps]: packages.md#package-maps
13271335
[percent-encoded]: url.md#percent-encoding-in-urls
13281336
[special scheme]: https://url.spec.whatwg.org/#special-scheme
13291337
[status code]: process.md#exit-codes

doc/api/modules.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,10 @@ 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+
343347
Putting together all of the above, here is the high-level algorithm
344348
in pseudocode of what `require()` does:
345349

@@ -1271,6 +1275,7 @@ This section was moved to
12711275
[GLOBAL_FOLDERS]: #loading-from-the-global-folders
12721276
[`"main"`]: packages.md#main
12731277
[`"type"`]: packages.md#type
1278+
[`--experimental-package-map`]: cli.md#--experimental-package-mappath
12741279
[`--trace-require-module`]: cli.md#--trace-require-modulemode
12751280
[`ERR_REQUIRE_ASYNC_MODULE`]: errors.md#err_require_async_module
12761281
[`ERR_UNSUPPORTED_DIR_IMPORT`]: errors.md#err_unsupported_dir_import
@@ -1295,5 +1300,6 @@ This section was moved to
12951300
[module namespace object]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import#module_namespace_object
12961301
[module resolution]: #all-together
12971302
[native addons]: addons.md
1303+
[Package maps]: packages.md#package-maps
12981304
[subpath exports]: packages.md#subpath-exports
12991305
[subpath imports]: packages.md#subpath-imports

doc/api/packages.md

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -947,6 +947,171 @@ $ node other.js
947947

948948
See [the package examples repository][] for details.
949949

950+
## Package maps
951+
952+
<!-- YAML
953+
added: REPLACEME
954+
-->
955+
956+
> Stability: 1 - Experimental
957+
958+
Package maps provide a mechanism to control package resolution without relying
959+
on the `node_modules` folder structure. When enabled via the
960+
[`--experimental-package-map`][] flag, Node.js uses a JSON configuration file
961+
to determine how bare specifiers are resolved.
962+
963+
This feature is useful for:
964+
965+
* **Monorepos**: Define explicit dependency relationships between workspace
966+
packages without symlinks or hoisting complexities.
967+
* **Dependency isolation**: Prevent packages from accessing undeclared
968+
dependencies (phantom dependencies).
969+
* **Multiple versions**: Allow different packages to depend on different
970+
versions of the same dependency.
971+
972+
### Enabling package maps
973+
974+
Package maps are enabled by passing the `--experimental-package-map` flag
975+
with a path to the configuration file:
976+
977+
```bash
978+
node --experimental-package-map=./package-map.json app.js
979+
```
980+
981+
### Configuration file format
982+
983+
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:
985+
986+
```json
987+
{
988+
"packages": {
989+
"app": {
990+
"name": "my-app",
991+
"path": "./packages/app",
992+
"dependencies": ["utils", "ui-lib"]
993+
},
994+
"utils": {
995+
"name": "@myorg/utils",
996+
"path": "./packages/utils",
997+
"dependencies": []
998+
},
999+
"ui-lib": {
1000+
"name": "@myorg/ui-lib",
1001+
"path": "./packages/ui-lib",
1002+
"dependencies": ["utils"]
1003+
}
1004+
}
1005+
}
1006+
```
1007+
1008+
Each package entry has the following fields:
1009+
1010+
* `path` {string} **Required.** Relative path from the configuration file to
1011+
the package directory.
1012+
* `name` {string} The package name used in import specifiers. If omitted, the
1013+
package cannot be imported by name but can still import its dependencies.
1014+
* `dependencies` {string\[]} Array of package keys that this package is allowed
1015+
to import. Defaults to an empty array.
1016+
1017+
### Resolution algorithm
1018+
1019+
When a bare specifier is encountered:
1020+
1021+
1. Node.js determines which package contains the importing file by checking
1022+
if the file path is within any package's `path`.
1023+
2. If the importing file is not within any mapped package, standard
1024+
`node_modules` resolution is used.
1025+
3. Node.js searches the importing package's `dependencies` array for an entry
1026+
whose `name` matches the specifier's package name.
1027+
4. If found, the specifier resolves to that dependency's `path`.
1028+
5. If the package exists in the map but is not in `dependencies`, an
1029+
[`ERR_PACKAGE_MAP_ACCESS_DENIED`][] error is thrown.
1030+
6. If the package does not exist in the map at all, standard `node_modules`
1031+
resolution is used as a fallback.
1032+
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.
1046+
1047+
### Multiple package versions
1048+
1049+
Different packages can depend on different versions of the same package by
1050+
using distinct keys:
1051+
1052+
```json
1053+
{
1054+
"packages": {
1055+
"app": {
1056+
"name": "app",
1057+
"path": "./app",
1058+
"dependencies": ["component-v2"]
1059+
},
1060+
"legacy": {
1061+
"name": "legacy",
1062+
"path": "./legacy",
1063+
"dependencies": ["component-v1"]
1064+
},
1065+
"component-v1": {
1066+
"name": "component",
1067+
"path": "./vendor/component-1.0.0",
1068+
"dependencies": []
1069+
},
1070+
"component-v2": {
1071+
"name": "component",
1072+
"path": "./vendor/component-2.0.0",
1073+
"dependencies": []
1074+
}
1075+
}
1076+
}
1077+
```
1078+
1079+
Both `app` and `legacy` can `import 'component'`, but they resolve to
1080+
different paths based on their declared dependencies.
1081+
1082+
### CommonJS and ES modules
1083+
1084+
Package maps work with both CommonJS (`require()`) and ES modules (`import`).
1085+
The resolution behavior is identical for both module systems.
1086+
1087+
```cjs
1088+
// CommonJS
1089+
const utils = require('@myorg/utils');
1090+
```
1091+
1092+
```mjs
1093+
// ES modules
1094+
import utils from '@myorg/utils';
1095+
```
1096+
1097+
### Fallback behavior
1098+
1099+
Package maps do not replace `node_modules` resolution entirely. Resolution
1100+
falls back to standard behavior when:
1101+
1102+
* The importing file is not within any package defined in the map.
1103+
* The specifier's package name is not found in any package's `name` field.
1104+
* The specifier is a relative path (`./` or `../`).
1105+
* The specifier is an absolute path or URL.
1106+
* The specifier refers to a Node.js builtin module (`node:fs`, etc.).
1107+
1108+
### Limitations
1109+
1110+
* Package maps must be a single static file; dynamic configuration is not
1111+
supported.
1112+
* Circular dependency detection is not performed by the package map resolver.
1113+
* The package map file is loaded synchronously at startup.
1114+
9501115
## Node.js `package.json` field definitions
9511116

9521117
This section describes the fields used by the Node.js runtime. Other tools (such
@@ -1177,7 +1342,9 @@ This field defines [subpath imports][] for the current package.
11771342
[`"type"`]: #type
11781343
[`--conditions` / `-C` flag]: #resolving-user-conditions
11791344
[`--experimental-addon-modules`]: cli.md#--experimental-addon-modules
1345+
[`--experimental-package-map`]: cli.md#--experimental-package-mappath
11801346
[`--no-addons` flag]: cli.md#--no-addons
1347+
[`ERR_PACKAGE_MAP_ACCESS_DENIED`]: errors.md#err_package_map_access_denied
11811348
[`ERR_PACKAGE_PATH_NOT_EXPORTED`]: errors.md#err_package_path_not_exported
11821349
[`ERR_UNKNOWN_FILE_EXTENSION`]: errors.md#err_unknown_file_extension
11831350
[`package.json`]: #nodejs-packagejson-field-definitions

doc/node.1

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,9 @@ This feature requires \fB--allow-worker\fR if used with the Permission Model.
712712
.It Fl -experimental-network-inspection
713713
Enable experimental support for the network inspection with Chrome DevTools.
714714
.
715+
.It Fl -experimental-package-map Ns = Ns Ar path
716+
Enable experimental package map resolution using the specified configuration file.
717+
.
715718
.It Fl -experimental-print-required-tla
716719
If the ES module being \fBrequire()\fR'd contains top-level \fBawait\fR, this flag
717720
allows Node.js to evaluate the module, try to locate the
@@ -1865,6 +1868,8 @@ one is included in the list below.
18651868
.It
18661869
\fB--experimental-modules\fR
18671870
.It
1871+
\fB--experimental-package-map\fR
1872+
.It
18681873
\fB--experimental-print-required-tla\fR
18691874
.It
18701875
\fB--experimental-quic\fR

0 commit comments

Comments
 (0)