Skip to content
Open
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
13 changes: 8 additions & 5 deletions doc/api/deprecations.md
Original file line number Diff line number Diff line change
Expand Up @@ -3404,6 +3404,9 @@ to change the value will be removed in a future version of Node.js.

<!-- YAML
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/65289
description: End-of-Life.
- version: v16.0.0
pr-url: https://github.com/nodejs/node/pull/37206
description: Runtime deprecation.
Expand All @@ -3415,13 +3418,13 @@ changes:
with `--pending-deprecation` support.
-->

Type: Runtime
Type: End-of-Life

Previously, `index.js` and extension searching lookups would apply to
`import 'pkg'` main entry point resolution, even when resolving ES modules.
`index.js` and extension searching lookups no longer apply to
`import 'pkg'` main entry point resolution 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.
All ES module main entry point resolutions require an explicit
[`"exports"` or `"main"` entry][] with the exact file extension.

### DEP0152: Extension PerformanceEntry properties

Expand Down
4 changes: 3 additions & 1 deletion doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,8 @@ the Node.js module resolution, see the [packages documentation](packages.md).

A file extension must be provided when using the `import` keyword to resolve
relative or absolute specifiers. Directory indexes (e.g. `'./startup/index.js'`)
must also be fully specified.
must also be fully specified. A package's [`"main"`][] field must also include
the exact file extension when the package is an ES module.

This behavior matches how `import` behaves in browser environments, assuming a
typically configured server.
Expand Down Expand Up @@ -1336,6 +1337,7 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][].
[URL]: https://url.spec.whatwg.org/
[WebAssembly JS String Builtins Proposal]: https://github.com/WebAssembly/js-string-builtins
[`"exports"`]: packages.md#exports
[`"main"`]: packages.md#main
[`"type"`]: packages.md#type
[`--experimental-package-map`]: cli.md#--experimental-package-mappath
[`--input-type`]: cli.md#--input-typetype
Expand Down
11 changes: 11 additions & 0 deletions doc/api/packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -1185,6 +1185,12 @@ The `"name"` field can be used in addition to the [`"exports"`][] field to

<!-- YAML
added: v0.4.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/65289
description: ES module `"main"` resolution requires the exact file
extension. Default index lookup and extension searching
are no longer supported.
-->

* Type: {string}
Expand All @@ -1198,6 +1204,11 @@ added: v0.4.0
The `"main"` field defines the entry point of a package when imported by name
via a `node_modules` lookup. Its value is a path.

When the package is an [ES module][] (for example, `"type": "module"`),
the `"main"` field must include the exact file extension. Default `index.js`
lookups and automatic extension resolution are not supported for ES modules.
Use the [`"exports"`][] field or a `"main"` value such as `"./index.js"`.

The [`"exports"`][] field, if it exists, takes precedence over the
`"main"` field when importing the package by name.

Expand Down
47 changes: 22 additions & 25 deletions lib/internal/modules/esm/resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,40 +113,37 @@ function emitInvalidSegmentDeprecation(target, request, match, pjsonUrl, interna
}

