diff --git a/doc/api/cli.md b/doc/api/cli.md index 013256d776bac7..2c4e26dc1e407b 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -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` + + + +> 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` @@ -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 diff --git a/doc/api/packages.md b/doc/api/packages.md index a42e1e041a00b6..9f55cbbb15939f 100644 --- a/doc/api/packages.md +++ b/doc/api/packages.md @@ -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 diff --git a/doc/node.1 b/doc/node.1 index dd10330d4f567a..715cf0fdc4bc2b 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -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. . diff --git a/lib/internal/main/check_syntax.js b/lib/internal/main/check_syntax.js index 50e0b7d6de67c4..9a19c1809fe102 100644 --- a/lib/internal/main/check_syntax.js +++ b/lib/internal/main/check_syntax.js @@ -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'); diff --git a/lib/internal/main/eval_stdin.js b/lib/internal/main/eval_stdin.js index d947af49a6a942..d71751e781b9b5 100644 --- a/lib/internal/main/eval_stdin.js +++ b/lib/internal/main/eval_stdin.js @@ -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); + } }); diff --git a/lib/internal/main/eval_string.js b/lib/internal/main/eval_string.js index dc59a2ce4f7709..908532b0b1865a 100644 --- a/lib/internal/main/eval_string.js +++ b/lib/internal/main/eval_string.js @@ -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 = diff --git a/lib/internal/modules/esm/formats.js b/lib/internal/modules/esm/formats.js index 4ab9aa6f032b7e..b4e8d7a69d306b 100644 --- a/lib/internal/modules/esm/formats.js +++ b/lib/internal/modules/esm/formats.js @@ -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, }; diff --git a/lib/internal/modules/esm/get_format.js b/lib/internal/modules/esm/get_format.js index b3c8a56c06c1cc..59ab89f6f76377 100644 --- a/lib/internal/modules/esm/get_format.js +++ b/lib/internal/modules/esm/get_format.js @@ -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); } diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js index 0d277915b3a01f..5aea5ca5460199 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -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); diff --git a/lib/internal/modules/run_main.js b/lib/internal/modules/run_main.js index ac1ffef0412b17..2e4dabd503a883 100644 --- a/lib/internal/modules/run_main.js +++ b/lib/internal/modules/run_main.js @@ -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'; } /** diff --git a/src/node_options.cc b/src/node_options.cc index 0285f422dfd62c..cf44003538f42f 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -114,12 +114,19 @@ void EnvironmentOptions::CheckOptions(std::vector* 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, diff --git a/src/node_options.h b/src/node_options.h index 2b9f3d3084651a..eed6216deb67b2 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -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; diff --git a/test/common/package.json b/test/common/package.json new file mode 100644 index 00000000000000..5bbefffbabee39 --- /dev/null +++ b/test/common/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/test/es-module/test-esm-type-flag-errors.js b/test/es-module/test-esm-type-field-errors.js similarity index 100% rename from test/es-module/test-esm-type-flag-errors.js rename to test/es-module/test-esm-type-field-errors.js diff --git a/test/es-module/test-esm-type-flag.mjs b/test/es-module/test-esm-type-field.mjs similarity index 100% rename from test/es-module/test-esm-type-flag.mjs rename to test/es-module/test-esm-type-field.mjs diff --git a/test/es-module/test-esm-type-flag-errors.mjs b/test/es-module/test-esm-type-flag-errors.mjs new file mode 100644 index 00000000000000..6d54eff94763ef --- /dev/null +++ b/test/es-module/test-esm-type-flag-errors.mjs @@ -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); + }); + }); diff --git a/test/es-module/test-esm-type-flag-loose-files.mjs b/test/es-module/test-esm-type-flag-loose-files.mjs new file mode 100644 index 00000000000000..ed95e1807f57c7 --- /dev/null +++ b/test/es-module/test-esm-type-flag-loose-files.mjs @@ -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); + }); + }); diff --git a/test/es-module/test-esm-type-flag-package-scopes.mjs b/test/es-module/test-esm-type-flag-package-scopes.mjs new file mode 100644 index 00000000000000..6b16e8a3ae223b --- /dev/null +++ b/test/es-module/test-esm-type-flag-package-scopes.mjs @@ -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); + }); +}); diff --git a/test/es-module/test-esm-type-flag-string-input.mjs b/test/es-module/test-esm-type-flag-string-input.mjs new file mode 100644 index 00000000000000..c4236c00c4f28f --- /dev/null +++ b/test/es-module/test-esm-type-flag-string-input.mjs @@ -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); + }); +}); diff --git a/test/es-module/test-esm-unknown-or-no-extension.js b/test/es-module/test-esm-unknown-or-no-extension.js index 3f0660e5aa9225..83ebcc6267bfc3 100644 --- a/test/es-module/test-esm-unknown-or-no-extension.js +++ b/test/es-module/test-esm-unknown-or-no-extension.js @@ -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/); } }); } diff --git a/test/parallel/test-esm-url-extname.js b/test/es-module/test-esm-url-extname.js similarity index 100% rename from test/parallel/test-esm-url-extname.js rename to test/es-module/test-esm-url-extname.js diff --git a/test/parallel/test-wasm-memory-out-of-bound.js b/test/es-module/test-wasm-memory-out-of-bound.js similarity index 100% rename from test/parallel/test-wasm-memory-out-of-bound.js rename to test/es-module/test-wasm-memory-out-of-bound.js diff --git a/test/parallel/test-wasm-simple.js b/test/es-module/test-wasm-simple.js similarity index 100% rename from test/parallel/test-wasm-simple.js rename to test/es-module/test-wasm-simple.js diff --git a/test/parallel/test-wasm-web-api.js b/test/es-module/test-wasm-web-api.js similarity index 100% rename from test/parallel/test-wasm-web-api.js rename to test/es-module/test-wasm-web-api.js diff --git a/test/fixtures/es-modules/imports-loose.mjs b/test/fixtures/es-modules/imports-loose.mjs new file mode 100644 index 00000000000000..13831e5db03e82 --- /dev/null +++ b/test/fixtures/es-modules/imports-loose.mjs @@ -0,0 +1 @@ +import './loose.js'; diff --git a/test/fixtures/es-modules/imports-noext.mjs b/test/fixtures/es-modules/imports-noext.mjs new file mode 100644 index 00000000000000..96eca54521b9d3 --- /dev/null +++ b/test/fixtures/es-modules/imports-noext.mjs @@ -0,0 +1 @@ +import './noext-esm'; diff --git a/test/fixtures/es-modules/loose.js b/test/fixtures/es-modules/loose.js new file mode 100644 index 00000000000000..69147a3b8ca027 --- /dev/null +++ b/test/fixtures/es-modules/loose.js @@ -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'); diff --git a/test/fixtures/es-modules/noext-esm b/test/fixtures/es-modules/noext-esm new file mode 100644 index 00000000000000..251d6e538a1fcf --- /dev/null +++ b/test/fixtures/es-modules/noext-esm @@ -0,0 +1,2 @@ +export default 'module'; +console.log('executed'); diff --git a/test/fixtures/es-modules/noext-wasm b/test/fixtures/es-modules/noext-wasm new file mode 100644 index 00000000000000..9e035904b2e4d0 Binary files /dev/null and b/test/fixtures/es-modules/noext-wasm differ diff --git a/test/fixtures/es-modules/package-type-module/index.js b/test/fixtures/es-modules/package-type-module/index.js index e8f4db3e164302..86d88056422197 100644 --- a/test/fixtures/es-modules/package-type-module/index.js +++ b/test/fixtures/es-modules/package-type-module/index.js @@ -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; diff --git a/test/fixtures/es-modules/package-type-module/node_modules/dep-with-package-json/dep.js b/test/fixtures/es-modules/package-type-module/node_modules/dep-with-package-json/dep.js new file mode 100644 index 00000000000000..0d702867cd5ccf --- /dev/null +++ b/test/fixtures/es-modules/package-type-module/node_modules/dep-with-package-json/dep.js @@ -0,0 +1,2 @@ +// Controlling package.json has no "type" field -> should still be CommonJS as it is in node_modules +module.exports = 42; diff --git a/test/fixtures/es-modules/package-type-module/node_modules/dep-with-package-json/noext-cjs b/test/fixtures/es-modules/package-type-module/node_modules/dep-with-package-json/noext-cjs new file mode 100644 index 00000000000000..7712b3bad54497 --- /dev/null +++ b/test/fixtures/es-modules/package-type-module/node_modules/dep-with-package-json/noext-cjs @@ -0,0 +1,3 @@ +// No package.json -> should still be CommonJS as it is in node_modules +module.exports = 42; +console.log('executed'); diff --git a/test/fixtures/es-modules/package-type-module/node_modules/dep-with-package-json/package.json b/test/fixtures/es-modules/package-type-module/node_modules/dep-with-package-json/package.json new file mode 100644 index 00000000000000..5ee78b14c414b2 --- /dev/null +++ b/test/fixtures/es-modules/package-type-module/node_modules/dep-with-package-json/package.json @@ -0,0 +1,7 @@ +{ + "name": "dep-with-package-json", + "version": "1.0.0", + "exports": { + "./*": "./*" + } +} diff --git a/test/fixtures/es-modules/package-type-module/node_modules/dep-with-package-json/run.js b/test/fixtures/es-modules/package-type-module/node_modules/dep-with-package-json/run.js new file mode 100644 index 00000000000000..7712b3bad54497 --- /dev/null +++ b/test/fixtures/es-modules/package-type-module/node_modules/dep-with-package-json/run.js @@ -0,0 +1,3 @@ +// No package.json -> should still be CommonJS as it is in node_modules +module.exports = 42; +console.log('executed'); diff --git a/test/fixtures/es-modules/package-type-module/node_modules/dep/dep.js b/test/fixtures/es-modules/package-type-module/node_modules/dep-without-package-json/dep.js similarity index 100% rename from test/fixtures/es-modules/package-type-module/node_modules/dep/dep.js rename to test/fixtures/es-modules/package-type-module/node_modules/dep-without-package-json/dep.js diff --git a/test/fixtures/es-modules/package-type-module/node_modules/dep-without-package-json/noext-cjs b/test/fixtures/es-modules/package-type-module/node_modules/dep-without-package-json/noext-cjs new file mode 100644 index 00000000000000..7712b3bad54497 --- /dev/null +++ b/test/fixtures/es-modules/package-type-module/node_modules/dep-without-package-json/noext-cjs @@ -0,0 +1,3 @@ +// No package.json -> should still be CommonJS as it is in node_modules +module.exports = 42; +console.log('executed'); diff --git a/test/fixtures/es-modules/package-type-module/node_modules/dep-without-package-json/run.js b/test/fixtures/es-modules/package-type-module/node_modules/dep-without-package-json/run.js new file mode 100644 index 00000000000000..7712b3bad54497 --- /dev/null +++ b/test/fixtures/es-modules/package-type-module/node_modules/dep-without-package-json/run.js @@ -0,0 +1,3 @@ +// No package.json -> should still be CommonJS as it is in node_modules +module.exports = 42; +console.log('executed'); diff --git a/test/fixtures/es-modules/package-type-module/noext-wasm b/test/fixtures/es-modules/package-type-module/noext-wasm new file mode 100644 index 00000000000000..9e035904b2e4d0 Binary files /dev/null and b/test/fixtures/es-modules/package-type-module/noext-wasm differ diff --git a/test/fixtures/es-modules/package-type-module/wasm-dep.mjs b/test/fixtures/es-modules/package-type-module/wasm-dep.mjs new file mode 100644 index 00000000000000..a0e28aa17b6bd9 --- /dev/null +++ b/test/fixtures/es-modules/package-type-module/wasm-dep.mjs @@ -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'); diff --git a/test/fixtures/es-modules/package-without-type/module.js b/test/fixtures/es-modules/package-without-type/module.js new file mode 100644 index 00000000000000..69147a3b8ca027 --- /dev/null +++ b/test/fixtures/es-modules/package-without-type/module.js @@ -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'); diff --git a/test/fixtures/es-modules/package-without-type/noext-esm b/test/fixtures/es-modules/package-without-type/noext-esm new file mode 100644 index 00000000000000..69147a3b8ca027 --- /dev/null +++ b/test/fixtures/es-modules/package-without-type/noext-esm @@ -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');