Skip to content

Commit f258111

Browse files
committed
modules: runtime deprecate subpath folder mappings
1 parent 4d16554 commit f258111

File tree

6 files changed

+143
-8
lines changed

6 files changed

+143
-8
lines changed

doc/api/deprecations.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2677,6 +2677,45 @@ In future versions of Node.js, `fs.rmdir(path, { recursive: true })` will throw
26772677
if `path` does not exist or is a file.
26782678
Use `fs.rm(path, { recursive: true, force: true })` instead.
26792679

2680+
### DEP0148: Folder mappings in `"exports"` (trailing `"/"`)
2681+
<!-- YAML
2682+
changes:
2683+
- version: REPLACEME
2684+
pr-url: https://github.com/nodejs/node/pull/35746
2685+
description: Runtime deprecation.
2686+
- version: v14.13.0
2687+
pr-url: https://github.com/nodejs/node/pull/34718
2688+
description: Documentation-only deprecation.
2689+
-->
2690+
2691+
Type: Runtime (supports [`--pending-deprecation`][])
2692+
2693+
Prior to [subpath patterns][] support, it was possible to define folder mappings
2694+
in the [subpath exports][] or [subpath imports][] fields using a trailing `"/"`:
2695+
2696+
```json
2697+
{
2698+
"exports": {
2699+
"./features/": "./features/"
2700+
}
2701+
}
2702+
```
2703+
2704+
This usage is deprecated for using direct [subpath patterns][]:
2705+
2706+
```json
2707+
{
2708+
"exports": {
2709+
"./features/*": "./features/*.js"
2710+
}
2711+
}
2712+
```
2713+
2714+
Without `--pending-deprecation`, runtime warnings occur only for exports
2715+
resolutions not in `node_modules`. This means there will not be deprecation
2716+
warnings for `"exports"` in dependencies. With `--pending-deprecation`, a
2717+
runtime warning results no matter where the `"exports"` usage occurs.
2718+
26802719
[Legacy URL API]: url.md#url_legacy_url_api
26812720
[NIST SP 800-38D]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
26822721
[RFC 6066]: https://tools.ietf.org/html/rfc6066#section-3
@@ -2801,3 +2840,6 @@ Use `fs.rm(path, { recursive: true, force: true })` instead.
28012840
[from_string_encoding]: buffer.md#buffer_static_method_buffer_from_string_encoding
28022841
[legacy `urlObject`]: url.md#url_legacy_urlobject
28032842
[static methods of `crypto.Certificate()`]: crypto.md#crypto_class_certificate
2843+
[subpath exports]: #packages_subpath_exports
2844+
[subpath imports]: #packages_subpath_imports
2845+
[subpath patterns]: #packages_subpath_patterns

lib/internal/modules/esm/resolve.js

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,33 @@ const userConditions = getOptionValue('--conditions');
5959
const DEFAULT_CONDITIONS = ObjectFreeze(['node', 'import', ...userConditions]);
6060
const DEFAULT_CONDITIONS_SET = new SafeSet(DEFAULT_CONDITIONS);
6161

62+
const pendingDeprecation = getOptionValue('--pending-deprecation');
63+
const emittedPackageWarnings = new SafeSet();
64+
function emitFolderMapDeprecation(match, pjsonUrl, isExports, base) {
65+
const pjsonPath = fileURLToPath(pjsonUrl);
66+
if (!pendingDeprecation) {
67+
const nodeModulesIndex = pjsonPath.lastIndexOf('/node_modules/');
68+
if (nodeModulesIndex !== -1) {
69+
const afterNodeModulesPath = pjsonPath.slice(nodeModulesIndex + 14, -13);
70+
try {
71+
const { packageSubpath } = parsePackageName(afterNodeModulesPath);
72+
if (packageSubpath === '.')
73+
return;
74+
} catch {}
75+
}
76+
}
77+
if (emittedPackageWarnings.has(pjsonPath + '|' + match))
78+
return;
79+
emittedPackageWarnings.add(pjsonPath + '|' + match);
80+
process.emitWarning(
81+
`Use of deprecated folder mapping "${match}" in the ${isExports ?
82+
'"exports"' : '"imports"'} field module resolution of the package at ${
83+
pjsonPath}${base ? ` imported from ${fileURLToPath(base)}` : ''}.\n` +
84+
`Update this package.json to use a subpath pattern like "${match}*".`,
85+
'DeprecationWarning',
86+
'DEP0148'
87+
);
88+
}
6289

