Skip to content

Commit 80d6d7f

Browse files
committed
esm: end-of-life DEP0151 main index lookup
ES module main entry resolution now requires an explicit "exports" or "main" field with the exact file extension. The legacy index.js and extension-searching lookups that previously succeeded with a DEP0151 warning now throw ERR_INVALID_PACKAGE_CONFIG. This is a semver-major change. It is triggered by `import 'pkg'` when the resolved package entry is an ES module and either has no "main"/"exports" field or has a "main" value that omits the file extension. CommonJS packages are unaffected. Refs: #37206 Refs: #36918 Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
1 parent f83e7df commit 80d6d7f

9 files changed

Lines changed: 97 additions & 58 deletions

File tree

doc/api/deprecations.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3404,6 +3404,9 @@ to change the value will be removed in a future version of Node.js.
34043404

34053405
<!-- YAML
34063406
changes:
3407+
- version: REPLACEME
3408+
pr-url: https://github.com/nodejs/node/pull/00000
3409+
description: End-of-Life.
34073410
- version: v16.0.0
34083411
pr-url: https://github.com/nodejs/node/pull/37206
34093412
description: Runtime deprecation.
@@ -3415,13 +3418,13 @@ changes:
34153418
with `--pending-deprecation` support.
34163419
-->
34173420

3418-
Type: Runtime
3421+
Type: End-of-Life
34193422

3420-
Previously, `index.js` and extension searching lookups would apply to
3421-
`import 'pkg'` main entry point resolution, even when resolving ES modules.
3423+
`index.js` and extension searching lookups no longer apply to
3424+
`import 'pkg'` main entry point resolution when resolving ES modules.
34223425

3423-
With this deprecation, all ES module main entry point resolutions require
3424-
an explicit [`"exports"` or `"main"` entry][] with the exact file extension.
3426+
All ES module main entry point resolutions require an explicit
3427+
[`"exports"` or `"main"` entry][] with the exact file extension.
34253428

34263429
### DEP0152: Extension PerformanceEntry properties
34273430

doc/api/esm.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,8 @@ the Node.js module resolution, see the [packages documentation](packages.md).
188188

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

193194
This behavior matches how `import` behaves in browser environments, assuming a
194195
typically configured server.
@@ -1336,6 +1337,7 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][].
13361337
[URL]: https://url.spec.whatwg.org/
13371338
[WebAssembly JS String Builtins Proposal]: https://github.com/WebAssembly/js-string-builtins
13381339
[`"exports"`]: packages.md#exports
1340+
[`"main"`]: packages.md#main
13391341
[`"type"`]: packages.md#type
13401342
[`--experimental-package-map`]: cli.md#--experimental-package-mappath
13411343
[`--input-type`]: cli.md#--input-typetype

doc/api/packages.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1185,6 +1185,12 @@ The `"name"` field can be used in addition to the [`"exports"`][] field to
11851185

11861186
<!-- YAML
11871187
added: v0.4.0
1188+
changes:
1189+
- version: REPLACEME
1190+
pr-url: https://github.com/nodejs/node/pull/00000
1191+
description: ES module `"main"` resolution requires the exact file
1192+
extension. Default index lookup and extension searching
1193+
are no longer supported.
11881194
-->
11891195

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

1207+
When the package is an [ES module][] (for example, `"type": "module"`),
1208+
the `"main"` field must include the exact file extension. Default `index.js`
1209+
lookups and automatic extension resolution are not supported for ES modules.
1210+
Use the [`"exports"`][] field or a `"main"` value such as `"./index.js"`.
1211+
12011212
The [`"exports"`][] field, if it exists, takes precedence over the
12021213
`"main"` field when importing the package by name.
12031214

lib/internal/modules/esm/resolve.js

Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -113,40 +113,37 @@ function emitInvalidSegmentDeprecation(target, request, match, pjsonUrl, interna
113113
}
114114

