From c4ca4d57dbe570859c5e2b93d78fb3dd8fbf0826 Mon Sep 17 00:00:00 2001 From: Izaak Schroeder Date: Mon, 26 Jun 2023 00:19:22 -0700 Subject: [PATCH] esm: refactor `DefaultModuleLoader` Fixes https://github.com/nodejs/node/issues/48515 Fixes https://github.com/nodejs/node/pull/48439 --- doc/api/errors.md | 17 --- lib/internal/errors.js | 5 - lib/internal/modules/esm/hooks.js | 56 ++++---- .../modules/esm/initialize_import_meta.js | 6 +- lib/internal/modules/esm/loader.js | 128 +++++++++++------- lib/internal/modules/esm/utils.js | 41 +----- test/es-module/test-esm-loader-chaining.mjs | 34 +++++ .../test-esm-loader-programmatically.mjs | 24 +++- .../loader-load-dynamic-import.mjs | 14 ++ .../loader-resolve-dynamic-import.mjs | 14 ++ 10 files changed, 195 insertions(+), 144 deletions(-) create mode 100644 test/fixtures/es-module-loaders/loader-load-dynamic-import.mjs create mode 100644 test/fixtures/es-module-loaders/loader-resolve-dynamic-import.mjs diff --git a/doc/api/errors.md b/doc/api/errors.md index 3e338a4e8f8cd6..5a8637a0ecc8b0 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -1233,23 +1233,6 @@ provided. Encoding provided to `TextDecoder()` API was not one of the [WHATWG Supported Encodings][]. - - -### `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,`; -do not omit the necessary trailing comma). -A future version of Node.js will support the programmatic -registration of loaders without needing to also use the flag. - ### `ERR_EVAL_ESM_CANNOT_PRINT` diff --git a/lib/internal/errors.js b/lib/internal/errors.js index a7120564c468fb..2206994e9277ee 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/hooks.js b/lib/internal/modules/esm/hooks.js index cef2897bd68967..89160847f854a1 100644 --- a/lib/internal/modules/esm/hooks.js +++ b/lib/internal/modules/esm/hooks.js @@ -80,7 +80,6 @@ let debug = require('internal/util/debuglog').debuglog('esm', (fn) => { // [2] `validate...()`s throw the wrong error - class Hooks { #chains = { /** @@ -126,16 +125,18 @@ class Hooks { */ async register(urlOrSpecifier, parentURL) { const moduleLoader = require('internal/process/esm_loader').esmLoader; - const keyedExports = await moduleLoader.import( urlOrSpecifier, parentURL, kEmptyObject, ); - this.addCustomLoader(urlOrSpecifier, keyedExports); } + allowImportMetaResolve() { + return false; + } + /** * Collect custom/user-defined module loader hook(s). * After all hooks have been collected, the global preload hook(s) must be initialized. @@ -150,13 +151,16 @@ class Hooks { } = pluckHooks(exports); if (globalPreload) { - ArrayPrototypePush(this.#chains.globalPreload, { fn: globalPreload, url }); + const next = this.#chains.globalPreload[this.#chains.globalPreload.length - 1]; + ArrayPrototypePush(this.#chains.globalPreload, { fn: globalPreload, url, next }); } if (resolve) { - ArrayPrototypePush(this.#chains.resolve, { fn: resolve, url }); + const next = this.#chains.resolve[this.#chains.resolve.length - 1]; + ArrayPrototypePush(this.#chains.resolve, { fn: resolve, url, next }); } if (load) { - ArrayPrototypePush(this.#chains.load, { fn: load, url }); + const next = this.#chains.load[this.#chains.load.length - 1]; + ArrayPrototypePush(this.#chains.load, { fn: load, url, next }); } } @@ -233,7 +237,6 @@ class Hooks { chainFinished: null, context, hookErrIdentifier: '', - hookIndex: chain.length - 1, hookName: 'resolve', shortCircuited: false, }; @@ -256,7 +259,7 @@ class Hooks { } }; - const nextResolve = nextHookFactory(chain, meta, { validateArgs, validateOutput }); + const nextResolve = nextHookFactory(chain[chain.length - 1], meta, { validateArgs, validateOutput }); const resolution = await nextResolve(originalSpecifier, context); const { hookErrIdentifier } = meta; // Retrieve the value after all settled @@ -333,6 +336,10 @@ class Hooks { }; } + resolveSync(_originalSpecifier, _parentURL, _importAssertions) { + throw new Error('`resolveSync` is not supported.'); + } + /** * Provide source that is understood by one of Node's translators. * @@ -349,7 +356,6 @@ class Hooks { chainFinished: null, context, hookErrIdentifier: '', - hookIndex: chain.length - 1, hookName: 'load', shortCircuited: false, }; @@ -391,7 +397,7 @@ class Hooks { } }; - const nextLoad = nextHookFactory(chain, meta, { validateArgs, validateOutput }); + const nextLoad = nextHookFactory(chain[chain.length - 1], meta, { validateArgs, validateOutput }); const loaded = await nextLoad(url, context); const { hookErrIdentifier } = meta; // Retrieve the value after all settled @@ -528,7 +534,10 @@ class HooksProxy { debug('wait for signal from worker'); AtomicsWait(this.#lock, WORKER_TO_MAIN_THREAD_NOTIFICATION, 0); const response = this.#worker.receiveMessageSync(); - if (response.message.status === 'exit') { return; } + if (response.message.status === 'exit') { + process.exit(response.message.body); + return; + } const { preloadScripts } = this.#unwrapMessage(response); this.#executePreloadScripts(preloadScripts); } @@ -684,15 +693,13 @@ function pluckHooks({ * A utility function to iterate through a hook chain, track advancement in the * chain, and generate and supply the `next` argument to the custom * hook. - * @param {KeyedHook[]} chain The whole hook chain. + * @param {Hook} current The first hook in the chain. * @param {object} meta Properties that change as the current hook advances * along the chain. * @param {boolean} meta.chainFinished Whether the end of the chain has been * reached AND invoked. * @param {string} meta.hookErrIdentifier A user-facing identifier to help * pinpoint where an error occurred. Ex "file:///foo.mjs 'resolve'". - * @param {number} meta.hookIndex A non-negative integer tracking the current - * position in the hook chain. * @param {string} meta.hookName The kind of hook the chain is (ex 'resolve') * @param {boolean} meta.shortCircuited Whether a hook signaled a short-circuit. * @param {(hookErrIdentifier, hookArgs) => void} validate A wrapper function @@ -700,13 +707,14 @@ function pluckHooks({ * validation within MUST throw. * @returns {function next(...hookArgs)} The next hook in the chain. */ -function nextHookFactory(chain, meta, { validateArgs, validateOutput }) { +function nextHookFactory(current, meta, { validateArgs, validateOutput }) { // First, prepare the current const { hookName } = meta; const { fn: hook, url: hookFilePath, - } = chain[meta.hookIndex]; + next, + } = current; // ex 'nextResolve' const nextHookName = `next${ @@ -714,16 +722,9 @@ function nextHookFactory(chain, meta, { validateArgs, validateOutput }) { StringPrototypeSlice(hookName, 1) }`; - // When hookIndex is 0, it's reached the default, which does not call next() - // so feed it a noop that blows up if called, so the problem is obvious. - const generatedHookIndex = meta.hookIndex; let nextNextHook; - if (meta.hookIndex > 0) { - // Now, prepare the next: decrement the pointer so the next call to the - // factory generates the next link in the chain. - meta.hookIndex--; - - nextNextHook = nextHookFactory(chain, meta, { validateArgs, validateOutput }); + if (next) { + nextNextHook = nextHookFactory(next, meta, { validateArgs, validateOutput }); } else { // eslint-disable-next-line func-name-matching nextNextHook = function chainAdvancedTooFar() { @@ -740,17 +741,16 @@ function nextHookFactory(chain, meta, { validateArgs, validateOutput }) { validateArgs(`${meta.hookErrIdentifier} hook's ${nextHookName}()`, arg0, context); - const outputErrIdentifier = `${chain[generatedHookIndex].url} '${hookName}' hook's ${nextHookName}()`; + const outputErrIdentifier = `${hookFilePath} '${hookName}' hook's ${nextHookName}()`; // Set when next is actually called, not just generated. - if (generatedHookIndex === 0) { meta.chainFinished = true; } + if (!next) { meta.chainFinished = true; } if (context) { // `context` has already been validated, so no fancy check needed. ObjectAssign(meta.context, context); } const output = await hook(arg0, meta.context, nextNextHook); - validateOutput(outputErrIdentifier, output); if (output?.shortCircuit === true) { meta.shortCircuited = true; } diff --git a/lib/internal/modules/esm/initialize_import_meta.js b/lib/internal/modules/esm/initialize_import_meta.js index c548f71bef837a..56ceaa63b0eac8 100644 --- a/lib/internal/modules/esm/initialize_import_meta.js +++ b/lib/internal/modules/esm/initialize_import_meta.js @@ -1,5 +1,7 @@ 'use strict'; +const { Symbol } = primordials; + const { getOptionValue } = require('internal/options'); const experimentalImportMetaResolve = getOptionValue('--experimental-import-meta-resolve'); @@ -14,7 +16,7 @@ function createImportMetaResolve(defaultParentUrl, loader) { let url; try { - ({ url } = loader.resolve(specifier, parentUrl)); + ({ url } = loader.resolveSync(specifier, parentUrl)); } catch (error) { if (error?.code === 'ERR_UNSUPPORTED_DIR_IMPORT') { ({ url } = error); @@ -38,7 +40,7 @@ function initializeImportMeta(meta, context, loader) { const { url } = context; // Alphabetical - if (experimentalImportMetaResolve && loader.loaderType !== 'internal') { + if (experimentalImportMetaResolve && loader.allowImportMetaResolve()) { meta.resolve = createImportMetaResolve(url, loader); } diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index b73ba2eb3c8154..5185bad356eb17 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -11,7 +11,6 @@ const { } = primordials; const { - ERR_ESM_LOADER_REGISTRATION_UNAVAILABLE, ERR_UNKNOWN_MODULE_FORMAT, } = require('internal/errors').codes; const { getOptionValue } = require('internal/options'); @@ -52,10 +51,10 @@ let hooksProxy; * @typedef {ArrayBuffer|TypedArray|string} ModuleSource */ - /** - * This class covers the default case of an module loader instance where no custom user loaders are used. - * The below CustomizedModuleLoader class extends this one to support custom user loader hooks. + * This class covers the base machinery of module loading. To add custom + * behavior you can pass a delegate object and this object will be used + * to do the loading/resolving/registration process. */ class DefaultModuleLoader { /** @@ -89,10 +88,20 @@ class DefaultModuleLoader { */ loaderType = 'default'; - constructor() { + /** + * Loader to pass requests to. + */ + #delegate; + + constructor(delegate) { if (getOptionValue('--experimental-network-imports')) { emitExperimentalWarning('Network Imports'); } + this.#delegate = delegate; + } + + setDelegate(delegate) { + this.#delegate = delegate; } async eval( @@ -138,14 +147,28 @@ class DefaultModuleLoader { * @returns {ModuleJob} The (possibly pending) module job */ getModuleJob(specifier, parentURL, importAssertions) { - const resolveResult = this.resolve(specifier, parentURL, importAssertions); + const jobPromise = this.#getModuleJob(specifier, parentURL, importAssertions); + return { + run() { + return PromisePrototypeThen(jobPromise, (job) => job.run()); + }, + get modulePromise() { + return PromisePrototypeThen(jobPromise, (job) => job.modulePromise); + }, + get linked() { + return PromisePrototypeThen(jobPromise, (job) => job.linked); + }, + }; + } + + async #getModuleJob(specifier, parentURL, importAssertions) { + const resolveResult = await this.resolve(specifier, parentURL, importAssertions); return this.getJobFromResolveResult(resolveResult, parentURL, importAssertions); } getJobFromResolveResult(resolveResult, parentURL, 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. @@ -230,6 +253,14 @@ class DefaultModuleLoader { return module.getNamespace(); } + register(specifier, parentUrl) { + if (this.#delegate) { + return this.#delegate.register(specifier, parentUrl); + } + this.#delegate = new CustomizedModuleLoader(); + return this.register(specifier, parentUrl); + } + /** * Resolve the location of the module. * @param {string} originalSpecifier The specified URL path of the module to @@ -240,6 +271,26 @@ class DefaultModuleLoader { * @returns {{ format: string, url: URL['href'] }} */ resolve(originalSpecifier, parentURL, importAssertions) { + if (this.#delegate) { + return this.#delegate.resolve(originalSpecifier, parentURL, importAssertions); + } + defaultResolve ??= require('internal/modules/esm/resolve').defaultResolve; + + const context = { + __proto__: null, + conditions: this.#defaultConditions, + importAssertions, + parentURL, + }; + + return defaultResolve(originalSpecifier, context); + } + + resolveSync(originalSpecifier, parentURL, importAssertions) { + if (this.#delegate) { + return this.#delegate.resolveSync(originalSpecifier, parentURL, importAssertions); + } + defaultResolve ??= require('internal/modules/esm/resolve').defaultResolve; const context = { @@ -260,12 +311,20 @@ class DefaultModuleLoader { */ async load(url, context) { defaultLoad ??= require('internal/modules/esm/load').defaultLoad; - - const result = await defaultLoad(url, context); + const result = this.#delegate ? + await this.#delegate.load(url, context) : + await defaultLoad(url, context); this.validateLoadResult(url, result?.format); return result; } + allowImportMetaResolve() { + if (this.#delegate?.allowImportMetaResolve) { + return this.#delegate.allowImportMetaResolve(); + } + return true; + } + validateLoadResult(url, format) { if (format == null) { require('internal/modules/esm/load').throwUnknownModuleFormat(url, format); @@ -280,14 +339,11 @@ class DefaultModuleLoader { } ObjectSetPrototypeOf(DefaultModuleLoader.prototype, null); - -class CustomizedModuleLoader extends DefaultModuleLoader { +class CustomizedModuleLoader { /** * Instantiate a module loader that uses user-provided custom loader hooks. */ constructor() { - super(); - getHooksProxy(); } @@ -313,28 +369,11 @@ class CustomizedModuleLoader extends DefaultModuleLoader { * @returns {{ format: string, url: URL['href'] }} */ resolve(originalSpecifier, parentURL, importAssertions) { - return hooksProxy.makeSyncRequest('resolve', originalSpecifier, parentURL, importAssertions); + return hooksProxy.makeAsyncRequest('resolve', originalSpecifier, parentURL, importAssertions); } - async #getModuleJob(specifier, parentURL, importAssertions) { - const resolveResult = await hooksProxy.makeAsyncRequest('resolve', specifier, parentURL, importAssertions); - - return this.getJobFromResolveResult(resolveResult, parentURL, importAssertions); - } - getModuleJob(specifier, parentURL, importAssertions) { - const jobPromise = this.#getModuleJob(specifier, parentURL, importAssertions); - - return { - run() { - return PromisePrototypeThen(jobPromise, (job) => job.run()); - }, - get modulePromise() { - return PromisePrototypeThen(jobPromise, (job) => job.modulePromise); - }, - get linked() { - return PromisePrototypeThen(jobPromise, (job) => job.linked); - }, - }; + resolveSync(originalSpecifier, parentURL, importAssertions) { + return hooksProxy.makeSyncRequest('resolve', originalSpecifier, parentURL, importAssertions); } /** @@ -343,24 +382,21 @@ class CustomizedModuleLoader extends DefaultModuleLoader { * @param {object} [context] Metadata about the module * @returns {Promise<{ format: ModuleFormat, source: ModuleSource }>} */ - async load(url, context) { - const result = await hooksProxy.makeAsyncRequest('load', url, context); - this.validateLoadResult(url, result?.format); - - return result; + load(url, context) { + return hooksProxy.makeAsyncRequest('load', url, context); } } - 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. - * @returns {DefaultModuleLoader | CustomizedModuleLoader} + * @returns {DefaultModuleLoader} */ function createModuleLoader(useCustomLoadersIfPresent = true) { + let delegate = null; 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. @@ -371,13 +407,14 @@ function createModuleLoader(useCustomLoadersIfPresent = true) { emitExperimentalWarning('Custom ESM Loaders'); emittedExperimentalWarning = true; } - return new CustomizedModuleLoader(); + delegate = new CustomizedModuleLoader(); } } - return new DefaultModuleLoader(); + return new DefaultModuleLoader(delegate); } + /** * Get the HooksProxy instance. If it is not defined, then create a new one. * @returns {HooksProxy} @@ -405,18 +442,11 @@ 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(); - } - const moduleLoader = require('internal/process/esm_loader').esmLoader; - moduleLoader.register(`${specifier}`, parentURL); } module.exports = { - DefaultModuleLoader, createModuleLoader, getHooksProxy, register, diff --git a/lib/internal/modules/esm/utils.js b/lib/internal/modules/esm/utils.js index 4e919cd833011c..8bbbbe76f18291 100644 --- a/lib/internal/modules/esm/utils.js +++ b/lib/internal/modules/esm/utils.js @@ -2,7 +2,6 @@ const { ArrayIsArray, - PromisePrototypeThen, SafeSet, SafeWeakMap, ObjectFreeze, @@ -14,7 +13,6 @@ const { } = require('internal/errors').codes; const { getOptionValue } = require('internal/options'); const { pathToFileURL } = require('internal/url'); -const { kEmptyObject } = require('internal/util'); const { setImportModuleDynamicallyCallback, setInitializeImportMetaObjectCallback, @@ -120,46 +118,17 @@ async function initializeHooks() { const { Hooks } = require('internal/modules/esm/hooks'); + const esmLoader = require('internal/process/esm_loader').esmLoader; + const hooks = new Hooks(); + esmLoader.setDelegate(hooks); - const { DefaultModuleLoader } = require('internal/modules/esm/loader'); - class ModuleLoader extends DefaultModuleLoader { - loaderType = 'internal'; - async #getModuleJob(specifier, parentURL, importAssertions) { - const resolveResult = await hooks.resolve(specifier, parentURL, importAssertions); - return this.getJobFromResolveResult(resolveResult, parentURL, importAssertions); - } - getModuleJob(specifier, parentURL, importAssertions) { - const jobPromise = this.#getModuleJob(specifier, parentURL, importAssertions); - return { - run() { - return PromisePrototypeThen(jobPromise, (job) => job.run()); - }, - get modulePromise() { - return PromisePrototypeThen(jobPromise, (job) => job.modulePromise); - }, - get linked() { - return PromisePrototypeThen(jobPromise, (job) => job.linked); - }, - }; - } - load(url, context) { return hooks.load(url, context); } - } - const privateModuleLoader = new ModuleLoader(); const parentURL = pathToFileURL(cwd).href; - - // TODO(jlenon7): reuse the `Hooks.register()` method for registering loaders. for (let i = 0; i < customLoaderURLs.length; i++) { - const customLoaderURL = customLoaderURLs[i]; - - // Importation must be handled by internal loader to avoid polluting user-land - const keyedExports = await privateModuleLoader.import( - customLoaderURL, + await hooks.register( + customLoaderURLs[i], parentURL, - kEmptyObject, ); - - hooks.addCustomLoader(customLoaderURL, keyedExports); } const preloadScripts = hooks.initializeGlobalPreload(); diff --git a/test/es-module/test-esm-loader-chaining.mjs b/test/es-module/test-esm-loader-chaining.mjs index 0f67d71ece0aa4..b43ac740500cd8 100644 --- a/test/es-module/test-esm-loader-chaining.mjs +++ b/test/es-module/test-esm-loader-chaining.mjs @@ -470,4 +470,38 @@ describe('ESM: loader chaining', { concurrency: true }, () => { assert.match(stderr, /'load' hook's nextLoad\(\) context/); assert.strictEqual(code, 1); }); + + it('should allow loaders to influence subsequent loader `import()` calls in `resolve`', async () => { + const { code, stderr, stdout } = await spawnPromisified( + execPath, + [ + '--loader', + fixtures.fileURL('es-module-loaders', 'loader-resolve-strip-xxx.mjs'), + '--loader', + fixtures.fileURL('es-module-loaders', 'loader-resolve-dynamic-import.mjs'), + ...commonArgs, + ], + { encoding: 'utf8' }, + ); + assert.strictEqual(stderr, ''); + assert.match(stdout, /resolve dynamic import/); // It did go thru resolve-dynamic + assert.strictEqual(code, 0); + }); + + it('should allow loaders to influence subsequent loader `import()` calls in `load`', async () => { + const { code, stderr, stdout } = await spawnPromisified( + execPath, + [ + '--loader', + fixtures.fileURL('es-module-loaders', 'loader-resolve-strip-xxx.mjs'), + '--loader', + fixtures.fileURL('es-module-loaders', 'loader-load-dynamic-import.mjs'), + ...commonArgs, + ], + { encoding: 'utf8' }, + ); + assert.strictEqual(stderr, ''); + assert.match(stdout, /load dynamic import/); // It did go thru load-dynamic + assert.strictEqual(code, 0); + }); }); diff --git a/test/es-module/test-esm-loader-programmatically.mjs b/test/es-module/test-esm-loader-programmatically.mjs index 0c20bbcb7519f8..43569e3366d458 100644 --- a/test/es-module/test-esm-loader-programmatically.mjs +++ b/test/es-module/test-esm-loader-programmatically.mjs @@ -182,15 +182,19 @@ describe('ESM: programmatically register loaders', { concurrency: true }, () => const lines = stdout.split('\n'); + // Resolve occurs twice because it is first used to resolve the `load` loader + // _AND THEN_ the `register` module. assert.match(lines[0], /resolve passthru/); - assert.match(lines[1], /load passthru/); - assert.match(lines[2], /Hello from dynamic import/); + assert.match(lines[1], /resolve passthru/); + assert.match(lines[2], /load passthru/); + assert.match(lines[3], /Hello from dynamic import/); - assert.strictEqual(lines[3], ''); + assert.strictEqual(lines[4], ''); }); - it('does not work without dummy CLI loader', async () => { + 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';" + @@ -198,10 +202,16 @@ describe('ESM: programmatically register loaders', { concurrency: true }, () => commonEvals.dynamicImport('console.log("Hello from dynamic import");'), ]); - assert.strictEqual(stdout, ''); - assert.strictEqual(code, 1); + assert.strictEqual(stderr, ''); + assert.strictEqual(code, 0); assert.strictEqual(signal, null); - assert.match(stderr, /ERR_ESM_LOADER_REGISTRATION_UNAVAILABLE/); + + 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 () => { diff --git a/test/fixtures/es-module-loaders/loader-load-dynamic-import.mjs b/test/fixtures/es-module-loaders/loader-load-dynamic-import.mjs new file mode 100644 index 00000000000000..96af5507d17212 --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-load-dynamic-import.mjs @@ -0,0 +1,14 @@ +import { writeSync } from 'node:fs'; + + +export async function load(url, context, next) { + if (url === 'node:fs' || url.includes('loader')) { + return next(url); + } + + // Here for asserting dynamic import + await import('xxx/loader-load-passthru.mjs'); + + writeSync(1, 'load dynamic import' + '\n'); // Signal that this specific hook ran + return next(url, context); +} diff --git a/test/fixtures/es-module-loaders/loader-resolve-dynamic-import.mjs b/test/fixtures/es-module-loaders/loader-resolve-dynamic-import.mjs new file mode 100644 index 00000000000000..edc2303ed9aa9e --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-resolve-dynamic-import.mjs @@ -0,0 +1,14 @@ +import { writeSync } from 'node:fs'; + + +export async function resolve(specifier, context, next) { + if (specifier === 'node:fs' || specifier.includes('loader')) { + return next(specifier); + } + + // Here for asserting dynamic import + await import('xxx/loader-resolve-passthru.mjs'); + + writeSync(1, 'resolve dynamic import' + '\n'); // Signal that this specific hook ran + return next(specifier); +}