Skip to content

Commit b63315d

Browse files
guybedfordRafaelGSS
authored andcommitted
module: exports & imports map invalid slash deprecation
PR-URL: #44477 Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: Jacob Smith <[email protected]>
1 parent 26f25c9 commit b63315d

File tree

10 files changed

+176
-65
lines changed

10 files changed

+176
-65
lines changed

doc/api/deprecations.md

+17
Original file line numberDiff line numberDiff line change
@@ -3182,6 +3182,23 @@ Type: Documentation-only
31823182

31833183
The [`--trace-atomics-wait`][] flag is deprecated.
31843184

3185+
### DEP0166: Double slashes in imports and exports targets
3186+
3187+
<!-- YAML
3188+
changes:
3189+
- version: REPLACEME
3190+
pr-url: https://github.com/nodejs/node/pull/44477
3191+
description: Documentation-only deprecation
3192+
with `--pending-deprecation` support.
3193+
-->
3194+
3195+
Type: Documentation-only (supports [`--pending-deprecation`][])
3196+
3197+
Package imports and exports targets mapping into paths including a double slash
3198+
(of _"/"_ or _"\\"_) are deprecated and will fail with a resolution validation
3199+
error in a future release. This same deprecation also applies to pattern matches
3200+
starting or ending in a slash.
3201+
31853202
[Legacy URL API]: url.md#legacy-url-api
31863203
[NIST SP 800-38D]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
31873204
[RFC 6066]: https://tools.ietf.org/html/rfc6066#section-3

doc/api/esm.md

