Skip to content

Commit 0f5a8c5

Browse files
guybedfordtargos
authored andcommitted
module: runtime deprecate subpath folder mappings
PR-URL: #35747 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Bradley Farias <bradley.meck@gmail.com> Reviewed-By: Rich Trott <rtrott@gmail.com> Reviewed-By: Myles Borins <myles.borins@gmail.com>
1 parent d95013f commit 0f5a8c5

File tree

7 files changed

+171
-8
lines changed

7 files changed

+171
-8
lines changed

doc/api/deprecations.md

+26
Original file line numberDiff line numberDiff line change
@@ -2677,6 +2677,28 @@ 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
2694+
[subpath folder mappings][] in the [subpath exports][] or
2695+
[subpath imports][] fields using a trailing `"/"`.
2696+
2697+
Without `--pending-deprecation`, runtime warnings occur only for exports
2698+
resolutions not in `node_modules`. This means there will not be deprecation
2699+
warnings for `"exports"` in dependencies. With `--pending-deprecation`, a
2700+
runtime warning results no matter where the `"exports"` usage occurs.
2701+
26802702
[Legacy URL API]: url.md#url_legacy_url_api
26812703
[NIST SP 800-38D]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
26822704
[RFC 6066]: https://tools.ietf.org/html/rfc6066#section-3
@@ -2801,3 +2823,7 @@ Use `fs.rm(path, { recursive: true, force: true })` instead.
28012823
[from_string_encoding]: buffer.md#buffer_static_method_buffer_from_string_encoding
28022824
[legacy `urlObject`]: url.md#url_legacy_urlobject
28032825
[static methods of `crypto.Certificate()`]: crypto.md#crypto_class_certificate
2826+
[subpath exports]: #packages_subpath_exports
2827+
[subpath folder mappings]: #packages_subpath_folder_mappings
2828+
[subpath imports]: #packages_subpath_imports
2829+
[subpath patterns]: #packages_subpath_patterns

doc/api/packages.md

+40
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,45 @@ treating the right hand side target pattern as a `**` glob against the list of
365365
files within the package. Because `node_modules` paths are forbidden in exports
366366
targets, this expansion is dependent on only the files of the package itself.
367367

368+
### Subpath folder mappings
369+
<!-- YAML
370+
changes:
371+
- version: REPLACEME
372+
pr-url: https://github.com/nodejs/node/pull/35746
373+
description: Runtime deprecation.
374+
- version: v14.13.0
375+
pr-url: https://github.com/nodejs/node/pull/34718
376+
description: Documentation-only deprecation.
377+
-->
378+
379+
> Stability: 0 - Deprecated: Use subpath patterns instead.
380+
381+
Before subpath patterns were supported, a trailing `"/"` suffix was used to
382+
support folder mappings:
383+
384+
```json
385+
{
386+
"exports": {
387+
"./features/": "./features/"
388+
}
389+
}
390+
```
391+
392+
_This feature will be removed in a future release._
393+
394+
Instead, use direct [subpath patterns][]:
395+
396+
```json
397+
{
398+
"exports": {
399+
"./features/*": "./features/*.js"
400+
}
401+
}
402+
```
403+
404+
The benefit of patterns over folder exports is that packages can always be
405+
imported by consumers without subpath file extensions being necessary.
406+
368407
### Exports sugar
369408

370409
If the `"."` export is the only export, the [`"exports"`][] field provides sugar
@@ -1028,5 +1067,6 @@ This field defines [subpath imports][] for the current package.
10281067
[self-reference]: #packages_self_referencing_a_package_using_its_name
10291068
[subpath exports]: #packages_subpath_exports
10301069
[subpath imports]: #packages_subpath_imports
1070+
[subpath patterns]: #packages_subpath_patterns
10311071
[the full specifier path]: esm.md#esm_mandatory_file_extensions
10321072
[the dual CommonJS/ES module packages section]: #packages_dual_commonjs_es_module_packages

lib/internal/modules/esm/resolve.js

+51-8
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const {
1616
String,
1717
StringPrototypeEndsWith,
1818
StringPrototypeIndexOf,
19+
StringPrototypeLastIndexOf,
1920
StringPrototypeReplace,
2021
StringPrototypeSlice,
2122
StringPrototypeSplit,
@@ -59,6 +60,36 @@ const userConditions = getOptionValue('--conditions');
5960
const DEFAULT_CONDITIONS = ObjectFreeze(['node', 'import', ...userConditions]);
6061
const DEFAULT_CONDITIONS_SET = new SafeSet(DEFAULT_CONDITIONS);
6162

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

6394
function getConditionsSet(conditions) {
6495
if (conditions !== undefined && conditions !== DEFAULT_CONDITIONS) {
@@ -507,6 +538,8 @@ function packageExportsResolve(
507538
conditions);
508539
if (resolved === null || resolved === undefined)
509540
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
541+
if (!pattern)
542+
emitFolderMapDeprecation(bestMatch, packageJSONUrl, true, base);
510543
return { resolved, exact: pattern };
511544
}
512545

@@ -556,8 +589,11 @@ function packageImportsResolve(name, base, conditions) {
556589
const resolved = resolvePackageTarget(
557590
packageJSONUrl, target, subpath, bestMatch, base, pattern, true,
558591
conditions);
559-
if (resolved !== null)
592+
if (resolved !== null) {
593+
if (!pattern)
594+
emitFolderMapDeprecation(bestMatch, packageJSONUrl, false, base);
560595
return { resolved, exact: pattern };
596+
}
561597
}
562598
}
563599
}
@@ -570,13 +606,7 @@ function getPackageType(url) {
570606
return packageConfig.type;
571607
}
572608

573-
/**
574-
* @param {string} specifier
575-
* @param {URL} base
576-
* @param {Set<string>} conditions
577-
* @returns {URL}
578-
*/
579-
function packageResolve(specifier, base, conditions) {
609+
function parsePackageName(specifier, base) {
580610
let separatorIndex = StringPrototypeIndexOf(specifier, '/');
581611
let validPackageName = true;
582612
let isScoped = false;
@@ -610,6 +640,19 @@ function packageResolve(specifier, base, conditions) {
610640
const packageSubpath = '.' + (separatorIndex === -1 ? '' :
611641
StringPrototypeSlice(specifier, separatorIndex));
612642

643+
return { packageName, packageSubpath, isScoped };
644+
}
645+
646+
/**
647+
* @param {string} specifier
648+
* @param {URL} base
649+
* @param {Set<string>} conditions
650+
* @returns {URL}
651+
*/
652+
function packageResolve(specifier, base, conditions) {
653+
const { packageName, packageSubpath, isScoped } =
654+
parsePackageName(specifier, base);
655+
613656
// ResolveSelf
614657
const packageConfig = getPackageScopeConfig(base);
615658
if (packageConfig.exists) {
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));
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));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import 'self/main.js';
2+
import '#self/main.js';
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)