'use strict'; const common = require('../common'); const fixtures = require('../common/fixtures'); const assert = require('node:assert'); const { spawnSync } = require('node:child_process'); const { symlinkSync, writeFileSync } = require('node:fs'); const path = require('node:path'); const { describe, it } = require('node:test'); const { pathToFileURL } = require('node:url'); const tmpdir = require('../common/tmpdir'); tmpdir.refresh(); const packageMapPath = fixtures.path('package-map/package-map.json'); // Generated at runtime because file:// URLs must be absolute, making them machine-dependent. const fileUrlFixturePath = tmpdir.resolve('package-map-file-url.json'); writeFileSync(fileUrlFixturePath, JSON.stringify({ packages: { 'root': { url: pathToFileURL(fixtures.path('package-map/root')).href, dependencies: { 'dep-a': 'dep-a' }, }, 'dep-a': { url: pathToFileURL(fixtures.path('package-map/dep-a')).href, dependencies: {}, }, }, })); describe('CJS: --experimental-package-map', { concurrency: !process.env.TEST_PARALLEL }, () => { describe('basic resolution', () => { it('resolves require() through package map', () => { const { status, stdout, stderr } = spawnSync(process.execPath, [ '--no-warnings', '--experimental-package-map', packageMapPath, '-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 subpath require()', () => { const { status, stdout, stderr } = spawnSync(process.execPath, [ '--no-warnings', '--experimental-package-map', packageMapPath, '-e', `const util = require('dep-a/lib/util'); console.log(util.default);`, ], { cwd: fixtures.path('package-map/root'), encoding: 'utf8', }); assert.strictEqual(stderr, ''); assert.match(stdout, /dep-a-util/); assert.strictEqual(status, 0, stderr); }); it('resolves transitive dependencies', () => { const { status, stdout, stderr } = spawnSync(process.execPath, [ '--no-warnings', '--experimental-package-map', packageMapPath, '-e', `const depB = require('dep-b'); console.log(depB.default);`, ], { cwd: fixtures.path('package-map/root'), encoding: 'utf8', }); assert.strictEqual(stderr, ''); assert.match(stdout, /dep-b using dep-a-value/); assert.strictEqual(status, 0, stderr); }); }); describe('resolution boundaries', () => { it('falls back for builtin modules', () => { const { status, stdout, stderr } = spawnSync(process.execPath, [ '--no-warnings', '--experimental-package-map', packageMapPath, '-e', `const fs = require('fs'); console.log(typeof fs.readFileSync);`, ], { cwd: fixtures.path('package-map/root'), encoding: 'utf8', }); assert.strictEqual(stderr, ''); assert.match(stdout, /function/); assert.strictEqual(status, 0, stderr); }); it('throws when parent not in map', () => { const { status, stderr } = spawnSync(process.execPath, [ '--no-warnings', '--experimental-package-map', packageMapPath, '-e', `require('dep-a');`, ], { cwd: tmpdir.path, encoding: 'utf8', }); assert.match(stderr, /ERR_PACKAGE_MAP_EXTERNAL_FILE/); assert.notStrictEqual(status, 0, stderr); }); }); describe('error handling', () => { it('throws for invalid JSON', () => { const { status, stderr } = spawnSync(process.execPath, [ '--no-warnings', '--experimental-package-map', fixtures.path('package-map/package-map-invalid-syntax.json'), '-e', `require('dep-a');`, ], { cwd: fixtures.path('package-map/root'), encoding: 'utf8', }); assert.match(stderr, /ERR_PACKAGE_MAP_INVALID/); assert.notStrictEqual(status, 0, stderr); }); it('throws for missing packages field', () => { const { status, stderr } = spawnSync(process.execPath, [ '--no-warnings', '--experimental-package-map', fixtures.path('package-map/package-map-invalid-schema.json'), '-e', `require('dep-a');`, ], { cwd: fixtures.path('package-map/root'), encoding: 'utf8', }); assert.match(stderr, /ERR_PACKAGE_MAP_INVALID/); assert.notStrictEqual(status, 0, stderr); }); it('throws for unsupported URL scheme in url', () => { const { status, stderr } = spawnSync(process.execPath, [ '--experimental-package-map', fixtures.path('package-map/package-map-https-path.json'), '-e', `require('dep-a');`, ], { cwd: fixtures.path('package-map/root'), encoding: 'utf8', }); assert.match(stderr, /ERR_PACKAGE_MAP_INVALID/); assert.match(stderr, /unsupported URL scheme/); assert.match(stderr, /https:\/\//); assert.notStrictEqual(status, 0, stderr); }); it('accepts file:// URLs in url', () => { const { status, stdout, stderr } = spawnSync(process.execPath, [ '--no-warnings', '--experimental-package-map', fileUrlFixturePath, '-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('throws for duplicate package URLs', () => { const { status, stderr } = spawnSync(process.execPath, [ '--no-warnings', '--experimental-package-map', fixtures.path('package-map/package-map-duplicate-path.json'), '-e', `require('dep-a');`, ], { cwd: fixtures.path('package-map/root'), encoding: 'utf8', }); assert.match(stderr, /ERR_PACKAGE_MAP_INVALID/); assert.match(stderr, /pkg-a/); assert.match(stderr, /pkg-b/); assert.notStrictEqual(status, 0, stderr); }); }); describe('conditional exports', () => { it('respects require condition in exports', () => { const { status, stdout, stderr } = spawnSync(process.execPath, [ '--no-warnings', '--experimental-package-map', packageMapPath, '-e', `const dep = require('dep-a'); console.log(dep.format);`, ], { cwd: fixtures.path('package-map/root'), encoding: 'utf8', }); assert.strictEqual(stderr, ''); assert.match(stdout, /cjs/); // Should get CJS export assert.strictEqual(status, 0, stderr); }); }); describe('disabled by default', () => { it('has no impact when flag not set', () => { const { status, stdout, stderr } = spawnSync(process.execPath, [ '--no-warnings', '-e', `const fs = require('fs'); console.log('ok');`, ], { encoding: 'utf8', }); assert.strictEqual(stderr, ''); assert.match(stdout, /ok/); assert.strictEqual(status, 0, stderr); }); }); describe('path containment edge cases', () => { it('does not match pkg-other as being inside pkg', () => { // Regression test: pkg-other (at ./pkg-other) should not be // incorrectly matched as inside pkg (at ./pkg) just because // the string "./pkg-other" starts with "./pkg" const { status, stdout, stderr } = spawnSync(process.execPath, [ '--no-warnings', '--experimental-package-map', fixtures.path('package-map/package-map-path-prefix.json'), '-e', `const pkg = require('pkg'); console.log(pkg.default);`, ], { cwd: fixtures.path('package-map/pkg-other'), encoding: 'utf8', }); assert.strictEqual(stderr, ''); assert.match(stdout, /pkg-value/); assert.strictEqual(status, 0, stderr); }); }); describe('external package paths', () => { it('resolves packages outside the package map directory via relative paths', () => { const { status, stdout, stderr } = spawnSync(process.execPath, [ '--no-warnings', '--experimental-package-map', fixtures.path('package-map/nested-project/package-map-external-deps.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); }); }); describe('same request resolves to different versions', () => { const versionForkMap = fixtures.path('package-map/package-map-version-fork.json'); it('resolves the same bare specifier to different packages depending on the importer', () => { // app-18 and app-19 both require 'react', but the package map wires // them to react@18 and react@19 respectively. const { status, stdout, stderr } = spawnSync(process.execPath, [ '--no-warnings', '--experimental-package-map', versionForkMap, '-e', `const app18 = require('app-18'); const app19 = require('app-19'); console.log(app18.default); console.log(app19.default);`, ], { cwd: fixtures.path('package-map/root'), encoding: 'utf8', }); assert.strictEqual(stderr, ''); assert.match(stdout, /app-18 using react 18/); assert.match(stdout, /app-19 using react 19/); assert.strictEqual(status, 0, stderr); }); }); describe('longest path wins', () => { const longestPathMap = fixtures.path('package-map/package-map-longest-path.json'); it('resolves from createRequire() paths with mixed separators', () => { const { status, stdout, stderr } = spawnSync(process.execPath, [ '--no-warnings', '--experimental-package-map', longestPathMap, '-e', `const { createRequire } = require('node:module'); const req = createRequire(process.cwd() + '/node_modules/inner/index.js'); const dep = req('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 nested package using its own dependencies, not the parent', () => { // Inner lives at ./root/node_modules/inner which is inside root's // path (./root). The longest matching path should win, so code in // inner should resolve dep-a (inner's dep), not be treated as root. const { status, stdout, stderr } = spawnSync(process.execPath, [ '--no-warnings', '--experimental-package-map', longestPathMap, '-e', `const inner = require('inner'); console.log(inner.default);`, ], { cwd: fixtures.path('package-map/root'), encoding: 'utf8', }); assert.strictEqual(stderr, ''); assert.match(stdout, /inner using dep-a-value/); assert.strictEqual(status, 0, stderr); }); it('falls back for undeclared dependency in nested package parent', () => { // Root does not list dep-a in its dependencies, so requiring it // from root should fail with MODULE_NOT_FOUND. const { status, stderr } = spawnSync(process.execPath, [ '--no-warnings', '--experimental-package-map', longestPathMap, '-e', `require('dep-a');`, ], { cwd: fixtures.path('package-map/root'), encoding: 'utf8', }); assert.match(stderr, /Cannot find module/); assert.notStrictEqual(status, 0); }); }); describe('symlink in ancestor of package map', { skip: !common.canCreateSymLink() && 'insufficient privileges for symlinks', }, () => { const symlinkDir = tmpdir.resolve('symlinked-package-map-cjs'); symlinkSync(fixtures.path('package-map'), symlinkDir, 'dir'); it('resolves through a symlinked ancestor directory', () => { const symlinkMapPath = path.join(symlinkDir, 'package-map.json'); const { status, stdout, stderr } = spawnSync(process.execPath, [ '--no-warnings', '--experimental-package-map', symlinkMapPath, '-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); }); }); });