From 887ee9be7ac74eb1e6481231b88ab972f34be454 Mon Sep 17 00:00:00 2001 From: LiviaMedeiros Date: Thu, 7 Sep 2023 20:19:45 +0800 Subject: [PATCH 1/8] module: support ES modules without file extension within `module` scope --- lib/internal/errors.js | 8 +------- lib/internal/modules/esm/get_format.js | 20 ++++---------------- 2 files changed, 5 insertions(+), 23 deletions(-) diff --git a/lib/internal/errors.js b/lib/internal/errors.js index bed02da76a3bab..6ac77686569cbe 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1690,13 +1690,7 @@ E('ERR_UNHANDLED_ERROR', E('ERR_UNKNOWN_BUILTIN_MODULE', 'No such built-in module: %s', Error); E('ERR_UNKNOWN_CREDENTIAL', '%s identifier does not exist: %s', Error); E('ERR_UNKNOWN_ENCODING', 'Unknown encoding: %s', TypeError); -E('ERR_UNKNOWN_FILE_EXTENSION', (ext, path, suggestion) => { - let msg = `Unknown file extension "${ext}" for ${path}`; - if (suggestion) { - msg += `. ${suggestion}`; - } - return msg; -}, TypeError); +E('ERR_UNKNOWN_FILE_EXTENSION', 'Unknown file extension %s for %s', TypeError); E('ERR_UNKNOWN_MODULE_FORMAT', 'Unknown module format: %s for URL %s', RangeError); E('ERR_UNKNOWN_SIGNAL', 'Unknown signal: %s', TypeError); diff --git a/lib/internal/modules/esm/get_format.js b/lib/internal/modules/esm/get_format.js index 4ac9c011d153f4..4ee7725db44f3f 100644 --- a/lib/internal/modules/esm/get_format.js +++ b/lib/internal/modules/esm/get_format.js @@ -7,7 +7,6 @@ const { StringPrototypeCharCodeAt, StringPrototypeSlice, } = primordials; -const { basename, relative } = require('path'); const { getOptionValue } = require('internal/options'); const { extensionFormatMap, @@ -16,7 +15,7 @@ const { const experimentalNetworkImports = getOptionValue('--experimental-network-imports'); -const { getPackageType, getPackageScopeConfig } = require('internal/modules/esm/resolve'); +const { getPackageType } = require('internal/modules/esm/resolve'); const { fileURLToPath } = require('internal/url'); const { ERR_UNKNOWN_FILE_EXTENSION } = require('internal/errors').codes; @@ -74,7 +73,7 @@ function extname(url) { */ function getFileProtocolModuleFormat(url, context, ignoreErrors) { const ext = extname(url); - if (ext === '.js') { + if (ext === '.js' || ext === '') { return getPackageType(url) === 'module' ? 'module' : 'commonjs'; } @@ -83,20 +82,9 @@ function getFileProtocolModuleFormat(url, context, ignoreErrors) { // Explicit undefined return indicates load hook should rerun format check if (ignoreErrors) { return undefined; } + const filepath = fileURLToPath(url); - let suggestion = ''; - if (getPackageType(url) === 'module' && ext === '') { - const config = getPackageScopeConfig(url); - const fileBasename = basename(filepath); - const relativePath = StringPrototypeSlice(relative(config.pjsonPath, filepath), 1); - suggestion = 'Loading extensionless files is not supported inside of ' + - '"type":"module" package.json contexts. The package.json file ' + - `${config.pjsonPath} caused this "type":"module" context. Try ` + - `changing ${filepath} to have a file extension. Note the "bin" ` + - 'field of package.json can point to a file with an extension, for example ' + - `{"type":"module","bin":{"${fileBasename}":"${relativePath}.js"}}`; - } - throw new ERR_UNKNOWN_FILE_EXTENSION(ext, filepath, suggestion); + throw new ERR_UNKNOWN_FILE_EXTENSION(ext, filepath); } /** From fcb8be93f08e423d247f40dfd255412511bc5b6e Mon Sep 17 00:00:00 2001 From: LiviaMedeiros Date: Thu, 7 Sep 2023 20:23:10 +0800 Subject: [PATCH 2/8] module: test ES modules without file extension within `module` scope --- .../test-esm-unknown-or-no-extension.js | 36 --------- test/parallel/test-esm-no-extension.mjs | 22 ++++++ test/parallel/test-esm-unknown-main.mjs | 79 +++++++++++++++++++ 3 files changed, 101 insertions(+), 36 deletions(-) delete mode 100644 test/es-module/test-esm-unknown-or-no-extension.js create mode 100644 test/parallel/test-esm-no-extension.mjs create mode 100644 test/parallel/test-esm-unknown-main.mjs diff --git a/test/es-module/test-esm-unknown-or-no-extension.js b/test/es-module/test-esm-unknown-or-no-extension.js deleted file mode 100644 index 3f0660e5aa9225..00000000000000 --- a/test/es-module/test-esm-unknown-or-no-extension.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict'; - -const { spawnPromisified } = require('../common'); -const fixtures = require('../common/fixtures.js'); -const assert = require('node:assert'); -const { execPath } = require('node:process'); -const { describe, it } = require('node:test'); - - -// 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`. -describe('ESM: extensionless and unknown specifiers', { concurrency: true }, () => { - for ( - const fixturePath of [ - '/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', - ] - ) { - it('should throw', async () => { - const entry = fixtures.path(fixturePath); - const { code, signal, stderr, stdout } = await spawnPromisified(execPath, [entry]); - - assert.strictEqual(code, 1); - assert.strictEqual(signal, null); - assert.strictEqual(stdout, ''); - assert.ok(stderr.includes('ERR_UNKNOWN_FILE_EXTENSION')); - if (fixturePath.includes('noext')) { - // Check for explanation to users - assert.ok(stderr.includes('extensionless')); - } - }); - } -}); diff --git a/test/parallel/test-esm-no-extension.mjs b/test/parallel/test-esm-no-extension.mjs new file mode 100644 index 00000000000000..047e124b3fd4e8 --- /dev/null +++ b/test/parallel/test-esm-no-extension.mjs @@ -0,0 +1,22 @@ +import * as common from '../common/index.mjs'; +import * as fixtures from '../common/fixtures.mjs'; +import { spawn } from 'node:child_process'; +import assert from 'node: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, [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/parallel/test-esm-unknown-main.mjs b/test/parallel/test-esm-unknown-main.mjs new file mode 100644 index 00000000000000..d460393e19dbc0 --- /dev/null +++ b/test/parallel/test-esm-unknown-main.mjs @@ -0,0 +1,79 @@ +import * as common from '../common/index.mjs'; +import * as fixtures from '../common/fixtures.mjs'; +import { spawn } from 'node:child_process'; +import assert from 'node:assert'; + +{ + const entry = fixtures.path( + '/es-modules/package-type-module/extension.unknown' + ); + 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); + })); +} +{ + const entry = fixtures.path( + '/es-modules/package-type-module/imports-unknownext.mjs' + ); + 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); + })); +} +{ + const entry = fixtures.path('/es-modules/package-type-module/noext-esm'); + const child = spawn(process.execPath, [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, [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'); + })); +} From bfaedc470c42f26e9a0644ea11e91625cd565f38 Mon Sep 17 00:00:00 2001 From: LiviaMedeiros Date: Thu, 7 Sep 2023 20:49:31 +0800 Subject: [PATCH 3/8] module: document ES modules without file extension within `module` scope --- doc/api/esm.md | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/doc/api/esm.md b/doc/api/esm.md index 79793aef9cfe24..8b6f9c1e33fb46 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -7,6 +7,9 @@ + +Enable extensionless modules support. + ### `--no-experimental-fetch`