Skip to content

Commit bc12241

Browse files
committed
Documents the dependencies field as a Map<string, string>, where key is bare identifier and value is package ID
1 parent 74bac97 commit bc12241

17 files changed

+150
-59
lines changed

doc/api/errors.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2505,18 +2505,18 @@ added: REPLACEME
25052505
-->
25062506

25072507
A package attempted to import another package that exists in the [package map][]
2508-
but is not listed in its `dependencies` array.
2508+
but is not listed in its `dependencies` object.
25092509

25102510
```mjs
2511-
// package-map.json declares "app" with dependencies: ["utils"]
2511+
// package-map.json declares "app" with dependencies: {"utils": "utils"}
25122512
// but "app" tries to import "secret-lib" which exists in the map
25132513

25142514
// In app/index.js
25152515
import secret from 'secret-lib'; // Throws ERR_PACKAGE_MAP_ACCESS_DENIED
25162516
```
25172517

25182518
To fix this error, add the required package to the importing package's
2519-
`dependencies` array in the package map configuration file.
2519+
`dependencies` object in the package map configuration file.
25202520

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

@@ -2546,26 +2546,28 @@ Error [ERR_PACKAGE_MAP_INVALID]: Invalid package map at "./missing.json": file n
25462546
added: REPLACEME
25472547
-->
25482548

2549-
A package's `dependencies` array in the [package map][] references a key that
2550-
is not defined in the `packages` object.
2549+
A package's `dependencies` object in the [package map][] references a package
2550+
key that is not defined in the `packages` object.
25512551

25522552
```json
25532553
{
25542554
"packages": {
25552555
"app": {
25562556
"name": "app",
25572557
"path": "./app",
2558-
"dependencies": ["nonexistent"]
2558+
"dependencies": {
2559+
"foo": "nonexistent"
2560+
}
25592561
}
25602562
}
25612563
}
25622564
```
25632565

2564-
In this example, `"nonexistent"` is referenced in `dependencies` but not
2566+
In this example, `"nonexistent"` is referenced as a dependency target but not
25652567
defined in `packages`, which will throw this error.
25662568

2567-
To fix this error, ensure all keys referenced in `dependencies` arrays are
2568-
defined in the `packages` object.
2569+
To fix this error, ensure all package keys referenced in `dependencies` values
2570+
are defined in the `packages` object.
25692571

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

doc/api/packages.md

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -989,17 +989,21 @@ Each key in `packages` is a unique identifier for a package entry:
989989
"app": {
990990
"name": "my-app",
991991
"path": "./packages/app",
992-
"dependencies": ["utils", "ui-lib"]
992+
"dependencies": {
993+
"@myorg/utils": "utils",
994+
"@myorg/ui-lib": "ui-lib"
995+
}
993996
},
994997
"utils": {
995998
"name": "@myorg/utils",
996-
"path": "./packages/utils",
997-
"dependencies": []
999+
"path": "./packages/utils"
9981000
},
9991001
"ui-lib": {
10001002
"name": "@myorg/ui-lib",
10011003
"path": "./packages/ui-lib",
1002-
"dependencies": ["utils"]
1004+
"dependencies": {
1005+
"@myorg/utils": "utils"
1006+
}
10031007
}
10041008
}
10051009
}
@@ -1011,8 +1015,10 @@ Each package entry has the following fields:
10111015
the package directory.
10121016
* `name` {string} The package name used in import specifiers. If omitted, the
10131017
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.
1018+
* `dependencies` {Object} An object mapping bare specifiers to package keys.
1019+
Each key is the import name used in source code, and each value is the
1020+
corresponding package key in the `packages` object. Defaults to an empty
1021+
object.
10161022

10171023
### Resolution algorithm
10181024

@@ -1022,9 +1028,9 @@ When a bare specifier is encountered:
10221028
if the file path is within any package's `path`.
10231029
2. If the importing file is not within any mapped package, a
10241030
`MODULE_NOT_FOUND` error is thrown.
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`.
1031+
3. Node.js looks up the specifier's package name in the importing package's
1032+
`dependencies` object to find the corresponding package key.
1033+
4. If found, the specifier resolves to the target package's `path`.
10281034
5. If the package exists in the map but is not in `dependencies`, an
10291035
[`ERR_PACKAGE_MAP_ACCESS_DENIED`][] error is thrown.
10301036
6. If the package does not exist in the map at all, a
@@ -1046,31 +1052,34 @@ then used to resolve the final file path.
10461052

10471053
### Multiple package versions
10481054

1049-
Different packages can depend on different versions of the same package by
1050-
using distinct keys:
1055+
Different packages can depend on different versions of the same package.
1056+
Because `dependencies` maps bare specifiers to package keys, two packages
1057+
can map the same specifier to different targets:
10511058

10521059
```json
10531060
{
10541061
"packages": {
10551062
"app": {
10561063
"name": "app",
10571064
"path": "./app",
1058-
"dependencies": ["component-v2"]
1065+
"dependencies": {
1066+
"component": "component-v2"
1067+
}
10591068
},
10601069
"legacy": {
10611070
"name": "legacy",
10621071
"path": "./legacy",
1063-
"dependencies": ["component-v1"]
1072+
"dependencies": {
1073+
"component": "component-v1"
1074+
}
10641075
},
10651076
"component-v1": {
10661077
"name": "component",
1067-
"path": "./vendor/component-1.0.0",
1068-
"dependencies": []
1078+
"path": "./vendor/component-1.0.0"
10691079
},
10701080
"component-v2": {
10711081
"name": "component",
1072-
"path": "./vendor/component-2.0.0",
1073-
"dependencies": []
1082+
"path": "./vendor/component-2.0.0"
10741083
}
10751084
}
10761085
}

