From 76f775e0ce4807bb2aa452912745e16539d1a8d4 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 --- lib/internal/modules/esm/hooks.js | 69 +++++- .../modules/esm/initialize_import_meta.js | 12 +- lib/internal/modules/esm/loader.js | 217 ++++++++++-------- lib/internal/modules/esm/utils.js | 41 +--- test/es-module/test-esm-loader-chaining.mjs | 34 +++ test/es-module/test-esm-loader-hooks.mjs | 2 +- .../test-esm-loader-programmatically.mjs | 24 +- .../loader-load-dynamic-import.mjs | 14 ++ .../loader-resolve-dynamic-import.mjs | 14 ++ 9 files changed, 280 insertions(+), 147 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/lib/internal/modules/esm/hooks.js b/lib/internal/modules/esm/hooks.js index cef2897bd68967..c47a4f98b1d2eb 100644 --- a/lib/internal/modules/esm/hooks.js +++ b/lib/internal/modules/esm/hooks.js @@ -81,8 +81,8 @@ let debug = require('internal/util/debuglog').debuglog('esm', (fn) => { // [2] `validate...()`s throw the wrong error -class Hooks { - #chains = { +function getDefaultChains() { + return { /** * Prior to ESM loading. These are called once before any modules are started. * @private @@ -115,9 +115,18 @@ class Hooks { }, ], }; +} + +class Hooks { + #chains; // Cache URLs we've already validated to avoid repeated validation - #validatedUrls = new SafeSet(); + #validatedUrls; + + constructor(chains = getDefaultChains(), validatedUrls = new SafeSet()) { + this.#chains = chains; + this.#validatedUrls = validatedUrls; + } /** * Import and register custom/user-defined module loader hook(s). @@ -125,17 +134,27 @@ class Hooks { * @param {string} parentURL */ async register(urlOrSpecifier, parentURL) { - const moduleLoader = require('internal/process/esm_loader').esmLoader; - - const keyedExports = await moduleLoader.import( + const esmLoader = require('internal/process/esm_loader').esmLoader; + const keyedExports = await esmLoader.import( urlOrSpecifier, parentURL, kEmptyObject, ); - this.addCustomLoader(urlOrSpecifier, keyedExports); } + allowImportMetaResolve() { + return false; + } + + getChains() { + return this.#chains; + } + + getValidatedUrls() { + return this.#validatedUrls; + } + /** * Collect custom/user-defined module loader hook(s). * After all hooks have been collected, the global preload hook(s) must be initialized. @@ -221,15 +240,16 @@ class Hooks { parentURL, importAssertions = { __proto__: null }, ) { + const chain = this.#chains.resolve; throwIfInvalidParentURL(parentURL); - const chain = this.#chains.resolve; const context = { conditions: getDefaultConditions(), importAssertions, parentURL, }; const meta = { + hooks: this, chainFinished: null, context, hookErrIdentifier: '', @@ -346,6 +366,7 @@ class Hooks { async load(url, context = {}) { const chain = this.#chains.load; const meta = { + hooks: this, chainFinished: null, context, hookErrIdentifier: '', @@ -528,7 +549,17 @@ 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') { + // TODO: I do not understand why this is necessary. + // node \ + // --no-warnings --experimental-loader 'data:text/javascript,process.exit(42)' + // ./test/fixtures/empty.js + // Does not trigger `this.#worker.on('exit', process.exit);`. + // I think it is because `makeSyncRequest` keeps waiting to see another + // message and blocks the thread from ANY other activity including the exit. + process.exit(response.message.body); + return; + } const { preloadScripts } = this.#unwrapMessage(response); this.#executePreloadScripts(preloadScripts); } @@ -749,7 +780,25 @@ function nextHookFactory(chain, meta, { validateArgs, validateOutput }) { ObjectAssign(meta.context, context); } - const output = await hook(arg0, meta.context, nextNextHook); + const esmLoader = require('internal/process/esm_loader').esmLoader; + + const chains = meta.hooks.getChains(); + const load = chain === chains.load ? chains.load.slice(0, generatedHookIndex) : chains.load; + const resolve = chain === chains.resolve ? chains.resolve.slice(0, generatedHookIndex) : chains.resolve; + let output; + if (load.length > 0 && resolve.length > 0) { + const nextChains = { + load, + resolve, + globalPreload: chains.globalPreload, + }; + const delegate = new Hooks(nextChains, meta.hooks.getValidatedUrls()); + output = await esmLoader.withDelegate(delegate, () => { + return hook(arg0, meta.context, nextNextHook); + }); + } else { + output = await hook(arg0, meta.context, nextNextHook); + } validateOutput(outputErrIdentifier, output); diff --git a/lib/internal/modules/esm/initialize_import_meta.js b/lib/internal/modules/esm/initialize_import_meta.js index c548f71bef837a..faba59dd479f0b 100644 --- a/lib/internal/modules/esm/initialize_import_meta.js +++ b/lib/internal/modules/esm/initialize_import_meta.js @@ -1,7 +1,14 @@ 'use strict'; +const { Symbol } = primordials; + const { getOptionValue } = require('internal/options'); const experimentalImportMetaResolve = getOptionValue('--experimental-import-meta-resolve'); +const kResolveSync = Symbol('sync'); + +const importAssertions = { + [kResolveSync]: true, +}; /** * Generate a function to be used as import.meta.resolve for a particular module. @@ -14,7 +21,7 @@ function createImportMetaResolve(defaultParentUrl, loader) { let url; try { - ({ url } = loader.resolve(specifier, parentUrl)); + ({ url } = loader.resolve(specifier, parentUrl, importAssertions)); } catch (error) { if (error?.code === 'ERR_UNSUPPORTED_DIR_IMPORT') { ({ url } = error); @@ -38,7 +45,7 @@ function initializeImportMeta(meta, context, loader) { const { url } = context; // Alphabetical - if (experimentalImportMetaResolve && loader.loaderType !== 'internal') { + if (experimentalImportMetaResolve && loader.allowImportMetaResolve()) { meta.resolve = createImportMetaResolve(url, loader); } @@ -49,4 +56,5 @@ function initializeImportMeta(meta, context, loader) { module.exports = { initializeImportMeta, + kResolveSync, }; diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index b73ba2eb3c8154..6dc5a08dd41281 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -20,7 +20,7 @@ const { emitExperimentalWarning } = require('internal/util'); const { getDefaultConditions, } = require('internal/modules/esm/utils'); -let defaultResolve, defaultLoad, importMetaInitializer; +let defaultResolve, defaultLoad, importMetaInitializer, kResolveSync; function newModuleMap() { const ModuleMap = require('internal/modules/esm/module_map'); @@ -52,12 +52,59 @@ let hooksProxy; * @typedef {ArrayBuffer|TypedArray|string} ModuleSource */ +class HooksProxyLoaderDelegate { + /** + * Instantiate a module loader that uses user-provided custom loader hooks. + */ + constructor() { + getHooksProxy(); + } + + /** + * Register some loader specifier. + * @param {string} originalSpecifier The specified URL path of the loader to + * be registered. + * @param {string} parentURL The parent URL from where the loader will be + * registered if using it package name as specifier + * @returns {{ format: string, url: URL['href'] }} + */ + register(originalSpecifier, parentURL) { + return hooksProxy.makeSyncRequest('register', originalSpecifier, parentURL); + } + + /** + * Resolve the location of the module. + * @param {string} originalSpecifier The specified URL path of the module to + * be resolved. + * @param {string} [parentURL] The URL path of the module's parent. + * @param {ImportAssertions} importAssertions Assertions from the import + * statement or expression. + * @returns {{ format: string, url: URL['href'] }} + */ + resolve(originalSpecifier, parentURL, importAssertions) { + if (importAssertions && importAssertions[kResolveSync]) { + return hooksProxy.makeSyncRequest('resolve', originalSpecifier, parentURL, importAssertions); + } + return hooksProxy.makeAsyncRequest('resolve', originalSpecifier, parentURL, importAssertions); + } + + /** + * Provide source that is understood by one of Node's translators. + * @param {URL['href']} url The URL/path of the module to be loaded + * @param {object} [context] Metadata about the module + * @returns {Promise<{ format: ModuleFormat, source: ModuleSource }>} + */ + load(url, context) { + return hooksProxy.makeAsyncRequest('load', url, context); + } +} /** - * 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 + * behaviour you can pass a delegate object and this object will be used + * to do the loading/resolving/registration process. */ -class DefaultModuleLoader { +class ModuleLoader { /** * The conditions for resolving packages if `--conditions` is not used. */ @@ -89,10 +136,20 @@ class DefaultModuleLoader { */ loaderType = 'default'; - constructor() { + /** + * Loader to pass requests to. + */ + #delegate = null; + + constructor(delegate = null) { if (getOptionValue('--experimental-network-imports')) { emitExperimentalWarning('Network Imports'); } + this.#delegate = delegate; + } + + setDelegate(delegate) { + this.#delegate = delegate; } async eval( @@ -138,14 +195,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. @@ -215,6 +286,22 @@ class DefaultModuleLoader { return job; } + /** + * Execute a given function with a specific loader delegate. + * @param {object} delegate The loader machinery to use while executing `fn` + * @param {Function} fn Function to execute. Can be async. + * @returns {*} The result of `fn`. + */ + async withDelegate(delegate, fn) { + const oldDelegate = this.#delegate; + this.#delegate = delegate; + try { + return await fn(); + } finally { + this.#delegate = oldDelegate; + } + } + /** * This method is usually called indirectly as part of the loading processes. * Use directly with caution. @@ -230,6 +317,19 @@ class DefaultModuleLoader { return module.getNamespace(); } + register(specifier, parentUrl) { + if (this.#delegate) { + return this.#delegate.register(specifier, parentUrl); + } + // TODO: Consider flagging this. + const flag = true; // getOptionValue('--experimental-loader-register'); + if (flag) { + this.#delegate = new HooksProxyLoaderDelegate(); + return this.register(specifier, parentUrl); + } + throw new ERR_ESM_LOADER_REGISTRATION_UNAVAILABLE(); + } + /** * Resolve the location of the module. * @param {string} originalSpecifier The specified URL path of the module to @@ -240,6 +340,9 @@ 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 = { @@ -260,12 +363,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 && this.#delegate.allowImportMetaResolve) { + return this.#delegate.allowImportMetaResolve(); + } + return true; + } + validateLoadResult(url, format) { if (format == null) { require('internal/modules/esm/load').throwUnknownModuleFormat(url, format); @@ -274,83 +385,12 @@ class DefaultModuleLoader { importMetaInitialize(meta, context) { importMetaInitializer ??= require('internal/modules/esm/initialize_import_meta').initializeImportMeta; + kResolveSync ??= require('internal/modules/esm/initialize_import_meta').kResolveSync; meta = importMetaInitializer(meta, context, this); return meta; } } -ObjectSetPrototypeOf(DefaultModuleLoader.prototype, null); - - -class CustomizedModuleLoader extends DefaultModuleLoader { - /** - * Instantiate a module loader that uses user-provided custom loader hooks. - */ - constructor() { - super(); - - getHooksProxy(); - } - - /** - * Register some loader specifier. - * @param {string} originalSpecifier The specified URL path of the loader to - * be registered. - * @param {string} parentURL The parent URL from where the loader will be - * registered if using it package name as specifier - * @returns {{ format: string, url: URL['href'] }} - */ - register(originalSpecifier, parentURL) { - return hooksProxy.makeSyncRequest('register', originalSpecifier, parentURL); - } - - /** - * Resolve the location of the module. - * @param {string} originalSpecifier The specified URL path of the module to - * be resolved. - * @param {string} [parentURL] The URL path of the module's parent. - * @param {ImportAssertions} importAssertions Assertions from the import - * statement or expression. - * @returns {{ format: string, url: URL['href'] }} - */ - resolve(originalSpecifier, parentURL, importAssertions) { - return hooksProxy.makeSyncRequest('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); - }, - }; - } - - /** - * Provide source that is understood by one of Node's translators. - * @param {URL['href']} url The URL/path of the module to be loaded - * @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; - } -} - +ObjectSetPrototypeOf(ModuleLoader.prototype, null); let emittedExperimentalWarning = false; /** @@ -361,6 +401,7 @@ let emittedExperimentalWarning = false; * @returns {DefaultModuleLoader | CustomizedModuleLoader} */ 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,11 +412,11 @@ function createModuleLoader(useCustomLoadersIfPresent = true) { emitExperimentalWarning('Custom ESM Loaders'); emittedExperimentalWarning = true; } - return new CustomizedModuleLoader(); + delegate = new HooksProxyLoaderDelegate(); } } - return new DefaultModuleLoader(); + return new ModuleLoader(delegate); } /** @@ -405,18 +446,12 @@ 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, + ModuleLoader, 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-hooks.mjs b/test/es-module/test-esm-loader-hooks.mjs index 4cd1e7297c08d7..2cfe012f953ad2 100644 --- a/test/es-module/test-esm-loader-hooks.mjs +++ b/test/es-module/test-esm-loader-hooks.mjs @@ -203,7 +203,7 @@ describe('Loader hooks', { concurrency: true }, () => { assert.strictEqual(signal, null); }); - it('should not leak internals or expose import.meta.resolve', async () => { + it('should not leak internals', async () => { const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ '--no-warnings', '--experimental-import-meta-resolve', 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); +}