From 366db481e47ac461f36b2450b8f2bb3537d9aa5e Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Tue, 12 Mar 2024 01:50:24 +0800 Subject: [PATCH] module: support require()ing synchronous ESM graphs This patch adds `require()` support for synchronous ESM graphs under the flag `--experimental-require-module` This is based on the the following design aspect of ESM: - The resolution can be synchronous (up to the host) - The evaluation of a synchronous graph (without top-level await) is also synchronous, and, by the time the module graph is instantiated (before evaluation starts), this is is already known. If `--experimental-require-module` is enabled, and the ECMAScript module being loaded by `require()` meets the following requirements: - Explicitly marked as an ES module with a `"type": "module"` field in the closest package.json or a `.mjs` extension. - Fully synchronous (contains no top-level `await`). `require()` will load the requested module as an ES Module, and return the module name space object. In this case it is similar to dynamic `import()` but is run synchronously and returns the name space object directly. ```mjs // point.mjs export function distance(a, b) { return (b.x - a.x) ** 2 + (b.y - a.y) ** 2; } class Point { constructor(x, y) { this.x = x; this.y = y; } } export default Point; ``` ```cjs const required = require('./point.mjs'); // [Module: null prototype] { // default: [class Point], // distance: [Function: distance] // } console.log(required); (async () => { const imported = await import('./point.mjs'); console.log(imported === required); // true })(); ``` If the module being `require()`'d contains top-level `await`, or the module graph it `import`s contains top-level `await`, [`ERR_REQUIRE_ASYNC_MODULE`][] will be thrown. In this case, users should load the asynchronous module using `import()`. If `--experimental-print-required-tla` is enabled, instead of throwing `ERR_REQUIRE_ASYNC_MODULE` before evaluation, Node.js will evaluate the module, try to locate the top-level awaits, and print their location to help users fix them. PR-URL: https://github.com/nodejs/node/pull/51977 Reviewed-By: Chengzhong Wu Reviewed-By: Matteo Collina Reviewed-By: Guy Bedford Reviewed-By: Antoine du Hamel Reviewed-By: Geoffrey Booth --- doc/api/cli.md | 27 +++ doc/api/errors.md | 16 ++ doc/api/esm.md | 8 +- doc/api/modules.md | 77 ++++++- doc/api/packages.md | 19 +- lib/internal/modules/cjs/loader.js | 114 +++++++++-- lib/internal/modules/esm/loader.js | 136 ++++++++++++- lib/internal/modules/esm/module_job.js | 67 +++++-- lib/internal/modules/esm/module_map.js | 4 +- lib/internal/modules/esm/translators.js | 32 ++- lib/internal/util/embedding.js | 2 +- src/module_wrap.cc | 189 ++++++++++++++++++ src/module_wrap.h | 8 + src/node_errors.h | 5 + src/node_options.cc | 11 + src/node_options.h | 2 + .../test-esm-cjs-load-error-note.mjs | 10 +- test/es-module/test-esm-loader-modulemap.js | 2 +- .../test-require-module-cached-tla.js | 14 ++ ...test-require-module-conditional-exports.js | 35 ++++ .../test-require-module-default-extension.js | 17 ++ .../test-require-module-dynamic-import-1.js | 32 +++ .../test-require-module-dynamic-import-2.js | 32 +++ test/es-module/test-require-module-errors.js | 48 +++++ .../es-module/test-require-module-implicit.js | 33 +++ test/es-module/test-require-module-preload.js | 72 +++++++ .../test-require-module-special-import.js | 11 + test/es-module/test-require-module-tla.js | 63 ++++++ test/es-module/test-require-module-twice.js | 21 ++ test/es-module/test-require-module.js | 62 ++++++ test/fixtures/es-modules/data-import.mjs | 2 + .../deprecated-folders-ignore/package.json | 1 - .../fixtures/es-modules/exports-both/load.cjs | 1 + .../exports-both/node_modules/dep/mod.cjs | 1 + .../exports-both/node_modules/dep/mod.mjs | 2 + .../node_modules/dep/package.json | 8 + .../exports-import-default/load.cjs | 1 + .../node_modules/dep/mod.js | 1 + .../node_modules/dep/mod.mjs | 2 + .../node_modules/dep/package.json | 8 + .../es-modules/exports-import-only/load.cjs | 2 + .../node_modules/dep/mod.js | 2 + .../node_modules/dep/package.json | 8 + .../es-modules/exports-require-only/load.cjs | 1 + .../node_modules/dep/mod.js | 1 + .../node_modules/dep/package.json | 7 + test/fixtures/es-modules/import-esm.mjs | 3 + test/fixtures/es-modules/imported-esm.mjs | 1 + test/fixtures/es-modules/network-import.mjs | 1 + .../package-default-extension/index.cjs | 1 + .../package-default-extension/index.mjs | 1 + test/fixtures/es-modules/reference-error.mjs | 3 + test/fixtures/es-modules/require-cjs.mjs | 5 + .../es-modules/require-reference-error.cjs | 2 + .../es-modules/require-syntax-error.cjs | 2 + .../es-modules/require-throw-error.cjs | 2 + test/fixtures/es-modules/required-cjs.js | 3 + .../es-modules/should-not-be-resolved.mjs | 1 + test/fixtures/es-modules/syntax-error.mjs | 1 + test/fixtures/es-modules/throw-error.mjs | 1 + test/fixtures/es-modules/tla/execution.mjs | 3 + .../es-modules/tla/require-execution.js | 1 + test/fixtures/es-modules/tla/resolved.mjs | 1 + 63 files changed, 1170 insertions(+), 79 deletions(-) create mode 100644 test/es-module/test-require-module-cached-tla.js create mode 100644 test/es-module/test-require-module-conditional-exports.js create mode 100644 test/es-module/test-require-module-default-extension.js create mode 100644 test/es-module/test-require-module-dynamic-import-1.js create mode 100644 test/es-module/test-require-module-dynamic-import-2.js create mode 100644 test/es-module/test-require-module-errors.js create mode 100644 test/es-module/test-require-module-implicit.js create mode 100644 test/es-module/test-require-module-preload.js create mode 100644 test/es-module/test-require-module-special-import.js create mode 100644 test/es-module/test-require-module-tla.js create mode 100644 test/es-module/test-require-module-twice.js create mode 100644 test/es-module/test-require-module.js create mode 100644 test/fixtures/es-modules/data-import.mjs create mode 100644 test/fixtures/es-modules/exports-both/load.cjs create mode 100644 test/fixtures/es-modules/exports-both/node_modules/dep/mod.cjs create mode 100644 test/fixtures/es-modules/exports-both/node_modules/dep/mod.mjs create mode 100644 test/fixtures/es-modules/exports-both/node_modules/dep/package.json create mode 100644 test/fixtures/es-modules/exports-import-default/load.cjs create mode 100644 test/fixtures/es-modules/exports-import-default/node_modules/dep/mod.js create mode 100644 test/fixtures/es-modules/exports-import-default/node_modules/dep/mod.mjs create mode 100644 test/fixtures/es-modules/exports-import-default/node_modules/dep/package.json create mode 100644 test/fixtures/es-modules/exports-import-only/load.cjs create mode 100644 test/fixtures/es-modules/exports-import-only/node_modules/dep/mod.js create mode 100644 test/fixtures/es-modules/exports-import-only/node_modules/dep/package.json create mode 100644 test/fixtures/es-modules/exports-require-only/load.cjs create mode 100644 test/fixtures/es-modules/exports-require-only/node_modules/dep/mod.js create mode 100644 test/fixtures/es-modules/exports-require-only/node_modules/dep/package.json create mode 100644 test/fixtures/es-modules/import-esm.mjs create mode 100644 test/fixtures/es-modules/imported-esm.mjs create mode 100644 test/fixtures/es-modules/network-import.mjs create mode 100644 test/fixtures/es-modules/package-default-extension/index.cjs create mode 100644 test/fixtures/es-modules/package-default-extension/index.mjs create mode 100644 test/fixtures/es-modules/reference-error.mjs create mode 100644 test/fixtures/es-modules/require-cjs.mjs create mode 100644 test/fixtures/es-modules/require-reference-error.cjs create mode 100644 test/fixtures/es-modules/require-syntax-error.cjs create mode 100644 test/fixtures/es-modules/require-throw-error.cjs create mode 100644 test/fixtures/es-modules/required-cjs.js create mode 100644 test/fixtures/es-modules/should-not-be-resolved.mjs create mode 100644 test/fixtures/es-modules/syntax-error.mjs create mode 100644 test/fixtures/es-modules/throw-error.mjs create mode 100644 test/fixtures/es-modules/tla/execution.mjs create mode 100644 test/fixtures/es-modules/tla/require-execution.js create mode 100644 test/fixtures/es-modules/tla/resolved.mjs diff --git a/doc/api/cli.md b/doc/api/cli.md index de3e00da7c7825..445cba786c347f 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -974,6 +974,18 @@ added: v11.8.0 Use the specified file as a security policy. +### `--experimental-require-module` + + + +> Stability: 1.1 - Active Developement + +Supports loading a synchronous ES module graph in `require()`. + +See [Loading ECMAScript modules using `require()`][]. + ### `--experimental-sea-config` + +This flag is only useful when `--experimental-require-module` is enabled. + +If the ES module being `require()`'d contains top-level await, this flag +allows Node.js to evaluate the module, try to locate the +top-level awaits, and print their location to help users find them. + ### `--prof` @@ -207,12 +251,24 @@ require(X) from module at path Y LOAD_AS_FILE(X) 1. If X is a file, load X as its file extension format. STOP -2. If X.js is a file, load X.js as JavaScript text. STOP -3. If X.json is a file, parse X.json to a JavaScript Object. STOP +2. If X.js is a file, + a. Find the closest package scope SCOPE to X. + b. If no scope was found, load X.js as a CommonJS module. STOP. + c. If the SCOPE/package.json contains "type" field, + 1. If the "type" field is "module", load X.js as an ECMAScript module. STOP. + 2. Else, load X.js as an CommonJS module. STOP. +3. If X.json is a file, load X.json to a JavaScript Object. STOP 4. If X.node is a file, load X.node as binary addon. STOP +5. If X.mjs is a file, and `--experimental-require-module` is enabled, + load X.mjs as an ECMAScript module. STOP LOAD_INDEX(X) -1. If X/index.js is a file, load X/index.js as JavaScript text. STOP +1. If X/index.js is a file + a. Find the closest package scope SCOPE to X. + b. If no scope was found, load X/index.js as a CommonJS module. STOP. + c. If the SCOPE/package.json contains "type" field, + 1. If the "type" field is "module", load X/index.js as an ECMAScript module. STOP. + 2. Else, load X/index.js as an CommonJS module. STOP. 2. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP 3. If X/index.node is a file, load X/index.node as binary addon. STOP @@ -1097,6 +1153,7 @@ This section was moved to [GLOBAL_FOLDERS]: #loading-from-the-global-folders [`"main"`]: packages.md#main [`"type"`]: packages.md#type +[`ERR_REQUIRE_ASYNC_MODULE`]: errors.md#err_require_async_module [`ERR_REQUIRE_ESM`]: errors.md#err_require_esm [`ERR_UNSUPPORTED_DIR_IMPORT`]: errors.md#err_unsupported_dir_import [`MODULE_NOT_FOUND`]: errors.md#module_not_found diff --git a/doc/api/packages.md b/doc/api/packages.md index 8a5efdc89c4853..4e3414c66f7f7a 100644 --- a/doc/api/packages.md +++ b/doc/api/packages.md @@ -133,14 +133,15 @@ There is the CommonJS module loader: `process.dlopen()`. * It treats all files that lack `.json` or `.node` extensions as JavaScript text files. -* It cannot be used to load ECMAScript modules (although it is possible to - [load ECMASCript modules from CommonJS modules][]). When used to load a - JavaScript text file that is not an ECMAScript module, it loads it as a - CommonJS module. +* It can only be used to [load ECMASCript modules from CommonJS modules][] if + the module graph is synchronous (that contains no top-level `await`) when + `--experimental-require-module` is enabled. + When used to load a JavaScript text file that is not an ECMAScript module, + the file will be loaded as a CommonJS module. There is the ECMAScript module loader: -* It is asynchronous. +* It is asynchronous, unless it's being used to load modules for `require()`. * It is responsible for handling `import` statements and `import()` expressions. * It is not monkey patchable, can be customized using [loader hooks][]. * It does not support folders as modules, directory indexes (e.g. @@ -623,9 +624,9 @@ specific to least specific as conditions should be defined: * `"require"` - matches when the package is loaded via `require()`. The referenced file should be loadable with `require()` although the condition matches regardless of the module format of the target file. Expected - formats include CommonJS, JSON, and native addons but not ES modules as - `require()` doesn't support them. _Always mutually exclusive with - `"import"`._ + formats include CommonJS, JSON, native addons, and ES modules + if `--experimental-require-module` is enabled. _Always mutually + exclusive with `"import"`._ * `"default"` - the generic fallback that always matches. Can be a CommonJS or ES module file. _This condition should always come last._ @@ -1371,7 +1372,7 @@ This field defines [subpath imports][] for the current package. [entry points]: #package-entry-points [folders as modules]: modules.md#folders-as-modules [import maps]: https://github.com/WICG/import-maps -[load ECMASCript modules from CommonJS modules]: modules.md#the-mjs-extension +[load ECMASCript modules from CommonJS modules]: modules.md#loading-ecmascript-modules-using-require [loader hooks]: esm.md#loaders [packages folder mapping]: https://github.com/WICG/import-maps#packages-via-trailing-slashes [self-reference]: #self-referencing-a-package-using-its-name diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 7bbd59e16330b5..34bec05ff72455 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -60,10 +60,11 @@ const { StringPrototypeSlice, StringPrototypeSplit, StringPrototypeStartsWith, + Symbol, } = primordials; -// Map used to store CJS parsing data. -const cjsParseCache = new SafeWeakMap(); +// Map used to store CJS parsing data or for ESM loading. +const cjsSourceCache = new SafeWeakMap(); /** * Map of already-loaded CJS modules to use. */ @@ -72,12 +73,14 @@ const cjsExportsCache = new SafeWeakMap(); // Set first due to cycle with ESM loader functions. module.exports = { cjsExportsCache, - cjsParseCache, + cjsSourceCache, initializeCJS, Module, wrapSafe, }; +const kIsMainSymbol = Symbol('kIsMainSymbol'); + const { BuiltinModule } = require('internal/bootstrap/realm'); const { maybeCacheSourceMap, @@ -395,6 +398,11 @@ function initializeCJS() { // TODO(joyeecheung): deprecate this in favor of a proper hook? Module.runMain = require('internal/modules/run_main').executeUserEntryPoint; + + if (getOptionValue('--experimental-require-module')) { + emitExperimentalWarning('Support for loading ES Module in require()'); + Module._extensions['.mjs'] = loadESMFromCJS; + } } // Given a module name, and a list of paths to test, returns the first @@ -601,6 +609,19 @@ function resolveExports(nmPath, request) { } } +// We don't cache this in case user extends the extensions. +function getDefaultExtensions() { + const extensions = ObjectKeys(Module._extensions); + if (!getOptionValue('--experimental-require-module')) { + return extensions; + } + // If the .mjs extension is added by --experimental-require-module, + // remove it from the supported default extensions to maintain + // compatibility. + // TODO(joyeecheung): allow both .mjs and .cjs? + return ArrayPrototypeFilter(extensions, (ext) => ext !== '.mjs' || Module._extensions['.mjs'] !== loadESMFromCJS); +} + /** * Get the absolute path to a module. * @param {string} request Relative or absolute file path @@ -702,7 +723,7 @@ Module._findPath = function(request, paths, isMain) { if (!filename) { // Try it with each of the extensions if (exts === undefined) { - exts = ObjectKeys(Module._extensions); + exts = getDefaultExtensions(); } filename = tryExtensions(basePath, exts, isMain); } @@ -711,7 +732,7 @@ Module._findPath = function(request, paths, isMain) { if (!filename && rc === 1) { // Directory. // try it with each of the extensions at "index" if (exts === undefined) { - exts = ObjectKeys(Module._extensions); + exts = getDefaultExtensions(); } filename = tryPackage(basePath, exts, isMain, request); } @@ -988,7 +1009,7 @@ Module._load = function(request, parent, isMain) { if (cachedModule !== undefined) { updateChildren(parent, cachedModule, true); if (!cachedModule.loaded) { - const parseCachedModule = cjsParseCache.get(cachedModule); + const parseCachedModule = cjsSourceCache.get(cachedModule); if (!parseCachedModule || parseCachedModule.loaded) { return getExportsForCircularRequire(cachedModule); } @@ -1010,6 +1031,9 @@ Module._load = function(request, parent, isMain) { setOwnProperty(process, 'mainModule', module); setOwnProperty(module.require, 'main', process.mainModule); module.id = '.'; + module[kIsMainSymbol] = true; + } else { + module[kIsMainSymbol] = false; } reportModuleToWatchMode(filename); @@ -1245,6 +1269,20 @@ let resolvedArgv; let hasPausedEntry = false; /** @type {import('vm').Script} */ +/** + * Resolve and evaluate it synchronously as ESM if it's ESM. + * @param {Module} mod CJS module instance + * @param {string} filename Absolute path of the file. + */ +function loadESMFromCJS(mod, filename) { + const source = getMaybeCachedSource(mod, filename); + const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader(); + const isMain = mod[kIsMainSymbol]; + // TODO(joyeecheung): we may want to invent optional special handling for default exports here. + // For now, it's good enough to be identical to what `import()` returns. + mod.exports = cascadedLoader.importSyncForRequire(filename, source, isMain); +} + /** * Wraps the given content in a script and runs it in a new context. * @param {string} filename The name of the file being loaded @@ -1270,11 +1308,16 @@ function wrapSafe(filename, content, cjsModuleInstance, codeCache) { ); // Cache the source map for the module if present. - if (script.sourceMapURL) { - maybeCacheSourceMap(filename, content, this, false, undefined, script.sourceMapURL); + const { sourceMapURL } = script; + if (sourceMapURL) { + maybeCacheSourceMap(filename, content, this, false, undefined, sourceMapURL); } - return runScriptInThisContext(script, true, false); + return { + __proto__: null, + function: runScriptInThisContext(script, true, false), + sourceMapURL, + }; } try { @@ -1292,7 +1335,7 @@ function wrapSafe(filename, content, cjsModuleInstance, codeCache) { maybeCacheSourceMap(filename, content, this, false, undefined, result.sourceMapURL); } - return result.function; + return result; } catch (err) { if (process.mainModule === cjsModuleInstance) { const { enrichCJSError } = require('internal/modules/esm/translators'); @@ -1307,8 +1350,9 @@ function wrapSafe(filename, content, cjsModuleInstance, codeCache) { * `exports`) to the file. Returns exception, if any. * @param {string} content The source code of the module * @param {string} filename The file path of the module + * @param {boolean} loadAsESM Whether it's known to be ESM via .mjs or "type" in package.json. */ -Module.prototype._compile = function(content, filename) { +Module.prototype._compile = function(content, filename, loadAsESM = false) { let moduleURL; let redirects; const manifest = policy()?.manifest; @@ -1318,8 +1362,20 @@ Module.prototype._compile = function(content, filename) { manifest.assertIntegrity(moduleURL, content); } - const compiledWrapper = wrapSafe(filename, content, this); + // TODO(joyeecheung): when the module is the entry point, consider allowing TLA. + // Only modules being require()'d really need to avoid TLA. + if (loadAsESM) { + // Pass the source into the .mjs extension handler indirectly through the cache. + cjsSourceCache.set(this, { source: content }); + loadESMFromCJS(this, filename); + return; + } + const { function: compiledWrapper } = wrapSafe(filename, content, this); + + // TODO(joyeecheung): the detection below is unnecessarily complex. Using the + // kIsMainSymbol, or a kBreakOnStartSymbol that gets passed from + // higher level instead of doing hacky detection here. let inspectorWrapper = null; if (getOptionValue('--inspect-brk') && process._eval == null) { if (!resolvedArgv) { @@ -1363,24 +1419,43 @@ Module.prototype._compile = function(content, filename) { }; /** - * Native handler for `.js` files. - * @param {Module} module The module to compile - * @param {string} filename The file path of the module + * Get the source code of a module, using cached ones if it's cached. + * @param {Module} mod Module instance whose source is potentially already cached. + * @param {string} filename Absolute path to the file of the module. + * @returns {string} */ -Module._extensions['.js'] = function(module, filename) { - // If already analyzed the source, then it will be cached. - const cached = cjsParseCache.get(module); +function getMaybeCachedSource(mod, filename) { + const cached = cjsSourceCache.get(mod); let content; if (cached?.source) { content = cached.source; cached.source = undefined; } else { + // TODO(joyeecheung): we can read a buffer instead to speed up + // compilation. content = fs.readFileSync(filename, 'utf8'); } + return content; +} + +/** + * Built-in handler for `.js` files. + * @param {Module} module The module to compile + * @param {string} filename The file path of the module + */ +Module._extensions['.js'] = function(module, filename) { + // If already analyzed the source, then it will be cached. + const content = getMaybeCachedSource(module, filename); + if (StringPrototypeEndsWith(filename, '.js')) { const pkg = packageJsonReader.readPackageScope(filename) || { __proto__: null }; // Function require shouldn't be used in ES modules. if (pkg.data?.type === 'module') { + if (getOptionValue('--experimental-require-module')) { + module._compile(content, filename, true); + return; + } + // This is an error path because `require` of a `.js` file in a `"type": "module"` scope is not allowed. const parent = moduleParentCache.get(module); const parentPath = parent?.filename; @@ -1413,7 +1488,8 @@ Module._extensions['.js'] = function(module, filename) { throw err; } } - module._compile(content, filename); + + module._compile(content, filename, false); }; /** diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index 7ab5078ffc9307..bdcd6028dacb39 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -15,16 +15,25 @@ const { hardenRegExp, } = primordials; +const assert = require('internal/assert'); const { ERR_REQUIRE_ESM, + ERR_NETWORK_IMPORT_DISALLOWED, ERR_UNKNOWN_MODULE_FORMAT, } = require('internal/errors').codes; const { getOptionValue } = require('internal/options'); -const { isURL } = require('internal/url'); -const { emitExperimentalWarning } = require('internal/util'); +const { isURL, pathToFileURL, URL } = require('internal/url'); +const { emitExperimentalWarning, kEmptyObject } = require('internal/util'); const { + registerModule, getDefaultConditions, } = require('internal/modules/esm/utils'); +const { kImplicitAssertType } = require('internal/modules/esm/assert'); +const { + maybeCacheSourceMap, +} = require('internal/source_map/source_map_cache'); +const { canParse } = internalBinding('url'); +const { ModuleWrap } = internalBinding('module_wrap'); let defaultResolve, defaultLoad, defaultLoadSync, importMetaInitializer; /** @@ -184,8 +193,6 @@ class ModuleLoader { async eval(source, url) { const evalInstance = (url) => { - const { ModuleWrap } = internalBinding('module_wrap'); - const { registerModule } = require('internal/modules/esm/utils'); const module = new ModuleWrap(url, undefined, source, 0, 0); registerModule(module, { __proto__: null, @@ -197,7 +204,7 @@ class ModuleLoader { return module; }; - const ModuleJob = require('internal/modules/esm/module_job'); + const { ModuleJob } = require('internal/modules/esm/module_job'); const job = new ModuleJob( this, url, undefined, evalInstance, false, false); this.loadCache.set(url, undefined, job); @@ -250,6 +257,118 @@ class ModuleLoader { return job; } + /** + * This constructs (creates, instantiates and evaluates) a module graph that + * is require()'d. + * @param {string} filename Resolved filename of the module being require()'d + * @param {string} source Source code. TODO(joyeecheung): pass the raw buffer. + * @param {string} isMain Whether this module is a main module. + * @returns {ModuleNamespaceObject} + */ + importSyncForRequire(filename, source, isMain) { + const url = pathToFileURL(filename).href; + let job = this.loadCache.get(url, kImplicitAssertType); + // This module is already loaded, check whether it's synchronous and return the + // namespace. + if (job !== undefined) { + return job.module.getNamespaceSync(); + } + + // TODO(joyeecheung): refactor this so that we pre-parse in C++ and hit the + // cache here, or use a carrier object to carry the compiled module script + // into the constructor to ensure cache hit. + const wrap = new ModuleWrap(url, undefined, source, 0, 0); + // Cache the source map for the module if present. + if (wrap.sourceMapURL) { + maybeCacheSourceMap(url, source, null, false, undefined, wrap.sourceMapURL); + } + const { registerModule } = require('internal/modules/esm/utils'); + // TODO(joyeecheung): refactor so that the default options are shared across + // the built-in loaders. + registerModule(wrap, { + __proto__: null, + initializeImportMeta: (meta, wrap) => this.importMetaInitialize(meta, { url }), + importModuleDynamically: (specifier, wrap, importAttributes) => { + return this.import(specifier, url, importAttributes); + }, + }); + + const inspectBrk = (isMain && getOptionValue('--inspect-brk')); + + const { ModuleJobSync } = require('internal/modules/esm/module_job'); + job = new ModuleJobSync(this, url, kEmptyObject, wrap, isMain, inspectBrk); + this.loadCache.set(url, kImplicitAssertType, job); + return job.runSync().namespace; + } + + /** + * Resolve individual module requests and create or get the cached ModuleWraps for + * each of them. This is only used to create a module graph being require()'d. + * @param {string} specifier Specifier of the the imported module. + * @param {string} parentURL Where the import comes from. + * @param {object} importAttributes import attributes from the import statement. + * @returns {ModuleWrap} + */ + getModuleWrapForRequire(specifier, parentURL, importAttributes) { + assert(getOptionValue('--experimental-require-module')); + + if (canParse(specifier)) { + const protocol = new URL(specifier).protocol; + if (protocol === 'https:' || protocol === 'http:') { + throw new ERR_NETWORK_IMPORT_DISALLOWED(specifier, parentURL, + 'ES modules cannot be loaded by require() from the network'); + } + assert(protocol === 'file:' || protocol === 'node:' || protocol === 'data:'); + } + + const requestKey = this.#resolveCache.serializeKey(specifier, importAttributes); + let resolveResult = this.#resolveCache.get(requestKey, parentURL); + if (resolveResult == null) { + resolveResult = this.defaultResolve(specifier, parentURL, importAttributes); + this.#resolveCache.set(requestKey, parentURL, resolveResult); + } + + const { url, format } = resolveResult; + const resolvedImportAttributes = resolveResult.importAttributes ?? importAttributes; + let job = this.loadCache.get(url, resolvedImportAttributes.type); + if (job !== undefined) { + // This module is previously imported before. We will return the module now and check + // asynchronicity of the entire graph later, after the graph is instantiated. + return job.module; + } + + defaultLoadSync ??= require('internal/modules/esm/load').defaultLoadSync; + const loadResult = defaultLoadSync(url, { format, importAttributes }); + const { responseURL, source } = loadResult; + let { format: finalFormat } = loadResult; + this.validateLoadResult(url, finalFormat); + if (finalFormat === 'commonjs') { + finalFormat = 'commonjs-sync'; + } else if (finalFormat === 'wasm') { + assert.fail('WASM is currently unsupported by require(esm)'); + } + + const translator = getTranslators().get(finalFormat); + if (!translator) { + throw new ERR_UNKNOWN_MODULE_FORMAT(finalFormat, responseURL); + } + + const isMain = (parentURL === undefined); + const wrap = FunctionPrototypeCall(translator, this, responseURL, source, isMain); + assert(wrap instanceof ModuleWrap); // No asynchronous translators should be called. + + if (process.env.WATCH_REPORT_DEPENDENCIES && process.send) { + process.send({ 'watch:import': [url] }); + } + + const inspectBrk = (isMain && getOptionValue('--inspect-brk')); + const { ModuleJobSync } = require('internal/modules/esm/module_job'); + job = new ModuleJobSync(this, url, importAttributes, wrap, isMain, inspectBrk); + + this.loadCache.set(url, importAttributes.type, job); + return job.module; + } + /** * Create and cache an object representing a loaded module. * @param {string} url The absolute URL that was resolved for this module @@ -277,8 +396,9 @@ class ModuleLoader { (url, isMain) => callTranslator(this.loadSync(url, context), isMain) : async (url, isMain) => callTranslator(await this.load(url, context), isMain); + const isMain = parentURL === undefined; const inspectBrk = ( - parentURL === undefined && + isMain && getOptionValue('--inspect-brk') ); @@ -286,13 +406,13 @@ class ModuleLoader { process.send({ 'watch:import': [url] }); } - const ModuleJob = require('internal/modules/esm/module_job'); + const { ModuleJob } = require('internal/modules/esm/module_job'); const job = new ModuleJob( this, url, importAttributes, moduleProvider, - parentURL === undefined, + isMain, inspectBrk, sync, ); diff --git a/lib/internal/modules/esm/module_job.js b/lib/internal/modules/esm/module_job.js index ba62fd4361a9a6..a9e0f72579b6f8 100644 --- a/lib/internal/modules/esm/module_job.js +++ b/lib/internal/modules/esm/module_job.js @@ -8,9 +8,9 @@ const { ObjectSetPrototypeOf, PromiseResolve, PromisePrototypeThen, - ReflectApply, RegExpPrototypeExec, RegExpPrototypeSymbolReplace, + ReflectApply, SafePromiseAllReturnArrayLike, SafePromiseAllReturnVoid, SafeSet, @@ -19,7 +19,7 @@ const { StringPrototypeStartsWith, } = primordials; -const { ModuleWrap } = internalBinding('module_wrap'); +const { ModuleWrap, kEvaluated } = internalBinding('module_wrap'); const { decorateErrorStack, kEmptyObject } = require('internal/util'); const { @@ -47,24 +47,29 @@ const isCommonJSGlobalLikeNotDefinedError = (errorMessage) => (globalLike) => errorMessage === `${globalLike} is not defined`, ); -/* A ModuleJob tracks the loading of a single Module, and the ModuleJobs of - * its dependencies, over time. */ -class ModuleJob { - // `loader` is the Loader instance used for loading dependencies. - // `moduleProvider` is a function - constructor(loader, url, importAttributes = { __proto__: null }, - moduleProvider, isMain, inspectBrk, sync = false) { +class ModuleJobBase { + constructor(loader, url, importAttributes, moduleWrapMaybePromise, isMain, inspectBrk) { this.loader = loader; this.importAttributes = importAttributes; this.isMain = isMain; this.inspectBrk = inspectBrk; this.url = url; + this.module = moduleWrapMaybePromise; + } +} - this.module = undefined; +/* A ModuleJob tracks the loading of a single Module, and the ModuleJobs of + * its dependencies, over time. */ +class ModuleJob extends ModuleJobBase { + // `loader` is the Loader instance used for loading dependencies. + constructor(loader, url, importAttributes = { __proto__: null }, + moduleProvider, isMain, inspectBrk, sync = false) { + const modulePromise = ReflectApply(moduleProvider, loader, [url, isMain]); + super(loader, url, importAttributes, modulePromise, isMain, inspectBrk); // Expose the promise to the ModuleWrap directly for linking below. // `this.module` is also filled in below. - this.modulePromise = ReflectApply(moduleProvider, loader, [url, isMain]); + this.modulePromise = modulePromise; if (sync) { this.module = this.modulePromise; @@ -247,5 +252,41 @@ class ModuleJob { return { __proto__: null, module: this.module }; } } -ObjectSetPrototypeOf(ModuleJob.prototype, null); -module.exports = ModuleJob; + +// This is a fully synchronous job and does not spawn additional threads in any way. +// All the steps are ensured to be synchronous and it throws on instantiating +// an asynchronous graph. +class ModuleJobSync extends ModuleJobBase { + constructor(loader, url, importAttributes, moduleWrap, isMain, inspectBrk) { + super(loader, url, importAttributes, moduleWrap, isMain, inspectBrk, true); + assert(this.module instanceof ModuleWrap); + const moduleRequests = this.module.getModuleRequestsSync(); + for (let i = 0; i < moduleRequests.length; ++i) { + const { 0: specifier, 1: attributes } = moduleRequests[i]; + const wrap = this.loader.getModuleWrapForRequire(specifier, url, attributes); + const isLast = (i === moduleRequests.length - 1); + // TODO(joyeecheung): make the resolution callback deal with both promisified + // an raw module wraps, then we don't need to wrap it with a promise here. + this.module.cacheResolvedWrapsSync(specifier, PromiseResolve(wrap), isLast); + } + } + + async run() { + const status = this.module.getStatus(); + assert(status === kEvaluated, + `A require()-d module that is imported again must be evaluated. Status = ${status}`); + return { __proto__: null, module: this.module }; + } + + runSync() { + this.module.instantiateSync(); + setHasStartedUserESMExecution(); + const namespace = this.module.evaluateSync(); + return { __proto__: null, module: this.module, namespace }; + } +} + +ObjectSetPrototypeOf(ModuleJobBase.prototype, null); +module.exports = { + ModuleJob, ModuleJobSync, ModuleJobBase, +}; diff --git a/lib/internal/modules/esm/module_map.js b/lib/internal/modules/esm/module_map.js index eab00386c413a5..ab1171eaa47b02 100644 --- a/lib/internal/modules/esm/module_map.js +++ b/lib/internal/modules/esm/module_map.js @@ -97,8 +97,8 @@ class LoadCache extends SafeMap { validateString(url, 'url'); validateString(type, 'type'); - const ModuleJob = require('internal/modules/esm/module_job'); - if (job instanceof ModuleJob !== true && + const { ModuleJobBase } = require('internal/modules/esm/module_job'); + if (job instanceof ModuleJobBase !== true && typeof job !== 'function') { throw new ERR_INVALID_ARG_TYPE('job', 'ModuleJob', job); } diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index 6772bbffd989d2..7312bd0b09f41a 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -44,7 +44,7 @@ const { } = require('internal/modules/helpers'); const { Module: CJSModule, - cjsParseCache, + cjsSourceCache, cjsExportsCache, } = require('internal/modules/cjs/loader'); const { fileURLToPath, pathToFileURL, URL } = require('internal/url'); @@ -81,7 +81,7 @@ let cjsParse; */ async function initCJSParse() { if (typeof WebAssembly === 'undefined') { - cjsParse = require('internal/deps/cjs-module-lexer/lexer').parse; + initCJSParseSync(); } else { const { parse, init } = require('internal/deps/cjs-module-lexer/dist/lexer'); @@ -89,11 +89,19 @@ async function initCJSParse() { await init(); cjsParse = parse; } catch { - cjsParse = require('internal/deps/cjs-module-lexer/lexer').parse; + initCJSParseSync(); } } } +function initCJSParseSync() { + // TODO(joyeecheung): implement a binding that directly compiles using + // v8::WasmModuleObject::Compile() synchronously. + if (cjsParse === undefined) { + cjsParse = require('internal/deps/cjs-module-lexer/lexer').parse; + } +} + const translators = new SafeMap(); exports.translators = translators; exports.enrichCJSError = enrichCJSError; @@ -162,7 +170,7 @@ async function importModuleDynamically(specifier, { url }, attributes) { } // Strategy for loading a standard JavaScript module. -translators.set('module', async function moduleStrategy(url, source, isMain) { +translators.set('module', function moduleStrategy(url, source, isMain) { assertBufferSource(source, true, 'load'); source = stringify(source); debug(`Translating StandardModule ${url}`); @@ -323,6 +331,16 @@ function createCJSModuleWrap(url, source, isMain, loadCJS = loadCJSModule) { } +translators.set('commonjs-sync', function requireCommonJS(url, source, isMain) { + initCJSParseSync(); + assert(!isMain); // This is only used by imported CJS modules. + + return createCJSModuleWrap(url, source, isMain, (module, source, url, filename) => { + assert(module === CJSModule._cache[filename]); + CJSModule._load(filename, null, false); + }); +}); + // Handle CommonJS modules referenced by `require` calls. // This translator function must be sync, as `require` is sync. translators.set('require-commonjs', (url, source, isMain) => { @@ -371,7 +389,7 @@ function cjsPreparseModuleExports(filename, source) { // TODO: Do we want to keep hitting the user mutable CJS loader here? let module = CJSModule._cache[filename]; if (module) { - const cached = cjsParseCache.get(module); + const cached = cjsSourceCache.get(module); if (cached) { return { module, exportNames: cached.exportNames }; } @@ -395,7 +413,7 @@ function cjsPreparseModuleExports(filename, source) { const exportNames = new SafeSet(new SafeArrayIterator(exports)); // Set first for cycles. - cjsParseCache.set(module, { source, exportNames }); + cjsSourceCache.set(module, { source, exportNames }); if (reexports.length) { module.filename = filename; @@ -522,6 +540,8 @@ translators.set('wasm', async function(url, source) { let compiled; try { + // TODO(joyeecheung): implement a binding that directly compiles using + // v8::WasmModuleObject::Compile() synchronously. compiled = await WebAssembly.compile(source); } catch (err) { err.message = errPath(url) + ': ' + err.message; diff --git a/lib/internal/util/embedding.js b/lib/internal/util/embedding.js index db9162369aae0d..7e4cd565492843 100644 --- a/lib/internal/util/embedding.js +++ b/lib/internal/util/embedding.js @@ -16,7 +16,7 @@ const { getCodePath, isSea } = internalBinding('sea'); function embedderRunCjs(contents) { const filename = process.execPath; - const compiledWrapper = wrapSafe( + const { function: compiledWrapper } = wrapSafe( isSea() ? getCodePath() : filename, contents); diff --git a/src/module_wrap.cc b/src/module_wrap.cc index 501e4d7b7ea180..c2e2aa37ddefc5 100644 --- a/src/module_wrap.cc +++ b/src/module_wrap.cc @@ -279,6 +279,61 @@ static Local createImportAttributesContainer( return attributes; } +void ModuleWrap::GetModuleRequestsSync( + const FunctionCallbackInfo& args) { + Realm* realm = Realm::GetCurrent(args); + Isolate* isolate = args.GetIsolate(); + + Local that = args.This(); + + ModuleWrap* obj; + ASSIGN_OR_RETURN_UNWRAP(&obj, that); + + CHECK(!obj->linked_); + + Local module = obj->module_.Get(isolate); + Local module_requests = module->GetModuleRequests(); + const int module_requests_length = module_requests->Length(); + + std::vector> requests; + requests.reserve(module_requests_length); + // call the dependency resolve callbacks + for (int i = 0; i < module_requests_length; i++) { + Local module_request = + module_requests->Get(realm->context(), i).As(); + Local raw_attributes = module_request->GetImportAssertions(); + std::vector> request = { + module_request->GetSpecifier(), + createImportAttributesContainer(realm, isolate, raw_attributes, 3), + }; + requests.push_back(Array::New(isolate, request.data(), request.size())); + } + + args.GetReturnValue().Set( + Array::New(isolate, requests.data(), requests.size())); +} + +void ModuleWrap::CacheResolvedWrapsSync( + const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + + CHECK_EQ(args.Length(), 3); + CHECK(args[0]->IsString()); + CHECK(args[1]->IsPromise()); + CHECK(args[2]->IsBoolean()); + + ModuleWrap* dependent; + ASSIGN_OR_RETURN_UNWRAP(&dependent, args.This()); + + Utf8Value specifier(isolate, args[0]); + dependent->resolve_cache_[specifier.ToString()].Reset(isolate, + args[1].As()); + + if (args[2].As()->Value()) { + dependent->linked_ = true; + } +} + void ModuleWrap::Link(const FunctionCallbackInfo& args) { Realm* realm = Realm::GetCurrent(args); Isolate* isolate = args.GetIsolate(); @@ -444,6 +499,129 @@ void ModuleWrap::Evaluate(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(result.ToLocalChecked()); } +void ModuleWrap::InstantiateSync(const FunctionCallbackInfo& args) { + Realm* realm = Realm::GetCurrent(args); + Isolate* isolate = args.GetIsolate(); + ModuleWrap* obj; + ASSIGN_OR_RETURN_UNWRAP(&obj, args.This()); + Local context = obj->context(); + Local module = obj->module_.Get(isolate); + Environment* env = realm->env(); + + { + TryCatchScope try_catch(env); + USE(module->InstantiateModule(context, ResolveModuleCallback)); + + // clear resolve cache on instantiate + obj->resolve_cache_.clear(); + + if (try_catch.HasCaught() && !try_catch.HasTerminated()) { + CHECK(!try_catch.Message().IsEmpty()); + CHECK(!try_catch.Exception().IsEmpty()); + AppendExceptionLine(env, + try_catch.Exception(), + try_catch.Message(), + ErrorHandlingMode::MODULE_ERROR); + try_catch.ReThrow(); + return; + } + } + + // If --experimental-print-required-tla is true, proceeds to evaluation even + // if it's async because we want to search for the TLA and help users locate + // them. + if (module->IsGraphAsync() && !env->options()->print_required_tla) { + THROW_ERR_REQUIRE_ASYNC_MODULE(env); + return; + } +} + +void ModuleWrap::EvaluateSync(const FunctionCallbackInfo& args) { + Realm* realm = Realm::GetCurrent(args); + Isolate* isolate = args.GetIsolate(); + ModuleWrap* obj; + ASSIGN_OR_RETURN_UNWRAP(&obj, args.This()); + Local context = obj->context(); + Local module = obj->module_.Get(isolate); + Environment* env = realm->env(); + + Local result; + { + TryCatchScope try_catch(env); + if (!module->Evaluate(context).ToLocal(&result)) { + if (try_catch.HasCaught()) { + if (!try_catch.HasTerminated()) { + try_catch.ReThrow(); + } + return; + } + } + } + + CHECK(result->IsPromise()); + Local promise = result.As(); + if (promise->State() == Promise::PromiseState::kRejected) { + Local exception = promise->Result(); + Local message = + v8::Exception::CreateMessage(isolate, exception); + AppendExceptionLine( + env, exception, message, ErrorHandlingMode::MODULE_ERROR); + isolate->ThrowException(exception); + return; + } + + if (module->IsGraphAsync()) { + CHECK(env->options()->print_required_tla); + auto stalled = module->GetStalledTopLevelAwaitMessage(isolate); + if (stalled.size() != 0) { + for (auto pair : stalled) { + Local message = std::get<1>(pair); + + std::string reason = "Error: unexpected top-level await at "; + std::string info = + FormatErrorMessage(isolate, context, "", message, true); + reason += info; + FPrintF(stderr, "%s\n", reason); + } + } + THROW_ERR_REQUIRE_ASYNC_MODULE(env); + return; + } + + CHECK_EQ(promise->State(), Promise::PromiseState::kFulfilled); + + args.GetReturnValue().Set(module->GetModuleNamespace()); +} + +void ModuleWrap::GetNamespaceSync(const FunctionCallbackInfo& args) { + Realm* realm = Realm::GetCurrent(args); + Isolate* isolate = args.GetIsolate(); + ModuleWrap* obj; + ASSIGN_OR_RETURN_UNWRAP(&obj, args.This()); + Local module = obj->module_.Get(isolate); + + switch (module->GetStatus()) { + case v8::Module::Status::kUninstantiated: + case v8::Module::Status::kInstantiating: + return realm->env()->ThrowError( + "cannot get namespace, module has not been instantiated"); + case v8::Module::Status::kEvaluating: + return THROW_ERR_REQUIRE_ASYNC_MODULE(realm->env()); + case v8::Module::Status::kInstantiated: + case v8::Module::Status::kEvaluated: + case v8::Module::Status::kErrored: + break; + default: + UNREACHABLE(); + } + + if (module->IsGraphAsync()) { + return THROW_ERR_REQUIRE_ASYNC_MODULE(realm->env()); + } + Local result = module->GetModuleNamespace(); + args.GetReturnValue().Set(result); +} + void ModuleWrap::GetNamespace(const FunctionCallbackInfo& args) { Realm* realm = Realm::GetCurrent(args); Isolate* isolate = args.GetIsolate(); @@ -776,6 +954,12 @@ void ModuleWrap::CreatePerIsolateProperties(IsolateData* isolate_data, ModuleWrap::kInternalFieldCount); SetProtoMethod(isolate, tpl, "link", Link); + SetProtoMethod(isolate, tpl, "getModuleRequestsSync", GetModuleRequestsSync); + SetProtoMethod( + isolate, tpl, "cacheResolvedWrapsSync", CacheResolvedWrapsSync); + SetProtoMethod(isolate, tpl, "instantiateSync", InstantiateSync); + SetProtoMethod(isolate, tpl, "evaluateSync", EvaluateSync); + SetProtoMethod(isolate, tpl, "getNamespaceSync", GetNamespaceSync); SetProtoMethod(isolate, tpl, "instantiate", Instantiate); SetProtoMethod(isolate, tpl, "evaluate", Evaluate); SetProtoMethod(isolate, tpl, "setExport", SetSyntheticExport); @@ -827,6 +1011,11 @@ void ModuleWrap::RegisterExternalReferences( registry->Register(New); registry->Register(Link); + registry->Register(GetModuleRequestsSync); + registry->Register(CacheResolvedWrapsSync); + registry->Register(InstantiateSync); + registry->Register(EvaluateSync); + registry->Register(GetNamespaceSync); registry->Register(Instantiate); registry->Register(Evaluate); registry->Register(SetSyntheticExport); diff --git a/src/module_wrap.h b/src/module_wrap.h index e17048357feca2..45a338b38e01c8 100644 --- a/src/module_wrap.h +++ b/src/module_wrap.h @@ -78,6 +78,14 @@ class ModuleWrap : public BaseObject { ~ModuleWrap() override; static void New(const v8::FunctionCallbackInfo& args); + static void GetModuleRequestsSync( + const v8::FunctionCallbackInfo& args); + static void CacheResolvedWrapsSync( + const v8::FunctionCallbackInfo& args); + static void InstantiateSync(const v8::FunctionCallbackInfo& args); + static void EvaluateSync(const v8::FunctionCallbackInfo& args); + static void GetNamespaceSync(const v8::FunctionCallbackInfo& args); + static void Link(const v8::FunctionCallbackInfo& args); static void Instantiate(const v8::FunctionCallbackInfo& args); static void Evaluate(const v8::FunctionCallbackInfo& args); diff --git a/src/node_errors.h b/src/node_errors.h index 30f66a7648bff4..ad40141ca92c5a 100644 --- a/src/node_errors.h +++ b/src/node_errors.h @@ -92,6 +92,7 @@ void OOMErrorHandler(const char* location, const v8::OOMDetails& details); V(ERR_MODULE_NOT_FOUND, Error) \ V(ERR_NON_CONTEXT_AWARE_DISABLED, Error) \ V(ERR_OUT_OF_RANGE, RangeError) \ + V(ERR_REQUIRE_ASYNC_MODULE, Error) \ V(ERR_SCRIPT_EXECUTION_INTERRUPTED, Error) \ V(ERR_SCRIPT_EXECUTION_TIMEOUT, Error) \ V(ERR_STRING_TOO_LONG, Error) \ @@ -192,6 +193,10 @@ ERRORS_WITH_CODE(V) "creating Workers") \ V(ERR_NON_CONTEXT_AWARE_DISABLED, \ "Loading non context-aware native addons has been disabled") \ + V(ERR_REQUIRE_ASYNC_MODULE, \ + "require() cannot be used on an ESM graph with top-level await. Use " \ + "import() instead. To see where the top-level await comes from, use " \ + "--experimental-print-required-tla.") \ V(ERR_SCRIPT_EXECUTION_INTERRUPTED, \ "Script execution was interrupted by `SIGINT`") \ V(ERR_TLS_PSK_SET_IDENTIY_HINT_FAILED, "Failed to set PSK identity hint") \ diff --git a/src/node_options.cc b/src/node_options.cc index 1ba0bfcd9b3096..4b3017546525dc 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -363,6 +363,17 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { "ES module syntax, try again to evaluate them as ES modules", &EnvironmentOptions::detect_module, kAllowedInEnvvar); + AddOption("--experimental-print-required-tla", + "Print pending top-level await. If --experimental-require-module " + "is true, evaluate asynchronous graphs loaded by `require()` but " + "do not run the microtasks, in order to to find and print " + "top-level await in the graph", + &EnvironmentOptions::print_required_tla, + kAllowedInEnvvar); + AddOption("--experimental-require-module", + "Allow loading explicit ES Modules in require().", + &EnvironmentOptions::require_module, + kAllowedInEnvvar); AddOption("--diagnostic-dir", "set dir for all output files" " (default: current working directory)", diff --git a/src/node_options.h b/src/node_options.h index 1357e5b42869e8..9e12730f878190 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -111,6 +111,8 @@ class EnvironmentOptions : public Options { bool abort_on_uncaught_exception = false; std::vector conditions; bool detect_module = false; + bool print_required_tla = false; + bool require_module = false; std::string dns_result_order; bool enable_source_maps = false; bool experimental_fetch = true; diff --git a/test/es-module/test-esm-cjs-load-error-note.mjs b/test/es-module/test-esm-cjs-load-error-note.mjs index 4df9e903eb627a..00ff5582c71bda 100644 --- a/test/es-module/test-esm-cjs-load-error-note.mjs +++ b/test/es-module/test-esm-cjs-load-error-note.mjs @@ -6,16 +6,16 @@ import { describe, it } from 'node:test'; // Expect note to be included in the error output -const expectedNote = 'To load an ES module, ' + -'set "type": "module" in the package.json ' + -'or use the .mjs extension.'; +// Don't match the following sentence because it can change as features are +// added. +const expectedNote = 'Warning: To load an ES module'; const mustIncludeMessage = { - getMessage: () => (stderr) => `${expectedNote} not found in ${stderr}`, + getMessage: (stderr) => `${expectedNote} not found in ${stderr}`, includeNote: true, }; const mustNotIncludeMessage = { - getMessage: () => (stderr) => `${expectedNote} must not be included in ${stderr}`, + getMessage: (stderr) => `${expectedNote} must not be included in ${stderr}`, includeNote: false, }; diff --git a/test/es-module/test-esm-loader-modulemap.js b/test/es-module/test-esm-loader-modulemap.js index 860775df0a2ce8..83125fce738139 100644 --- a/test/es-module/test-esm-loader-modulemap.js +++ b/test/es-module/test-esm-loader-modulemap.js @@ -6,7 +6,7 @@ require('../common'); const { strictEqual, throws } = require('assert'); const { createModuleLoader } = require('internal/modules/esm/loader'); const { LoadCache, ResolveCache } = require('internal/modules/esm/module_map'); -const ModuleJob = require('internal/modules/esm/module_job'); +const { ModuleJob } = require('internal/modules/esm/module_job'); const createDynamicModule = require( 'internal/modules/esm/create_dynamic_module'); diff --git a/test/es-module/test-require-module-cached-tla.js b/test/es-module/test-require-module-cached-tla.js new file mode 100644 index 00000000000000..d98b012c349aa1 --- /dev/null +++ b/test/es-module/test-require-module-cached-tla.js @@ -0,0 +1,14 @@ +// Flags: --experimental-require-module +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +(async () => { + await import('../fixtures/es-modules/tla/resolved.mjs'); + assert.throws(() => { + require('../fixtures/es-modules/tla/resolved.mjs'); + }, { + code: 'ERR_REQUIRE_ASYNC_MODULE', + }); +})().then(common.mustCall()); diff --git a/test/es-module/test-require-module-conditional-exports.js b/test/es-module/test-require-module-conditional-exports.js new file mode 100644 index 00000000000000..354c8b72abc7a1 --- /dev/null +++ b/test/es-module/test-require-module-conditional-exports.js @@ -0,0 +1,35 @@ +// Flags: --experimental-require-module +'use strict'; + +require('../common'); +const assert = require('assert'); +const { isModuleNamespaceObject } = require('util/types'); + +// If only "require" exports are defined, return "require" exports. +{ + const mod = require('../fixtures/es-modules/exports-require-only/load.cjs'); + assert.deepStrictEqual({ ...mod }, { type: 'cjs' }); + assert(!isModuleNamespaceObject(mod)); +} + +// If only "import" exports are defined, throw ERR_PACKAGE_PATH_NOT_EXPORTED +// instead of falling back to it, because the request comes from require(). +assert.throws(() => { + require('../fixtures/es-modules/exports-import-only/load.cjs'); +}, { + code: 'ERR_PACKAGE_PATH_NOT_EXPORTED' +}); + +// If both are defined, "require" is used. +{ + const mod = require('../fixtures/es-modules/exports-both/load.cjs'); + assert.deepStrictEqual({ ...mod }, { type: 'cjs' }); + assert(!isModuleNamespaceObject(mod)); +} + +// If "import" and "default" are defined, "default" is used. +{ + const mod = require('../fixtures/es-modules/exports-import-default/load.cjs'); + assert.deepStrictEqual({ ...mod }, { type: 'cjs' }); + assert(!isModuleNamespaceObject(mod)); +} diff --git a/test/es-module/test-require-module-default-extension.js b/test/es-module/test-require-module-default-extension.js new file mode 100644 index 00000000000000..7c49e21aba9a15 --- /dev/null +++ b/test/es-module/test-require-module-default-extension.js @@ -0,0 +1,17 @@ +// Flags: --experimental-require-module +'use strict'; + +require('../common'); +const assert = require('assert'); +const { isModuleNamespaceObject } = require('util/types'); + +const mod = require('../fixtures/es-modules/package-default-extension/index.mjs'); +assert.deepStrictEqual({ ...mod }, { entry: 'mjs' }); +assert(isModuleNamespaceObject(mod)); + +assert.throws(() => { + const mod = require('../fixtures/es-modules/package-default-extension'); + console.log(mod); // In case it succeeds, log the result for debugging. +}, { + code: 'MODULE_NOT_FOUND', +}); diff --git a/test/es-module/test-require-module-dynamic-import-1.js b/test/es-module/test-require-module-dynamic-import-1.js new file mode 100644 index 00000000000000..000e31485f559e --- /dev/null +++ b/test/es-module/test-require-module-dynamic-import-1.js @@ -0,0 +1,32 @@ +// Flags: --experimental-require-module +'use strict'; + +// Tests that previously dynamically import()'ed results are reference equal to +// require()'d results. +const common = require('../common'); +const assert = require('assert'); +const path = require('path'); +const { pathToFileURL } = require('url'); + +(async () => { + const modules = [ + '../fixtures/es-module-loaders/module-named-exports.mjs', + '../fixtures/es-modules/import-esm.mjs', + '../fixtures/es-modules/require-cjs.mjs', + '../fixtures/es-modules/cjs-exports.mjs', + '../common/index.mjs', + '../fixtures/es-modules/package-type-module/index.js', + ]; + for (const id of modules) { + const url = pathToFileURL(path.resolve(__dirname, id)); + const imported = await import(url); + const required = require(id); + assert.strictEqual(imported, required, + `import()'ed and require()'ed result of ${id} was not reference equal`); + } + + const id = '../fixtures/es-modules/data-import.mjs'; + const imported = await import(id); + const required = require(id); + assert.strictEqual(imported.data, required.data); +})().then(common.mustCall()); diff --git a/test/es-module/test-require-module-dynamic-import-2.js b/test/es-module/test-require-module-dynamic-import-2.js new file mode 100644 index 00000000000000..6c31c04f0b2e77 --- /dev/null +++ b/test/es-module/test-require-module-dynamic-import-2.js @@ -0,0 +1,32 @@ +// Flags: --experimental-require-module +'use strict'; + +// Tests that previously dynamically require()'ed results are reference equal to +// import()'d results. +const common = require('../common'); +const assert = require('assert'); +const { pathToFileURL } = require('url'); +const path = require('path'); + +(async () => { + const modules = [ + '../fixtures/es-module-loaders/module-named-exports.mjs', + '../fixtures/es-modules/import-esm.mjs', + '../fixtures/es-modules/require-cjs.mjs', + '../fixtures/es-modules/cjs-exports.mjs', + '../common/index.mjs', + '../fixtures/es-modules/package-type-module/index.js', + ]; + for (const id of modules) { + const url = pathToFileURL(path.resolve(__dirname, id)); + const required = require(id); + const imported = await import(url); + assert.strictEqual(imported, required, + `import()'ed and require()'ed result of ${id} was not reference equal`); + } + + const id = '../fixtures/es-modules/data-import.mjs'; + const required = require(id); + const imported = await import(id); + assert.strictEqual(imported.data, required.data); +})().then(common.mustCall()); diff --git a/test/es-module/test-require-module-errors.js b/test/es-module/test-require-module-errors.js new file mode 100644 index 00000000000000..b54f8f96af3006 --- /dev/null +++ b/test/es-module/test-require-module-errors.js @@ -0,0 +1,48 @@ +// Flags: --experimental-require-module +'use strict'; + +require('../common'); +const assert = require('assert'); +const { spawnSyncAndExit } = require('../common/child_process'); +const fixtures = require('../common/fixtures'); + +spawnSyncAndExit(process.execPath, [ + '--experimental-require-module', + fixtures.path('es-modules/require-syntax-error.cjs'), +], { + status: 1, + signal: null, + stderr(output) { + assert.match(output, /var foo bar;/); + assert.match(output, /SyntaxError: Unexpected identifier 'bar'/); + return true; + }, +}); + +spawnSyncAndExit(process.execPath, [ + '--experimental-require-module', + fixtures.path('es-modules/require-reference-error.cjs'), +], { + status: 1, + signal: null, + trim: true, + stdout: 'executed', + stderr(output) { + assert.match(output, /module\.exports = { hello: 'world' };/); + assert.match(output, /ReferenceError: module is not defined/); + return true; + }, +}); + +spawnSyncAndExit(process.execPath, [ + '--experimental-require-module', + fixtures.path('es-modules/require-throw-error.cjs'), +], { + status: 1, + signal: null, + stderr(output) { + assert.match(output, /throw new Error\('test'\);/); + assert.match(output, /Error: test/); + return true; + }, +}); diff --git a/test/es-module/test-require-module-implicit.js b/test/es-module/test-require-module-implicit.js new file mode 100644 index 00000000000000..5b5a4a4bbb47b0 --- /dev/null +++ b/test/es-module/test-require-module-implicit.js @@ -0,0 +1,33 @@ +// Flags: --experimental-require-module +'use strict'; + +// Tests that require()ing modules without explicit module type information +// warns and errors. +require('../common'); +const assert = require('assert'); +const { isModuleNamespaceObject } = require('util/types'); + +assert.throws(() => { + require('../fixtures/es-modules/package-without-type/noext-esm'); +}, { + message: /Unexpected token 'export'/ +}); + +assert.throws(() => { + require('../fixtures/es-modules/loose.js'); +}, { + message: /Unexpected token 'export'/ +}); + +{ + // .mjs should not be matched as default extensions. + const id = '../fixtures/es-modules/should-not-be-resolved'; + assert.throws(() => { + require(id); + }, { + code: 'MODULE_NOT_FOUND' + }); + const mod = require(`${id}.mjs`); + assert.deepStrictEqual({ ...mod }, { hello: 'world' }); + assert(isModuleNamespaceObject(mod)); +} diff --git a/test/es-module/test-require-module-preload.js b/test/es-module/test-require-module-preload.js new file mode 100644 index 00000000000000..cd51e201b63df8 --- /dev/null +++ b/test/es-module/test-require-module-preload.js @@ -0,0 +1,72 @@ +'use strict'; + +require('../common'); +const { spawnSyncAndExitWithoutError } = require('../common/child_process'); +const fixtures = require('../common/fixtures'); + +const stderr = /ExperimentalWarning: Support for loading ES Module in require/; + +// Test named exports. +{ + spawnSyncAndExitWithoutError( + process.execPath, + [ '--experimental-require-module', '-r', fixtures.path('../fixtures/es-module-loaders/module-named-exports.mjs') ], + { + stderr, + } + ); +} + +// Test ESM that import ESM. +{ + spawnSyncAndExitWithoutError( + process.execPath, + [ '--experimental-require-module', '-r', fixtures.path('../fixtures/es-modules/import-esm.mjs') ], + { + stderr, + stdout: 'world', + trim: true, + } + ); +} + +// Test ESM that import CJS. +{ + spawnSyncAndExitWithoutError( + process.execPath, + [ '--experimental-require-module', '-r', fixtures.path('../fixtures/es-modules/cjs-exports.mjs') ], + { + stdout: 'ok', + stderr, + trim: true, + } + ); +} + +// Test ESM that require() CJS. +// Can't use the common/index.mjs here because that checks the globals, and +// -r injects a bunch of globals. +{ + spawnSyncAndExitWithoutError( + process.execPath, + [ '--experimental-require-module', '-r', fixtures.path('../fixtures/es-modules/require-cjs.mjs') ], + { + stdout: 'world', + stderr, + trim: true, + } + ); +} + +// Test "type": "module" and "main" field in package.json. +{ + spawnSyncAndExitWithoutError( + process.execPath, + [ '--experimental-require-module', '-r', fixtures.path('../fixtures/es-modules/package-type-module') ], + { + stdout: 'package-type-module', + stderr, + trim: true, + } + ); +} diff --git a/test/es-module/test-require-module-special-import.js b/test/es-module/test-require-module-special-import.js new file mode 100644 index 00000000000000..3ff03d08e8d1d0 --- /dev/null +++ b/test/es-module/test-require-module-special-import.js @@ -0,0 +1,11 @@ +// Flags: --experimental-require-module +'use strict'; + +require('../common'); +const assert = require('assert'); + +assert.throws(() => { + require('../fixtures/es-modules/network-import.mjs'); +}, { + code: 'ERR_NETWORK_IMPORT_DISALLOWED' +}); diff --git a/test/es-module/test-require-module-tla.js b/test/es-module/test-require-module-tla.js new file mode 100644 index 00000000000000..9b38b1cab3fcb5 --- /dev/null +++ b/test/es-module/test-require-module-tla.js @@ -0,0 +1,63 @@ +// Flags: --experimental-require-module +'use strict'; + +require('../common'); +const assert = require('assert'); +const { spawnSyncAndExit } = require('../common/child_process'); +const fixtures = require('../common/fixtures'); + +const message = /require\(\) cannot be used on an ESM graph with top-level await/; +const code = 'ERR_REQUIRE_ASYNC_MODULE'; + +assert.throws(() => { + require('../fixtures/es-modules/tla/rejected.mjs'); +}, { message, code }); + +assert.throws(() => { + require('../fixtures/es-modules/tla/unresolved.mjs'); +}, { message, code }); + + +assert.throws(() => { + require('../fixtures/es-modules/tla/resolved.mjs'); +}, { message, code }); + +// Test TLA in inner graphs. +assert.throws(() => { + require('../fixtures/es-modules/tla/parent.mjs'); +}, { message, code }); + +{ + spawnSyncAndExit(process.execPath, [ + '--experimental-require-module', + fixtures.path('es-modules/tla/require-execution.js'), + ], { + signal: null, + status: 1, + stderr(output) { + assert.doesNotMatch(output, /I am executed/); + assert.match(output, message); + return true; + }, + stdout: '' + }); +} + +{ + spawnSyncAndExit(process.execPath, [ + '--experimental-require-module', + '--experimental-print-required-tla', + fixtures.path('es-modules/tla/require-execution.js'), + ], { + signal: null, + status: 1, + stderr(output) { + assert.match(output, /I am executed/); + assert.match(output, /Error: unexpected top-level await at.*execution\.mjs:3/); + assert.match(output, /await Promise\.resolve\('hi'\)/); + assert.match(output, message); + return true; + }, + stdout: '' + }); +} diff --git a/test/es-module/test-require-module-twice.js b/test/es-module/test-require-module-twice.js new file mode 100644 index 00000000000000..5312cda8506a6e --- /dev/null +++ b/test/es-module/test-require-module-twice.js @@ -0,0 +1,21 @@ +// Flags: --experimental-require-module +'use strict'; + +require('../common'); +const assert = require('assert'); + +const modules = [ + '../fixtures/es-module-loaders/module-named-exports.mjs', + '../fixtures/es-modules/import-esm.mjs', + '../fixtures/es-modules/require-cjs.mjs', + '../fixtures/es-modules/cjs-exports.mjs', + '../common/index.mjs', + '../fixtures/es-modules/package-type-module/index.js', +]; + +for (const id of modules) { + const first = require(id); + const second = require(id); + assert.strictEqual(first, second, + `the results of require('${id}') twice are not reference equal`); +} diff --git a/test/es-module/test-require-module.js b/test/es-module/test-require-module.js new file mode 100644 index 00000000000000..631f5d731a5c86 --- /dev/null +++ b/test/es-module/test-require-module.js @@ -0,0 +1,62 @@ +// Flags: --experimental-require-module +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { isModuleNamespaceObject } = require('util/types'); + +common.expectWarning( + 'ExperimentalWarning', + 'Support for loading ES Module in require() is an experimental feature ' + + 'and might change at any time' +); + +// Test named exports. +{ + const mod = require('../fixtures/es-module-loaders/module-named-exports.mjs'); + assert.deepStrictEqual({ ...mod }, { foo: 'foo', bar: 'bar' }); + assert(isModuleNamespaceObject(mod)); +} + +// Test ESM that import ESM. +{ + const mod = require('../fixtures/es-modules/import-esm.mjs'); + assert.deepStrictEqual({ ...mod }, { hello: 'world' }); + assert(isModuleNamespaceObject(mod)); +} + +// Test ESM that import CJS. +{ + const mod = require('../fixtures/es-modules/cjs-exports.mjs'); + assert.deepStrictEqual({ ...mod }, {}); + assert(isModuleNamespaceObject(mod)); +} + +// Test ESM that require() CJS. +{ + const mjs = require('../common/index.mjs'); + // Only comparing a few properties because the ESM version of test/common doesn't + // re-export everything from the CJS version. + assert.strictEqual(common.mustCall, mjs.mustCall); + assert.strictEqual(common.localIPv6Hosts, mjs.localIPv6Hosts); + assert(!isModuleNamespaceObject(common)); + assert(isModuleNamespaceObject(mjs)); +} + +// Test "type": "module" and "main" field in package.json. +// Also, test default export. +{ + const mod = require('../fixtures/es-modules/package-type-module'); + assert.deepStrictEqual({ ...mod }, { default: 'package-type-module' }); + assert(isModuleNamespaceObject(mod)); +} + +// Test data: import. +{ + const mod = require('../fixtures/es-modules/data-import.mjs'); + assert.deepStrictEqual({ ...mod }, { + data: { hello: 'world' }, + id: 'data:text/javascript,export default %7B%20hello%3A%20%22world%22%20%7D' + }); + assert(isModuleNamespaceObject(mod)); +} diff --git a/test/fixtures/es-modules/data-import.mjs b/test/fixtures/es-modules/data-import.mjs new file mode 100644 index 00000000000000..e67c0b4696e139 --- /dev/null +++ b/test/fixtures/es-modules/data-import.mjs @@ -0,0 +1,2 @@ +export { default as data } from 'data:text/javascript,export default %7B%20hello%3A%20%22world%22%20%7D'; +export const id = 'data:text/javascript,export default %7B%20hello%3A%20%22world%22%20%7D'; diff --git a/test/fixtures/es-modules/deprecated-folders-ignore/package.json b/test/fixtures/es-modules/deprecated-folders-ignore/package.json index 52a3a1e8a8b787..3dbc1ca591c055 100644 --- a/test/fixtures/es-modules/deprecated-folders-ignore/package.json +++ b/test/fixtures/es-modules/deprecated-folders-ignore/package.json @@ -1,4 +1,3 @@ { "type": "module" } - diff --git a/test/fixtures/es-modules/exports-both/load.cjs b/test/fixtures/es-modules/exports-both/load.cjs new file mode 100644 index 00000000000000..8b01d84f780b49 --- /dev/null +++ b/test/fixtures/es-modules/exports-both/load.cjs @@ -0,0 +1 @@ +module.exports = require('dep'); diff --git a/test/fixtures/es-modules/exports-both/node_modules/dep/mod.cjs b/test/fixtures/es-modules/exports-both/node_modules/dep/mod.cjs new file mode 100644 index 00000000000000..267c18747d99bf --- /dev/null +++ b/test/fixtures/es-modules/exports-both/node_modules/dep/mod.cjs @@ -0,0 +1 @@ +module.exports = { type: "cjs" }; diff --git a/test/fixtures/es-modules/exports-both/node_modules/dep/mod.mjs b/test/fixtures/es-modules/exports-both/node_modules/dep/mod.mjs new file mode 100644 index 00000000000000..ba57355b40d5d0 --- /dev/null +++ b/test/fixtures/es-modules/exports-both/node_modules/dep/mod.mjs @@ -0,0 +1,2 @@ +export const type = "mjs"; + diff --git a/test/fixtures/es-modules/exports-both/node_modules/dep/package.json b/test/fixtures/es-modules/exports-both/node_modules/dep/package.json new file mode 100644 index 00000000000000..d4be8cb35eda9f --- /dev/null +++ b/test/fixtures/es-modules/exports-both/node_modules/dep/package.json @@ -0,0 +1,8 @@ +{ + "exports": { + ".": { + "import": "./mod.mjs", + "require": "./mod.cjs" + } + } +} diff --git a/test/fixtures/es-modules/exports-import-default/load.cjs b/test/fixtures/es-modules/exports-import-default/load.cjs new file mode 100644 index 00000000000000..8b01d84f780b49 --- /dev/null +++ b/test/fixtures/es-modules/exports-import-default/load.cjs @@ -0,0 +1 @@ +module.exports = require('dep'); diff --git a/test/fixtures/es-modules/exports-import-default/node_modules/dep/mod.js b/test/fixtures/es-modules/exports-import-default/node_modules/dep/mod.js new file mode 100644 index 00000000000000..267c18747d99bf --- /dev/null +++ b/test/fixtures/es-modules/exports-import-default/node_modules/dep/mod.js @@ -0,0 +1 @@ +module.exports = { type: "cjs" }; diff --git a/test/fixtures/es-modules/exports-import-default/node_modules/dep/mod.mjs b/test/fixtures/es-modules/exports-import-default/node_modules/dep/mod.mjs new file mode 100644 index 00000000000000..ba57355b40d5d0 --- /dev/null +++ b/test/fixtures/es-modules/exports-import-default/node_modules/dep/mod.mjs @@ -0,0 +1,2 @@ +export const type = "mjs"; + diff --git a/test/fixtures/es-modules/exports-import-default/node_modules/dep/package.json b/test/fixtures/es-modules/exports-import-default/node_modules/dep/package.json new file mode 100644 index 00000000000000..de60e166d628a8 --- /dev/null +++ b/test/fixtures/es-modules/exports-import-default/node_modules/dep/package.json @@ -0,0 +1,8 @@ +{ + "exports": { + ".": { + "import": "./mod.mjs", + "default": "./mod.js" + } + } +} diff --git a/test/fixtures/es-modules/exports-import-only/load.cjs b/test/fixtures/es-modules/exports-import-only/load.cjs new file mode 100644 index 00000000000000..ec9c535a04352d --- /dev/null +++ b/test/fixtures/es-modules/exports-import-only/load.cjs @@ -0,0 +1,2 @@ +module.exports = require('dep'); + diff --git a/test/fixtures/es-modules/exports-import-only/node_modules/dep/mod.js b/test/fixtures/es-modules/exports-import-only/node_modules/dep/mod.js new file mode 100644 index 00000000000000..e25304b3a9d21e --- /dev/null +++ b/test/fixtures/es-modules/exports-import-only/node_modules/dep/mod.js @@ -0,0 +1,2 @@ +export const type = 'mjs'; + diff --git a/test/fixtures/es-modules/exports-import-only/node_modules/dep/package.json b/test/fixtures/es-modules/exports-import-only/node_modules/dep/package.json new file mode 100644 index 00000000000000..7b208f585f5a07 --- /dev/null +++ b/test/fixtures/es-modules/exports-import-only/node_modules/dep/package.json @@ -0,0 +1,8 @@ +{ + "type": "module", + "exports": { + ".": { + "import": "./mod.js" + } + } +} diff --git a/test/fixtures/es-modules/exports-require-only/load.cjs b/test/fixtures/es-modules/exports-require-only/load.cjs new file mode 100644 index 00000000000000..8b01d84f780b49 --- /dev/null +++ b/test/fixtures/es-modules/exports-require-only/load.cjs @@ -0,0 +1 @@ +module.exports = require('dep'); diff --git a/test/fixtures/es-modules/exports-require-only/node_modules/dep/mod.js b/test/fixtures/es-modules/exports-require-only/node_modules/dep/mod.js new file mode 100644 index 00000000000000..267c18747d99bf --- /dev/null +++ b/test/fixtures/es-modules/exports-require-only/node_modules/dep/mod.js @@ -0,0 +1 @@ +module.exports = { type: "cjs" }; diff --git a/test/fixtures/es-modules/exports-require-only/node_modules/dep/package.json b/test/fixtures/es-modules/exports-require-only/node_modules/dep/package.json new file mode 100644 index 00000000000000..134ee945df7d20 --- /dev/null +++ b/test/fixtures/es-modules/exports-require-only/node_modules/dep/package.json @@ -0,0 +1,7 @@ +{ + "exports": { + ".": { + "require": "./mod.js" + } + } +} diff --git a/test/fixtures/es-modules/import-esm.mjs b/test/fixtures/es-modules/import-esm.mjs new file mode 100644 index 00000000000000..d8c0d983dda3d2 --- /dev/null +++ b/test/fixtures/es-modules/import-esm.mjs @@ -0,0 +1,3 @@ +import { hello } from './imported-esm.mjs'; +console.log(hello); +export { hello }; diff --git a/test/fixtures/es-modules/imported-esm.mjs b/test/fixtures/es-modules/imported-esm.mjs new file mode 100644 index 00000000000000..35f468bf4856d8 --- /dev/null +++ b/test/fixtures/es-modules/imported-esm.mjs @@ -0,0 +1 @@ +export const hello = 'world'; diff --git a/test/fixtures/es-modules/network-import.mjs b/test/fixtures/es-modules/network-import.mjs new file mode 100644 index 00000000000000..529d563b4d982f --- /dev/null +++ b/test/fixtures/es-modules/network-import.mjs @@ -0,0 +1 @@ +import 'http://example.com/foo.js'; diff --git a/test/fixtures/es-modules/package-default-extension/index.cjs b/test/fixtures/es-modules/package-default-extension/index.cjs new file mode 100644 index 00000000000000..cb312f0f7dac8b --- /dev/null +++ b/test/fixtures/es-modules/package-default-extension/index.cjs @@ -0,0 +1 @@ +module.exports = { entry: 'cjs' }; diff --git a/test/fixtures/es-modules/package-default-extension/index.mjs b/test/fixtures/es-modules/package-default-extension/index.mjs new file mode 100644 index 00000000000000..aac4141efdb58a --- /dev/null +++ b/test/fixtures/es-modules/package-default-extension/index.mjs @@ -0,0 +1 @@ +export const entry = 'mjs'; diff --git a/test/fixtures/es-modules/reference-error.mjs b/test/fixtures/es-modules/reference-error.mjs new file mode 100644 index 00000000000000..15be06aad9a6d5 --- /dev/null +++ b/test/fixtures/es-modules/reference-error.mjs @@ -0,0 +1,3 @@ +// Reference errors are not thrown until reference happens. +console.log('executed'); +module.exports = { hello: 'world' }; diff --git a/test/fixtures/es-modules/require-cjs.mjs b/test/fixtures/es-modules/require-cjs.mjs new file mode 100644 index 00000000000000..a79a9b004c2237 --- /dev/null +++ b/test/fixtures/es-modules/require-cjs.mjs @@ -0,0 +1,5 @@ +import { createRequire } from "node:module"; +const require = createRequire(import.meta.url); +const exports = require('./required-cjs'); +console.log(exports.hello); +export default exports; diff --git a/test/fixtures/es-modules/require-reference-error.cjs b/test/fixtures/es-modules/require-reference-error.cjs new file mode 100644 index 00000000000000..9d90c022cc1cc4 --- /dev/null +++ b/test/fixtures/es-modules/require-reference-error.cjs @@ -0,0 +1,2 @@ +'use strict'; +require('./reference-error.mjs'); diff --git a/test/fixtures/es-modules/require-syntax-error.cjs b/test/fixtures/es-modules/require-syntax-error.cjs new file mode 100644 index 00000000000000..918dc06f369c73 --- /dev/null +++ b/test/fixtures/es-modules/require-syntax-error.cjs @@ -0,0 +1,2 @@ +'use strict'; +require('./syntax-error.mjs'); diff --git a/test/fixtures/es-modules/require-throw-error.cjs b/test/fixtures/es-modules/require-throw-error.cjs new file mode 100644 index 00000000000000..6fdedc5400ce5a --- /dev/null +++ b/test/fixtures/es-modules/require-throw-error.cjs @@ -0,0 +1,2 @@ +'use strict'; +require('./throw-error.mjs'); diff --git a/test/fixtures/es-modules/required-cjs.js b/test/fixtures/es-modules/required-cjs.js new file mode 100644 index 00000000000000..69f1fd8c3776c1 --- /dev/null +++ b/test/fixtures/es-modules/required-cjs.js @@ -0,0 +1,3 @@ +module.exports = { + hello: 'world', +}; diff --git a/test/fixtures/es-modules/should-not-be-resolved.mjs b/test/fixtures/es-modules/should-not-be-resolved.mjs new file mode 100644 index 00000000000000..35f468bf4856d8 --- /dev/null +++ b/test/fixtures/es-modules/should-not-be-resolved.mjs @@ -0,0 +1 @@ +export const hello = 'world'; diff --git a/test/fixtures/es-modules/syntax-error.mjs b/test/fixtures/es-modules/syntax-error.mjs new file mode 100644 index 00000000000000..c2cd118b23b133 --- /dev/null +++ b/test/fixtures/es-modules/syntax-error.mjs @@ -0,0 +1 @@ +var foo bar; diff --git a/test/fixtures/es-modules/throw-error.mjs b/test/fixtures/es-modules/throw-error.mjs new file mode 100644 index 00000000000000..bc9eaa01354ace --- /dev/null +++ b/test/fixtures/es-modules/throw-error.mjs @@ -0,0 +1 @@ +throw new Error('test'); diff --git a/test/fixtures/es-modules/tla/execution.mjs b/test/fixtures/es-modules/tla/execution.mjs new file mode 100644 index 00000000000000..c060945ba02f21 --- /dev/null +++ b/test/fixtures/es-modules/tla/execution.mjs @@ -0,0 +1,3 @@ +import process from 'node:process'; +process._rawDebug('I am executed'); +await Promise.resolve('hi'); diff --git a/test/fixtures/es-modules/tla/require-execution.js b/test/fixtures/es-modules/tla/require-execution.js new file mode 100644 index 00000000000000..8d3ec9d107a838 --- /dev/null +++ b/test/fixtures/es-modules/tla/require-execution.js @@ -0,0 +1 @@ +require('./execution.mjs'); diff --git a/test/fixtures/es-modules/tla/resolved.mjs b/test/fixtures/es-modules/tla/resolved.mjs new file mode 100644 index 00000000000000..ff717caf5a2e3c --- /dev/null +++ b/test/fixtures/es-modules/tla/resolved.mjs @@ -0,0 +1 @@ +await Promise.resolve('hello');