lib/internal/modules/package_map.js

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ let emittedWarning = false;
3232
/**
3333
* @typedef {object} PackageMapEntry
3434
* @property {string} path - Absolute path to package on disk
35-
* @property {Set<string>} dependencies - Set of package keys this package can access
35+
* @property {Map<string, string>} dependencies - Map from bare specifier to package key
3636
* @property {string|undefined} name - Package name (undefined = nameless package)
3737
*/
3838

@@ -85,7 +85,7 @@ class PackageMap {
8585

8686
this.#packages.set(key, {
8787
path: absolutePath,
88-
dependencies: new SafeSet(entry.dependencies ?? []),
88+
dependencies: new SafeMap(ObjectEntries(entry.dependencies ?? {})),
8989
name: entry.name, // Undefined for nameless packages
9090
});
9191

@@ -166,20 +166,9 @@ class PackageMap {
166166
const { packageName, subpath } = parsePackageName(specifier);
167167
const parentEntry = this.#packages.get(parentKey);
168168

169-
// Find matching dependency by name
170-
let targetKey = null;
171-
for (const depKey of parentEntry.dependencies) {
172-
const depEntry = this.#packages.get(depKey);
173-
if (!depEntry) {
174-
throw new ERR_PACKAGE_MAP_KEY_NOT_FOUND(depKey, this.#configPath);
175-
}
176-
if (depEntry.name === packageName) {
177-
targetKey = depKey;
178-
break;
179-
}
180-
}
169+
const targetKey = parentEntry.dependencies.get(packageName);
181170

182-
if (targetKey === null) {
171+
if (targetKey === undefined) {
183172
// Check if package exists anywhere in map but isn't accessible
184173
if (this.#nameToKeys.has(packageName)) {
185174
throw new ERR_PACKAGE_MAP_ACCESS_DENIED(
@@ -193,6 +182,10 @@ class PackageMap {
193182
}
194183

195184
const targetEntry = this.#packages.get(targetKey);
185+
if (!targetEntry) {
186+
throw new ERR_PACKAGE_MAP_KEY_NOT_FOUND(targetKey, this.#configPath);
187+
}
188+
196189
return { packagePath: targetEntry.path, subpath };
197190
}
198191
}

test/es-module/test-esm-package-map.mjs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,27 @@ describe('ESM: --experimental-package-map', () => {
261261
});
262262
});
263263

264+
// =========== Same Request, Different Versions ===========
265+
266+
describe('same request resolves to different versions', () => {
267+
const versionForkMap = fixtures.path('package-map/package-map-version-fork.json');
268+
269+
it('resolves the same bare specifier to different packages depending on the importer', async () => {
270+
// app-18 and app-19 both import 'react', but the package map wires
271+
// them to react@18 and react@19 respectively.
272+
const { code, stdout, stderr } = await spawnPromisified(execPath, [
273+
'--experimental-package-map', versionForkMap,
274+
'--input-type=module',
275+
'--eval',
276+
`import app18 from 'app-18'; import app19 from 'app-19'; console.log(app18); console.log(app19);`,
277+
], { cwd: fixtures.path('package-map/root') });
278+
279+
assert.strictEqual(code, 0, stderr);
280+
assert.match(stdout, /app-18 using react 18/);
281+
assert.match(stdout, /app-19 using react 19/);
282+
});
283+
});
284+
264285
// =========== Longest Path Wins ===========
265286

266287
describe('longest path wins', () => {
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import react from 'react';
2+
export default `app-18 using react ${react.version}`;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "app-18",
3+
"type": "module",
4+
"exports": {
5+
".": "./index.js"
6+
}
7+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import react from 'react';
2+
export default `app-19 using react ${react.version}`;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "app-19",
3+
"type": "module",
4+
"exports": {
5+
".": "./index.js"
6+
}
7+
}

test/fixtures/package-map/nested-project/package-map-external-deps.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
"app": {
44
"name": "app",
55
"path": "./src",
6-
"dependencies": ["dep-a"]
6+
"dependencies": {"dep-a": "dep-a"}
77
},
88
"dep-a": {
99
"name": "dep-a",
1010
"path": "../dep-a",
11-
"dependencies": []
11+
"dependencies": {}
1212
}
1313
}
1414
}

test/fixtures/package-map/package-map-longest-path.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@
33
"root": {
44
"name": "root",
55
"path": "./root",
6-
"dependencies": ["inner"]
6+
"dependencies": {"inner": "inner"}
77
},
88
"inner": {
99
"name": "inner",
1010
"path": "./root/node_modules/inner",
11-
"dependencies": ["dep-a"]
11+
"dependencies": {"dep-a": "dep-a"}
1212
},
1313
"dep-a": {
1414
"name": "dep-a",
1515
"path": "./dep-a",
16-
"dependencies": []
16+
"dependencies": {}
1717
}
1818
}
1919
}

0 commit comments

Comments
 (0)