esm: --experimental-default-type flag to flip module defaults

PR-URL: https://github.com/nodejs/node/pull/49869
Reviewed-By: Guy Bedford <guybedford@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
This commit is contained in:
Geoffrey Booth 2023-09-28 23:18:44 -07:00 committed by GitHub
parent 5570c29780
commit 85301803e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 541 additions and 41 deletions

View File

@ -587,6 +587,36 @@ On Windows, using `cmd.exe` a single quote will not work correctly because it
only recognizes double `"` for quoting. In Powershell or Git bash, both `'`
and `"` are usable.
### `--experimental-default-type=type`
<!-- YAML
added:
- REPLACEME
-->
> Stability: 1.0 - Early development
Define which module system, `module` or `commonjs`, to use for the following:
* String input provided via `--eval` or STDIN, if `--input-type` is unspecified.
* Files ending in `.js` or with no extension, if there is no `package.json` file
present in the same folder or any parent folder.
* Files ending in `.js` or with no extension, if the nearest parent
`package.json` field lacks a `"type"` field; unless the `package.json` folder
or any parent folder is inside a `node_modules` folder.
In other words, `--experimental-default-type=module` flips all the places where
Node.js currently defaults to CommonJS to instead default to ECMAScript modules,
with the exception of folders and subfolders below `node_modules`, for backward
compatibility.
Under `--experimental-default-type=module` and `--experimental-wasm-modules`,
files with no extension will be treated as WebAssembly if they begin with the
WebAssembly magic number (`\0asm`); otherwise they will be treated as ES module
JavaScript.
### `--experimental-import-meta-resolve`
<!-- YAML
@ -2243,6 +2273,7 @@ Node.js options that are allowed are:
* `--enable-network-family-autoselection`
* `--enable-source-maps`
* `--experimental-abortcontroller`
* `--experimental-default-type`
* `--experimental-import-meta-resolve`
* `--experimental-json-modules`
* `--experimental-loader`

View File

