Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -1219,6 +1219,28 @@ added:

Enable experimental support for the network inspection with Chrome DevTools.

### `--experimental-package-map=<path>`

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental

Enable experimental package map resolution. The `path` argument specifies the
location of a JSON configuration file that defines package resolution mappings.

```bash
node --experimental-package-map=./package-map.json app.js
```

When enabled, bare specifier resolution consults the package map before
falling back to standard `node_modules` resolution. This allows explicit
control over which packages can import which dependencies.

See [Package maps][] for details on the configuration file format and
resolution algorithm.

### `--experimental-print-required-tla`

<!-- YAML
Expand Down Expand Up @@ -3604,6 +3626,7 @@ one is included in the list below.
* `--experimental-json-modules`
* `--experimental-loader`
* `--experimental-modules`
* `--experimental-package-map`
* `--experimental-print-required-tla`
* `--experimental-quic`
* `--experimental-require-module`
Expand Down Expand Up @@ -4197,6 +4220,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
[Navigator API]: globals.md#navigator
[Node.js issue tracker]: https://github.com/nodejs/node/issues
[OSSL_PROVIDER-legacy]: https://www.openssl.org/docs/man3.0/man7/OSSL_PROVIDER-legacy.html
[Package maps]: packages.md#package-maps
[Permission Model]: permissions.md#permission-model
[REPL]: repl.md
[ScriptCoverage]: https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-ScriptCoverage
Expand Down
72 changes: 72 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -2496,6 +2496,77 @@ A given value is out of the accepted range.
The `package.json` [`"imports"`][] field does not define the given internal
package specifier mapping.

<a id="ERR_PACKAGE_MAP_EXTERNAL_FILE"></a>

### `ERR_PACKAGE_MAP_EXTERNAL_FILE`

<!-- YAML
added: REPLACEME
-->

A module attempted to resolve a bare specifier using the [package map][], but
the importing file is not located within any package defined in the map.

```console
$ node --experimental-package-map=./package-map.json /tmp/script.js
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
```

To fix this error, ensure the importing file is inside one of the package
directories listed in the package map, or add a new package entry whose `path`
covers the importing file.

<a id="ERR_PACKAGE_MAP_INVALID"></a>

### `ERR_PACKAGE_MAP_INVALID`

<!-- YAML
added: REPLACEME
-->

The [package map][] configuration file is invalid. This can occur when:

* The file does not exist at the specified path.
* The file contains invalid JSON.
* The file is missing the required `packages` object.
* A package entry is missing the required `path` field.
* Two package entries have the same `path` value.

```console
$ node --experimental-package-map=./missing.json app.js
Error [ERR_PACKAGE_MAP_INVALID]: Invalid package map at "./missing.json": file not found
```

<a id="ERR_PACKAGE_MAP_KEY_NOT_FOUND"></a>

### `ERR_PACKAGE_MAP_KEY_NOT_FOUND`

<!-- YAML
added: REPLACEME
-->

A package's `dependencies` object in the [package map][] references a package
key that is not defined in the `packages` object.

```json
{
"packages": {
"app": {
"path": "./app",
"dependencies": {
"foo": "nonexistent"
}
}
}
}
```

In this example, `"nonexistent"` is referenced as a dependency target but not
defined in `packages`, which will throw this error.

To fix this error, ensure all package keys referenced in `dependencies` values
are defined in the `packages` object.

<a id="ERR_PACKAGE_PATH_NOT_EXPORTED"></a>

### `ERR_PACKAGE_PATH_NOT_EXPORTED`
Expand Down Expand Up @@ -4463,6 +4534,7 @@ An error occurred trying to allocate memory. This should never happen.
[domains]: domain.md
[event emitter-based]: events.md#class-eventemitter
[file descriptors]: https://en.wikipedia.org/wiki/File_descriptor
[package map]: packages.md#package-maps
[relative URL]: https://url.spec.whatwg.org/#relative-url-string
[self-reference a package using its name]: packages.md#self-referencing-a-package-using-its-name
[special scheme]: https://url.spec.whatwg.org/#special-scheme
Expand Down
8 changes: 8 additions & 0 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,12 @@ The default loader has the following properties
* Fails on unknown extensions for `file:` loading
(supports only `.cjs`, `.js`, and `.mjs`)

When the [`--experimental-package-map`][] flag is enabled, bare specifier
resolution first consults the package map configuration. If the importing
module is within a mapped package and the specifier matches a declared
dependency, the package map resolution takes precedence. See [Package maps][]
for details.

### Resolution algorithm

