From b6ad757ed14f757442b1f4b62ebbd4af34760f3d Mon Sep 17 00:00:00 2001 From: Geoffrey Booth Date: Sun, 19 Jan 2020 22:40:26 -0800 Subject: [PATCH] module: drop support for extensionless main entry points in esm PR-URL: https://github.com/nodejs/node/pull/31415 Backport-PR-URL: https://github.com/nodejs/node/pull/32280 Reviewed-By: Guy Bedford Reviewed-By: Gus Caplan Reviewed-By: Bradley Farias Reviewed-By: Rich Trott --- doc/api/esm.md | 47 +++++------ lib/internal/modules/esm/get_format.js | 2 +- test/es-module/test-esm-no-extension.js | 27 ------- test/es-module/test-esm-unknown-main.js | 81 ------------------- .../test-esm-unknown-or-no-extension.js | 36 +++++++++ 5 files changed, 57 insertions(+), 136 deletions(-) delete mode 100644 test/es-module/test-esm-no-extension.js delete mode 100644 test/es-module/test-esm-unknown-main.js create mode 100644 test/es-module/test-esm-unknown-or-no-extension.js diff --git a/doc/api/esm.md b/doc/api/esm.md index 0dc403e1b53524..5728b359a4b143 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -36,9 +36,8 @@ 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`. @@ -53,18 +52,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 @@ -84,14 +82,12 @@ node --experimental-modules 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 @@ -109,14 +105,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 --experimental-modules my-app.js`) but also to files referenced by `import` @@ -1610,12 +1605,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 _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_) diff --git a/lib/internal/modules/esm/get_format.js b/lib/internal/modules/esm/get_format.js index 2c215ab5378a40..69ba2398129908 100644 --- a/lib/internal/modules/esm/get_format.js +++ b/lib/internal/modules/esm/get_format.js @@ -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 { diff --git a/test/es-module/test-esm-no-extension.js b/test/es-module/test-esm-no-extension.js deleted file mode 100644 index 81b8e5b4327ad0..00000000000000 --- a/test/es-module/test-esm-no-extension.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -const common = require('../common'); -const fixtures = require('../common/fixtures'); -const { spawn } = require('child_process'); -const assert = require('assert'); - -const entry = fixtures.path('/es-modules/package-type-module/noext-esm'); - -// Run a module that does not have extension. -// This is to ensure that "type": "module" applies to extensionless files. - -const child = spawn(process.execPath, [ - '--experimental-modules', - entry -]); - -let stdout = ''; -child.stdout.setEncoding('utf8'); -child.stdout.on('data', (data) => { - stdout += data; -}); -child.on('close', common.mustCall((code, signal) => { - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - assert.strictEqual(stdout, 'executed\n'); -})); diff --git a/test/es-module/test-esm-unknown-main.js b/test/es-module/test-esm-unknown-main.js deleted file mode 100644 index 12163efb659d81..00000000000000 --- a/test/es-module/test-esm-unknown-main.js +++ /dev/null @@ -1,81 +0,0 @@ -'use strict'; - -const common = require('../common'); -const fixtures = require('../common/fixtures'); -const { spawn } = require('child_process'); -const assert = require('assert'); - -{ - const entry = fixtures.path( - '/es-modules/package-type-module/extension.unknown' - ); - const child = spawn(process.execPath, ['--experimental-modules', 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); - })); -} -{ - const entry = fixtures.path( - '/es-modules/package-type-module/imports-unknownext.mjs' - ); - const child = spawn(process.execPath, ['--experimental-modules', 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); - })); -} -{ - const entry = fixtures.path('/es-modules/package-type-module/noext-esm'); - const child = spawn(process.execPath, ['--experimental-modules', entry]); - let stdout = ''; - child.stdout.setEncoding('utf8'); - child.stdout.on('data', (data) => { - stdout += data; - }); - child.on('close', common.mustCall((code, signal) => { - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - assert.strictEqual(stdout, 'executed\n'); - })); -} -{ - const entry = fixtures.path( - '/es-modules/package-type-module/imports-noext.mjs' - ); - const child = spawn(process.execPath, ['--experimental-modules', entry]); - let stdout = ''; - child.stdout.setEncoding('utf8'); - child.stdout.on('data', (data) => { - stdout += data; - }); - child.on('close', common.mustCall((code, signal) => { - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - assert.strictEqual(stdout, 'executed\n'); - })); -} diff --git a/test/es-module/test-esm-unknown-or-no-extension.js b/test/es-module/test-esm-unknown-or-no-extension.js new file mode 100644 index 00000000000000..c1cafbe8886413 --- /dev/null +++ b/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, ['--experimental-modules', 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); + })); +});