diff --git a/lib/internal/modules/package_map.js b/lib/internal/modules/package_map.js index 38e7e12206c9..1e01eb389a21 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 fc7c45c631ad..611fe833314e 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 000000000000..b1647cc8877d --- /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 000000000000..ef24742fa6c4 --- /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 000000000000..e29d7c955b16 --- /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 87786d62738a..08ec289346a6 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', () => {