node/test/parallel/test-require-package-map.js
Maël Nison 2f2b81095b
loader: implement package maps
Signed-off-by: Mael Nison <nison.mael@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/62239
Reviewed-By: Aviv Keller <me@aviv.sh>
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
2026-06-12 16:02:01 +00:00

363 lines
12 KiB
JavaScript

'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 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);
});
});
});