Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

modules: runtime deprecate subpath folder mappings #35747

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -369,6 +369,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._
guybedford marked this conversation as resolved.
Show resolved Hide resolved

Instead, use direct [subpath patterns][]:
guybedford marked this conversation as resolved.
Show resolved Hide resolved

```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

> Stability: 1 - Experimental
Expand Down Expand Up @@ -1040,5 +1079,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}*".`,
Comment on lines +85 to +88
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it be reasonable in this message to mention which node versions support subpath patterns, so package authors don't unknowingly make a breaking change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have version information in the documentation for this under the exports field.

Keeping it up to date here might be tricky though, especially with a backport still in progress.

It's a good point though - we could possibly delay this landing until the 12 backport is released if you'd prefer.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should print version information here. It should be available in the docs and to the best of my knowledge we d not have that information available in any other deprecation warnings

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair. How likely are other deprecation warnings to cause package breakage compared to this one? Maybe we could at least say "Please check the docs for which node versions support subpath patterns" and link them there?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Subpath patterns are on 14.13, 15 and will be backported to 12 very soon so I'm just not sure what value there is in muddying the advice for users here? The deprecation has a clear action and that is to move away from the feature or upgrade to subpaths.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just because node doesn’t support a version doesn’t mean package authors don’t. It’s obv fine if you don’t want to take my suggestion, but i think the risk of causing breakage is far greater than the risk of someone remaining on slash patterns due to an extra sentence.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed there is a risk, but my concern is that any advice with respect to being cautious about subpaths support will be outdated by the time most users read it.

Even if we weren't backporting to Node.js 12, a deprecation of a feature on 15/16 that points to using a feature only available on 14 seems fine to me and I'm not sure there is precedent for warning users from using a feature (from the web to Node.js) just because it's not available 3 versions back.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(within the platform warning itself)

'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/": "./"
}
}