diff --git a/doc/api/deprecations.md b/doc/api/deprecations.md index c37f6748deb9ab..26bb9ff3315837 100644 --- a/doc/api/deprecations.md +++ b/doc/api/deprecations.md @@ -2653,10 +2653,28 @@ In future versions of Node.js, `fs.rmdir(path, { recursive: true })` will throw on nonexistent paths, or when given a file as a target. Use `fs.rm(path, { recursive: true, force: true })` instead. +### DEP0XXX: Main index lookup and extension searching + + +Type: Documentation-only (supports [`--pending-deprecation`][]) + +Previously, `index.js` and extension searching lookups would apply to +`import 'pkg'` main entry point resolution, even when resolving ES modules. + +With this deprecation, all ES module main entry point resolutions require +an explicit [`"exports"` or `"main"` entry][] with the exact file extension. + [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 [WHATWG URL API]: url.md#url_the_whatwg_url_api +[`"exports"` or `"main"` entry]: packages.md#packages_main_entry_point_export [`--pending-deprecation`]: cli.md#cli_pending_deprecation [`--throw-deprecation`]: cli.md#cli_throw_deprecation [`Buffer.allocUnsafeSlow(size)`]: buffer.md#buffer_static_method_buffer_allocunsafeslow_size diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js index d354dc47b1be6b..308eacd306bc28 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -59,6 +59,37 @@ const userConditions = getOptionValue('--conditions'); const DEFAULT_CONDITIONS = ObjectFreeze(['node', 'import', ...userConditions]); const DEFAULT_CONDITIONS_SET = new SafeSet(DEFAULT_CONDITIONS); +const pendingDeprecation = getOptionValue('--pending-deprecation'); + +function emitLegacyIndexDeprecation(url, packageJSONUrl, base, main) { + if (!pendingDeprecation) + return; + const { format } = defaultGetFormat(url); + if (format !== 'module') + return; + const path = fileURLToPath(url); + const pkgPath = fileURLToPath(new URL('.', packageJSONUrl)); + const basePath = fileURLToPath(base); + if (main) + process.emitWarning( + `Package ${pkgPath} has a "main" field set to ${JSONStringify(main)}, ` + + `excluding the full filename and extension to the resolved file at "${ + StringPrototypeSlice(path, pkgPath.length)}", imported from ${ + basePath}.\n Automatic extension resolution of the "main" field is` + + 'deprecated for ES modules.', + 'DeprecationWarning', + 'DEP0150' + ); + else + process.emitWarning( + `No "main" or "exports" field defined in the package.json for ${pkgPath + } resolving the main entry point "${ + StringPrototypeSlice(path, pkgPath.length)}", imported from ${basePath + }.\nDefault "index" lookups for the main are deprecated for ES modules.`, + 'DeprecationWarning', + 'DEP0150' + ); +} function getConditionsSet(conditions) { if (conditions !== undefined && conditions !== DEFAULT_CONDITIONS) { @@ -181,41 +212,33 @@ function legacyMainResolve(packageJSONUrl, packageConfig, base) { if (fileExists(guess = new URL(`./${packageConfig.main}`, packageJSONUrl))) { return guess; - } - if (fileExists(guess = new URL(`./${packageConfig.main}.js`, - packageJSONUrl))) { - return guess; - } - if (fileExists(guess = new URL(`./${packageConfig.main}.json`, - packageJSONUrl))) { - return guess; - } - if (fileExists(guess = new URL(`./${packageConfig.main}.node`, - packageJSONUrl))) { - return guess; - } - if (fileExists(guess = new URL(`./${packageConfig.main}/index.js`, - packageJSONUrl))) { - return guess; - } - if (fileExists(guess = new URL(`./${packageConfig.main}/index.json`, - packageJSONUrl))) { - return guess; - } - if (fileExists(guess = new URL(`./${packageConfig.main}/index.node`, - packageJSONUrl))) { + } else if (fileExists(guess = new URL(`./${packageConfig.main}.js`, + packageJSONUrl))); + else if (fileExists(guess = new URL(`./${packageConfig.main}.json`, + packageJSONUrl))); + else if (fileExists(guess = new URL(`./${packageConfig.main}.node`, + packageJSONUrl))); + else if (fileExists(guess = new URL(`./${packageConfig.main}/index.js`, + packageJSONUrl))); + else if (fileExists(guess = new URL(`./${packageConfig.main}/index.json`, + packageJSONUrl))); + else if (fileExists(guess = new URL(`./${packageConfig.main}/index.node`, + packageJSONUrl))); + else guess = undefined; + if (guess) { + emitLegacyIndexDeprecation(guess, packageJSONUrl, base, + packageConfig.main); return guess; } // Fallthrough. } - if (fileExists(guess = new URL('./index.js', packageJSONUrl))) { - return guess; - } + if (fileExists(guess = new URL('./index.js', packageJSONUrl))); // So fs. - if (fileExists(guess = new URL('./index.json', packageJSONUrl))) { - return guess; - } - if (fileExists(guess = new URL('./index.node', packageJSONUrl))) { + else if (fileExists(guess = new URL('./index.json', packageJSONUrl))); + else if (fileExists(guess = new URL('./index.node', packageJSONUrl))); + else guess = undefined; + if (guess) { + emitLegacyIndexDeprecation(guess, packageJSONUrl, base, packageConfig.main); return guess; } // Not found. @@ -864,3 +887,6 @@ module.exports = { packageExportsResolve, packageImportsResolve }; + +// cycle +const { defaultGetFormat } = require('internal/modules/esm/get_format'); diff --git a/test/es-module/test-esm-exports.mjs b/test/es-module/test-esm-exports.mjs index d234099732e3aa..f153b46e321638 100644 --- a/test/es-module/test-esm-exports.mjs +++ b/test/es-module/test-esm-exports.mjs @@ -35,7 +35,7 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js'; ['pkgexports-sugar', { default: 'main' }], // Path patterns ['pkgexports/subpath/sub-dir1', { default: 'main' }], - ['pkgexports/features/dir1', { default: 'main' }] + ['pkgexports/features/dir1', { default: 'main' }], ]); if (isRequire) { @@ -44,6 +44,11 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js'; validSpecifiers.set('pkgexports/subpath/dir1/', { default: 'main' }); validSpecifiers.set('pkgexports/subpath/dir2', { default: 'index' }); validSpecifiers.set('pkgexports/subpath/dir2/', { default: 'index' }); + } else { + // No exports or main field + validSpecifiers.set('no_exports', { default: 'index' }); + // Main field without extension + validSpecifiers.set('default_index', { default: 'main' }); } for (const [validSpecifier, expected] of validSpecifiers) { diff --git a/test/fixtures/node_modules/default_index/index.js b/test/fixtures/node_modules/default_index/index.js new file mode 100644 index 00000000000000..748e47637e97fa --- /dev/null +++ b/test/fixtures/node_modules/default_index/index.js @@ -0,0 +1 @@ +export default 'main' diff --git a/test/fixtures/node_modules/default_index/package.json b/test/fixtures/node_modules/default_index/package.json new file mode 100644 index 00000000000000..7665d7a8037428 --- /dev/null +++ b/test/fixtures/node_modules/default_index/package.json @@ -0,0 +1,4 @@ +{ + "main": "index", + "type": "module" +} diff --git a/test/fixtures/node_modules/no_exports/index.js b/test/fixtures/node_modules/no_exports/index.js new file mode 100644 index 00000000000000..8d97dd4b70ce59 --- /dev/null +++ b/test/fixtures/node_modules/no_exports/index.js @@ -0,0 +1 @@ +export default 'index' diff --git a/test/fixtures/node_modules/no_exports/package.json b/test/fixtures/node_modules/no_exports/package.json new file mode 100644 index 00000000000000..3dbc1ca591c055 --- /dev/null +++ b/test/fixtures/node_modules/no_exports/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +}