Skip to content

Commit

Permalink
module: runtime deprecate subpath folder mappings
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
guybedford authored and MylesBorins committed Oct 30, 2020
1 parent 0ddd69e commit c9acb9e
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 8 deletions.
26 changes: 26 additions & 0 deletions doc/api/deprecations.md
Original file line number Diff line number Diff line change
Expand Up @@ -2677,6 +2677,28 @@ In future versions of Node.js, `fs.rmdir(path, { recursive: true })` will throw
if `path` does not exist or is a file.
Use `fs.rm(path, { recursive: true, force: true })` instead.

### DEP0148: Folder mappings in `"exports"` (trailing `"/"`)
<!-- YAML
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/35746
description: Runtime deprecation.
- version: v14.13.0
pr-url: https://github.com/nodejs/node/pull/34718
description: Documentation-only deprecation.
-->

Type: Runtime (supports [`--pending-deprecation`][])

Prior to [subpath patterns][] support, it was possible to define
[subpath folder mappings][] in the [subpath exports][] or
[subpath imports][] fields using a trailing `"/"`.

Without `--pending-deprecation`, runtime warnings occur only for exports
resolutions not in `node_modules`. This means there will not be deprecation
warnings for `"exports"` in dependencies. With `--pending-deprecation`, a
runtime warning results no matter where the `"exports"` usage occurs.

[Legacy URL API]: url.md#url_legacy_url_api
[NIST SP 800-38D]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
[RFC 6066]: https://tools.ietf.org/html/rfc6066#section-3
Expand Down Expand Up @@ -2801,3 +2823,7 @@ Use `fs.rm(path, { recursive: true, force: true })` instead.
[from_string_encoding]: buffer.md#buffer_static_method_buffer_from_string_encoding
[legacy `urlObject`]: url.md#url_legacy_urlobject
[static methods of `crypto.Certificate()`]: crypto.md#crypto_class_certificate
[subpath exports]: #packages_subpath_exports
[subpath folder mappings]: #packages_subpath_folder_mappings
[subpath imports]: #packages_subpath_imports
[subpath patterns]: #packages_subpath_patterns
40 changes: 40 additions & 0 deletions doc/api/packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,45 @@ treating the right hand side target pattern as a `**` glob against the list of
files within the package. Because `node_modules` paths are forbidden in exports
targets, this expansion is dependent on only the files of the package itself.

### Subpath folder mappings
<!-- YAML
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/35746
description: Runtime deprecation.
- version: v14.13.0
pr-url: https://github.com/nodejs/node/pull/34718
description: Documentation-only deprecation.
-->

> Stability: 0 - Deprecated: Use subpath patterns instead.
Before subpath patterns were supported, a trailing `"/"` suffix was used to
support folder mappings:

```json
{
"exports": {
"./features/": "./features/"
}
}
```

_This feature will be removed in a future release._

Instead, use direct [subpath patterns][]:

```json
{
"exports": {
"./features/*": "./features/*.js"
}
}
```

The benefit of patterns over folder exports is that packages can always be
imported by consumers without subpath file extensions being necessary.

### Exports sugar