6390
function getConditionsSet(conditions) {
6491
if (conditions !== undefined && conditions !== DEFAULT_CONDITIONS) {
@@ -507,6 +534,8 @@ function packageExportsResolve(
507534
conditions);
508535
if (resolved === null || resolved === undefined)
509536
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
537+
if (!pattern)
538+
emitFolderMapDeprecation(bestMatch, packageJSONUrl, true, base);
510539
return { resolved, exact: pattern };
511540
}
512541

@@ -556,8 +585,11 @@ function packageImportsResolve(name, base, conditions) {
556585
const resolved = resolvePackageTarget(
557586
packageJSONUrl, target, subpath, bestMatch, base, pattern, true,
558587
conditions);
559-
if (resolved !== null)
588+
if (resolved !== null) {
589+
if (!pattern)
590+
emitFolderMapDeprecation(bestMatch, packageJSONUrl, false, base);
560591
return { resolved, exact: pattern };
592+
}
561593
}
562594
}
563595
}
@@ -570,13 +602,7 @@ function getPackageType(url) {
570602
return packageConfig.type;
571603
}
572604

573-
/**
574-
* @param {string} specifier
575-
* @param {URL} base
576-
* @param {Set<string>} conditions
577-
* @returns {URL}
578-
*/
579-
function packageResolve(specifier, base, conditions) {
605+
function parsePackageName(specifier, base) {
580606
let separatorIndex = StringPrototypeIndexOf(specifier, '/');
581607
let validPackageName = true;
582608
let isScoped = false;
@@ -610,6 +636,19 @@ function packageResolve(specifier, base, conditions) {
610636
const packageSubpath = '.' + (separatorIndex === -1 ? '' :
611637
StringPrototypeSlice(specifier, separatorIndex));
612638

639+
return { packageName, packageSubpath, isScoped };
640+
}
641+
642+
/**
643+
* @param {string} specifier
644+
* @param {URL} base
645+
* @param {Set<string>} conditions
646+
* @returns {URL}
647+
*/
648+
function packageResolve(specifier, base, conditions) {
649+
const { packageName, packageSubpath, isScoped } =
650+
parsePackageName(specifier, base);
651+
613652
// ResolveSelf
614653
const packageConfig = getPackageScopeConfig(base);
615654
if (packageConfig.exists) {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Flags: --pending-deprecation
2+
import { mustCall } from '../common/index.mjs';
3+
import assert from 'assert';
4+
5+
let curWarning = 0;
6+
const expectedWarnings = [
7+
'"./sub/"',
8+
'"./fallbackdir/"',
9+
'"./subpath/"'
10+
];
11+
12+
process.addListener('warning', mustCall((warning) => {
13+
assert(warning.stack.includes(expectedWarnings[curWarning++]), warning.stack);
14+
}, expectedWarnings.length));
15+
16+
(async () => {
17+
await import('./test-esm-exports.mjs');
18+
})()
19+
.catch((err) => console.error(err));
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { mustCall } from '../common/index.mjs';
2+
import assert from 'assert';
3+
import fixtures from '../common/fixtures.js';
4+
import { pathToFileURL } from 'url';
5+
6+
const selfDeprecatedFolders =
7+
fixtures.path('/es-modules/self-deprecated-folders/main.js');
8+
9+
let curWarning = 0;
10+
const expectedWarnings = [
11+
'"./" in the "exports" field',
12+
'"#self/" in the "imports" field'
13+
];
14+
15+
process.addListener('warning', mustCall((warning) => {
16+
assert(warning.stack.includes(expectedWarnings[curWarning++]), warning.stack);
17+
}, expectedWarnings.length));
18+
19+
(async () => {
20+
await import(pathToFileURL(selfDeprecatedFolders));
21+
})()
22+
.catch((err) => console.error(err));
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import 'self/main.js';
2+
import '#self/main.js';
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "self",
3+
"type": "module",
4+
"exports": {
5+
".": "./main.js",
6+
"./": "./"
7+
},
8+
"imports": {
9+
"#self/": "./"
10+
}
11+
}

0 commit comments

Comments
 (0)