Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
module: drop support for extensionless main entry points in esm
  • Loading branch information
GeoffreyBooth committed Jan 20, 2020
commit cdacf7ddc942661b20410211ca770966e7e46377
54 changes: 22 additions & 32 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ initial input, or when referenced by `import` statements within ES module code:

* Files ending in `.mjs`.

* Files ending in `.js`, or extensionless files when run as main entry points on
the command line, when the nearest parent `package.json` file contains a
* 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
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 run as main entry points on
the command line, when the nearest parent `package.json` file contains a
* 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 @@ -1042,8 +1037,7 @@ a URL should be interpreted. This can be one of the following:
```js
/**
* @param {string} url
* @param {object} context
* @param {string} context.parentURL
* @param {object} context (currently empty)
* @param {function} defaultGetFormat
* @returns {object} response
* @returns {string} response.format
Expand Down Expand Up @@ -1367,15 +1361,13 @@ updates.
In the following algorithms, all subroutine errors are propagated as errors
of these top-level routines unless stated otherwise.

_isMain_ is **true** when resolving the Node.js application entry point.

_defaultEnv_ is the conditional environment name priority array,
`["node", "import"]`.

<details>
<summary>Resolver algorithm specification</summary>

**ESM_RESOLVE**(_specifier_, _parentURL_, _isMain_)
**ESM_RESOLVE**(_specifier_, _parentURL_)

> 1. Let _resolvedURL_ be **undefined**.
> 1. If _specifier_ is a valid URL, then
Expand All @@ -1396,7 +1388,7 @@ _defaultEnv_ is the conditional environment name priority array,
> 1. If the file at _resolvedURL_ does not exist, then
> 1. Throw a _Module Not Found_ error.
> 1. Set _resolvedURL_ to the real path of _resolvedURL_.
> 1. Let _format_ be the result of **ESM_FORMAT**(_resolvedURL_, _isMain_).
> 1. Let _format_ be the result of **ESM_FORMAT**(_resolvedURL_).
> 1. Load _resolvedURL_ as module format, _format_.

**PACKAGE_RESOLVE**(_packageSpecifier_, _parentURL_)
Expand Down Expand Up @@ -1549,7 +1541,7 @@ _defaultEnv_ is the conditional environment name priority array,
> 1. Return _resolved_.
> 1. Throw a _Module Not Found_ error.

**ESM_FORMAT**(_url_, _isMain_)
**ESM_FORMAT**(_url_)

> 1. Assert: _url_ corresponds to an existing file.
> 1. Let _pjson_ be the result of **READ_PACKAGE_SCOPE**(_url_).
Expand All @@ -1558,12 +1550,10 @@ _defaultEnv_ is the conditional environment name priority array,
> 1. If _url_ ends in _".cjs"_, then
> 1. Return _"commonjs"_.
> 1. If _pjson?.type_ exists and is _"module"_, then
> 1. If _isMain_ is **true** or _url_ ends in _".js"_, then
> 1. If _url_ ends in _".js"_, then
> 1. Return _"module"_.
> 1. Throw an _Unsupported File Extension_ error.
> 1. Otherwise,
> 1. If _isMain_ is **true**, then
> 1. Return _"commonjs"_.
> 1. Throw an _Unsupported File Extension_ error.

**READ_PACKAGE_SCOPE**(_url_)
Expand Down
8 changes: 5 additions & 3 deletions lib/internal/modules/esm/get_format.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,13 @@ function defaultGetFormat(url, context, defaultGetFormat) {
return { format };
} else if (parsed.protocol === 'file:') {
const ext = extname(parsed.pathname);
let format = extensionFormatMap[ext];
const isMain = context.parentURL === undefined;
if (ext === '.js' || (!format && isMain))
let format;
if (ext === '.js') {
format = getPackageType(parsed.href) === TYPE_MODULE ?
'module' : 'commonjs';
} else {
format = extensionFormatMap[ext];
}
if (!format) {
if (experimentalSpeciferResolution === 'node') {
process.emitWarning(
Expand Down
2 changes: 1 addition & 1 deletion lib/internal/modules/esm/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ class Loader {
}

const getFormatResponse = await this._getFormat(
url, { parentURL }, defaultGetFormat);
url, {}, defaultGetFormat);
if (typeof getFormatResponse !== 'object') {
throw new ERR_INVALID_RETURN_VALUE(
'object', 'loader getFormat', getFormatResponse);
Expand Down
24 changes: 0 additions & 24 deletions test/es-module/test-esm-no-extension.js

This file was deleted.

36 changes: 36 additions & 0 deletions test/es-module/test-esm-unknown-or-no-extension.js
Original file line number Diff line number Diff line change
@@ -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);
}));
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import './noext-esm';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import './extension.unknown';