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

module: drop support for extensionless files in ESM #31415

Closed
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
49 changes: 21 additions & 28 deletions doc/api/esm.md
Expand Up @@ -33,9 +33,8 @@ initial input, or when referenced by `import` statements within ES module code:

* Files ending in `.mjs`.

* Files ending in `.js`, or extensionless files, when the nearest parent
`package.json` file contains a top-level field `"type"` with a value of
`"module"`.
* Files ending in `.js` when the nearest parent `package.json` file contains a
top-level field `"type"` with a value of `"module"`.

* Strings passed in as an argument to `--eval` or `--print`, or piped to
`node` via `STDIN`, with the flag `--input-type=module`.
Expand All @@ -50,18 +49,17 @@ or when referenced by `import` statements within ES module code:

* Files ending in `.cjs`.

* Files ending in `.js`, or extensionless files, when the nearest parent
`package.json` file contains a top-level field `"type"` with a value of
`"commonjs"`.
* Files ending in `.js` when the nearest parent `package.json` file contains a
top-level field `"type"` with a value of `"commonjs"`.

* Strings passed in as an argument to `--eval` or `--print`, or piped to
`node` via `STDIN`, with the flag `--input-type=commonjs`.

### `package.json` `"type"` field

Files ending with `.js` or lacking any extension will be loaded as ES modules
when the nearest parent `package.json` file contains a top-level field `"type"`
with a value of `"module"`.
Files ending with `.js` will be loaded as ES modules when the nearest parent
`package.json` file contains a top-level field `"type"` with a value of
`"module"`.

The nearest parent `package.json` is defined as the first `package.json` found
when searching in the current folder, that folder’s parent, and so on up
Expand All @@ -81,14 +79,12 @@ node my-app.js # Runs as ES module
```

If the nearest parent `package.json` lacks a `"type"` field, or contains
`"type": "commonjs"`, extensionless and `.js` files are treated as CommonJS.
If the volume root is reached and no `package.json` is found,
Node.js defers to the default, a `package.json` with no `"type"`
field. "Extensionless" refers to file paths which do not contain
an extension as opposed to optionally dropping a file extension in a specifier.
`"type": "commonjs"`, `.js` files are treated as CommonJS. If the volume root is
reached and no `package.json` is found, Node.js defers to the default, a
`package.json` with no `"type"` field.

`import` statements of `.js` and extensionless files are treated as ES modules
if the nearest parent `package.json` contains `"type": "module"`.
`import` statements of `.js` files are treated as ES modules if the nearest
parent `package.json` contains `"type": "module"`.

```js
// my-app.js, part of the same example as above
Expand All @@ -106,14 +102,13 @@ as ES modules and `.cjs` files are always treated as CommonJS.

### Package Scope and File Extensions

A folder containing a `package.json` file, and all subfolders below that
folder down until the next folder containing another `package.json`, is
considered a _package scope_. The `"type"` field defines how `.js` and
extensionless files should be treated within a particular `package.json` file’s
package scope. Every package in a project’s `node_modules` folder contains its
own `package.json` file, so each project’s dependencies have their own package
scopes. A `package.json` lacking a `"type"` field is treated as if it contained
`"type": "commonjs"`.
A folder containing a `package.json` file, and all subfolders below that folder
down until the next folder containing another `package.json`, is considered a
_package scope_. The `"type"` field defines how `.js` files should be treated
within a particular `package.json` file’s package scope. Every package in a
project’s `node_modules` folder contains its own `package.json` file, so each
project’s dependencies have their own package scopes. A `package.json` lacking a
`"type"` field is treated as if it contained `"type": "commonjs"`.

The package scope applies not only to initial entry points (`node my-app.js`)
but also to files referenced by `import` statements and `import()` expressions.
Expand Down Expand Up @@ -1548,19 +1543,17 @@ _defaultEnv_ is the conditional environment name priority array,

**ESM_FORMAT**(_url_)

> 1. Assert: _url_ corresponds to an existing file pathname.
> 1. Assert: _url_ corresponds to an existing file.
> 1. Let _pjson_ be the result of **READ_PACKAGE_SCOPE**(_url_).
> 1. If _url_ ends in _".mjs"_, then
> 1. Return _"module"_.
> 1. If _url_ ends in _".cjs"_, then
> 1. Return _"commonjs"_.
> 1. If _pjson?.type_ exists and is _"module"_, then
> 1. If _url_ ends in _".js"_ or lacks a file extension, then
> 1. If _url_ ends in _".js"_, then
> 1. Return _"module"_.
> 1. Throw an _Unsupported File Extension_ error.
> 1. Otherwise,
> 1. If _url_ lacks a file extension, then
> 1. Return _"commonjs"_.
> 1. Throw an _Unsupported File Extension_ error.

**READ_PACKAGE_SCOPE**(_url_)
Expand Down
2 changes: 1 addition & 1 deletion lib/internal/modules/esm/get_format.js
Expand Up @@ -55,7 +55,7 @@ function defaultGetFormat(url, context, defaultGetFormat) {
} else if (parsed.protocol === 'file:') {
const ext = extname(parsed.pathname);
let format;
if (ext === '.js' || ext === '') {
if (ext === '.js') {
format = getPackageType(parsed.href) === TYPE_MODULE ?
'module' : 'commonjs';
} else {
Expand Down
24 changes: 0 additions & 24 deletions test/es-module/test-esm-no-extension.js

This file was deleted.

81 changes: 0 additions & 81 deletions test/es-module/test-esm-unknown-main.js

This file was deleted.

36 changes: 36 additions & 0 deletions test/es-module/test-esm-unknown-or-no-extension.js
@@ -0,0 +1,36 @@
'use strict';

const common = require('../common');
const fixtures = require('../common/fixtures');
const { spawn } = require('child_process');
const assert = require('assert');

// In a "type": "module" package scope, files with unknown extensions or no
// extensions should throw; both when used as a main entry point and also when
// referenced via `import`.

[
'/es-modules/package-type-module/noext-esm',
'/es-modules/package-type-module/imports-noext.mjs',
'/es-modules/package-type-module/extension.unknown',
'/es-modules/package-type-module/imports-unknownext.mjs',
].forEach((fixturePath) => {
const entry = fixtures.path(fixturePath);
const child = spawn(process.execPath, [entry]);
let stdout = '';
let stderr = '';
child.stderr.setEncoding('utf8');
child.stdout.setEncoding('utf8');
child.stdout.on('data', (data) => {
stdout += data;
});
child.stderr.on('data', (data) => {
stderr += data;
});
child.on('close', common.mustCall((code, signal) => {
assert.strictEqual(code, 1);
assert.strictEqual(signal, null);
assert.strictEqual(stdout, '');
assert.ok(stderr.indexOf('ERR_UNKNOWN_FILE_EXTENSION') !== -1);
}));
});