From 820b7104c528c7a0d6613750cf219fdb9920dec1 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Sun, 17 Jul 2022 19:38:43 +0200 Subject: [PATCH] esm: fix erroneous re-initialization of ESMLoader PR-URL: https://github.com/nodejs/node/pull/43763 Reviewed-By: Geoffrey Booth Reviewed-By: Minwoo Jung Reviewed-By: Guy Bedford --- lib/internal/process/esm_loader.js | 10 +++++++- test/es-module/test-esm-initialization.mjs | 30 ++++++++++++++++++++++ test/fixtures/es-modules/runmain.mjs | 7 +++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 test/es-module/test-esm-initialization.mjs create mode 100644 test/fixtures/es-modules/runmain.mjs 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 {}