diff --git a/lib/internal/process/esm_loader.js b/lib/internal/process/esm_loader.js index 4634ff5a9f5101..eeb6ad77a7ec88 100644 --- a/lib/internal/process/esm_loader.js +++ b/lib/internal/process/esm_loader.js @@ -40,14 +40,20 @@ async function importModuleDynamicallyCallback(wrap, specifier, assertions) { }; const esmLoader = new ESMLoader(); - exports.esmLoader = esmLoader; +// Module.runMain() causes loadESM() to re-run (which it should do); however, this should NOT cause +// ESM to be re-initialised; doing so causes duplicate custom loaders to be added to the public +// esmLoader. +let isESMInitialized = false; + /** * Causes side-effects: user-defined loader hooks are added to esmLoader. * @returns {void} */ async function initializeLoader() { + if (isESMInitialized) { return; } + const { getOptionValue } = require('internal/options'); const customLoaders = getOptionValue('--experimental-loader'); @@ -75,6 +81,8 @@ async function initializeLoader() { // Hooks must then be added to external/public loader // (so they're triggered in userland) await esmLoader.addCustomLoaders(keyedExportsList); + + isESMInitialized = true; } exports.loadESM = async function loadESM(callback) { diff --git a/test/es-module/test-esm-initialization.mjs b/test/es-module/test-esm-initialization.mjs new file mode 100644 index 00000000000000..ab756c7a3619e1 --- /dev/null +++ b/test/es-module/test-esm-initialization.mjs @@ -0,0 +1,30 @@ +import '../common/index.mjs'; +import * as fixtures from '../common/fixtures.mjs'; +import assert from 'node:assert'; +import { spawnSync } from 'node:child_process'; + + +{ // Verify unadulterated source is loaded when there are no loaders + const { status, stderr, stdout } = spawnSync( + process.execPath, + [ + '--loader', + fixtures.fileURL('es-module-loaders', 'loader-resolve-passthru.mjs'), + '--no-warnings', + fixtures.path('es-modules', 'runmain.mjs'), + ], + { encoding: 'utf8' }, + ); + + // Length minus 1 because the first match is the needle. + const resolveHookRunCount = (stdout.match(/resolve passthru/g)?.length ?? 0) - 1; + + assert.strictEqual(stderr, ''); + /** + * resolveHookRunCount = 2: + * 1. fixtures/…/runmain.mjs + * 2. node:module (imported by fixtures/…/runmain.mjs) + */ + assert.strictEqual(resolveHookRunCount, 2); + assert.strictEqual(status, 0); +} diff --git a/test/fixtures/es-modules/runmain.mjs b/test/fixtures/es-modules/runmain.mjs new file mode 100644 index 00000000000000..5ceb86b66c76ce --- /dev/null +++ b/test/fixtures/es-modules/runmain.mjs @@ -0,0 +1,7 @@ +import { runMain } from 'node:module'; + +try { await import.meta.resolve('doesnt-matter.mjs') } catch {} + +runMain(); + +try { await import.meta.resolve('doesnt-matter.mjs') } catch {}