The algorithm to load an ES module specifier is given through the
Expand Down Expand Up @@ -1297,12 +1303,14 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][].
[Loading ECMAScript modules using `require()`]: modules.md#loading-ecmascript-modules-using-require
[Module customization hooks]: module.md#customization-hooks
[Node.js Module Resolution And Loading Algorithm]: #resolution-algorithm-specification
[Package maps]: packages.md#package-maps
[Source Phase Imports]: https://github.com/tc39/proposal-source-phase-imports
[Terminology]: #terminology
[URL]: https://url.spec.whatwg.org/
[WebAssembly JS String Builtins Proposal]: https://github.com/WebAssembly/js-string-builtins
[`"exports"`]: packages.md#exports
[`"type"`]: packages.md#type
[`--experimental-package-map`]: cli.md#--experimental-package-mappath
[`--input-type`]: cli.md#--input-typetype
[`data:` URLs]: https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/data
[`export`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export
Expand Down
6 changes: 6 additions & 0 deletions doc/api/modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,10 @@ This feature can be detected by checking if
To get the exact filename that will be loaded when `require()` is called, use
the `require.resolve()` function.

When the [`--experimental-package-map`][] flag is enabled, bare specifier
resolution first consults the package map before searching `node_modules`
directories. See [Package maps][] for details.

Putting together all of the above, here is the high-level algorithm
in pseudocode of what `require()` does:

Expand Down Expand Up @@ -1269,8 +1273,10 @@ This section was moved to
[Determining module system]: packages.md#determining-module-system
[ECMAScript Modules]: esm.md
[GLOBAL_FOLDERS]: #loading-from-the-global-folders
[Package maps]: packages.md#package-maps
[`"main"`]: packages.md#main
[`"type"`]: packages.md#type
[`--experimental-package-map`]: cli.md#--experimental-package-mappath
[`--trace-require-module`]: cli.md#--trace-require-modulemode
[`ERR_REQUIRE_ASYNC_MODULE`]: errors.md#err_require_async_module
[`ERR_UNSUPPORTED_DIR_IMPORT`]: errors.md#err_unsupported_dir_import
Expand Down
166 changes: 166 additions & 0 deletions doc/api/packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,169 @@ $ node other.js

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

## Package maps

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental

Package maps provide a mechanism to control package resolution without relying
on the `node_modules` folder structure. When enabled via the
[`--experimental-package-map`][] flag, Node.js uses a JSON configuration file
to determine how bare specifiers are resolved.

This feature is useful for:

* **Monorepos**: Define explicit dependency relationships between workspace
packages without symlinks or hoisting complexities.
* **Dependency isolation**: Prevent packages from accessing undeclared
dependencies (phantom dependencies).
* **Multiple versions**: Allow different packages to depend on different
versions of the same dependency.

### Enabling package maps

Package maps are enabled by passing the `--experimental-package-map` flag
with a path to the configuration file:

```bash
node --experimental-package-map=./package-map.json app.js
```

### Configuration file format

The package map configuration file is a JSON file with a `packages` object.
Each key in `packages` is a unique identifier for a package entry:

```json
{
"packages": {
"app": {
"path": "./packages/app",
"dependencies": {
"@myorg/utils": "utils",
"@myorg/ui-lib": "ui-lib"
}
},
"utils": {
"path": "./packages/utils"
},
"ui-lib": {
"path": "./packages/ui-lib",
"dependencies": {
"@myorg/utils": "utils"
}
}
}
}
```

Each package entry has the following fields:

* `path` {string} **Required.** Relative path from the configuration file to
the package directory. Each path must be unique across all packages in the
map; duplicate paths will throw an [`ERR_PACKAGE_MAP_INVALID`][] error.
* `dependencies` {Object} An object mapping bare specifiers to package keys.
Each key is the import name used in source code, and each value is the
corresponding package key in the `packages` object. Defaults to an empty
object.

### Resolution algorithm

When a bare specifier is encountered:

1. Node.js determines which package contains the importing file by checking
if the file path is within any package's `path`.
2. If the importing file is not within any mapped package, an
[`ERR_PACKAGE_MAP_EXTERNAL_FILE`][] error is thrown.
3. Node.js looks up the specifier's package name in the importing package's
`dependencies` object to find the corresponding package key.
4. If found, the specifier resolves to the target package's `path`.
5. If the specifier is not in `dependencies`, a
`MODULE_NOT_FOUND` error is thrown.

### Subpath resolution

Package maps support importing subpaths. Given the configuration above:

```js
// In packages/app/index.js
import { helper } from '@myorg/utils'; // Resolves to ./packages/utils
import { format } from '@myorg/utils/format'; // Resolves to ./packages/utils/format
```

The subpath portion of the specifier is preserved and appended to the resolved
package path. The target package's `package.json` [`"exports"`][] field is
then used to resolve the final file path.

### Multiple package versions

Different packages can depend on different versions of the same package.
Because `dependencies` maps bare specifiers to package keys, two packages
can map the same specifier to different targets:

```json
{
"packages": {
"app": {
"path": "./app",
"dependencies": {
"component": "component-v2"
}
},
"legacy": {
"path": "./legacy",
"dependencies": {
"component": "component-v1"
}
},
"component-v1": {
"path": "./vendor/component-1.0.0"
},
"component-v2": {
"path": "./vendor/component-2.0.0"
}
}
}
```

Both `app` and `legacy` can `import 'component'`, but they resolve to
different paths based on their declared dependencies.

### CommonJS and ES modules

Package maps work with both CommonJS (`require()`) and ES modules (`import`).
The resolution behavior is identical for both module systems.

```cjs
// CommonJS
const utils = require('@myorg/utils');
```

```mjs
// ES modules
import utils from '@myorg/utils';
```

### Interaction with other resolution

Package maps only apply to bare specifiers that are not Node.js builtin
modules. The following cases are not affected by package maps and continue
to use standard resolution:

* Relative paths (`./` or `../`).
* Absolute paths or URLs.
* Node.js builtin modules (`node:fs`, etc.).

### Limitations

* Package maps must be a single static file; dynamic configuration is not
supported.
* Circular dependency detection is not performed by the package map resolver.
* The package map file is loaded synchronously at startup.

## Node.js `package.json` field definitions

This section describes the fields used by the Node.js runtime. Other tools (such
Expand Down Expand Up @@ -1177,7 +1340,10 @@ This field defines [subpath imports][] for the current package.
[`"type"`]: #type
[`--conditions` / `-C` flag]: #resolving-user-conditions
[`--experimental-addon-modules`]: cli.md#--experimental-addon-modules
[`--experimental-package-map`]: cli.md#--experimental-package-mappath
[`--no-addons` flag]: cli.md#--no-addons
[`ERR_PACKAGE_MAP_EXTERNAL_FILE`]: errors.md#err_package_map_external_file
[`ERR_PACKAGE_MAP_INVALID`]: errors.md#err_package_map_invalid
[`ERR_PACKAGE_PATH_NOT_EXPORTED`]: errors.md#err_package_path_not_exported
[`ERR_UNKNOWN_FILE_EXTENSION`]: errors.md#err_unknown_file_extension
[`package.json`]: #nodejs-packagejson-field-definitions
Expand Down
5 changes: 5 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,9 @@ This feature requires \fB--allow-worker\fR if used with the Permission Model.
.It Fl -experimental-network-inspection
Enable experimental support for the network inspection with Chrome DevTools.
.
.It Fl -experimental-package-map Ns = Ns Ar path
Enable experimental package map resolution using the specified configuration file.
.
.It Fl -experimental-print-required-tla
If the ES module being \fBrequire()\fR'd contains top-level \fBawait\fR, this flag
allows Node.js to evaluate the module, try to locate the
Expand Down Expand Up @@ -1865,6 +1868,8 @@ one is included in the list below.
.It
\fB--experimental-modules\fR
.It
\fB--experimental-package-map\fR
.It
\fB--experimental-print-required-tla\fR
.It
\fB--experimental-quic\fR
Expand Down
9 changes: 9 additions & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1661,6 +1661,15 @@ E('ERR_PACKAGE_IMPORT_NOT_DEFINED', (specifier, packagePath, base) => {
return `Package import specifier "${specifier}" is not defined${packagePath ?
` in package ${packagePath}package.json` : ''} imported from ${base}`;
}, TypeError);
E('ERR_PACKAGE_MAP_EXTERNAL_FILE', (specifier, parentPath, configPath) => {
return `Cannot resolve "${specifier}" from "${parentPath}": file is not within any package defined in ${configPath}`;
}, Error);
E('ERR_PACKAGE_MAP_INVALID', (configPath, reason) => {
return `Invalid package-map.json at "${configPath}": ${reason}`;
}, SyntaxError);
E('ERR_PACKAGE_MAP_KEY_NOT_FOUND', (key, configPath) => {
return `Package key "${key}" referenced in dependencies but not defined in ${configPath}`;
}, Error);
E('ERR_PACKAGE_PATH_NOT_EXPORTED', (pkgPath, subpath, base = undefined) => {
if (subpath === '.')
return `No "exports" main defined in ${pkgPath}package.json${base ?
Expand Down
Loading
Loading