115115
/**
116-
* Emits a deprecation warning if the given URL is a module and
117-
* the package.json file does not define a "main" or "exports" field.
116+
* Throws if the given URL is an ES module resolved via legacy index lookup
117+
* or "main" extension searching rather than an explicit "exports" or "main"
118+
* path with the exact file extension.
118119
* @param {URL} url - The URL of the module being resolved.
119120
* @param {string} path - The path of the module being resolved.
120121
* @param {string} pkgPath - The path of the parent dir of the package.json file for the module.
121122
* @param {string | URL} [base] - The base URL for the module being resolved.
122123
* @param {string} [main] - The "main" field from the package.json file.
123124
*/
124-
function emitLegacyIndexDeprecation(url, path, pkgPath, base, main) {
125-
if (process.noDeprecation) {
125+
function throwIfLegacyIndexESM(url, path, pkgPath, base, main) {
126+
const format = defaultGetFormatWithoutErrors(url);
127+
if (format !== 'module') {
126128
return;
127129
}
128-
const format = defaultGetFormatWithoutErrors(url);
129-
if (format !== 'module') { return; }
130130
const basePath = fileURLToPath(base);
131+
const packageJSONPath = join(pkgPath, 'package.json');
131132
if (!main) {
132-
process.emitWarning(
133-
`No "main" or "exports" field defined in the package.json for ${pkgPath
134-
} resolving the main entry point "${
135-
StringPrototypeSlice(path, pkgPath.length)}", imported from ${basePath
136-
}.\nDefault "index" lookups for the main are deprecated for ES modules.`,
137-
'DeprecationWarning',
138-
'DEP0151',
139-
);
140-
} else if (resolve(pkgPath, main) !== path) {
141-
process.emitWarning(
142-
`Package ${pkgPath} has a "main" field set to "${main}", ` +
143-
`excluding the full filename and extension to the resolved file at "${
144-
StringPrototypeSlice(path, pkgPath.length)}", imported from ${
145-
basePath}.\n Automatic extension resolution of the "main" field is ` +
146-
'deprecated for ES modules.',
147-
'DeprecationWarning',
148-
'DEP0151',
149-
);
133+
throw new ERR_INVALID_PACKAGE_CONFIG(
134+
packageJSONPath,
135+
basePath,
136+
'Default "index" lookups for the main are not supported for ES ' +
137+
'modules. Add an explicit "exports" or "main" entry with the exact ' +
138+
'file extension.');
139+
}
140+
if (resolve(pkgPath, main) !== path) {
141+
throw new ERR_INVALID_PACKAGE_CONFIG(
142+
packageJSONPath,
143+
basePath,
144+
'Automatic extension resolution of the "main" field is not supported ' +
145+
'for ES modules. The "main" field must include the exact file ' +
146+
'extension.');
150147
}
151148
}
152149

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

209-
emitLegacyIndexDeprecation(resolvedUrl, resolvedPath, pkgPath, base, packageConfig.main);
206+
throwIfLegacyIndexESM(resolvedUrl, resolvedPath, pkgPath, base, packageConfig.main);
210207

211208
return resolvedUrl;
212209
}

test/es-module/test-esm-exports-deprecations.mjs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@ const expectedWarnings = [
1717
'".//internal/test.js"',
1818
'".//internal//test.js"',
1919
'"./////internal/////test.js"',
20-
'no_exports',
21-
'default_index',
2220
];
2321

2422
process.addListener('warning', mustCall((warning) => {

test/es-module/test-esm-exports.mjs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,18 @@ import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js';
5858
]);
5959

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

6775
for (const [validSpecifier, expected] of validSpecifiers) {

test/es-module/test-esm-extension-lookup-deprecation.mjs

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ describe('ESM in main field', { concurrency: !process.env.TEST_PARALLEL }, () =>
4747
assert.strictEqual(code, 0);
4848
});
4949

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

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

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

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

117-
assert.match(stderr, /\[DEP0151\]/);
120+
assert.match(stderr, /ERR_INVALID_PACKAGE_CONFIG/);
121+
assert.match(stderr, /Automatic extension resolution of the "main" field is not supported/);
122+
assert.strictEqual(stdout, '');
123+
assert.strictEqual(code, 1);
124+
});
125+
it('should still resolve CommonJS packages via index lookup', async () => {
126+
const cwd = tmpdir.resolve(Math.random().toString());
127+
const pkgPath = path.join(cwd, './node_modules/pkg/');
128+
await mkdir(pkgPath, { recursive: true });
129+
await writeFile(path.join(pkgPath, './index.js'), 'console.log("Hello World!")');
130+
await writeFile(path.join(pkgPath, './package.json'), JSON.stringify({}));
131+
const { code, stdout, stderr } = await spawnPromisified(execPath, [
132+
'--input-type=module',
133+
'--eval', 'import "pkg"',
134+
], { cwd });
135+
136+
assert.strictEqual(stderr, '');
118137
assert.match(stdout, /^Hello World!\r?\n$/);
119138
assert.strictEqual(code, 0);
120139
});
Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { mustNotCall } from '../common/index.mjs';
1+
import '../common/index.mjs';
22
import assert from 'assert';
33
import { importFixture } from '../fixtures/pkgexports.mjs';
44

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

test/fixtures/es-module-specifiers/node_modules/implicit-main-type-module/package.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)