diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 651a2fa99cb715..df3fcb13873015 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1036,11 +1036,6 @@ E('ERR_ENCODING_INVALID_ENCODED_DATA', function(encoding, ret) { }, TypeError); E('ERR_ENCODING_NOT_SUPPORTED', 'The "%s" encoding is not supported', RangeError); -E('ERR_ESM_LOADER_REGISTRATION_UNAVAILABLE', 'Programmatically registering custom ESM loaders ' + - 'currently requires at least one custom loader to have been registered via the --experimental-loader ' + - 'flag. A no-op loader registered via CLI is sufficient (for example: `--experimental-loader ' + - '"data:text/javascript,"` with the necessary trailing comma). A future version of Node.js ' + - 'will remove this requirement.', Error); E('ERR_EVAL_ESM_CANNOT_PRINT', '--print cannot be used with ESM input', Error); E('ERR_EVENT_RECURSION', 'The event "%s" is already being dispatched', Error); E('ERR_FALSY_VALUE_REJECTION', function(reason) { diff --git a/lib/internal/modules/esm/initialize_import_meta.js b/lib/internal/modules/esm/initialize_import_meta.js index c548f71bef837a..2188fea74d1a47 100644 --- a/lib/internal/modules/esm/initialize_import_meta.js +++ b/lib/internal/modules/esm/initialize_import_meta.js @@ -2,19 +2,26 @@ const { getOptionValue } = require('internal/options'); const experimentalImportMetaResolve = getOptionValue('--experimental-import-meta-resolve'); +let debug = require('internal/util/debuglog').debuglog('esm', (fn) => { + debug = fn; +}); /** * Generate a function to be used as import.meta.resolve for a particular module. * @param {string} defaultParentUrl The default base to use for resolution - * @param {typeof import('./loader.js').ModuleLoader} loader Reference to the current module loader * @returns {(specifier: string, parentUrl?: string) => string} Function to assign to import.meta.resolve */ -function createImportMetaResolve(defaultParentUrl, loader) { +function createImportMetaResolve(defaultParentUrl) { + debug('createImportMetaResolve(): %o', { defaultParentUrl }); + return function resolve(specifier, parentUrl = defaultParentUrl) { + const moduleLoader = require('internal/process/esm_loader').esmLoader; let url; + debug('import.meta.resolve(%o) %s', { specifier, parentUrl }, moduleLoader.constructor.name); + try { - ({ url } = loader.resolve(specifier, parentUrl)); + ({ url } = moduleLoader.resolve(specifier, parentUrl)); } catch (error) { if (error?.code === 'ERR_UNSUPPORTED_DIR_IMPORT') { ({ url } = error); @@ -31,15 +38,16 @@ function createImportMetaResolve(defaultParentUrl, loader) { * Create the `import.meta` object for a module. * @param {object} meta * @param {{url: string}} context - * @param {typeof import('./loader.js').ModuleLoader} loader Reference to the current module loader * @returns {{url: string, resolve?: Function}} */ -function initializeImportMeta(meta, context, loader) { - const { url } = context; +function initializeImportMeta(meta, { url }) { + debug('initializeImportMeta for %s', url); + + const { isLoaderWorker } = require('internal/modules/esm/utils'); // Alphabetical - if (experimentalImportMetaResolve && loader.loaderType !== 'internal') { - meta.resolve = createImportMetaResolve(url, loader); + if (experimentalImportMetaResolve && !isLoaderWorker()) { + meta.resolve = createImportMetaResolve(url); } meta.url = url; diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index b73ba2eb3c8154..03f7a830ecb449 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -11,16 +11,19 @@ const { } = primordials; const { - ERR_ESM_LOADER_REGISTRATION_UNAVAILABLE, ERR_UNKNOWN_MODULE_FORMAT, } = require('internal/errors').codes; const { getOptionValue } = require('internal/options'); const { pathToFileURL } = require('internal/url'); -const { emitExperimentalWarning } = require('internal/util'); +const { emitExperimentalWarning, kEmptyObject } = require('internal/util'); const { getDefaultConditions, } = require('internal/modules/esm/utils'); -let defaultResolve, defaultLoad, importMetaInitializer; +let defaultResolve; +let defaultLoad; +let debug = require('internal/util/debuglog').debuglog('esm', (fn) => { + debug = fn; +}); function newModuleMap() { const ModuleMap = require('internal/modules/esm/module_map'); @@ -99,22 +102,28 @@ class DefaultModuleLoader { source, url = pathToFileURL(`${process.cwd()}/[eval${++this.evalIndex}]`).href, ) { - const evalInstance = (url) => { - const { ModuleWrap } = internalBinding('module_wrap'); - const { setCallbackForWrap } = require('internal/modules/esm/utils'); - const module = new ModuleWrap(url, undefined, source, 0, 0); - setCallbackForWrap(module, { - initializeImportMeta: (meta, wrap) => this.importMetaInitialize(meta, { url }), - importModuleDynamically: (specifier, { url }, importAssertions) => { - return this.import(specifier, url, importAssertions); - }, - }); - - return module; - }; + const { ModuleWrap } = internalBinding('module_wrap'); + const moduleWrapper = new ModuleWrap(url, undefined, source, 0, 0); + const { setCallbackForWrap } = require('internal/modules/esm/utils'); + setCallbackForWrap(moduleWrapper, { + initializeImportMeta(meta, wrap) { + importMetaInitializer ??= require('internal/modules/esm/initialize_import_meta').initializeImportMeta; + importMetaInitializer(meta, { url }); + }, + importModuleDynamically(specifier, { url }, importAssertions) { + const moduleLoader = require('internal/process/esm_loader').esmLoader; + debug('ModuleLoader::eval::importModuleDynamically(%o)', { specifier, url, moduleLoader }); + return moduleLoader.import(specifier, url, importAssertions); + }, + }); const ModuleJob = require('internal/modules/esm/module_job'); const job = new ModuleJob( - this, url, undefined, evalInstance, false, false); + url, + moduleWrapper, + undefined, + false, // TODO: why isMain false in eval? it is the root module + false, + ); this.moduleMap.set(url, undefined, job); const { module } = await job.run(); @@ -142,19 +151,26 @@ class DefaultModuleLoader { return this.getJobFromResolveResult(resolveResult, parentURL, importAssertions); } - getJobFromResolveResult(resolveResult, parentURL, importAssertions) { + async getJobFromResolveResult( + resolveResult, + parentURL, + importAssertions = resolveResult.importAssertions, + ) { const { url, format } = resolveResult; - const resolvedImportAssertions = resolveResult.importAssertions ?? importAssertions; - - let job = this.moduleMap.get(url, resolvedImportAssertions.type); - // CommonJS will set functions for lazy job evaluation. - if (typeof job === 'function') { - this.moduleMap.set(url, undefined, job = job()); + // `job` may be a Promise, which the engine INSANELY _sometimes_ considers undefined and + // sometimes not. This is why we use `has` instead of `get` to check for its existence. + // ! Do NOT try to check against its value; the engine will **gladly** screw you over. + if (!this.moduleMap.has(url, importAssertions.type)) { + debug('getJobFromResolveResult: did NOT find existing entry for %s', url) + return this.#createModuleJob(url, parentURL, importAssertions, format); } - if (job === undefined) { - job = this.#createModuleJob(url, resolvedImportAssertions, parentURL, format); + let job = this.moduleMap.get(url, importAssertions.type); + debug('getJobFromResolveResult: found existing entry for %s %o', url, job); + + if (typeof job === 'function') { // CommonJS will set functions for lazy job evaluation. + this.moduleMap.set(url, undefined, job = job()); } return job; @@ -163,34 +179,13 @@ class DefaultModuleLoader { /** * Create and cache an object representing a loaded module. * @param {string} url The absolute URL that was resolved for this module - * @param {Record} importAssertions Validations for the - * module import. - * @param {string} [parentURL] The absolute URL of the module importing this - * one, unless this is the Node.js entry point - * @param {string} [format] The format hint possibly returned by the - * `resolve` hook + * @param {string} [parentURL] The absolute URL of the module importing this one, unless this is + * the Node.js entry point + * @param {Record} importAssertions Validations for the module import. + * @param {string} [format] The format hint possibly returned by the `resolve` hook * @returns {Promise} The (possibly pending) module job */ - #createModuleJob(url, importAssertions, parentURL, format) { - const moduleProvider = async (url, isMain) => { - const { - format: finalFormat, - responseURL, - source, - } = await this.load(url, { - format, - importAssertions, - }); - - const translator = getTranslators().get(finalFormat); - - if (!translator) { - throw new ERR_UNKNOWN_MODULE_FORMAT(finalFormat, responseURL); - } - - return FunctionPrototypeCall(translator, this, responseURL, source, isMain); - }; - + async #createModuleJob(url, parentURL, importAssertions, format) { const inspectBrk = ( parentURL === undefined && getOptionValue('--inspect-brk') @@ -200,16 +195,40 @@ class DefaultModuleLoader { process.send({ 'watch:import': [url] }); } + debug('#createModuleJob (before load)', { url, parentURL, importAssertions, format }) + + // ! The module needing creation is not added to ModuleMap before the next `getModuleJob()`, + // ! which is a problem. + const { + format: finalFormat, + responseURL, + source, + } = await this.load(url, { + format, + importAssertions, + }); + + const translator = getTranslators().get(finalFormat); + + if (!translator) { + throw new ERR_UNKNOWN_MODULE_FORMAT(finalFormat, responseURL); + } + + const isMain = parentURL === undefined; + debug('#createModuleJob (loaded, untranslated) %o', { url, finalFormat, responseURL, source }); + const moduleWrapper = FunctionPrototypeCall(translator, this, responseURL, source, isMain); + const ModuleJob = require('internal/modules/esm/module_job'); const job = new ModuleJob( - this, - url, + url, // TODO: should this be resolvedURL? + moduleWrapper, importAssertions, - moduleProvider, - parentURL === undefined, + isMain, inspectBrk, ); + debug('#createModuleJob (translated) %o', { url, job }); + this.moduleMap.set(url, importAssertions.type, job); return job; @@ -224,8 +243,8 @@ class DefaultModuleLoader { * module import. * @returns {Promise} */ - async import(specifier, parentURL, importAssertions) { - const moduleJob = this.getModuleJob(specifier, parentURL, importAssertions); + async import(specifier, parentURL = undefined, importAssertions = { __proto__: null }) { + const moduleJob = await this.getModuleJob(specifier, parentURL, importAssertions); const { module } = await moduleJob.run(); return module.getNamespace(); } @@ -271,12 +290,6 @@ class DefaultModuleLoader { require('internal/modules/esm/load').throwUnknownModuleFormat(url, format); } } - - importMetaInitialize(meta, context) { - importMetaInitializer ??= require('internal/modules/esm/initialize_import_meta').initializeImportMeta; - meta = importMetaInitializer(meta, context, this); - return meta; - } } ObjectSetPrototypeOf(DefaultModuleLoader.prototype, null); @@ -285,10 +298,21 @@ class CustomizedModuleLoader extends DefaultModuleLoader { /** * Instantiate a module loader that uses user-provided custom loader hooks. */ - constructor() { + constructor({ + cjsCache, + evalIndex, + moduleMap, + } = kEmptyObject) { super(); - getHooksProxy(); + if (cjsCache != null) { this.cjsCache = cjsCache; } + if (evalIndex != null) { this.evalIndex = evalIndex; } + if (moduleMap != null) { this.moduleMap = moduleMap; } + + if (!hooksProxy) { + const { HooksProxy } = require('internal/modules/esm/hooks'); + hooksProxy = new HooksProxy(); + } } /** @@ -318,12 +342,12 @@ class CustomizedModuleLoader extends DefaultModuleLoader { async #getModuleJob(specifier, parentURL, importAssertions) { const resolveResult = await hooksProxy.makeAsyncRequest('resolve', specifier, parentURL, importAssertions); - +debug('#getModuleJob', { [specifier]: resolveResult }) return this.getJobFromResolveResult(resolveResult, parentURL, importAssertions); } getModuleJob(specifier, parentURL, importAssertions) { const jobPromise = this.#getModuleJob(specifier, parentURL, importAssertions); - +debug('getModuleJob', { [specifier]: jobPromise }) return { run() { return PromisePrototypeThen(jobPromise, (job) => job.run()); @@ -357,40 +381,29 @@ let emittedExperimentalWarning = false; * A loader instance is used as the main entry point for loading ES modules. Currently, this is a singleton; there is * only one used for loading the main module and everything in its dependency graph, though separate instances of this * class might be instantiated as part of bootstrap for other purposes. - * @param {boolean} useCustomLoadersIfPresent If the user has provided loaders via the --loader flag, use them. + * @param {boolean} forceCustomizedLoaderInMain Ignore whether custom loader(s) have been provided + * via CLI and instantiate a CustomizedModuleLoader instance regardless. * @returns {DefaultModuleLoader | CustomizedModuleLoader} */ -function createModuleLoader(useCustomLoadersIfPresent = true) { - if (useCustomLoadersIfPresent && - // Don't spawn a new worker if we're already in a worker thread created by instantiating CustomizedModuleLoader; - // doing so would cause an infinite loop. - !require('internal/modules/esm/utils').isLoaderWorker()) { - const userLoaderPaths = getOptionValue('--experimental-loader'); - if (userLoaderPaths.length > 0) { +function createModuleLoader(customizationSetup) { + // Don't spawn a new worker if we're already in a worker thread (doing so would cause an infinite loop). + if (!require('internal/modules/esm/utils').isLoaderWorker()) { + if ( + customizationSetup || + getOptionValue('--experimental-loader').length > 0 + ) { if (!emittedExperimentalWarning) { emitExperimentalWarning('Custom ESM Loaders'); emittedExperimentalWarning = true; } - return new CustomizedModuleLoader(); + debug('instantiating CustomizedModuleLoader'); + return new CustomizedModuleLoader(customizationSetup); } } return new DefaultModuleLoader(); } -/** - * Get the HooksProxy instance. If it is not defined, then create a new one. - * @returns {HooksProxy} - */ -function getHooksProxy() { - if (!hooksProxy) { - const { HooksProxy } = require('internal/modules/esm/hooks'); - hooksProxy = new HooksProxy(); - } - - return hooksProxy; -} - /** * Register a single loader programmatically. * @param {string} specifier @@ -405,12 +418,13 @@ function getHooksProxy() { * ``` */ function register(specifier, parentURL = 'data:') { - // TODO: Remove this limitation in a follow-up before `register` is released publicly - if (getOptionValue('--experimental-loader').length < 1) { - throw new ERR_ESM_LOADER_REGISTRATION_UNAVAILABLE(); - } + let moduleLoader = require('internal/process/esm_loader').esmLoader; - const moduleLoader = require('internal/process/esm_loader').esmLoader; + if (!(moduleLoader instanceof CustomizedModuleLoader)) { + debug('register called on DefaultModuleLoader; switching to CustomizedModuleLoader'); + moduleLoader = createModuleLoader(moduleLoader); + require('internal/process/esm_loader').esmLoader = moduleLoader; + } moduleLoader.register(`${specifier}`, parentURL); } @@ -418,6 +432,5 @@ function register(specifier, parentURL = 'data:') { module.exports = { DefaultModuleLoader, createModuleLoader, - getHooksProxy, register, }; diff --git a/lib/internal/modules/esm/module_job.js b/lib/internal/modules/esm/module_job.js index 2cf2813a6dcf7f..f5d2bab5a8e6d2 100644 --- a/lib/internal/modules/esm/module_job.js +++ b/lib/internal/modules/esm/module_job.js @@ -8,7 +8,6 @@ const { ObjectSetPrototypeOf, PromiseResolve, PromisePrototypeThen, - ReflectApply, RegExpPrototypeExec, RegExpPrototypeSymbolReplace, SafePromiseAllReturnArrayLike, @@ -27,6 +26,9 @@ const { } = require('internal/source_map/source_map_cache'); const assert = require('internal/assert'); const resolvedPromise = PromiseResolve(); +let debug = require('internal/util/debuglog').debuglog('esm', (fn) => { + debug = fn; +}); const noop = FunctionPrototype; @@ -45,41 +47,67 @@ 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. */ +/** + * 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, importAssertions = { __proto__: null }, - moduleProvider, isMain, inspectBrk) { - this.loader = loader; + /** + * Deep dependency jobs wrappers are instantiated, and module wrapper is instantiated. + */ + instantiated; + + module; + + /** + * + * @param {URL['href']} url The resolved url of the module. + * @param {ModuleWrap} moduleWrapper + * @param {*} importAssertions + * @param {boolean} isMain Whether this is the main module. + * @param {boolean} inspectBrk Whether to invoke the inspector and break on start. + */ + constructor( + url, + moduleWrapper, + importAssertions = { __proto__: null }, + isMain, + inspectBrk, + ) { this.importAssertions = importAssertions; this.isMain = isMain; this.inspectBrk = inspectBrk; + this.modulePromise = moduleWrapper; - this.module = undefined; - // Expose the promise to the ModuleWrap directly for linking below. - // `this.module` is also filled in below. - this.modulePromise = ReflectApply(moduleProvider, loader, [url, isMain]); + debug('ModuleJob::constructor(%o)', { url, importAssertions, isMain, inspectBrk }); // Wait for the ModuleWrap instance being linked with all dependencies. const link = async () => { this.module = await this.modulePromise; assert(this.module instanceof ModuleWrap); + debug('ModuleJob…link(): %o', { url, module: this.module }); // Explicitly keeping track of dependency jobs is needed in order // to flatten out the dependency graph below in `_instantiate()`, // so that circular dependencies can't cause a deadlock by two of // these `link` callbacks depending on each other. const dependencyJobs = []; - const promises = this.module.link((specifier, assertions) => { - const job = this.loader.getModuleJob(specifier, url, assertions); + debug('this.module.link: %o', { [url]: this.module.link.toString() }); + const promises = this.module.link(async (specifier, assertions) => { + const moduleLoader = require('internal/process/esm_loader').esmLoader; + debug('this.module.link(%o)', { + specifier, + parentURL: url, + assertions, + moduleLoader: moduleLoader.constructor.name + }); + const job = await moduleLoader.getModuleJob(specifier, url, assertions); + debug('this.module.link() job: %o', job); ArrayPrototypePush(dependencyJobs, job); return job.modulePromise; }); - if (promises !== undefined) - await SafePromiseAllReturnVoid(promises); + if (promises !== undefined) await SafePromiseAllReturnVoid(promises); return SafePromiseAllReturnArrayLike(dependencyJobs); }; @@ -88,16 +116,11 @@ class ModuleJob { // This promise is awaited later anyway, so silence // 'unhandled rejection' warnings. PromisePrototypeThen(this.linked, undefined, noop); - - // instantiated == deep dependency jobs wrappers are instantiated, - // and module wrapper is instantiated. - this.instantiated = undefined; } instantiate() { - if (this.instantiated === undefined) { - this.instantiated = this._instantiate(); - } + this.instantiated ??= this._instantiate(); + return this.instantiated; } @@ -128,8 +151,7 @@ class ModuleJob { // stack trace originates in module_job, not the file itself. A hidden // symbol with filename could be set in node_errors.cc to facilitate this. if (!getSourceMapsEnabled() && - StringPrototypeIncludes(e.message, - ' does not provide an export named')) { + StringPrototypeIncludes(e.message, ' does not provide an export named')) { const splitStack = StringPrototypeSplit(e.stack, '\n'); const parentFileUrl = RegExpPrototypeSymbolReplace( /:\d+$/, @@ -138,17 +160,16 @@ class ModuleJob { ); const { 1: childSpecifier, 2: name } = RegExpPrototypeExec( /module '(.*)' does not provide an export named '(.+)'/, - e.message); - const { url: childFileURL } = await this.loader.resolve( - childSpecifier, parentFileUrl, + e.message, ); + const moduleLoader = require('internal/process/esm_loader').esmLoader; + const { url: childFileURL } = await moduleLoader.resolve(childSpecifier, parentFileUrl); let format; try { // This might throw for non-CommonJS modules because we aren't passing // in the import assertions and some formats require them; but we only // care about CommonJS for the purposes of this error message. - ({ format } = - await this.loader.load(childFileURL)); + ({ format } = await moduleLoader.load(childFileURL)); } catch { // Continue regardless of error. } diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index 267d89f1d44730..39659284220e49 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -103,21 +103,26 @@ function errPath(url) { return url; } -async function importModuleDynamically(specifier, { url }, assertions) { - return asyncESM.esmLoader.import(specifier, url, assertions); -} +let importMetaInitializer; // Strategy for loading a standard JavaScript module. translators.set('module', async function moduleStrategy(url, source, isMain) { assertBufferSource(source, true, 'load'); source = stringify(source); maybeCacheSourceMap(url, source); - debug(`Translating StandardModule ${url}`); + debug(`Translating ESModule ${url}`); const module = new ModuleWrap(url, undefined, source, 0, 0); const { setCallbackForWrap } = require('internal/modules/esm/utils'); setCallbackForWrap(module, { - initializeImportMeta: (meta, wrap) => this.importMetaInitialize(meta, { url }), - importModuleDynamically, + initializeImportMeta(meta, wrap) { + importMetaInitializer ??= require('internal/modules/esm/initialize_import_meta').initializeImportMeta; + importMetaInitializer(meta, { url }); + }, + importModuleDynamically(specifier, { url }, importAssertions) { + const moduleLoader = require('internal/process/esm_loader').esmLoader; + debug('Translator("module")::importModuleDynamically(%o)', { specifier, url, moduleLoader }); + return moduleLoader.import(specifier, url, importAssertions); + }, }); return module; }); @@ -142,8 +147,7 @@ function enrichCJSError(err, content, filename) { // Strategy for loading a node-style CommonJS module const isWindows = process.platform === 'win32'; -translators.set('commonjs', async function commonjsStrategy(url, source, - isMain) { +translators.set('commonjs', async function commonjsStrategy(url, source, isMain) { debug(`Translating CJSModule ${url}`); const filename = fileURLToPath(new URL(url)); diff --git a/lib/internal/modules/esm/utils.js b/lib/internal/modules/esm/utils.js index 4e919cd833011c..5e74958403007c 100644 --- a/lib/internal/modules/esm/utils.js +++ b/lib/internal/modules/esm/utils.js @@ -75,7 +75,7 @@ function initializeImportMetaObject(wrap, meta) { if (callbackMap.has(wrap)) { const { initializeImportMeta } = callbackMap.get(wrap); if (initializeImportMeta !== undefined) { - meta = initializeImportMeta(meta, getModuleFromWrap(wrap) || wrap); + meta = initializeImportMeta(meta, (getModuleFromWrap(wrap) || wrap)); } } } @@ -84,8 +84,7 @@ async function importModuleDynamicallyCallback(wrap, specifier, assertions) { if (callbackMap.has(wrap)) { const { importModuleDynamically } = callbackMap.get(wrap); if (importModuleDynamically !== undefined) { - return importModuleDynamically( - specifier, getModuleFromWrap(wrap) || wrap, assertions); + return importModuleDynamically(specifier, (getModuleFromWrap(wrap) || wrap), assertions); } } throw new ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING(); diff --git a/lib/internal/modules/run_main.js b/lib/internal/modules/run_main.js index 0bfe7b11241416..4c522fd579d05b 100644 --- a/lib/internal/modules/run_main.js +++ b/lib/internal/modules/run_main.js @@ -52,8 +52,9 @@ function runMainESM(mainPath) { handleMainPromise(loadESM((esmLoader) => { const main = path.isAbsolute(mainPath) ? - pathToFileURL(mainPath).href : mainPath; - return esmLoader.import(main, undefined, { __proto__: null }); + pathToFileURL(mainPath).href : + mainPath; + return esmLoader.import(main); })); } diff --git a/lib/internal/process/esm_loader.js b/lib/internal/process/esm_loader.js index 9a84ed944e87c4..8d01b1870e6dd1 100644 --- a/lib/internal/process/esm_loader.js +++ b/lib/internal/process/esm_loader.js @@ -11,15 +11,21 @@ const { } = require('internal/process/execution'); const { pathToFileURL } = require('internal/url'); const { kEmptyObject } = require('internal/util'); +let debug = require('internal/util/debuglog').debuglog('esm', (fn) => { + debug = fn; +}); let esmLoader; module.exports = { get esmLoader() { - return esmLoader ??= createModuleLoader(true); + return esmLoader ??= createModuleLoader(); + }, + set esmLoader(supplantingLoader) { + if (supplantingLoader) esmLoader = supplantingLoader; }, async loadESM(callback) { - esmLoader ??= createModuleLoader(true); + esmLoader ??= createModuleLoader(); try { const userImports = getOptionValue('--import'); if (userImports.length > 0) { diff --git a/test/es-module/test-esm-loader-programmatically.mjs b/test/es-module/test-esm-loader-programmatically.mjs index 0c20bbcb7519f8..26d18c6b53f93c 100644 --- a/test/es-module/test-esm-loader-programmatically.mjs +++ b/test/es-module/test-esm-loader-programmatically.mjs @@ -189,19 +189,26 @@ describe('ESM: programmatically register loaders', { concurrency: true }, () => assert.strictEqual(lines[3], ''); }); - it('does not work without dummy CLI loader', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--input-type=module', - '--eval', - "import { register } from 'node:module';" + - commonEvals.register(fixtures.fileURL('es-module-loaders', 'loader-load-passthru.mjs')) + - commonEvals.dynamicImport('console.log("Hello from dynamic import");'), - ]); + it('works without a CLI flag', async () => { + const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ + '--no-warnings', + '--input-type=module', + '--eval', + "import { register } from 'node:module';" + + commonEvals.register(fixtures.fileURL('es-module-loaders', 'loader-load-passthru.mjs')) + + commonEvals.dynamicImport('console.log("Hello from dynamic import");'), + ]); - assert.strictEqual(stdout, ''); - assert.strictEqual(code, 1); - assert.strictEqual(signal, null); - assert.match(stderr, /ERR_ESM_LOADER_REGISTRATION_UNAVAILABLE/); + assert.strictEqual(stderr, ''); + assert.strictEqual(code, 0); + assert.strictEqual(signal, null); + + const lines = stdout.split('\n'); + + assert.match(lines[0], /load passthru/); + assert.match(lines[1], /Hello from dynamic import/); + + assert.strictEqual(lines[2], ''); }); it('does not work with a loader specifier that does not exist', async () => {