diff --git a/lib/internal/main/check_syntax.js b/lib/internal/main/check_syntax.js index 1b32a4d569f494..467a740363f772 100644 --- a/lib/internal/main/check_syntax.js +++ b/lib/internal/main/check_syntax.js @@ -52,7 +52,8 @@ function loadESMIfNeeded(cb) { const hasModulePreImport = getOptionValue('--import').length > 0; if (hasModulePreImport) { - const { loadESM } = require('internal/process/esm_loader'); + const { loadESM, init } = require('internal/process/esm_loader'); + init(); loadESM(cb); return; } diff --git a/lib/internal/main/eval_stdin.js b/lib/internal/main/eval_stdin.js index d947af49a6a942..310cffaaca94e7 100644 --- a/lib/internal/main/eval_stdin.js +++ b/lib/internal/main/eval_stdin.js @@ -25,12 +25,15 @@ readStdin((code) => { const print = getOptionValue('--print'); const loadESM = getOptionValue('--import').length > 0; - if (getOptionValue('--input-type') === 'module') + if (getOptionValue('--input-type') === 'module') { + require('internal/process/esm_loader').init(); evalModule(code, print); - else + } else { evalScript('[stdin]', code, getOptionValue('--inspect-brk'), print, loadESM); + require('internal/process/esm_loader').initIfNeeded(); + } }); diff --git a/lib/internal/main/eval_string.js b/lib/internal/main/eval_string.js index ec6a2d51af5450..4495f18d0aa75f 100644 --- a/lib/internal/main/eval_string.js +++ b/lib/internal/main/eval_string.js @@ -25,9 +25,11 @@ markBootstrapComplete(); const source = getOptionValue('--eval'); const print = getOptionValue('--print'); const loadESM = getOptionValue('--import').length > 0; -if (getOptionValue('--input-type') === 'module') +const esmLoader = require('internal/process/esm_loader'); +if (getOptionValue('--input-type') === 'module') { + esmLoader.init(); evalModule(source, print); -else { +} else { // For backward compatibility, we want the identifier crypto to be the // `node:crypto` module rather than WebCrypto. const isUsingCryptoIdentifier = @@ -54,4 +56,5 @@ else { getOptionValue('--inspect-brk'), print, loadESM); + esmLoader.initIfNeeded(); } diff --git a/lib/internal/main/repl.js b/lib/internal/main/repl.js index da1764a9c80d95..97cb1b8ac61c9e 100644 --- a/lib/internal/main/repl.js +++ b/lib/internal/main/repl.js @@ -36,6 +36,7 @@ if (process.env.NODE_REPL_EXTERNAL_MODULE) { } const esmLoader = require('internal/process/esm_loader'); + esmLoader.init(); esmLoader.loadESM(() => { console.log(`Welcome to Node.js ${process.version}.\n` + 'Type ".help" for more information.'); diff --git a/lib/internal/main/run_main_module.js b/lib/internal/main/run_main_module.js index 51331270a2161f..b754e0784bfbc8 100644 --- a/lib/internal/main/run_main_module.js +++ b/lib/internal/main/run_main_module.js @@ -11,6 +11,8 @@ prepareMainThreadExecution(true); markBootstrapComplete(); +require('internal/process/esm_loader').init(); + // Necessary to reset RegExp statics before user code runs. RegExpPrototypeExec(/^/, ''); diff --git a/lib/internal/main/test_runner.js b/lib/internal/main/test_runner.js index 5ce9e51e4b6af6..f29d88a9b9098b 100644 --- a/lib/internal/main/test_runner.js +++ b/lib/internal/main/test_runner.js @@ -22,6 +22,9 @@ if (isUsingInspector()) { inspectPort = process.debugPort; } +// We might need ESM loader for custom test reporters. +require('internal/process/esm_loader').initIfNeeded(); + run({ concurrency, inspectPort, watch: getOptionValue('--watch'), setup: setupTestReporters }) .once('test:fail', () => { process.exitCode = kGenericUserError; diff --git a/lib/internal/main/worker_thread.js b/lib/internal/main/worker_thread.js index 4d8938e58bdf33..84f82e8d9f8556 100644 --- a/lib/internal/main/worker_thread.js +++ b/lib/internal/main/worker_thread.js @@ -162,11 +162,13 @@ port.on('message', (message) => { }); ArrayPrototypeSplice(process.argv, 1, 0, name); evalScript(name, filename); + require('internal/process/esm_loader').initIfNeeded(); break; } case 'module': { const { evalModule } = require('internal/process/execution'); + require('internal/process/esm_loader').init(); PromisePrototypeThen(evalModule(filename), undefined, (e) => { workerOnGlobalUncaughtException(e, true); }); @@ -179,6 +181,7 @@ port.on('message', (message) => { // XXX: the monkey-patchability here should probably be deprecated. ArrayPrototypeSplice(process.argv, 1, 0, filename); const CJSLoader = require('internal/modules/cjs/loader'); + require('internal/process/esm_loader').initIfNeeded(); CJSLoader.Module.runMain(filename); break; } diff --git a/lib/internal/modules/esm/utils.js b/lib/internal/modules/esm/utils.js index 1c01dd13b3ab21..12d00970d480e2 100644 --- a/lib/internal/modules/esm/utils.js +++ b/lib/internal/modules/esm/utils.js @@ -115,7 +115,11 @@ async function initializeHooks() { const hooks = new Hooks(); const { DefaultModuleLoader } = require('internal/modules/esm/loader'); + const { init } = require('internal/process/esm_loader'); class ModuleLoader extends DefaultModuleLoader { + // eslint-disable-next-line no-useless-constructor + constructor() { super(); } + loaderType = 'internal'; async #getModuleJob(specifier, parentURL, importAssertions) { const resolveResult = await hooks.resolve(specifier, parentURL, importAssertions); @@ -156,6 +160,8 @@ async function initializeHooks() { const preloadScripts = hooks.initializeGlobalPreload(); + init(privateModuleLoader); + return { __proto__: null, hooks, preloadScripts }; } diff --git a/lib/internal/process/esm_loader.js b/lib/internal/process/esm_loader.js index e7d70ebbca1ca4..f34e39763482bc 100644 --- a/lib/internal/process/esm_loader.js +++ b/lib/internal/process/esm_loader.js @@ -1,21 +1,59 @@ 'use strict'; +const { + ArrayPrototypePush, + PromisePrototypeThen, + ReflectApply, + SafeMap, +} = primordials; + +const assert = require('internal/assert'); const { createModuleLoader } = require('internal/modules/esm/loader'); const { getOptionValue } = require('internal/options'); const { hasUncaughtExceptionCaptureCallback, } = require('internal/process/execution'); const { pathToFileURL } = require('internal/url'); -const { kEmptyObject } = require('internal/util'); +const { kEmptyObject, createDeferredPromise } = require('internal/util'); let esmLoader; +let init = false; module.exports = { get esmLoader() { - return esmLoader ??= createModuleLoader(true); + return esmLoader ??= { __proto__: null, cjsCache: new SafeMap(), import() { + const { promise, resolve, reject } = createDeferredPromise(); + ArrayPrototypePush(this.importRequests, { arguments: arguments, resolve, reject }); + return promise; + }, importRequests: [] }; + }, + initIfNeeded() { + // TODO: we could try to avoid loading ESM loader on CJS-only codebase + return module.exports.init(); + }, + init(loader = undefined) { + assert(!init); + init = true; + + loader ??= createModuleLoader(true); + + if (esmLoader != null) { + for (const { 0: key, 1: value } of esmLoader.cjsCache) { + // Getting back the values from the mocked loader. + loader.cjsCache.set(key, value); + } + for (let i = 0; i < esmLoader.importRequests.length; i++) { + PromisePrototypeThen( + ReflectApply(loader.import, loader, esmLoader.importRequests[i].arguments), + esmLoader.importRequests[i].resolve, + esmLoader.importRequests[i].reject, + ); + } + } + esmLoader = loader; }, async loadESM(callback) { - esmLoader ??= createModuleLoader(true); + const { esmLoader } = module.exports; try { const userImports = getOptionValue('--import'); if (userImports.length > 0) { diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js index 53cda5737f07c0..1f360e536b9b4d 100644 --- a/lib/internal/process/pre_execution.js +++ b/lib/internal/process/pre_execution.js @@ -120,8 +120,8 @@ function prepareExecution(options) { } function setupUserModules() { - initializeCJSLoader(); initializeESMLoader(); + initializeCJSLoader(); const CJSLoader = require('internal/modules/cjs/loader'); assert(!CJSLoader.hasLoadedAnyUserCJSModule); loadPreloadModules(); diff --git a/test/es-module/test-esm-loader-hooks.mjs b/test/es-module/test-esm-loader-hooks.mjs index 76d55fd7815cdc..7f1a042d5adc0d 100644 --- a/test/es-module/test-esm-loader-hooks.mjs +++ b/test/es-module/test-esm-loader-hooks.mjs @@ -195,4 +195,43 @@ describe('Loader hooks', { concurrency: true }, () => { assert.strictEqual(code, 0); assert.strictEqual(signal, null); }); + + it('should let users require and import along loaders', async () => { + const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ + '--no-warnings', + '--require', + fixtures.path('printA.js'), + '--import', + fixtures.fileURL('printB.js'), + '--experimental-loader', + fixtures.fileURL('empty.js'), + '--eval', + 'setTimeout(() => console.log("C"),99)', + ]); + + assert.strictEqual(stderr, ''); + assert.match(stdout, /^A\r?\nA\r?\nB\r?\nC\r?\n$/); + assert.strictEqual(code, 0); + assert.strictEqual(signal, null); + }); + + it('should let users require and import along loaders with ESM', async () => { + const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ + '--no-warnings', + '--require', + fixtures.path('printA.js'), + '--import', + fixtures.fileURL('printB.js'), + '--experimental-loader', + fixtures.fileURL('empty.js'), + '--input-type=module', + '--eval', + 'setTimeout(() => console.log("C"),99)', + ]); + + assert.strictEqual(stderr, ''); + assert.match(stdout, /^A\r?\nA\r?\nB\r?\nC\r?\n$/); + assert.strictEqual(code, 0); + assert.strictEqual(signal, null); + }); }); diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index 0a671eb95eb6d4..4f66bbd39e2f37 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -93,6 +93,11 @@ const expectedModules = new Set([ 'NativeModule internal/net', 'NativeModule internal/dns/utils', 'NativeModule internal/process/pre_execution', + 'NativeModule internal/modules/esm/loader', + 'NativeModule internal/process/esm_loader', + 'NativeModule internal/modules/esm/assert', + 'NativeModule internal/modules/esm/module_map', + 'NativeModule internal/modules/esm/translators', ]); if (!common.isMainThread) {