+29-35
Original file line numberDiff line numberDiff line change
@@ -1349,8 +1349,7 @@ The resolver can throw the following errors:
13491349
> 1. Set _mainExport_ to _exports_\[_"."_].
13501350
> 4. If _mainExport_ is not **undefined**, then
13511351
> 1. Let _resolved_ be the result of **PACKAGE\_TARGET\_RESOLVE**(
1352-
> _packageURL_, _mainExport_, _""_, **false**, **false**,
1353-
> _conditions_).
1352+
> _packageURL_, _mainExport_, **null**, **false**, _conditions_).
13541353
> 2. If _resolved_ is not **null** or **undefined**, return _resolved_.
13551354
> 3. Otherwise, if _exports_ is an Object and all keys of _exports_ start with
13561355
> _"."_, then
@@ -1381,7 +1380,7 @@ _isImports_, _conditions_)
13811380
> 1. If _matchKey_ is a key of _matchObj_ and does not contain _"\*"_, then
13821381
> 1. Let _target_ be the value of _matchObj_\[_matchKey_].
13831382
> 2. Return the result of **PACKAGE\_TARGET\_RESOLVE**(_packageURL_,
1384-
> _target_, _""_, **false**, _isImports_, _conditions_).
1383+
> _target_, **null**, _isImports_, _conditions_).
13851384
> 2. Let _expansionKeys_ be the list of keys of _matchObj_ containing only a
13861385
> single _"\*"_, sorted by the sorting function **PATTERN\_KEY\_COMPARE**
13871386
> which orders in descending order of specificity.
@@ -1395,11 +1394,11 @@ _isImports_, _conditions_)
13951394
> _patternTrailer_ and the length of _matchKey_ is greater than or
13961395
> equal to the length of _expansionKey_, then
13971396
> 1. Let _target_ be the value of _matchObj_\[_expansionKey_].
1398-
> 2. Let _subpath_ be the substring of _matchKey_ starting at the
1397+
> 2. Let _patternMatch_ be the substring of _matchKey_ starting at the
13991398
> index of the length of _patternBase_ up to the length of
14001399
> _matchKey_ minus the length of _patternTrailer_.
14011400
> 3. Return the result of **PACKAGE\_TARGET\_RESOLVE**(_packageURL_,
1402-
> _target_, _subpath_, **true**, _isImports_, _conditions_).
1401+
> _target_, _patternMatch_, _isImports_, _conditions_).
14031402
> 4. Return **null**.
14041403
14051404
**PATTERN\_KEY\_COMPARE**(_keyA_, _keyB_)
@@ -1418,37 +1417,32 @@ _isImports_, _conditions_)
14181417
> 10. If the length of _keyB_ is greater than the length of _keyA_, return 1.
14191418
> 11. Return 0.
14201419
1421-
**PACKAGE\_TARGET\_RESOLVE**(_packageURL_, _target_, _subpath_, _pattern_,
1422-
_internal_, _conditions_)
1420+
**PACKAGE\_TARGET\_RESOLVE**(_packageURL_, _target_, _patternMatch_,
1421+
_isImports_, _conditions_)
14231422
14241423
> 1. If _target_ is a String, then
1425-
> 1. If _pattern_ is **false**, _subpath_ has non-zero length and _target_
1426-
> does not end with _"/"_, throw an _Invalid Module Specifier_ error.
1427-
> 2. If _target_ does not start with _"./"_, then
1428-
> 1. If _internal_ is **true** and _target_ does not start with _"../"_ or
1429-
> _"/"_ and is not a valid URL, then
1430-
> 1. If _pattern_ is **true**, then
1431-
> 1. Return **PACKAGE\_RESOLVE**(_target_ with every instance of
1432-
> _"\*"_ replaced by _subpath_, _packageURL_ + _"/"_).
1433-
> 2. Return **PACKAGE\_RESOLVE**(_target_ + _subpath_,
1434-
> _packageURL_ + _"/"_).
1435-
> 2. Otherwise, throw an _Invalid Package Target_ error.
1436-
> 3. If _target_ split on _"/"_ or _"\\"_ contains any _"."_, _".."_, or
1437-
> _"node\_modules"_ segments after the first segment, case insensitive and
1438-
> including percent encoded variants, throw an _Invalid Package Target_
1439-
> error.
1440-
> 4. Let _resolvedTarget_ be the URL resolution of the concatenation of
1424+
> 1. If _target_ does not start with _"./"_, then
1425+
> 1. If _isImports_ is **false**, or if _target_ starts with _"../"_ or
1426+
> _"/"_, or if _target_ is a valid URL, then
1427+
> 1. Throw an _Invalid Package Target_ error.
1428+
> 2. If _patternMatch_ is a String, then
1429+
> 1. Return **PACKAGE\_RESOLVE**(_target_ with every instance of _"\*"_
1430+
> replaced by _patternMatch_, _packageURL_ + _"/"_).
1431+
> 3. Return **PACKAGE\_RESOLVE**(_target_, _packageURL_ + _"/"_).
1432+
> 2. If _target_ split on _"/"_ or _"\\"_ contains any _""_, _"."_, _".."_,
1433+
> or _"node\_modules"_ segments after the first _"."_ segment, case
1434+
> insensitive and including percent encoded variants, throw an _Invalid
1435+
> Package Target_ error.
1436+
> 3. Let _resolvedTarget_ be the URL resolution of the concatenation of
14411437
> _packageURL_ and _target_.
1442-
> 5. Assert: _resolvedTarget_ is contained in _packageURL_.
1443-
> 6. If _subpath_ split on _"/"_ or _"\\"_ contains any _"."_, _".."_, or
1444-
> _"node\_modules"_ segments, case insensitive and including percent
1445-
> encoded variants, throw an _Invalid Module Specifier_ error.
1446-
> 7. If _pattern_ is **true**, then
1447-
> 1. Return the URL resolution of _resolvedTarget_ with every instance of
1448-
> _"\*"_ replaced with _subpath_.
1449-
> 8. Otherwise,
1450-
> 1. Return the URL resolution of the concatenation of _subpath_ and
1451-
> _resolvedTarget_.
1438+
> 4. Assert: _resolvedTarget_ is contained in _packageURL_.
1439+
> 5. If _patternMatch_ is **null**, then
1440+
> 1. Return _resolvedTarget_.
1441+
> 6. If _patternMatch_ split on _"/"_ or _"\\"_ contains any _""_, _"."_,
1442+
> _".."_, or _"node\_modules"_ segments, case insensitive and including
1443+
> percent encoded variants, throw an _Invalid Module Specifier_ error.
1444+
> 7. Return the URL resolution of _resolvedTarget_ with every instance of
1445+
> _"\*"_ replaced with _patternMatch_.
14521446
> 2. Otherwise, if _target_ is a non-null Object, then
14531447
> 1. If _exports_ contains any index property keys, as defined in ECMA-262
14541448
> [6.1.7 Array Index][], throw an _Invalid Package Configuration_ error.
@@ -1457,7 +1451,7 @@ _internal_, _conditions_)
14571451
> then
14581452
> 1. Let _targetValue_ be the value of the _p_ property in _target_.
14591453
> 2. Let _resolved_ be the result of **PACKAGE\_TARGET\_RESOLVE**(
1460-
> _packageURL_, _targetValue_, _subpath_, _pattern_, _internal_,
1454+
> _packageURL_, _targetValue_, _patternMatch_, _isImports_,
14611455
> _conditions_).
14621456
> 3. If _resolved_ is equal to **undefined**, continue the loop.
14631457
> 4. Return _resolved_.
@@ -1466,7 +1460,7 @@ _internal_, _conditions_)
14661460
> 1. If \_target.length is zero, return **null**.
14671461
> 2. For each item _targetValue_ in _target_, do
14681462
> 1. Let _resolved_ be the result of **PACKAGE\_TARGET\_RESOLVE**(
1469-
> _packageURL_, _targetValue_, _subpath_, _pattern_, _internal_,
1463+
> _packageURL_, _targetValue_, _patternMatch_, _isImports_,
14701464
> _conditions_), continuing the loop on any _Invalid Package Target_
14711465
> error.
14721466
> 2. If _resolved_ is **undefined**, continue the loop.

