Commit 0551dee5f99 for nodejs
commit 0551dee5f99eab5e09c8bbf9bf4d288b1195f538
Author: Hilary Asogwa <76795262+Dansatch@users.noreply.github.com>
Date: Fri Sep 25 14:49:42 2026 +0100
module: normalize package map paths
Signed-off-by: dansatch <dansatch98@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/66054
Fixes: https://github.com/nodejs/node/issues/66043
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
diff --git a/lib/internal/modules/package_map.js b/lib/internal/modules/package_map.js
index 38e7e12206c..1e01eb389a2 100644
--- a/lib/internal/modules/package_map.js
+++ b/lib/internal/modules/package_map.js
@@ -105,7 +105,7 @@ class PackageMap {
let absolutePath;
try {
- absolutePath = fileURLToPath(packageURL);
+ absolutePath = pathResolve(fileURLToPath(packageURL));
} catch (err) {
const error = new ERR_PACKAGE_MAP_INVALID(
this.#configPath,
diff --git a/test/es-module/test-esm-package-map-paths.mjs b/test/es-module/test-esm-package-map-paths.mjs
index fc7c45c631a..611fe833314 100644
--- a/test/es-module/test-esm-package-map-paths.mjs
+++ b/test/es-module/test-esm-package-map-paths.mjs
@@ -27,6 +27,18 @@ spawnSyncAndAssert(process.execPath, [
trim: true,
});
+// Directory-form URLs match package files even when the map is nested below them.
+spawnSyncAndAssert(process.execPath, [
+ '--no-warnings',
+ '--experimental-package-map',
+ fixtures.path('package-map/nested-project/map-dir/package-map-parent-url.json'),
+ '--input-type=module',
+ '--eval', `import dep from 'dep-a'; console.log(dep);`,
+], { cwd: fixtures.path('package-map/nested-project/src') }, {
+ stdout: /dep-a-value/,
+ trim: true,
+});
+
// URL fields are decoded as URLs before being converted to paths.
spawnSyncAndAssert(process.execPath, [
'--no-warnings',
diff --git a/test/fixtures/package-map/nested-project/map-dir/package-map-parent-url.json b/test/fixtures/package-map/nested-project/map-dir/package-map-parent-url.json
new file mode 100644
index 00000000000..b1647cc8877
--- /dev/null
+++ b/test/fixtures/package-map/nested-project/map-dir/package-map-parent-url.json
@@ -0,0 +1,12 @@
+{
+ "packages": {
+ "app": {
+ "url": "..",
+ "dependencies": {"dep-a": "dep-a"}
+ },
+ "dep-a": {
+ "url": "../../dep-a/",
+ "dependencies": {}
+ }
+ }
+}
diff --git a/test/fixtures/package-map/package-map-directory-urls.json b/test/fixtures/package-map/package-map-directory-urls.json
new file mode 100644
index 00000000000..ef24742fa6c
--- /dev/null
+++ b/test/fixtures/package-map/package-map-directory-urls.json
@@ -0,0 +1,12 @@
+{
+ "packages": {
+ "root": {
+ "url": "./root/",
+ "dependencies": {"dep-a": "dep-a"}
+ },
+ "dep-a": {
+ "url": "./dep-a/",
+ "dependencies": {}
+ }
+ }
+}
diff --git a/test/fixtures/package-map/package-map-duplicate-directory-path.json b/test/fixtures/package-map/package-map-duplicate-directory-path.json
new file mode 100644
index 00000000000..e29d7c955b1
--- /dev/null
+++ b/test/fixtures/package-map/package-map-duplicate-directory-path.json
@@ -0,0 +1,16 @@
+{
+ "packages": {
+ "root": {
+ "url": "./root",
+ "dependencies": {"dep-a": "dep-a"}
+ },
+ "dep-a": {
+ "url": "./dep-a",
+ "dependencies": {}
+ },
+ "dep-a-directory": {
+ "url": "./dep-a/",
+ "dependencies": {}
+ }
+ }
+}
diff --git a/test/parallel/test-require-package-map.js b/test/parallel/test-require-package-map.js
index 87786d62738..08ec289346a 100644
--- a/test/parallel/test-require-package-map.js
+++ b/test/parallel/test-require-package-map.js
@@ -32,6 +32,40 @@ writeFileSync(fileUrlFixturePath, JSON.stringify({
describe('CJS: --experimental-package-map', { concurrency: !process.env.TEST_PARALLEL }, () => {
describe('basic resolution', () => {
+ it('resolves dependencies from a parent directory URL', () => {
+ const { status, stdout, stderr } = spawnSync(process.execPath, [
+ '--no-warnings',
+ '--experimental-package-map',
+ fixtures.path('package-map/nested-project/map-dir/package-map-parent-url.json'),
+ '-e',
+ `const dep = require('dep-a'); console.log(dep.default);`,
+ ], {
+ cwd: fixtures.path('package-map/nested-project/src'),
+ encoding: 'utf8',
+ });
+
+ assert.strictEqual(stderr, '');
+ assert.match(stdout, /dep-a-value/);
+ assert.strictEqual(status, 0, stderr);
+ });
+
+ it('resolves packages with directory-form URLs', () => {
+ const { status, stdout, stderr } = spawnSync(process.execPath, [
+ '--no-warnings',
+ '--experimental-package-map',
+ fixtures.path('package-map/package-map-directory-urls.json'),
+ '-e',
+ `const dep = require('dep-a'); console.log(dep.default);`,
+ ], {
+ cwd: fixtures.path('package-map/root'),
+ encoding: 'utf8',
+ });
+
+ assert.strictEqual(stderr, '');
+ assert.match(stdout, /dep-a-value/);
+ assert.strictEqual(status, 0, stderr);
+ });
+
it('resolves require() through package map', () => {
const { status, stdout, stderr } = spawnSync(process.execPath, [
'--no-warnings',
@@ -197,6 +231,23 @@ describe('CJS: --experimental-package-map', { concurrency: !process.env.TEST_PAR
assert.match(stderr, /pkg-b/);
assert.notStrictEqual(status, 0, stderr);
});
+
+ it('throws for package URLs differing only by a trailing separator', () => {
+ const { status, stderr } = spawnSync(process.execPath, [
+ '--no-warnings',
+ '--experimental-package-map',
+ fixtures.path('package-map/package-map-duplicate-directory-path.json'),
+ '-e',
+ `require('dep-a');`,
+ ], {
+ cwd: fixtures.path('package-map/root'),
+ encoding: 'utf8',
+ });
+
+ assert.match(stderr, /ERR_PACKAGE_MAP_INVALID/);
+ assert.match(stderr, /dep-a-directory/);
+ assert.notStrictEqual(status, 0, stderr);
+ });
});
describe('conditional exports', () => {