diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e4241de7d42e75..e1fe4d0add3daf 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -96,7 +96,6 @@ /doc/api/packages.md @nodejs/loaders /lib/internal/bootstrap/realm.js @nodejs/loaders /lib/internal/modules/* @nodejs/loaders -/lib/internal/process/esm_loader.js @nodejs/loaders /lib/internal/process/execution.js @nodejs/loaders /lib/module.js @nodejs/loaders /src/module_wrap* @nodejs/loaders @nodejs/vm diff --git a/lib/internal/main/check_syntax.js b/lib/internal/main/check_syntax.js index 9a19c1809fe102..5a7ab5dc19e4e7 100644 --- a/lib/internal/main/check_syntax.js +++ b/lib/internal/main/check_syntax.js @@ -50,8 +50,7 @@ function loadESMIfNeeded(cb) { const hasModulePreImport = getOptionValue('--import').length > 0; if (hasModulePreImport) { - const { loadESM } = require('internal/process/esm_loader'); - loadESM(cb); + require('internal/modules/run_main').runEntryPointWithESMLoader(cb); return; } cb(); @@ -76,7 +75,5 @@ async function checkSyntax(source, filename) { return; } - const { loadESM } = require('internal/process/esm_loader'); - const { handleMainPromise } = require('internal/modules/run_main'); - handleMainPromise(loadESM((loader) => wrapSafe(filename, source))); + wrapSafe(filename, source); } diff --git a/lib/internal/main/eval_stdin.js b/lib/internal/main/eval_stdin.js index d71751e781b9b5..3ee4bcdb1d853b 100644 --- a/lib/internal/main/eval_stdin.js +++ b/lib/internal/main/eval_stdin.js @@ -10,7 +10,7 @@ const { const { getOptionValue } = require('internal/options'); const { - evalModule, + evalModuleEntryPoint, evalScript, readStdin, } = require('internal/process/execution'); @@ -24,15 +24,15 @@ readStdin((code) => { process._eval = code; const print = getOptionValue('--print'); - const loadESM = getOptionValue('--import').length > 0; + const shouldLoadESM = getOptionValue('--import').length > 0; if (getOptionValue('--input-type') === 'module' || (getOptionValue('--experimental-default-type') === 'module' && getOptionValue('--input-type') !== 'commonjs')) { - evalModule(code, print); + evalModuleEntryPoint(code, print); } else { evalScript('[stdin]', code, getOptionValue('--inspect-brk'), print, - loadESM); + shouldLoadESM); } }); diff --git a/lib/internal/main/eval_string.js b/lib/internal/main/eval_string.js index 908532b0b1865a..1125aa8d98e5aa 100644 --- a/lib/internal/main/eval_string.js +++ b/lib/internal/main/eval_string.js @@ -13,7 +13,7 @@ const { prepareMainThreadExecution, markBootstrapComplete, } = require('internal/process/pre_execution'); -const { evalModule, evalScript } = require('internal/process/execution'); +const { evalModuleEntryPoint, evalScript } = require('internal/process/execution'); const { addBuiltinLibsToObject } = require('internal/modules/helpers'); const { getOptionValue } = require('internal/options'); @@ -24,10 +24,10 @@ markBootstrapComplete(); const source = getOptionValue('--eval'); const print = getOptionValue('--print'); -const loadESM = getOptionValue('--import').length > 0 || getOptionValue('--experimental-loader').length > 0; +const shouldLoadESM = getOptionValue('--import').length > 0 || getOptionValue('--experimental-loader').length > 0; if (getOptionValue('--input-type') === 'module' || (getOptionValue('--experimental-default-type') === 'module' && getOptionValue('--input-type') !== 'commonjs')) { - evalModule(source, print); + evalModuleEntryPoint(source, print); } else { // For backward compatibility, we want the identifier crypto to be the // `node:crypto` module rather than WebCrypto. @@ -54,5 +54,5 @@ if (getOptionValue('--input-type') === 'module' || ) : source, getOptionValue('--inspect-brk'), print, - loadESM); + shouldLoadESM); } diff --git a/lib/internal/main/repl.js b/lib/internal/main/repl.js index da1764a9c80d95..f7aa3a3e2602fa 100644 --- a/lib/internal/main/repl.js +++ b/lib/internal/main/repl.js @@ -35,8 +35,7 @@ if (process.env.NODE_REPL_EXTERNAL_MODULE) { process.exit(kInvalidCommandLineArgument); } - const esmLoader = require('internal/process/esm_loader'); - esmLoader.loadESM(() => { + require('internal/modules/run_main').runEntryPointWithESMLoader(() => { console.log(`Welcome to Node.js ${process.version}.\n` + 'Type ".help" for more information.'); @@ -64,5 +63,7 @@ if (process.env.NODE_REPL_EXTERNAL_MODULE) { getOptionValue('--inspect-brk'), getOptionValue('--print')); } + // The TLAs in the REPL are still run as scripts, just transformed as async + // IIFEs for the REPL code itself to await on. }); } diff --git a/lib/internal/main/worker_thread.js b/lib/internal/main/worker_thread.js index 56697c3b2c2209..30f7a5f79e50fd 100644 --- a/lib/internal/main/worker_thread.js +++ b/lib/internal/main/worker_thread.js @@ -170,8 +170,8 @@ port.on('message', (message) => { } case 'module': { - const { evalModule } = require('internal/process/execution'); - PromisePrototypeThen(evalModule(filename), undefined, (e) => { + const { evalModuleEntryPoint } = require('internal/process/execution'); + PromisePrototypeThen(evalModuleEntryPoint(filename), undefined, (e) => { workerOnGlobalUncaughtException(e, true); }); break; diff --git a/lib/internal/modules/esm/handle_process_exit.js b/lib/internal/modules/esm/handle_process_exit.js deleted file mode 100644 index 4689ef6bb204c0..00000000000000 --- a/lib/internal/modules/esm/handle_process_exit.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; - -const { exitCodes: { kUnfinishedTopLevelAwait } } = internalBinding('errors'); - -/** - * Handle a Promise from running code that potentially does Top-Level Await. - * In that case, it makes sense to set the exit code to a specific non-zero value - * if the main code never finishes running. - */ -function handleProcessExit() { - process.exitCode ??= kUnfinishedTopLevelAwait; -} - -module.exports = { - handleProcessExit, -}; diff --git a/lib/internal/modules/esm/hooks.js b/lib/internal/modules/esm/hooks.js index 6f04769f47d26d..a4ff342e645ead 100644 --- a/lib/internal/modules/esm/hooks.js +++ b/lib/internal/modules/esm/hooks.js @@ -161,8 +161,8 @@ class Hooks { * loader (user-land) to the worker. */ async register(urlOrSpecifier, parentURL, data) { - const moduleLoader = require('internal/process/esm_loader').esmLoader; - const keyedExports = await moduleLoader.import( + const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader(); + const keyedExports = await cascadedLoader.import( urlOrSpecifier, parentURL, kEmptyObject, diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index c0e3cdb36e1c02..fd4faa3bd4af62 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -20,7 +20,7 @@ const { ERR_UNKNOWN_MODULE_FORMAT, } = require('internal/errors').codes; const { getOptionValue } = require('internal/options'); -const { pathToFileURL, isURL } = require('internal/url'); +const { isURL } = require('internal/url'); const { emitExperimentalWarning } = require('internal/util'); const { getDefaultConditions, @@ -85,11 +85,6 @@ class ModuleLoader { */ #defaultConditions = getDefaultConditions(); - /** - * The index for assigning unique URLs to anonymous module evaluation - */ - evalIndex = 0; - /** * Registry of resolved specifiers */ @@ -187,10 +182,7 @@ class ModuleLoader { } } - async eval( - source, - url = pathToFileURL(`${process.cwd()}/[eval${++this.evalIndex}]`).href, - ) { + async eval(source, url) { const evalInstance = (url) => { const { ModuleWrap } = internalBinding('module_wrap'); const { registerModule } = require('internal/modules/esm/utils'); @@ -214,6 +206,7 @@ class ModuleLoader { return { __proto__: null, namespace: module.getNamespace(), + module, }; } @@ -568,6 +561,23 @@ function getHooksProxy() { return hooksProxy; } +let cascadedLoader; + +/** + * This is a singleton ESM loader that integrates the loader hooks, if any. + * It it used by other internal built-ins when they need to load ESM code + * while also respecting hooks. + * When built-ins need access to this loader, they should do + * require('internal/module/esm/loader').getOrInitializeCascadedLoader() + * lazily only right before the loader is actually needed, and don't do it + * in the top-level, to avoid circular dependencies. + * @returns {ModuleLoader} + */ +function getOrInitializeCascadedLoader() { + cascadedLoader ??= createModuleLoader(); + return cascadedLoader; +} + /** * Register a single loader programmatically. * @param {string|import('url').URL} specifier @@ -598,12 +608,11 @@ function getHooksProxy() { * ``` */ function register(specifier, parentURL = undefined, options) { - const moduleLoader = require('internal/process/esm_loader').esmLoader; if (parentURL != null && typeof parentURL === 'object' && !isURL(parentURL)) { options = parentURL; parentURL = options.parentURL; } - moduleLoader.register( + getOrInitializeCascadedLoader().register( specifier, parentURL ?? 'data:', options?.data, @@ -614,5 +623,6 @@ function register(specifier, parentURL = undefined, options) { module.exports = { createModuleLoader, getHooksProxy, + getOrInitializeCascadedLoader, register, }; diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index ca547699d00ed1..61e489a6cedd7a 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -55,7 +55,6 @@ const { const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache'); const moduleWrap = internalBinding('module_wrap'); const { ModuleWrap } = moduleWrap; -const asyncESM = require('internal/process/esm_loader'); const { emitWarningSync } = require('internal/process/warning'); const { internalCompileFunction } = require('internal/vm'); const { @@ -157,7 +156,8 @@ function errPath(url) { * @returns {Promise} The imported module. */ async function importModuleDynamically(specifier, { url }, attributes) { - return asyncESM.esmLoader.import(specifier, url, attributes); + const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader(); + return cascadedLoader.import(specifier, url, attributes); } // Strategy for loading a standard JavaScript module. @@ -243,6 +243,7 @@ function loadCJSModule(module, source, url, filename) { const compiledWrapper = compileResult.function; + const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader(); const __dirname = dirname(filename); // eslint-disable-next-line func-name-matching,func-style const requireFn = function require(specifier) { @@ -261,7 +262,7 @@ function loadCJSModule(module, source, url, filename) { } specifier = `${pathToFileURL(path)}`; } - const job = asyncESM.esmLoader.getModuleJobSync(specifier, url, importAttributes); + const job = cascadedLoader.getModuleJobSync(specifier, url, importAttributes); job.runSync(); return cjsCache.get(job.url).exports; }; @@ -272,7 +273,7 @@ function loadCJSModule(module, source, url, filename) { specifier = `${pathToFileURL(path)}`; } } - const { url: resolvedURL } = asyncESM.esmLoader.resolveSync(specifier, url, kEmptyObject); + const { url: resolvedURL } = cascadedLoader.resolveSync(specifier, url, kEmptyObject); return StringPrototypeStartsWith(resolvedURL, 'file://') ? fileURLToPath(resolvedURL) : resolvedURL; }); setOwnProperty(requireFn, 'main', process.mainModule); diff --git a/lib/internal/modules/esm/utils.js b/lib/internal/modules/esm/utils.js index 39e87338dc5a79..d7867864bba714 100644 --- a/lib/internal/modules/esm/utils.js +++ b/lib/internal/modules/esm/utils.js @@ -32,7 +32,6 @@ const { const { emitExperimentalWarning, getCWDURL, - getLazy, } = require('internal/util'); const { setImportModuleDynamicallyCallback, @@ -181,9 +180,6 @@ function initializeImportMetaObject(symbol, meta) { } } } -const getCascadedLoader = getLazy( - () => require('internal/process/esm_loader').esmLoader, -); /** * Proxy the dynamic import to the default loader. @@ -194,7 +190,8 @@ const getCascadedLoader = getLazy( */ function defaultImportModuleDynamically(specifier, attributes, referrerName) { const parentURL = normalizeReferrerURL(referrerName); - return getCascadedLoader().import(specifier, parentURL, attributes); + const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader(); + return cascadedLoader.import(specifier, parentURL, attributes); } /** @@ -263,10 +260,10 @@ async function initializeHooks() { const customLoaderURLs = getOptionValue('--experimental-loader'); const { Hooks } = require('internal/modules/esm/hooks'); - const esmLoader = require('internal/process/esm_loader').esmLoader; + const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader(); const hooks = new Hooks(); - esmLoader.setCustomizations(hooks); + cascadedLoader.setCustomizations(hooks); // We need the loader customizations to be set _before_ we start invoking // `--require`, otherwise loops can happen because a `--require` script diff --git a/lib/internal/modules/run_main.js b/lib/internal/modules/run_main.js index 1f03c313121db0..ca401044c0178c 100644 --- a/lib/internal/modules/run_main.js +++ b/lib/internal/modules/run_main.js @@ -7,6 +7,15 @@ const { const { containsModuleSyntax } = internalBinding('contextify'); const { getOptionValue } = require('internal/options'); const path = require('path'); +const { pathToFileURL } = require('internal/url'); +const { kEmptyObject, getCWDURL } = require('internal/util'); +const { + hasUncaughtExceptionCaptureCallback, +} = require('internal/process/execution'); +const { + triggerUncaughtException, + exitCodes: { kUnfinishedTopLevelAwait }, +} = internalBinding('errors'); /** * Get the absolute path to the main entry point. @@ -87,35 +96,58 @@ function shouldUseESMLoader(mainPath) { } /** - * Run the main entry point through the ESM Loader. - * @param {string} mainPath - Absolute path for the main entry point + * Handle a Promise from running code that potentially does Top-Level Await. + * In that case, it makes sense to set the exit code to a specific non-zero value + * if the main code never finishes running. */ -function runMainESM(mainPath) { - const { loadESM } = require('internal/process/esm_loader'); - const { pathToFileURL } = require('internal/url'); - const main = pathToFileURL(mainPath).href; - - handleMainPromise(loadESM((esmLoader) => { - return esmLoader.import(main, undefined, { __proto__: null }); - })); +function handleProcessExit() { + process.exitCode ??= kUnfinishedTopLevelAwait; } /** - * Handle process exit events around the main entry point promise. - * @param {Promise} promise - Main entry point promise + * @param {function(ModuleLoader):ModuleWrap|undefined} callback */ -async function handleMainPromise(promise) { - const { - handleProcessExit, - } = require('internal/modules/esm/handle_process_exit'); +async function asyncRunEntryPointWithESMLoader(callback) { process.on('exit', handleProcessExit); + const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader(); try { - return await promise; + const userImports = getOptionValue('--import'); + if (userImports.length > 0) { + const parentURL = getCWDURL().href; + for (let i = 0; i < userImports.length; i++) { + await cascadedLoader.import(userImports[i], parentURL, kEmptyObject); + } + } else { + cascadedLoader.forceLoadHooks(); + } + await callback(cascadedLoader); + } catch (err) { + if (hasUncaughtExceptionCaptureCallback()) { + process._fatalException(err); + return; + } + triggerUncaughtException( + err, + true, /* fromPromise */ + ); } finally { process.off('exit', handleProcessExit); } } +/** + * This initializes the ESM loader and runs --import (if any) before executing the + * callback to run the entry point. + * If the callback intends to evaluate a ESM module as entry point, it should return + * the corresponding ModuleWrap so that stalled TLA can be checked a process exit. + * @param {function(ModuleLoader):ModuleWrap|undefined} callback + * @returns {Promise} + */ +function runEntryPointWithESMLoader(callback) { + const promise = asyncRunEntryPointWithESMLoader(callback); + return promise; +} + /** * Parse the CLI main entry point string and run it. * For backwards compatibility, we have to run a bunch of monkey-patchable code that belongs to the CJS loader (exposed @@ -128,7 +160,14 @@ function executeUserEntryPoint(main = process.argv[1]) { const resolvedMain = resolveMainPath(main); const useESMLoader = shouldUseESMLoader(resolvedMain); if (useESMLoader) { - runMainESM(resolvedMain || main); + const mainPath = resolvedMain || main; + const mainURL = pathToFileURL(mainPath).href; + + runEntryPointWithESMLoader((cascadedLoader) => { + // Note that if the graph contains unfinished TLA, this may never resolve + // even after the event loop stops running. + return cascadedLoader.import(mainURL, undefined, { __proto__: null }, true); + }); } else { // Module._load is the monkey-patchable CJS module loader. const { Module } = require('internal/modules/cjs/loader'); @@ -138,5 +177,6 @@ function executeUserEntryPoint(main = process.argv[1]) { module.exports = { executeUserEntryPoint, - handleMainPromise, + runEntryPointWithESMLoader, + handleProcessExit, }; diff --git a/lib/internal/process/esm_loader.js b/lib/internal/process/esm_loader.js deleted file mode 100644 index 0865d7ceef66b7..00000000000000 --- a/lib/internal/process/esm_loader.js +++ /dev/null @@ -1,40 +0,0 @@ -'use strict'; - -const { createModuleLoader } = require('internal/modules/esm/loader'); -const { getOptionValue } = require('internal/options'); -const { - hasUncaughtExceptionCaptureCallback, -} = require('internal/process/execution'); -const { kEmptyObject, getCWDURL } = require('internal/util'); - -let esmLoader; - -module.exports = { - get esmLoader() { - return esmLoader ??= createModuleLoader(); - }, - async loadESM(callback) { - esmLoader ??= createModuleLoader(); - try { - const userImports = getOptionValue('--import'); - if (userImports.length > 0) { - const parentURL = getCWDURL().href; - for (let i = 0; i < userImports.length; i++) { - await esmLoader.import(userImports[i], parentURL, kEmptyObject); - } - } else { - esmLoader.forceLoadHooks(); - } - await callback(esmLoader); - } catch (err) { - if (hasUncaughtExceptionCaptureCallback()) { - process._fatalException(err); - return; - } - internalBinding('errors').triggerUncaughtException( - err, - true, /* fromPromise */ - ); - } - }, -}; diff --git a/lib/internal/process/execution.js b/lib/internal/process/execution.js index 5de5edfb2d5524..e69add7394e60f 100644 --- a/lib/internal/process/execution.js +++ b/lib/internal/process/execution.js @@ -15,6 +15,7 @@ const { ERR_EVAL_ESM_CANNOT_PRINT, }, } = require('internal/errors'); +const { pathToFileURL } = require('internal/url'); const { exitCodes: { kGenericUserError } } = internalBinding('errors'); const { @@ -46,19 +47,30 @@ function tryGetCwd() { } } -function evalModule(source, print) { +let evalIndex = 0; +function getEvalModuleUrl() { + return pathToFileURL(`${process.cwd()}/[eval${++evalIndex}]`).href; +} + +/** + * Evaluate an ESM entry point and return the promise that gets fulfilled after + * it finishes evaluation. + * @param {string} source Source code the ESM + * @param {boolean} print Whether the result should be printed. + * @returns {Promise} + */ +function evalModuleEntryPoint(source, print) { if (print) { throw new ERR_EVAL_ESM_CANNOT_PRINT(); } - const { loadESM } = require('internal/process/esm_loader'); - const { handleMainPromise } = require('internal/modules/run_main'); RegExpPrototypeExec(/^/, ''); // Necessary to reset RegExp statics before user code runs. - return handleMainPromise(loadESM((loader) => loader.eval(source))); + return require('internal/modules/run_main').runEntryPointWithESMLoader( + (loader) => loader.eval(source, getEvalModuleUrl(), true), + ); } function evalScript(name, body, breakFirstLine, print, shouldLoadESM = false) { const CJSModule = require('internal/modules/cjs/loader').Module; - const { pathToFileURL } = require('internal/url'); const cwd = tryGetCwd(); const origModule = globalThis.module; // Set e.g. when called from the REPL. @@ -67,15 +79,12 @@ function evalScript(name, body, breakFirstLine, print, shouldLoadESM = false) { module.filename = path.join(cwd, name); module.paths = CJSModule._nodeModulePaths(cwd); - const { handleMainPromise } = require('internal/modules/run_main'); - const asyncESM = require('internal/process/esm_loader'); const baseUrl = pathToFileURL(module.filename).href; - const { loadESM } = asyncESM; if (getOptionValue('--experimental-detect-module') && getOptionValue('--input-type') === '' && getOptionValue('--experimental-default-type') === '' && containsModuleSyntax(body, name)) { - return evalModule(body, print); + return evalModuleEntryPoint(body, print); } const runScript = () => { @@ -92,8 +101,8 @@ function evalScript(name, body, breakFirstLine, print, shouldLoadESM = false) { const result = module._compile(script, `${name}-wrapper`)(() => { const hostDefinedOptionId = Symbol(name); async function importModuleDynamically(specifier, _, importAttributes) { - const loader = asyncESM.esmLoader; - return loader.import(specifier, baseUrl, importAttributes); + const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader(); + return cascadedLoader.import(specifier, baseUrl, importAttributes); } const script = makeContextifyScript( body, // code @@ -118,9 +127,10 @@ function evalScript(name, body, breakFirstLine, print, shouldLoadESM = false) { }; if (shouldLoadESM) { - return handleMainPromise(loadESM(runScript)); + require('internal/modules/run_main').runEntryPointWithESMLoader(runScript); + return; } - return runScript(); + runScript(); } const exceptionHandlerState = { @@ -228,7 +238,7 @@ function readStdin(callback) { module.exports = { readStdin, tryGetCwd, - evalModule, + evalModuleEntryPoint, evalScript, onGlobalUncaughtException: createOnGlobalUncaughtException(), setUncaughtExceptionCaptureCallback, diff --git a/lib/internal/process/per_thread.js b/lib/internal/process/per_thread.js index b45f2a61e0ddaf..85777c9e4a3ed5 100644 --- a/lib/internal/process/per_thread.js +++ b/lib/internal/process/per_thread.js @@ -173,9 +173,7 @@ function wrapProcessMethods(binding) { memoryUsage.rss = rss; function exit(code) { - const { - handleProcessExit, - } = require('internal/modules/esm/handle_process_exit'); + const { handleProcessExit } = require('internal/modules/run_main'); process.off('exit', handleProcessExit); if (arguments.length !== 0) { diff --git a/lib/internal/test_runner/utils.js b/lib/internal/test_runner/utils.js index 990fe595062324..339631f08fe945 100644 --- a/lib/internal/test_runner/utils.js +++ b/lib/internal/test_runner/utils.js @@ -160,8 +160,8 @@ async function getReportersMap(reporters, destinations) { parentURL = 'file:///'; } - const { esmLoader } = require('internal/process/esm_loader'); - reporter = await esmLoader.import(name, parentURL, { __proto__: null }); + const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader(); + reporter = await cascadedLoader.import(name, parentURL, { __proto__: null }); } if (reporter?.default) { diff --git a/lib/repl.js b/lib/repl.js index 1fbce42888c9a2..d16f8882211a42 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -462,9 +462,8 @@ function REPLServer(prompt, // Continue regardless of error. } async function importModuleDynamically(specifier, _, importAttributes) { - const asyncESM = require('internal/process/esm_loader'); - return asyncESM.esmLoader.import(specifier, parentURL, - importAttributes); + const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader(); + return cascadedLoader.import(specifier, parentURL, importAttributes); } // `experimentalREPLAwait` is set to true by default. // Shall be false in case `--no-experimental-repl-await` flag is used.