lib/internal/modules/esm/resolve.js

+74-24
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const {
3333
Stats,
3434
} = require('fs');
3535
const { getOptionValue } = require('internal/options');
36+
const pendingDeprecation = getOptionValue('--pending-deprecation');
3637
// Do not eagerly grab .manifest, it may be in TDZ
3738
const policy = getOptionValue('--experimental-policy') ?
3839
require('internal/process/policy') :
@@ -98,6 +99,23 @@ function emitTrailingSlashPatternDeprecation(match, pjsonUrl, base) {
9899
);
99100
}
100101

102+
const doubleSlashRegEx = /[/\\][/\\]/;
103+
104+
function emitInvalidSegmentDeprecation(target, request, match, pjsonUrl, base) {
105+
if (!pendingDeprecation) { return; }
106+
const pjsonPath = fileURLToPath(pjsonUrl);
107+
const double = RegExpPrototypeExec(doubleSlashRegEx, target) !== null;
108+
process.emitWarning(
109+
`Use of deprecated ${double ? 'double slash' :
110+
'leading or trailing slash matching'} resolving "${target}" for module ` +
111+
`request "${request}" ${request !== match ? `matched to "${match}" ` : ''
112+
}in the "exports" field module resolution of the package at ${pjsonPath}${
113+
base ? ` imported from ${fileURLToPath(base)}` : ''}.`,
114+
'DeprecationWarning',
115+
'DEP0166'
116+
);
117+
}
118+
101119
/**
102120
* @param {URL} url
103121
* @param {URL} packageJSONUrl
@@ -344,15 +362,17 @@ function throwExportsNotFound(subpath, packageJSONUrl, base) {
344362

345363
/**
346364
*
347-
* @param {string | URL} subpath
365+
* @param {string} request
366+
* @param {string} match
348367
* @param {URL} packageJSONUrl
349368
* @param {boolean} internal
350369
* @param {string | URL | undefined} base
351370
*/
352-
function throwInvalidSubpath(subpath, packageJSONUrl, internal, base) {
353-
const reason = `request is not a valid subpath for the "${internal ?
354-
'imports' : 'exports'}" resolution of ${fileURLToPath(packageJSONUrl)}`;
355-
throw new ERR_INVALID_MODULE_SPECIFIER(subpath, reason,
371+
function throwInvalidSubpath(request, match, packageJSONUrl, internal, base) {
372+
const reason = `request is not a valid match in pattern "${match}" for the "${
373+
internal ? 'imports' : 'exports'}" resolution of ${
374+
fileURLToPath(packageJSONUrl)}`;
375+
throw new ERR_INVALID_MODULE_SPECIFIER(request, reason,
356376
base && fileURLToPath(base));
357377
}
358378

@@ -368,12 +388,22 @@ function throwInvalidPackageTarget(
368388
internal, base && fileURLToPath(base));
369389
}
370390

371-
const invalidSegmentRegEx = /(^|\\|\/)((\.|%2e)(\.|%2e)?|(n|%6e|%4e)(o|%6f|%4f)(d|%64|%44)(e|%65|%45)(_|%5f)(m|%6d|%4d)(o|%6f|%4f)(d|%64|%44)(u|%75|%55)(l|%6c|%4c)(e|%65|%45)(s|%73|%53))(\\|\/|$)/i;
391+
const invalidSegmentRegEx = /(^|\\|\/)((\.|%2e)(\.|%2e)?|(n|%6e|%4e)(o|%6f|%4f)(d|%64|%44)(e|%65|%45)(_|%5f)(m|%6d|%4d)(o|%6f|%4f)(d|%64|%44)(u|%75|%55)(l|%6c|%4c)(e|%65|%45)(s|%73|%53))?(\\|\/|$)/i;
392+
const deprecatedInvalidSegmentRegEx = /(^|\\|\/)((\.|%2e)(\.|%2e)?|(n|%6e|%4e)(o|%6f|%4f)(d|%64|%44)(e|%65|%45)(_|%5f)(m|%6d|%4d)(o|%6f|%4f)(d|%64|%44)(u|%75|%55)(l|%6c|%4c)(e|%65|%45)(s|%73|%53))(\\|\/|$)/i;
372393
const invalidPackageNameRegEx = /^\.|%|\\/;
373394
const patternRegEx = /\*/g;
374395

375396
function resolvePackageTargetString(
376-
target, subpath, match, packageJSONUrl, base, pattern, internal, conditions) {
397+
target,
398+
subpath,
399+
match,
400+
packageJSONUrl,
401+
base,
402+
pattern,
403+
internal,
404+
isPathMap,
405+
conditions,
406+
) {
377407

378408
if (subpath !== '' && !pattern && target[target.length - 1] !== '/')
379409
throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base);
@@ -399,8 +429,21 @@ function resolvePackageTargetString(
399429
throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base);
400430
}
401431