/**
* Emits a deprecation warning if the given URL is a module and
* the package.json file does not define a "main" or "exports" field.
* Throws if the given URL is an ES module resolved via legacy index lookup
* or "main" extension searching rather than an explicit "exports" or "main"
* path with the exact file extension.
* @param {URL} url - The URL of the module being resolved.
* @param {string} path - The path of the module being resolved.
* @param {string} pkgPath - The path of the parent dir of the package.json file for the module.
* @param {string | URL} [base] - The base URL for the module being resolved.
* @param {string} [main] - The "main" field from the package.json file.
*/
function emitLegacyIndexDeprecation(url, path, pkgPath, base, main) {
if (process.noDeprecation) {
function throwIfLegacyIndexESM(url, path, pkgPath, base, main) {
const format = defaultGetFormatWithoutErrors(url);
if (format !== 'module') {
return;
}
const format = defaultGetFormatWithoutErrors(url);
if (format !== 'module') { return; }
const basePath = fileURLToPath(base);
const packageJSONPath = join(pkgPath, 'package.json');
if (!main) {
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',
'DEP0151',
);
} else if (resolve(pkgPath, main) !== path) {
process.emitWarning(
`Package ${pkgPath} has a "main" field set to "${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',
'DEP0151',
);
throw new ERR_INVALID_PACKAGE_CONFIG(
packageJSONPath,
basePath,
'Default "index" lookups for the main are not supported for ES ' +
'modules. Add an explicit "exports" or "main" entry with the exact ' +
'file extension.');
}
if (resolve(pkgPath, main) !== path) {
throw new ERR_INVALID_PACKAGE_CONFIG(
packageJSONPath,
basePath,
'Automatic extension resolution of the "main" field is not supported ' +
'for ES modules. The "main" field must include the exact file ' +
'extension.');
}
}

Expand Down Expand Up @@ -206,7 +203,7 @@ function legacyMainResolve(packageJSONUrl, packageConfig, base) {
const resolvedPath = resolve(pkgPath, maybeMain + legacyMainResolveExtensions[resolvedOption]);
const resolvedUrl = pathToFileURL(resolvedPath);

emitLegacyIndexDeprecation(resolvedUrl, resolvedPath, pkgPath, base, packageConfig.main);
throwIfLegacyIndexESM(resolvedUrl, resolvedPath, pkgPath, base, packageConfig.main);

return resolvedUrl;
}
Expand Down
2 changes: 0 additions & 2 deletions test/es-module/test-esm-exports-deprecations.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ const expectedWarnings = [
'".//internal/test.js"',
'".//internal//test.js"',
'"./////internal/////test.js"',
'no_exports',
'default_index',
];

process.addListener('warning', mustCall((warning) => {
Expand Down
16 changes: 12 additions & 4 deletions test/es-module/test-esm-exports.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,18 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js';
]);

if (!isRequire) {
// No exports or main field
validSpecifiers.set('no_exports', { default: 'index' });
// Main field without extension
validSpecifiers.set('default_index', { default: 'main' });
// DEP0151 End-of-Life: ESM main resolution requires an explicit
// "exports" or "main" entry with the exact file extension.
const legacyMainErrors = new Map([
['no_exports', 'Default "index" lookups'],
['default_index', 'Automatic extension resolution'],
]);
for (const [specifier, message] of legacyMainErrors) {
loadFixture(specifier).catch(mustCall((err) => {
assert.strictEqual(err.code, 'ERR_INVALID_PACKAGE_CONFIG');
assertIncludes(err.message, message);
}));
}
}

for (const [validSpecifier, expected] of validSpecifiers) {
Expand Down
47 changes: 33 additions & 14 deletions test/es-module/test-esm-extension-lookup-deprecation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ describe('ESM in main field', { concurrency: !process.env.TEST_PARALLEL }, () =>
assert.strictEqual(code, 0);
});

it('should emit warning when "main" and "exports" are missing', async () => {
it('should throw when "main" and "exports" are missing', async () => {
const cwd = tmpdir.resolve(Math.random().toString());
const pkgPath = path.join(cwd, './node_modules/pkg/');
await mkdir(pkgPath, { recursive: true });
Expand All @@ -60,11 +60,12 @@ describe('ESM in main field', { concurrency: !process.env.TEST_PARALLEL }, () =>
'--eval', 'import "pkg"',
], { cwd });

assert.match(stderr, /\[DEP0151\]/);
assert.match(stdout, /^Hello World!\r?\n$/);
assert.strictEqual(code, 0);
assert.match(stderr, /ERR_INVALID_PACKAGE_CONFIG/);
assert.match(stderr, /Default "index" lookups for the main are not supported for ES modules/);
assert.strictEqual(stdout, '');
assert.strictEqual(code, 1);
});
it('should emit warning when "main" is falsy', async () => {
it('should throw when "main" is falsy', async () => {
const cwd = tmpdir.resolve(Math.random().toString());
const pkgPath = path.join(cwd, './node_modules/pkg/');
await mkdir(pkgPath, { recursive: true });
Expand All @@ -78,11 +79,12 @@ describe('ESM in main field', { concurrency: !process.env.TEST_PARALLEL }, () =>
'--eval', 'import "pkg"',
], { cwd });

assert.match(stderr, /\[DEP0151\]/);
assert.match(stdout, /^Hello World!\r?\n$/);
assert.strictEqual(code, 0);
assert.match(stderr, /ERR_INVALID_PACKAGE_CONFIG/);
assert.match(stderr, /Default "index" lookups for the main are not supported for ES modules/);
assert.strictEqual(stdout, '');
assert.strictEqual(code, 1);
});
it('should emit warning when "main" is a relative path without extension', async () => {
it('should throw when "main" is a relative path without extension', async () => {
const cwd = tmpdir.resolve(Math.random().toString());
const pkgPath = path.join(cwd, './node_modules/pkg/');
await mkdir(pkgPath, { recursive: true });
Expand All @@ -96,11 +98,12 @@ describe('ESM in main field', { concurrency: !process.env.TEST_PARALLEL }, () =>
'--eval', 'import "pkg"',
], { cwd });

assert.match(stderr, /\[DEP0151\]/);
assert.match(stdout, /^Hello World!\r?\n$/);
assert.strictEqual(code, 0);
assert.match(stderr, /ERR_INVALID_PACKAGE_CONFIG/);
assert.match(stderr, /Automatic extension resolution of the "main" field is not supported/);
assert.strictEqual(stdout, '');
assert.strictEqual(code, 1);
});
it('should emit warning when "main" is an absolute path without extension', async () => {
it('should throw when "main" is an absolute path without extension', async () => {
const cwd = tmpdir.resolve(Math.random().toString());
const pkgPath = path.join(cwd, './node_modules/pkg/');
await mkdir(pkgPath, { recursive: true });
Expand All @@ -114,7 +117,23 @@ describe('ESM in main field', { concurrency: !process.env.TEST_PARALLEL }, () =>
'--eval', 'import "pkg"',
], { cwd });

assert.match(stderr, /\[DEP0151\]/);
assert.match(stderr, /ERR_INVALID_PACKAGE_CONFIG/);
assert.match(stderr, /Automatic extension resolution of the "main" field is not supported/);
assert.strictEqual(stdout, '');
assert.strictEqual(code, 1);
});
it('should still resolve CommonJS packages via index lookup', async () => {
const cwd = tmpdir.resolve(Math.random().toString());
const pkgPath = path.join(cwd, './node_modules/pkg/');
await mkdir(pkgPath, { recursive: true });
await writeFile(path.join(pkgPath, './index.js'), 'console.log("Hello World!")');
await writeFile(path.join(pkgPath, './package.json'), JSON.stringify({}));
const { code, stdout, stderr } = await spawnPromisified(execPath, [
'--input-type=module',
'--eval', 'import "pkg"',
], { cwd });

assert.strictEqual(stderr, '');
assert.match(stdout, /^Hello World!\r?\n$/);
assert.strictEqual(code, 0);
});
Expand Down
13 changes: 7 additions & 6 deletions test/es-module/test-esm-type-main.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { mustNotCall } from '../common/index.mjs';
import '../common/index.mjs';
import assert from 'assert';
import { importFixture } from '../fixtures/pkgexports.mjs';

(async () => {
const m = await importFixture('type-main');
assert.strictEqual(m.default, 'asdf');
})()
.catch(mustNotCall);
// DEP0151 End-of-Life: "type": "module" with a "main" field that omits
// the file extension no longer resolves.
await assert.rejects(importFixture('type-main'), {
code: 'ERR_INVALID_PACKAGE_CONFIG',
message: /Automatic extension resolution of the "main" field is not supported/,
});

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading