Skip to content
4 changes: 1 addition & 3 deletions doc/api/packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -393,8 +393,7 @@ and other JavaScript runtimes, using the extensionless style can result in
bloated import map definitions. Explicit file extensions can avoid this issue by
enabling the import map to utilize a [packages folder mapping][] to map multiple
subpaths where possible instead of a separate map entry per package subpath
export. This also mirrors the requirement of using [the full specifier path][]
in relative and absolute import specifiers.
export.

### Exports sugar

Expand Down Expand Up @@ -1352,4 +1351,3 @@ This field defines [subpath imports][] for the current package.
[subpath imports]: #subpath-imports
[supported package managers]: corepack.md#supported-package-managers
[the dual CommonJS/ES module packages section]: #dual-commonjses-module-packages
[the full specifier path]: esm.md#mandatory-file-extensions
4 changes: 3 additions & 1 deletion lib/internal/modules/esm/get_format.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const {

const experimentalNetworkImports =
getOptionValue('--experimental-network-imports');
const experimentalExtensionlessModules =
getOptionValue('--experimental-extensionless-modules');
const { getPackageType, getPackageScopeConfig } = require('internal/modules/esm/resolve');
const { fileURLToPath } = require('internal/url');
const { ERR_UNKNOWN_FILE_EXTENSION } = require('internal/errors').codes;
Expand Down Expand Up @@ -74,7 +76,7 @@ function extname(url) {
*/
function getFileProtocolModuleFormat(url, context, ignoreErrors) {
const ext = extname(url);
if (ext === '.js') {
if (ext === '.js' || (experimentalExtensionlessModules && ext === '')) {
return getPackageType(url) === 'module' ? 'module' : 'commonjs';
}

Expand Down
23 changes: 23 additions & 0 deletions test/parallel/test-esm-no-extension.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Flags: --experimental-extensionless-modules
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');
}));
80 changes: 80 additions & 0 deletions test/parallel/test-esm-unknown-main.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Flags: --experimental-extensionless-modules
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');
}));
}