402-
if (RegExpPrototypeExec(invalidSegmentRegEx, StringPrototypeSlice(target, 2)) !== null)
403-
throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base);
432+
if (RegExpPrototypeExec(invalidSegmentRegEx, StringPrototypeSlice(target, 2)) !== null) {
433+
if (RegExpPrototypeExec(deprecatedInvalidSegmentRegEx, StringPrototypeSlice(target, 2)) === null) {
434+
if (!isPathMap) {
435+
const request = pattern ?
436+
StringPrototypeReplace(match, '*', () => subpath) :
437+
match + subpath;
438+
const resolvedTarget = pattern ?
439+
RegExpPrototypeSymbolReplace(patternRegEx, target, () => subpath) :
440+
target;
441+
emitInvalidSegmentDeprecation(resolvedTarget, request, match, packageJSONUrl, base);
442+
}
443+
} else {
444+
throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base);
445+
}
446+
}
404447

405448
const resolved = new URL(target, packageJSONUrl);
406449
const resolvedPath = resolved.pathname;
@@ -412,18 +455,22 @@ function resolvePackageTargetString(
412455
if (subpath === '') return resolved;
413456

414457
if (RegExpPrototypeExec(invalidSegmentRegEx, subpath) !== null) {
415-
const request = pattern ?
416-
StringPrototypeReplace(match, '*', () => subpath) : match + subpath;
417-
throwInvalidSubpath(request, packageJSONUrl, internal, base);
458+
const request = pattern ? StringPrototypeReplace(match, '*', () => subpath) : match + subpath;
459+
if (RegExpPrototypeExec(deprecatedInvalidSegmentRegEx, subpath) === null) {
460+
if (!isPathMap) {
461+
const resolvedTarget = pattern ?
462+
RegExpPrototypeSymbolReplace(patternRegEx, target, () => subpath) :
463+
target;
464+
emitInvalidSegmentDeprecation(resolvedTarget, request, match, packageJSONUrl, base);
465+
}
466+
} else {
467+
throwInvalidSubpath(request, match, packageJSONUrl, internal, base);
468+
}
418469
}
419470

420471
if (pattern) {
421472
return new URL(
422-
RegExpPrototypeSymbolReplace(
423-
patternRegEx,
424-
resolved.href,
425-
() => subpath
426-
)
473+
RegExpPrototypeSymbolReplace(patternRegEx, resolved.href, () => subpath)
427474
);
428475
}
429476

@@ -441,11 +488,11 @@ function isArrayIndex(key) {
441488
}
442489

443490
function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath,
444-
base, pattern, internal, conditions) {
491+
base, pattern, internal, isPathMap, conditions) {
445492
if (typeof target === 'string') {
446493
return resolvePackageTargetString(
447494
target, subpath, packageSubpath, packageJSONUrl, base, pattern, internal,
448-
conditions);
495+
isPathMap, conditions);
449496
} else if (ArrayIsArray(target)) {
450497
if (target.length === 0) {
451498
return null;
@@ -458,7 +505,7 @@ function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath,
458505
try {
459506
resolveResult = resolvePackageTarget(
460507
packageJSONUrl, targetItem, subpath, packageSubpath, base, pattern,
461-
internal, conditions);
508+
internal, isPathMap, conditions);
462509
} catch (e) {
463510
lastException = e;
464511
if (e.code === 'ERR_INVALID_PACKAGE_TARGET') {
@@ -494,7 +541,7 @@ function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath,
494541
const conditionalTarget = target[key];
495542
const resolveResult = resolvePackageTarget(
496543
packageJSONUrl, conditionalTarget, subpath, packageSubpath, base,
497-
pattern, internal, conditions);
544+
pattern, internal, isPathMap, conditions);
498545
if (resolveResult === undefined)
499546
continue;
500547
return resolveResult;
@@ -557,7 +604,8 @@ function packageExportsResolve(
557604
!StringPrototypeEndsWith(packageSubpath, '/')) {
558605
const target = exports[packageSubpath];
559606
const resolveResult = resolvePackageTarget(
560-
packageJSONUrl, target, '', packageSubpath, base, false, false, conditions
607+
packageJSONUrl, target, '', packageSubpath, base, false, false, false,
608+
conditions
561609
);
562610

563611
if (resolveResult == null) {
@@ -608,6 +656,7 @@ function packageExportsResolve(
608656
base,
609657
true,
610658
false,
659+
StringPrototypeEndsWith(packageSubpath, '/'),
611660
conditions);
612661

613662
if (resolveResult == null) {
@@ -654,7 +703,8 @@ function packageImportsResolve(name, base, conditions) {
654703
if (ObjectPrototypeHasOwnProperty(imports, name) &&
655704
!StringPrototypeIncludes(name, '*')) {
656705
const resolveResult = resolvePackageTarget(
657-
packageJSONUrl, imports[name], '', name, base, false, true, conditions
706+
packageJSONUrl, imports[name], '', name, base, false, true, false,
707+
conditions
658708
);
659709
if (resolveResult != null) {
660710
return resolveResult;
@@ -687,7 +737,7 @@ function packageImportsResolve(name, base, conditions) {
687737
const resolveResult = resolvePackageTarget(packageJSONUrl, target,
688738
bestMatchSubpath,
689739
bestMatch, base, true,
690-
true, conditions);
740+
true, false, conditions);
691741
if (resolveResult != null) {
692742
return resolveResult;
693743
}

test/es-module/test-esm-exports-deprecations.mjs

+13
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
1+
// Flags: --pending-deprecation
12
import { mustCall } from '../common/index.mjs';
23
import assert from 'assert';
34

45
let curWarning = 0;
56
const expectedWarnings = [
7+
'Use of deprecated leading or trailing slash',
8+
'Use of deprecated double slash',
9+
'.//asdf.js',
10+
'".//internal/test.js"',
11+
'".//internal//test.js"',
12+
'"./////internal/////test.js"',
613
'"./trailing-pattern-slash/"',
14+
'"./subpath/dir1/dir1.js"',
15+
'"./subpath//dir1/dir1.js"',
16+
'.//asdf.js',
17+
'".//internal/test.js"',
18+
'".//internal//test.js"',
19+
'"./////internal/////test.js"',
720
'no_exports',
821
'default_index',
922
];

test/es-module/test-esm-exports.mjs

+14-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js';
4141
['pkgexports/a/b/dir1/dir1', { default: 'main' }],
4242

4343
// Deprecated:
44+
// Double slashes:
45+
['pkgexports/a//dir1/dir1', { default: 'main' }],
46+
// double slash target
47+
['pkgexports/doubleslash', { default: 'asdf' }],
48+
// Null target with several slashes
49+
['pkgexports/sub//internal/test.js', { default: 'internal only' }],
50+
['pkgexports/sub//internal//test.js', { default: 'internal only' }],
51+
['pkgexports/sub/////internal/////test.js', { default: 'internal only' }],
52+
// trailing slash
4453
['pkgexports/trailing-pattern-slash/',
4554
{ default: 'trailing-pattern-slash' }],
4655
]);
@@ -74,7 +83,11 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js';
7483
['pkgexports/invalid1', './invalid1'],
7584
['pkgexports/invalid4', './invalid4'],
7685
// Null mapping
86+
['pkgexports/sub/internal/test.js', './sub/internal/test.js'],
87+
['pkgexports/sub/internal//test.js', './sub/internal//test.js'],
7788
['pkgexports/null', './null'],
89+
['pkgexports//null', './/null'],
90+
['pkgexports/////null', './////null'],
7891
['pkgexports/null/subpath', './null/subpath'],
7992
// Empty fallback
8093
['pkgexports/nofallback1', './nofallback1'],
@@ -133,7 +146,7 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js';
133146
loadFixture(specifier).catch(mustCall((err) => {
134147
strictEqual(err.code, 'ERR_INVALID_MODULE_SPECIFIER');
135148
assertStartsWith(err.message, 'Invalid module ');
136-
assertIncludes(err.message, 'is not a valid subpath');
149+
assertIncludes(err.message, 'is not a valid match in pattern');
137150
assertIncludes(err.message, subpath);
138151
}));
139152
}

0 commit comments

Comments
 (0)