Skip to content

Commit 72ed592

Browse files
committed
Tweaks (remove name, validate duplicated paths)
1 parent 58adeb4 commit 72ed592

15 files changed

+92
-206
lines changed

doc/api/errors.md

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2496,27 +2496,25 @@ 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>
2499+
<a id="ERR_PACKAGE_MAP_EXTERNAL_FILE"></a>
25002500

2501-
### `ERR_PACKAGE_MAP_ACCESS_DENIED`
2501+
### `ERR_PACKAGE_MAP_EXTERNAL_FILE`
25022502

25032503
<!-- YAML
25042504
added: REPLACEME
25052505
-->
25062506

2507-
A package attempted to import another package that exists in the [package map][]
2508-
but is not listed in its `dependencies` object.
2507+
A module attempted to resolve a bare specifier using the [package map][], but
2508+
the importing file is not located within any package defined in the map.
25092509

2510-
```mjs
2511-
// package-map.json declares "app" with dependencies: {"utils": "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
2510+
```console
2511+
$ node --experimental-package-map=./package-map.json /tmp/script.js
2512+
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
25162513
```
25172514

2518-
To fix this error, add the required package to the importing package's
2519-
`dependencies` object in the package map configuration file.
2515+
To fix this error, ensure the importing file is inside one of the package
2516+
directories listed in the package map, or add a new package entry whose `path`
2517+
covers the importing file.
25202518

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

@@ -2532,6 +2530,7 @@ The [package map][] configuration file is invalid. This can occur when:
25322530
* The file contains invalid JSON.
25332531
* The file is missing the required `packages` object.
25342532
* A package entry is missing the required `path` field.
2533+
* Two package entries have the same `path` value.
25352534

25362535
```console
25372536
$ node --experimental-package-map=./missing.json app.js
@@ -2553,7 +2552,6 @@ key that is not defined in the `packages` object.
25532552
{
25542553
"packages": {
25552554
"app": {
2556-
"name": "app",
25572555
"path": "./app",
25582556
"dependencies": {
25592557
"foo": "nonexistent"

doc/api/packages.md

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -987,19 +987,16 @@ Each key in `packages` is a unique identifier for a package entry:
987987
{
988988
"packages": {
989989
"app": {
990-
"name": "my-app",
991990
"path": "./packages/app",
992991
"dependencies": {
993992
"@myorg/utils": "utils",
994993
"@myorg/ui-lib": "ui-lib"
995994
}
996995
},
997996
"utils": {
998-
"name": "@myorg/utils",
999997
"path": "./packages/utils"
1000998
},
1001999
"ui-lib": {
1002-
"name": "@myorg/ui-lib",
10031000
"path": "./packages/ui-lib",
10041001
"dependencies": {
10051002
"@myorg/utils": "utils"
@@ -1012,9 +1009,8 @@ Each key in `packages` is a unique identifier for a package entry:
10121009
Each package entry has the following fields:
10131010

10141011
* `path` {string} **Required.** Relative path from the configuration file to
1015-
the package directory.
1016-
* `name` {string} The package name used in import specifiers. If omitted, the
1017-
package cannot be imported by name but can still import its dependencies.
1012+
the package directory. Each path must be unique across all packages in the
1013+
map; duplicate paths will throw an [`ERR_PACKAGE_MAP_INVALID`][] error.
10181014
* `dependencies` {Object} An object mapping bare specifiers to package keys.
10191015
Each key is the import name used in source code, and each value is the
10201016
corresponding package key in the `packages` object. Defaults to an empty
@@ -1026,14 +1022,12 @@ When a bare specifier is encountered:
10261022

10271023
1. Node.js determines which package contains the importing file by checking
10281024
if the file path is within any package's `path`.
1029-
2. If the importing file is not within any mapped package, a
1030-
`MODULE_NOT_FOUND` error is thrown.
1025+
2. If the importing file is not within any mapped package, an
1026+
[`ERR_PACKAGE_MAP_EXTERNAL_FILE`][] error is thrown.
10311027
3. Node.js looks up the specifier's package name in the importing package's
10321028
`dependencies` object to find the corresponding package key.
10331029
4. If found, the specifier resolves to the target package's `path`.
1034-
5. If the package exists in the map but is not in `dependencies`, an
1035-
[`ERR_PACKAGE_MAP_ACCESS_DENIED`][] error is thrown.
1036-
6. If the package does not exist in the map at all, a
1030+
5. If the specifier is not in `dependencies`, a
10371031
`MODULE_NOT_FOUND` error is thrown.
10381032

10391033
### Subpath resolution
@@ -1060,25 +1054,21 @@ can map the same specifier to different targets:
10601054
{
10611055
"packages": {
10621056
"app": {
1063-
"name": "app",
10641057
"path": "./app",
10651058
"dependencies": {
10661059
"component": "component-v2"
10671060
}
10681061
},
10691062
"legacy": {
1070-
"name": "legacy",
10711063
"path": "./legacy",
10721064
"dependencies": {
10731065
"component": "component-v1"
10741066
}
10751067
},
10761068
"component-v1": {
1077-
"name": "component",
10781069
"path": "./vendor/component-1.0.0"
10791070
},
10801071
"component-v2": {
1081-
"name": "component",
10821072
"path": "./vendor/component-2.0.0"
10831073
}
10841074
}
@@ -1352,7 +1342,8 @@ This field defines [subpath imports][] for the current package.
13521342
[`--experimental-addon-modules`]: cli.md#--experimental-addon-modules
13531343
[`--experimental-package-map`]: cli.md#--experimental-package-mappath
13541344
[`--no-addons` flag]: cli.md#--no-addons
1355-
[`ERR_PACKAGE_MAP_ACCESS_DENIED`]: errors.md#err_package_map_access_denied
1345+
[`ERR_PACKAGE_MAP_EXTERNAL_FILE`]: errors.md#err_package_map_external_file
1346+
[`ERR_PACKAGE_MAP_INVALID`]: errors.md#err_package_map_invalid
13561347
[`ERR_PACKAGE_PATH_NOT_EXPORTED`]: errors.md#err_package_path_not_exported
13571348
[`ERR_UNKNOWN_FILE_EXTENSION`]: errors.md#err_unknown_file_extension
13581349
[`package.json`]: #nodejs-packagejson-field-definitions

lib/internal/errors.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1661,8 +1661,8 @@ E('ERR_PACKAGE_IMPORT_NOT_DEFINED', (specifier, packagePath, base) => {
16611661
return `Package import specifier "${specifier}" is not defined${packagePath ?
16621662
` in package ${packagePath}package.json` : ''} imported from ${base}`;
16631663
}, TypeError);
1664-
E('ERR_PACKAGE_MAP_ACCESS_DENIED', (specifier, fromKey, configPath) => {
1665-
return `Package "${specifier}" is not a declared dependency of "${fromKey}" in ${configPath}`;
1664+
E('ERR_PACKAGE_MAP_EXTERNAL_FILE', (specifier, parentPath, configPath) => {
1665+
return `Cannot resolve "${specifier}" from "${parentPath}": file is not within any package defined in ${configPath}`;
16661666
}, Error);
16671667
E('ERR_PACKAGE_MAP_INVALID', (configPath, reason) => {
16681668
return `Invalid package-map.json at "${configPath}": ${reason}`;

lib/internal/modules/package_map.js

Lines changed: 21 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ const {
44
JSONParse,
55
ObjectEntries,
66
SafeMap,
7-
SafeSet,
87
StringPrototypeIndexOf,
98
StringPrototypeSlice,
109
} = primordials;
@@ -19,7 +18,7 @@ const getPackageMapPath = getLazy(() =>
1918
const fs = require('fs');
2019

2120
const {
22-
ERR_PACKAGE_MAP_ACCESS_DENIED,
21+
ERR_PACKAGE_MAP_EXTERNAL_FILE,
2322
ERR_PACKAGE_MAP_INVALID,
2423
ERR_PACKAGE_MAP_KEY_NOT_FOUND,
2524
} = require('internal/errors').codes;
@@ -33,7 +32,6 @@ let emittedWarning = false;
3332
* @typedef {object} PackageMapEntry
3433
* @property {string} path - Absolute path to package on disk
3534
* @property {Map<string, string>} dependencies - Map from bare specifier to package key
36-
* @property {string|undefined} name - Package name (undefined = nameless package)
3735
*/
3836

3937
class PackageMap {
@@ -43,14 +41,10 @@ class PackageMap {
4341
#basePath;
4442
/** @type {Map<string, PackageMapEntry>} */
4543
#packages;
46-
/** @type {Map<string, Set<string>>} */
47-
#nameToKeys;
48-
/** @type {Map<string, Set<string>>} */
49-
#pathToKeys;
44+
/** @type {Map<string, string>} */
45+
#pathToKey;
5046
/** @type {Map<string, string|null>} */
5147
#pathToKeyCache;
52-
/** @type {Map<string, object>} */
53-
#resolveCache;
5448

5549
/**
5650
* @param {string} configPath
@@ -60,10 +54,8 @@ class PackageMap {
6054
this.#configPath = configPath;
6155
this.#basePath = dirname(configPath);
6256
this.#packages = new SafeMap();
63-
this.#nameToKeys = new SafeMap();
64-
this.#pathToKeys = new SafeMap();
57+
this.#pathToKey = new SafeMap();
6558
this.#pathToKeyCache = new SafeMap();
66-
this.#resolveCache = new SafeMap();
6759

6860
this.#parse(data);
6961
}
@@ -86,22 +78,19 @@ class PackageMap {
8678
this.#packages.set(key, {
8779
path: absolutePath,
8880
dependencies: new SafeMap(ObjectEntries(entry.dependencies ?? {})),
89-
name: entry.name, // Undefined for nameless packages
9081
});
9182

92-
// Index by name (only named packages)
93-
if (entry.name !== undefined) {
94-
if (!this.#nameToKeys.has(entry.name)) {
95-
this.#nameToKeys.set(entry.name, new SafeSet());
96-
}
97-
this.#nameToKeys.get(entry.name).add(key);
83+
// TODO(arcanis): Duplicates should be tolerated, but it requires changing module IDs
84+
// to include the package ID whenever a package map is used.
85+
if (this.#pathToKey.has(absolutePath)) {
86+
const existingKey = this.#pathToKey.get(absolutePath);
87+
throw new ERR_PACKAGE_MAP_INVALID(
88+
this.#configPath,
89+
`packages "${existingKey}" and "${key}" have the same path "${entry.path}"; this will be supported in the future`,
90+
);
9891
}
9992

100-
// Index by path
101-
if (!this.#pathToKeys.has(absolutePath)) {
102-
this.#pathToKeys.set(absolutePath, new SafeSet());
103-
}
104-
this.#pathToKeys.get(absolutePath).add(key);
93+
this.#pathToKey.set(absolutePath, key);
10594
}
10695
}
10796

@@ -117,9 +106,8 @@ class PackageMap {
117106
// Walk up to find containing package
118107
let checkPath = filePath;
119108
while (true) {
120-
const keys = this.#pathToKeys.get(checkPath);
121-
if (keys && keys.size > 0) {
122-
const key = keys.values().next().value;
109+
const key = this.#pathToKey.get(checkPath);
110+
if (key !== undefined) {
123111
this.#pathToKeyCache.set(filePath, key);
124112
return key;
125113
}
@@ -135,49 +123,26 @@ class PackageMap {
135123
/**
136124
* Main resolution method.
137125
* Returns the package path and subpath for the specifier, or undefined if
138-
* resolution should fall back to standard resolution.
126+
* the specifier is not in the parent package's dependencies.
127+
* Throws ERR_PACKAGE_MAP_EXTERNAL_FILE if parentPath is not within any
128+
* mapped package.
139129
* @param {string} specifier
140130
* @param {string} parentPath - File path of the importing module
141131
* @returns {{packagePath: string, subpath: string}|undefined}
142132
*/
143133
resolve(specifier, parentPath) {
144134
const parentKey = this.#getKeyForPath(parentPath);
145135

146-
// Parent not in map - let the caller throw the appropriate error
147-
if (parentKey === null) { return undefined; }
148-
149-
// Check cache
150-
const cacheKey = `${parentKey}\0${specifier}`;
151-
if (this.#resolveCache.has(cacheKey)) {
152-
return this.#resolveCache.get(cacheKey);
136+
if (parentKey === null) {
137+
throw new ERR_PACKAGE_MAP_EXTERNAL_FILE(specifier, parentPath, this.#configPath);
153138
}
154139

155-
const result = this.#resolveUncached(specifier, parentKey);
156-
this.#resolveCache.set(cacheKey, result);
157-
return result;
158-
}
159-
160-
/**
161-
* @param {string} specifier
162-
* @param {string} parentKey
163-
* @returns {{packagePath: string, subpath: string}|undefined}
164-
*/
165-
#resolveUncached(specifier, parentKey) {
166140
const { packageName, subpath } = parsePackageName(specifier);
167141
const parentEntry = this.#packages.get(parentKey);
168142

169143
const targetKey = parentEntry.dependencies.get(packageName);
170-
171144
if (targetKey === undefined) {
172-
// Check if package exists anywhere in map but isn't accessible
173-
if (this.#nameToKeys.has(packageName)) {
174-
throw new ERR_PACKAGE_MAP_ACCESS_DENIED(
175-
specifier,
176-
parentKey,
177-
this.#configPath,
178-
);
179-
}
180-
// Package not in map at all - let the caller throw the appropriate error
145+
// Package not in parent's dependencies - let the caller throw the appropriate error
181146
return undefined;
182147
}
183148

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

Lines changed: 18 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -49,39 +49,6 @@ describe('ESM: --experimental-package-map', () => {
4949
});
5050
});
5151

52-
// =========== Access Control ===========
53-
54-
describe('dependency access control', () => {
55-
it('throws ERR_PACKAGE_MAP_ACCESS_DENIED for undeclared dependency', async () => {
56-
const { code, stderr } = await spawnPromisified(execPath, [
57-
'--experimental-package-map', packageMapPath,
58-
'--input-type=module',
59-
'--eval',
60-
`import x from 'not-a-dep';`,
61-
], { cwd: fixtures.path('package-map/root') });
62-
63-
assert.notStrictEqual(code, 0);
64-
assert.match(stderr, /ERR_PACKAGE_MAP_ACCESS_DENIED/);
65-
assert.match(stderr, /not-a-dep/);
66-
assert.match(stderr, /root/); // parent package name
67-
});
68-
69-
it('includes package key in error for nameless packages', async () => {
70-
const { code, stderr } = await spawnPromisified(execPath, [
71-
'--experimental-package-map',
72-
fixtures.path('package-map/package-map-nameless.json'),
73-
'--input-type=module',
74-
'--eval',
75-
`import x from 'forbidden';`,
76-
], { cwd: fixtures.path('package-map/nameless-pkg') });
77-
78-
assert.notStrictEqual(code, 0);
79-
assert.match(stderr, /ERR_PACKAGE_MAP_ACCESS_DENIED/);
80-
// Should show key since package is nameless
81-
assert.match(stderr, /nameless/);
82-
});
83-
});
84-
8552
// =========== Fallback Behavior ===========
8653

8754
describe('resolution boundaries', () => {
@@ -106,7 +73,7 @@ describe('ESM: --experimental-package-map', () => {
10673
], { cwd: '/tmp' }); // Not in any mapped package
10774

10875
assert.notStrictEqual(code, 0);
109-
assert.match(stderr, /ERR_MODULE_NOT_FOUND/);
76+
assert.match(stderr, /ERR_PACKAGE_MAP_EXTERNAL_FILE/);
11077
});
11178
});
11279

@@ -162,6 +129,20 @@ describe('ESM: --experimental-package-map', () => {
162129
assert.match(stderr, /ERR_PACKAGE_MAP_INVALID/);
163130
assert.match(stderr, /not found/);
164131
});
132+
133+
it('throws ERR_PACKAGE_MAP_INVALID for duplicate package paths', async () => {
134+
const { code, stderr } = await spawnPromisified(execPath, [
135+
'--experimental-package-map',
136+
fixtures.path('package-map/package-map-duplicate-path.json'),
137+
'--input-type=module',
138+
'--eval', `import x from 'dep-a';`,
139+
], { cwd: fixtures.path('package-map/root') });
140+
141+
assert.notStrictEqual(code, 0);
142+
assert.match(stderr, /ERR_PACKAGE_MAP_INVALID/);
143+
assert.match(stderr, /pkg-a/);
144+
assert.match(stderr, /pkg-b/);
145+
});
165146
});
166147

167148
// =========== Zero Overhead When Disabled ===========
@@ -302,9 +283,9 @@ describe('ESM: --experimental-package-map', () => {
302283
assert.match(stdout, /inner using dep-a-value/);
303284
});
304285

305-
it('denies access to nested package deps from parent package', async () => {
286+
it('falls back for undeclared dependency in nested package parent', async () => {
306287
// Root does not list dep-a in its dependencies, so importing it
307-
// from root should fail even though inner (nested inside root) can.
288+
// from root should fail with ERR_MODULE_NOT_FOUND.
308289
const { code, stderr } = await spawnPromisified(execPath, [
309290
'--experimental-package-map', longestPathMap,
310291
'--input-type=module',
@@ -313,7 +294,7 @@ describe('ESM: --experimental-package-map', () => {
313294
], { cwd: fixtures.path('package-map/root') });
314295

315296
assert.notStrictEqual(code, 0);
316-
assert.match(stderr, /ERR_PACKAGE_MAP_ACCESS_DENIED/);
297+
assert.match(stderr, /ERR_MODULE_NOT_FOUND/);
317298
});
318299
});
319300
});

0 commit comments

Comments
 (0)