If the `"."` export is the only export, the [`"exports"`][] field provides sugar
Expand Down Expand Up @@ -1028,5 +1067,6 @@ This field defines [subpath imports][] for the current package.
[self-reference]: #packages_self_referencing_a_package_using_its_name
[subpath exports]: #packages_subpath_exports
[subpath imports]: #packages_subpath_imports
[subpath patterns]: #packages_subpath_patterns
[the full specifier path]: esm.md#esm_mandatory_file_extensions
[the dual CommonJS/ES module packages section]: #packages_dual_commonjs_es_module_packages
59 changes: 51 additions & 8 deletions lib/internal/modules/esm/resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const {
String,
StringPrototypeEndsWith,
StringPrototypeIndexOf,
StringPrototypeLastIndexOf,
StringPrototypeReplace,
StringPrototypeSlice,
StringPrototypeSplit,
Expand Down Expand Up @@ -59,6 +60,36 @@ const userConditions = getOptionValue('--conditions');
const DEFAULT_CONDITIONS = ObjectFreeze(['node', 'import', ...userConditions]);
const DEFAULT_CONDITIONS_SET = new SafeSet(DEFAULT_CONDITIONS);

const pendingDeprecation = getOptionValue('--pending-deprecation');
const emittedPackageWarnings = new SafeSet();
function emitFolderMapDeprecation(match, pjsonUrl, isExports, base) {
const pjsonPath = fileURLToPath(pjsonUrl);
if (!pendingDeprecation) {
const nodeModulesIndex = StringPrototypeLastIndexOf(pjsonPath,
'/node_modules/');
if (nodeModulesIndex !== -1) {
const afterNodeModulesPath = StringPrototypeSlice(pjsonPath,
nodeModulesIndex + 14,
-13);
try {
const { packageSubpath } = parsePackageName(afterNodeModulesPath);
if (packageSubpath === '.')
return;
} catch {}
}
}
if (emittedPackageWarnings.has(pjsonPath + '|' + match))
return;
emittedPackageWarnings.add(pjsonPath + '|' + match);
process.emitWarning(
`Use of deprecated folder mapping "${match}" in the ${isExports ?
'"exports"' : '"imports"'} field module resolution of the package at ${
pjsonPath}${base ? ` imported from ${fileURLToPath(base)}` : ''}.\n` +
`Update this package.json to use a subpath pattern like "${match}*".`,
'DeprecationWarning',
'DEP0148'
);
}

function getConditionsSet(conditions) {
if (conditions !== undefined && conditions !== DEFAULT_CONDITIONS) {
Expand Down Expand Up @@ -507,6 +538,8 @@ function packageExportsResolve(
conditions);
if (resolved === null || resolved === undefined)
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
if (!pattern)
emitFolderMapDeprecation(bestMatch, packageJSONUrl, true, base);
return { resolved, exact: pattern };
}

Expand Down Expand Up @@ -556,8 +589,11 @@ function packageImportsResolve(name, base, conditions) {
const resolved = resolvePackageTarget(
packageJSONUrl, target, subpath, bestMatch, base, pattern, true,
conditions);
if (resolved !== null)
if (resolved !== null) {
if (!pattern)
emitFolderMapDeprecation(bestMatch, packageJSONUrl, false, base);
return { resolved, exact: pattern };
}
}
}
}
Expand All @@ -570,13 +606,7 @@ function getPackageType(url) {
return packageConfig.type;
}

/**
* @param {string} specifier
* @param {URL} base
* @param {Set<string>} conditions
* @returns {URL}
*/
function packageResolve(specifier, base, conditions) {
function parsePackageName(specifier, base) {
let separatorIndex = StringPrototypeIndexOf(specifier, '/');
let validPackageName = true;
let isScoped = false;
Expand Down Expand Up @@ -610,6 +640,19 @@ function packageResolve(specifier, base, conditions) {
const packageSubpath = '.' + (separatorIndex === -1 ? '' :
StringPrototypeSlice(specifier, separatorIndex));

return { packageName, packageSubpath, isScoped };
}

/**
* @param {string} specifier
* @param {URL} base
* @param {Set<string>} conditions
* @returns {URL}
*/
function packageResolve(specifier, base, conditions) {
const { packageName, packageSubpath, isScoped } =
parsePackageName(specifier, base);

// ResolveSelf
const packageConfig = getPackageScopeConfig(base);
if (packageConfig.exists) {
Expand Down
19 changes: 19 additions & 0 deletions test/es-module/test-esm-exports-pending-deprecations.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Flags: --pending-deprecation
import { mustCall } from '../common/index.mjs';
import assert from 'assert';

let curWarning = 0;
const expectedWarnings = [
'"./sub/"',
'"./fallbackdir/"',
'"./subpath/"'
];

process.addListener('warning', mustCall((warning) => {
assert(warning.stack.includes(expectedWarnings[curWarning++]), warning.stack);
}, expectedWarnings.length));

(async () => {
await import('./test-esm-exports.mjs');
})()
.catch((err) => console.error(err));
22 changes: 22 additions & 0 deletions test/es-module/test-esm-local-deprecations.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { mustCall } from '../common/index.mjs';
import assert from 'assert';
import fixtures from '../common/fixtures.js';
import { pathToFileURL } from 'url';

const selfDeprecatedFolders =
fixtures.path('/es-modules/self-deprecated-folders/main.js');

let curWarning = 0;
const expectedWarnings = [
'"./" in the "exports" field',
'"#self/" in the "imports" field'
];

process.addListener('warning', mustCall((warning) => {
assert(warning.stack.includes(expectedWarnings[curWarning++]), warning.stack);
}, expectedWarnings.length));

(async () => {
await import(pathToFileURL(selfDeprecatedFolders));
})()
.catch((err) => console.error(err));
2 changes: 2 additions & 0 deletions test/fixtures/es-modules/self-deprecated-folders/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import 'self/main.js';
import '#self/main.js';
11 changes: 11 additions & 0 deletions test/fixtures/es-modules/self-deprecated-folders/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "self",
"type": "module",
"exports": {
".": "./main.js",
"./": "./"
},
"imports": {
"#self/": "./"
}
}

0 comments on commit c9acb9e

Please sign in to comment.