@ -106,10 +106,11 @@ provides interoperability between them and its original module format,
Node.js has two module systems: [CommonJS][] modules and ECMAScript modules.
Authors can tell Node.js to use the ECMAScript modules loader
via the `.mjs` file extension, the `package.json` [`"type"`][] field, or the
[`--input-type`][] flag. Outside of those cases, Node.js will use the CommonJS
module loader. See [Determining module system][] for more details.
Authors can tell Node.js to use the ECMAScript modules loader via the `.mjs`
file extension, the `package.json` [`"type"`][] field, the [`--input-type`][]
flag, or the [`--experimental-default-type`][] flag. Outside of those cases,
Node.js will use the CommonJS module loader. See [Determining module system][]
for more details.
<!-- Anchors to make sure old links find a target -->
@ -1059,6 +1060,7 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][].
[URL]: https://url.spec.whatwg.org/
[`"exports"`]: packages.md#exports
[`"type"`]: packages.md#type
[`--experimental-default-type`]: cli.md#--experimental-default-typetype
[`--input-type`]: cli.md#--input-typetype
[`data:` URLs]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
[`export`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export

View File

@ -55,6 +55,8 @@ along with a reference for the [`package.json`][] fields defined by Node.js.
## Determining module system
### Introduction
Node.js will treat the following as [ES modules][] when passed to `node` as the
initial input, or when referenced by `import` statements or `import()`
expressions:
@ -67,14 +69,9 @@ expressions:
* Strings passed in as an argument to `--eval`, or piped to `node` via `STDIN`,
with the flag `--input-type=module`.
Node.js will treat as [CommonJS][] all other forms of input, such as `.js` files
where the nearest parent `package.json` file contains no top-level `"type"`
field, or string input without the flag `--input-type`. This behavior is to
preserve backward compatibility. However, now that Node.js supports both
CommonJS and ES modules, it is best to be explicit whenever possible. Node.js
will treat the following as CommonJS when passed to `node` as the initial input,
or when referenced by `import` statements, `import()` expressions, or
`require()` expressions:
Node.js will treat the following as [CommonJS][] when passed to `node` as the
initial input, or when referenced by `import` statements or `import()`
expressions:
* Files with a `.cjs` extension.
@ -84,11 +81,30 @@ or when referenced by `import` statements, `import()` expressions, or
* Strings passed in as an argument to `--eval` or `--print`, or piped to `node`
via `STDIN`, with the flag `--input-type=commonjs`.
Package authors should include the [`"type"`][] field, even in packages where
all sources are CommonJS. Being explicit about the `type` of the package will
future-proof the package in case the default type of Node.js ever changes, and
it will also make things easier for build tools and loaders to determine how the
files in the package should be interpreted.
Aside from these explicit cases, there are other cases where Node.js defaults to
one module system or the other based on the value of the
[`--experimental-default-type`][] flag:
* Files ending in `.js` or with no extension, if there is no `package.json` file
present in the same folder or any parent folder.
* Files ending in `.js` or with no extension, if the nearest parent
`package.json` field lacks a `"type"` field; unless the folder is inside a
`node_modules` folder. (Package scopes under `node_modules` are always treated
as CommonJS when the `package.json` file lacks a `"type"` field, regardless
of `--experimental-default-type`, for backward compatibility.)
* Strings passed in as an argument to `--eval` or piped to `node` via `STDIN`,
when `--input-type` is unspecified.
This flag currently defaults to `"commonjs"`, but it may change in the future to
default to `"module"`. For this reason it is best to be explicit wherever
possible; in particular, package authors should always include the [`"type"`][]
field in their `package.json` files, even in packages where all sources are
CommonJS. Being explicit about the `type` of the package will future-proof the
package in case the default type of Node.js ever changes, and it will also make
things easier for build tools and loaders to determine how the files in the
package should be interpreted.
### Modules loaders
@ -1337,6 +1353,7 @@ This field defines [subpath imports][] for the current package.
[`"packageManager"`]: #packagemanager
[`"type"`]: #type
[`--conditions` / `-C` flag]: #resolving-user-conditions
[`--experimental-default-type`]: cli.md#--experimental-default-typetype
[`--no-addons` flag]: cli.md#--no-addons
[`ERR_PACKAGE_PATH_NOT_EXPORTED`]: errors.md#err_package_path_not_exported
[`esm`]: https://github.com/standard-things/esm#readme

View File

@ -152,6 +152,11 @@ Requires Node.js to be built with
.It Fl -enable-source-maps
Enable Source Map V3 support for stack traces.
.
.It Fl -experimental-default-type Ns = Ns Ar type
Interpret as either ES modules or CommonJS modules input via --eval or STDIN, when --input-type is unspecified;
.js or extensionless files with no sibling or parent package.json;
.js or extensionless files whose nearest parent package.json lacks a "type" field, unless under node_modules.
.
.It Fl -experimental-global-webcrypto
Expose the Web Crypto API on the global scope.
.

View File

@ -60,7 +60,8 @@ function loadESMIfNeeded(cb) {
async function checkSyntax(source, filename) {
let isModule = true;
if (filename === '[stdin]' || filename === '[eval]') {
isModule = getOptionValue('--input-type') === 'module';
isModule = getOptionValue('--input-type') === 'module' ||
(getOptionValue('--experimental-default-type') === 'module' && getOptionValue('--input-type') !== 'commonjs');
} else {
const { defaultResolve } = require('internal/modules/esm/resolve');
const { defaultGetFormat } = require('internal/modules/esm/get_format');

View File

@ -25,12 +25,14 @@ readStdin((code) => {
const print = getOptionValue('--print');
const loadESM = getOptionValue('--import').length > 0;
if (getOptionValue('--input-type') === 'module')
if (getOptionValue('--input-type') === 'module' ||
(getOptionValue('--experimental-default-type') === 'module' && getOptionValue('--input-type') !== 'commonjs')) {
evalModule(code, print);
else
} else {
evalScript('[stdin]',
code,
getOptionValue('--inspect-brk'),
print,
loadESM);
}
});

View File

@ -25,9 +25,10 @@ markBootstrapComplete();
const source = getOptionValue('--eval');
const print = getOptionValue('--print');
const loadESM = getOptionValue('--import').length > 0 || getOptionValue('--experimental-loader').length > 0;
if (getOptionValue('--input-type') === 'module')
if (getOptionValue('--input-type') === 'module' ||
(getOptionValue('--experimental-default-type') === 'module' && getOptionValue('--input-type') !== 'commonjs')) {
evalModule(source, print);
else {
} else {
// For backward compatibility, we want the identifier crypto to be the
// `node:crypto` module rather than WebCrypto.
const isUsingCryptoIdentifier =

View File

@ -2,9 +2,12 @@
const {
RegExpPrototypeExec,
Uint8Array,
} = primordials;
const { getOptionValue } = require('internal/options');
const { closeSync, openSync, readSync } = require('fs');
const experimentalWasmModules = getOptionValue('--experimental-wasm-modules');
const extensionFormatMap = {
@ -35,7 +38,33 @@ function mimeToFormat(mime) {
return null;
}
/**
* For extensionless files in a `module` package scope, or a default `module` scope enabled by the
* `--experimental-default-type` flag, we check the file contents to disambiguate between ES module JavaScript and Wasm.
* We do this by taking advantage of the fact that all Wasm files start with the header `0x00 0x61 0x73 0x6d` (`_asm`).
* @param {URL} url
*/
function getFormatOfExtensionlessFile(url) {
if (!experimentalWasmModules) { return 'module'; }
const magic = new Uint8Array(4);
let fd;
try {
// TODO(@anonrig): Optimize the following by having a single C++ call
fd = openSync(url);
readSync(fd, magic, 0, 4); // Only read the first four bytes
if (magic[0] === 0x00 && magic[1] === 0x61 && magic[2] === 0x73 && magic[3] === 0x6d) {
return 'wasm';
}
} finally {
if (fd !== undefined) { closeSync(fd); }
}
return 'module';
}
module.exports = {
extensionFormatMap,
getFormatOfExtensionlessFile,
mimeToFormat,
};

View File

@ -1,9 +1,11 @@
'use strict';
const {
RegExpPrototypeExec,
ObjectPrototypeHasOwnProperty,
PromisePrototypeThen,
PromiseResolve,
StringPrototypeIncludes,
StringPrototypeCharCodeAt,
StringPrototypeSlice,
} = primordials;
@ -11,11 +13,15 @@ const { basename, relative } = require('path');
const { getOptionValue } = require('internal/options');
const {
extensionFormatMap,
getFormatOfExtensionlessFile,
mimeToFormat,
} = require('internal/modules/esm/formats');
const experimentalNetworkImports =
getOptionValue('--experimental-network-imports');
const defaultTypeFlag = getOptionValue('--experimental-default-type');
// The next line is where we flip the default to ES modules someday.
const defaultType = defaultTypeFlag === 'module' ? 'module' : 'commonjs';
const { getPackageType, getPackageScopeConfig } = require('internal/modules/esm/resolve');
const { fileURLToPath } = require('internal/url');
const { ERR_UNKNOWN_FILE_EXTENSION } = require('internal/errors').codes;
@ -66,6 +72,18 @@ function extname(url) {
return '';
}
/**
* Determine whether the given file URL is under a `node_modules` folder.
* This function assumes that the input has already been verified to be a `file:` URL,
* and is a file rather than a folder.
* @param {URL} url
*/
function underNodeModules(url) {
if (url.protocol !== 'file:') { return false; } // We determine module types for other protocols based on MIME header
return StringPrototypeIncludes(url.pathname, '/node_modules/');
}
/**
* @param {URL} url
* @param {{parentURL: string}} context
@ -74,8 +92,37 @@ function extname(url) {
*/
function getFileProtocolModuleFormat(url, context, ignoreErrors) {
const ext = extname(url);
if (ext === '.js') {
return getPackageType(url) === 'module' ? 'module' : 'commonjs';
const packageType = getPackageType(url);
if (packageType !== 'none') {
return packageType;
}
// The controlling `package.json` file has no `type` field.
if (defaultType === 'module') {
// An exception to the type flag making ESM the default everywhere is that package scopes under `node_modules`
// should retain the assumption that a lack of a `type` field means CommonJS.
return underNodeModules(url) ? 'commonjs' : 'module';
}
return 'commonjs';
}
if (ext === '') {
const packageType = getPackageType(url);
if (defaultType === 'commonjs') { // Legacy behavior
if (packageType === 'none' || packageType === 'commonjs') {
return 'commonjs';
}
// If package type is `module`, fall through to the error case below
} else { // Else defaultType === 'module'
if (underNodeModules(url)) { // Exception for package scopes under `node_modules`
return 'commonjs';
}
if (packageType === 'none' || packageType === 'module') {
return getFormatOfExtensionlessFile(url);
} // Else packageType === 'commonjs'
return 'commonjs';
}
}
const format = extensionFormatMap[ext];
@ -89,12 +136,10 @@ function getFileProtocolModuleFormat(url, context, ignoreErrors) {
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"}}`;
suggestion = 'Loading extensionless files is not supported inside of "type":"module" package.json contexts ' +
`without --experimental-default-type=module. 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);
}

View File

@ -35,7 +35,7 @@ const preserveSymlinks = getOptionValue('--preserve-symlinks');
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
const experimentalNetworkImports =
getOptionValue('--experimental-network-imports');
const typeFlag = getOptionValue('--input-type');
const inputTypeFlag = getOptionValue('--input-type');
const { URL, pathToFileURL, fileURLToPath, isURL } = require('internal/url');
const { getCWDURL } = require('internal/util');
const { canParse: URLCanParse } = internalBinding('url');
@ -1112,7 +1112,7 @@ function defaultResolve(specifier, context = {}) {
// input, to avoid user confusion over how expansive the effect of the
// flag should be (i.e. entry point only, package scope surrounding the
// entry point, etc.).
if (typeFlag) { throw new ERR_INPUT_TYPE_NOT_ALLOWED(); }
if (inputTypeFlag) { throw new ERR_INPUT_TYPE_NOT_ALLOWED(); }
}
conditions = getConditionsSet(conditions);

View File

@ -43,12 +43,15 @@ function shouldUseESMLoader(mainPath) {
*/
const userImports = getOptionValue('--import');
if (userLoaders.length > 0 || userImports.length > 0) { return true; }
const { readPackageScope } = require('internal/modules/cjs/loader');
// Determine the module format of the main
// Determine the module format of the entry point.
if (mainPath && StringPrototypeEndsWith(mainPath, '.mjs')) { return true; }
if (!mainPath || StringPrototypeEndsWith(mainPath, '.cjs')) { return false; }
const { readPackageScope } = require('internal/modules/cjs/loader');
const pkg = readPackageScope(mainPath);
return pkg && pkg.data.type === 'module';
// No need to guard `pkg` as it can only be an object or `false`.
return pkg.data?.type === 'module' || getOptionValue('--experimental-default-type') === 'module';
}
/**

View File

@ -114,12 +114,19 @@ void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors,
errors->push_back("--policy-integrity cannot be empty");
}
if (!module_type.empty()) {
if (module_type != "commonjs" && module_type != "module") {
if (!input_type.empty()) {
if (input_type != "commonjs" && input_type != "module") {
errors->push_back("--input-type must be \"module\" or \"commonjs\"");
}
}
if (!type.empty()) {
if (type != "commonjs" && type != "module") {
errors->push_back("--experimental-default-type must be "
"\"module\" or \"commonjs\"");
}
}
if (syntax_check_only && has_eval_string) {
errors->push_back("either --check or --eval can be used, not both");
}
@ -474,7 +481,7 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
kAllowedInEnvvar);
AddOption("--input-type",
"set module type for string input",
&EnvironmentOptions::module_type,
&EnvironmentOptions::input_type,
kAllowedInEnvvar);
AddOption(
"--experimental-specifier-resolution", "", NoOp{}, kAllowedInEnvvar);
@ -646,6 +653,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"show stack traces on process warnings",
&EnvironmentOptions::trace_warnings,
kAllowedInEnvvar);
AddOption("--experimental-default-type",
"set module system to use by default",
&EnvironmentOptions::type,
kAllowedInEnvvar);
AddOption("--extra-info-on-fatal-exception",
"hide extra information on fatal exception that causes exit",
&EnvironmentOptions::extra_info_on_fatal_exception,

View File

@ -117,7 +117,8 @@ class EnvironmentOptions : public Options {
bool experimental_https_modules = false;
bool experimental_wasm_modules = false;
bool experimental_import_meta_resolve = false;
std::string module_type;
std::string input_type; // Value of --input-type
std::string type; // Value of --experimental-default-type
std::string experimental_policy;
std::string experimental_policy_integrity;
bool has_policy_integrity_string = false;

3
test/common/package.json Normal file
View File

@ -0,0 +1,3 @@
{
"type": "commonjs"
}

View File

@ -0,0 +1,31 @@
import { spawnPromisified } from '../common/index.mjs';
import * as fixtures from '../common/fixtures.mjs';
import { describe, it } from 'node:test';
import { match, strictEqual } from 'node:assert';
describe('--experimental-default-type=module should not affect the interpretation of files with unknown extensions',
{ concurrency: true }, () => {
it('should error on an entry point with an unknown extension', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(process.execPath, [
'--experimental-default-type=module',
fixtures.path('es-modules/package-type-module/extension.unknown'),
]);
match(stderr, /ERR_UNKNOWN_FILE_EXTENSION/);
strictEqual(stdout, '');
strictEqual(code, 1);
strictEqual(signal, null);
});
it('should error on an import with an unknown extension', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(process.execPath, [
'--experimental-default-type=module',
fixtures.path('es-modules/package-type-module/imports-unknownext.mjs'),
]);
match(stderr, /ERR_UNKNOWN_FILE_EXTENSION/);
strictEqual(stdout, '');
strictEqual(code, 1);
strictEqual(signal, null);
});
});

View File

@ -0,0 +1,75 @@
// Flags: --experimental-default-type=module --experimental-wasm-modules
import { spawnPromisified } from '../common/index.mjs';
import * as fixtures from '../common/fixtures.mjs';
import { describe, it } from 'node:test';
import { strictEqual } from 'node:assert';
describe('the type flag should change the interpretation of certain files outside of any package scope',
{ concurrency: true }, () => {
it('should run as ESM a .js file that is outside of any package scope', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(process.execPath, [
'--experimental-default-type=module',
fixtures.path('es-modules/loose.js'),
]);
strictEqual(stderr, '');
strictEqual(stdout, 'executed\n');
strictEqual(code, 0);
strictEqual(signal, null);
});
it('should run as ESM an extensionless JavaScript file that is outside of any package scope', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(process.execPath, [
'--experimental-default-type=module',
fixtures.path('es-modules/noext-esm'),
]);
strictEqual(stderr, '');
strictEqual(stdout, 'executed\n');
strictEqual(code, 0);
strictEqual(signal, null);
});
it('should run as Wasm an extensionless Wasm file that is outside of any package scope', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(process.execPath, [
'--experimental-default-type=module',
'--experimental-wasm-modules',
'--no-warnings',
fixtures.path('es-modules/noext-wasm'),
]);
strictEqual(stderr, '');
strictEqual(stdout, '');
strictEqual(code, 0);
strictEqual(signal, null);
});
it('should import as ESM a .js file that is outside of any package scope', async () => {
const { default: defaultExport } = await import(fixtures.fileURL('es-modules/loose.js'));
strictEqual(defaultExport, 'module');
});
it('should import as ESM an extensionless JavaScript file that is outside of any package scope',
async () => {
const { default: defaultExport } = await import(fixtures.fileURL('es-modules/noext-esm'));
strictEqual(defaultExport, 'module');
});
it('should import as Wasm an extensionless Wasm file that is outside of any package scope', async () => {
const { add } = await import(fixtures.fileURL('es-modules/noext-wasm'));
strictEqual(add(1, 2), 3);
});
it('should check as ESM input passed via --check', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(process.execPath, [
'--experimental-default-type=module',
'--check',
fixtures.path('es-modules/loose.js'),
]);
strictEqual(stderr, '');
strictEqual(stdout, '');
strictEqual(code, 0);
strictEqual(signal, null);
});
});

View File

@ -0,0 +1,150 @@
// Flags: --experimental-default-type=module --experimental-wasm-modules
import { spawnPromisified } from '../common/index.mjs';
import * as fixtures from '../common/fixtures.mjs';
import { describe, it } from 'node:test';
import { strictEqual } from 'node:assert';
describe('the type flag should change the interpretation of certain files within a "type": "module" package scope',
{ concurrency: true }, () => {
it('should run as ESM an extensionless JavaScript file within a "type": "module" scope', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(process.execPath, [
'--experimental-default-type=module',
fixtures.path('es-modules/package-type-module/noext-esm'),
]);
strictEqual(stderr, '');
strictEqual(stdout, 'executed\n');
strictEqual(code, 0);
strictEqual(signal, null);
});
it('should import an extensionless JavaScript file within a "type": "module" scope', async () => {
const { default: defaultExport } =
await import(fixtures.fileURL('es-modules/package-type-module/noext-esm'));
strictEqual(defaultExport, 'module');
});
it('should run as Wasm an extensionless Wasm file within a "type": "module" scope', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(process.execPath, [
'--experimental-default-type=module',
'--experimental-wasm-modules',
'--no-warnings',
fixtures.path('es-modules/package-type-module/noext-wasm'),
]);
strictEqual(stderr, '');
strictEqual(stdout, 'executed\n');
strictEqual(code, 0);
strictEqual(signal, null);
});
it('should import as Wasm an extensionless Wasm file within a "type": "module" scope', async () => {
const { add } = await import(fixtures.fileURL('es-modules/package-type-module/noext-wasm'));
strictEqual(add(1, 2), 3);
});
});
describe(`the type flag should change the interpretation of certain files within a package scope that lacks a
"type" field and is not under node_modules`, { concurrency: true }, () => {
it('should run as ESM a .js file within package scope that has no defined "type" and is not under node_modules',
async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(process.execPath, [
'--experimental-default-type=module',
fixtures.path('es-modules/package-without-type/module.js'),
]);
strictEqual(stderr, '');
strictEqual(stdout, 'executed\n');
strictEqual(code, 0);
strictEqual(signal, null);
});
it(`should run as ESM an extensionless JavaScript file within a package scope that has no defined "type" and is not
under node_modules`, async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(process.execPath, [
'--experimental-default-type=module',
fixtures.path('es-modules/package-without-type/noext-esm'),
]);
strictEqual(stderr, '');
strictEqual(stdout, 'executed\n');
strictEqual(code, 0);
strictEqual(signal, null);
});
it(`should run as Wasm an extensionless Wasm file within a package scope that has no defined "type" and is not under
node_modules`, async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(process.execPath, [
'--experimental-default-type=module',
'--experimental-wasm-modules',
'--no-warnings',
fixtures.path('es-modules/noext-wasm'),
]);
strictEqual(stderr, '');
strictEqual(stdout, '');
strictEqual(code, 0);
strictEqual(signal, null);
});
it('should import as ESM a .js file within package scope that has no defined "type" and is not under node_modules',
async () => {
const { default: defaultExport } = await import(fixtures.fileURL('es-modules/package-without-type/module.js'));
strictEqual(defaultExport, 'module');
});
it(`should import as ESM an extensionless JavaScript file within a package scope that has no defined "type" and is
not under node_modules`, async () => {
const { default: defaultExport } = await import(fixtures.fileURL('es-modules/package-without-type/noext-esm'));
strictEqual(defaultExport, 'module');
});
it(`should import as Wasm an extensionless Wasm file within a package scope that has no defined "type" and is not
under node_modules`, async () => {
const { add } = await import(fixtures.fileURL('es-modules/noext-wasm'));
strictEqual(add(1, 2), 3);
});
});
describe(`the type flag should NOT change the interpretation of certain files within a package scope that lacks a
"type" field and is under node_modules`, { concurrency: true }, () => {
it('should run as CommonJS a .js file within package scope that has no defined "type" and is under node_modules',
async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(process.execPath, [
'--experimental-default-type=module',
fixtures.path('es-modules/package-type-module/node_modules/dep-with-package-json/run.js'),
]);
strictEqual(stderr, '');
strictEqual(stdout, 'executed\n');
strictEqual(code, 0);
strictEqual(signal, null);
});
it(`should import as CommonJS a .js file within a package scope that has no defined "type" and is under
node_modules`, async () => {
const { default: defaultExport } =
await import(fixtures.fileURL('es-modules/package-type-module/node_modules/dep-with-package-json/run.js'));
strictEqual(defaultExport, 42);
});
it(`should run as CommonJS an extensionless JavaScript file within a package scope that has no defined "type" and is
under node_modules`, async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(process.execPath, [
'--experimental-default-type=module',
fixtures.path('es-modules/package-type-module/node_modules/dep-with-package-json/noext-cjs'),
]);
strictEqual(stderr, '');
strictEqual(stdout, 'executed\n');
strictEqual(code, 0);
strictEqual(signal, null);
});
it(`should import as CommonJS an extensionless JavaScript file within a package scope that has no defined "type" and
is under node_modules`, async () => {
const { default: defaultExport } =
await import(fixtures.fileURL('es-modules/package-type-module/node_modules/dep-with-package-json/noext-cjs'));
strictEqual(defaultExport, 42);
});
});

View File

@ -0,0 +1,44 @@
import { spawnPromisified } from '../common/index.mjs';
import { spawn } from 'node:child_process';
import { describe, it } from 'node:test';
import { strictEqual, match } from 'node:assert';
describe('the type flag should change the interpretation of string input', { concurrency: true }, () => {
it('should run as ESM input passed via --eval', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(process.execPath, [
'--experimental-default-type=module',
'--eval',
'import "data:text/javascript,console.log(42)"',
]);
strictEqual(stderr, '');
strictEqual(stdout, '42\n');
strictEqual(code, 0);
strictEqual(signal, null);
});
// ESM is unsupported for --print via --input-type=module
it('should run as ESM input passed via STDIN', async () => {
const child = spawn(process.execPath, [
'--experimental-default-type=module',
]);
child.stdin.end('console.log(typeof import.meta.resolve)');
match((await child.stdout.toArray()).toString(), /^function\r?\n$/);
});
it('should be overridden by --input-type', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(process.execPath, [
'--experimental-default-type=module',
'--input-type=commonjs',
'--eval',
'console.log(require("process").version)',
]);
strictEqual(stderr, '');
strictEqual(stdout, `${process.version}\n`);
strictEqual(code, 0);
strictEqual(signal, null);
});
});

View File

@ -26,10 +26,10 @@ describe('ESM: extensionless and unknown specifiers', { concurrency: true }, ()
assert.strictEqual(code, 1);
assert.strictEqual(signal, null);
assert.strictEqual(stdout, '');
assert.ok(stderr.includes('ERR_UNKNOWN_FILE_EXTENSION'));
assert.match(stderr, /ERR_UNKNOWN_FILE_EXTENSION/);
if (fixturePath.includes('noext')) {
// Check for explanation to users
assert.ok(stderr.includes('extensionless'));
assert.match(stderr, /extensionless/);
}
});
}

View File

@ -0,0 +1 @@
import './loose.js';

View File

@ -0,0 +1 @@
import './noext-esm';

3
test/fixtures/es-modules/loose.js vendored Normal file
View File

@ -0,0 +1,3 @@
// This file can be run or imported only if `--experimental-default-type=module` is set.
export default 'module';
console.log('executed');

2
test/fixtures/es-modules/noext-esm vendored Normal file
View File

@ -0,0 +1,2 @@
export default 'module';
console.log('executed');

BIN
test/fixtures/es-modules/noext-wasm vendored Normal file

Binary file not shown.

View File

@ -1,4 +1,4 @@
import 'dep/dep.js';
import 'dep-without-package-json/dep.js';
const identifier = 'package-type-module';
console.log(identifier);
export default identifier;

View File

@ -0,0 +1,2 @@
// Controlling package.json has no "type" field -> should still be CommonJS as it is in node_modules
module.exports = 42;

View File

@ -0,0 +1,3 @@
// No package.json -> should still be CommonJS as it is in node_modules
module.exports = 42;
console.log('executed');

View File

@ -0,0 +1,7 @@
{
"name": "dep-with-package-json",
"version": "1.0.0",
"exports": {
"./*": "./*"
}
}

View File

@ -0,0 +1,3 @@
// No package.json -> should still be CommonJS as it is in node_modules
module.exports = 42;
console.log('executed');

View File

@ -0,0 +1,3 @@
// No package.json -> should still be CommonJS as it is in node_modules
module.exports = 42;
console.log('executed');

View File

@ -0,0 +1,3 @@
// No package.json -> should still be CommonJS as it is in node_modules
module.exports = 42;
console.log('executed');

Binary file not shown.

View File

@ -0,0 +1,15 @@
import { strictEqual } from 'assert';
export function jsFn () {
state = 'WASM JS Function Executed';
return 42;
}
export let state = 'JS Function Executed';
export function jsInitFn () {
strictEqual(state, 'JS Function Executed');
state = 'WASM Start Executed';
}
console.log('executed');

View File

@ -0,0 +1,3 @@
// This file can be run or imported only if `--experimental-default-type=module` is set.
export default 'module';
console.log('executed');

View File

@ -0,0 +1,3 @@
// This file can be run or imported only if `--experimental-default-type=module` is set.
export default 'module';
console.log('executed');