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>
363 lines
12 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
});
|