current commit
261017.7 · module: fix require(esm) on the module.register() hooks thread
'use strict';

const {
  ArrayPrototypeJoin,
  ArrayPrototypeMap,
  ArrayPrototypeReduce,
  FunctionPrototypeCall,
  JSONStringify,
  ObjectSetPrototypeOf,
  Promise,
  PromisePrototypeThen,
  RegExpPrototypeSymbolReplace,
  encodeURIComponent,
  globalThis,
  hardenRegExp,
} = primordials;

const { LoadCache, ResolveCache } = require('internal/modules/esm/module_map');
const { ModuleJob, ModuleJobSync } = require('internal/modules/esm/module_job');
// This is needed to avoid cycles in esm/resolve <-> cjs/loader
const {
  kIsExecuting,
  kRequiredModuleSymbol,
} = require('internal/modules/cjs/loader');
const { imported_cjs_symbol } = internalBinding('symbols');
const {
  privateSymbols: {
    entry_point_module_private_symbol,
  },
} = internalBinding('util');

const assert = require('internal/assert');
const {
  ERR_REQUIRE_CYCLE_MODULE,
  ERR_REQUIRE_ESM,
  ERR_REQUIRE_ESM_RACE_CONDITION,
  ERR_UNKNOWN_MODULE_FORMAT,
} = require('internal/errors').codes;
const { getOptionValue } = require('internal/options');
const { isURL, pathToFileURL } = require('internal/url');
const {
  getDeprecationWarningEmitter,
  kEmptyObject,
} = require('internal/util');
const {
  compileSourceTextModule,
  SourceTextModuleTypes: { kUser },
  getDefaultConditions,
  shouldSpawnLoaderHookWorker,
  requestTypes: { kImportInRequiredESM, kRequireInImportedCJS, kImportInImportedESM },
} = require('internal/modules/esm/utils');
/**
 * @typedef {import('./utils.js').ModuleRequestType} ModuleRequestType
 */
const { kImplicitTypeAttribute } = require('internal/modules/esm/assert');
const {
  ModuleWrap,
  kEvaluated,
  kEvaluating,
  kEvaluationPhase,
  kInstantiated,
  kUninstantiated,
  kErrored,
  kSourcePhase,
  throwIfPromiseRejected,
  setImportMetaResolveInitializer,
} = internalBinding('module_wrap');
const {
  urlToFilename,
} = require('internal/modules/helpers');
const {
  resolveHooks: syncResolveHooks,
  resolveWithHooks: resolveWithSyncHooks,
  loadHooks: syncLoadHooks,
  loadWithHooks: loadWithSyncHooks,
  validateLoadSloppy,
} = require('internal/modules/customization_hooks');

const { tracingChannel } = require('diagnostics_channel');
const onImport = tracingChannel('module.import');

let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
  debug = fn;
});

const { isPromise } = require('internal/util/types');

/**
 * @typedef {import('./hooks.js').AsyncLoaderHookWorker} AsyncLoaderHookWorker
 * @typedef {import('./module_job.js').ModuleJobBase} ModuleJobBase
 * @typedef {import('url').URL} URL
 */

const { translators } = require('internal/modules/esm/translators');
const { defaultResolve } = require('internal/modules/esm/resolve');
const { defaultLoadSync, throwUnknownModuleFormat } = require('internal/modules/esm/load');

/**
 * @typedef {import('../cjs/loader.js').Module} CJSModule
 */

/**
 * @typedef {Record<string, any>} ModuleExports
 */

/**
 * @typedef {'builtin'|'commonjs'|'json'|'module'|'text'|'wasm'} ModuleFormat
 */

/**
 * @typedef {ArrayBuffer|TypedArray|string} ModuleSource
 */

/**
 * @typedef {{format: string, url: string, isResolvedBySyncHooks: boolean}} ResolveResult
 */

/**
 * @typedef {{
 *   url: string,
 *   format: ModuleFormat,
 *   source: ModuleSource,
 *   responseURL?: string,
 *   translatorKey: string,
 *   isResolvedBySyncHooks: boolean,
 *   isSourceLoadedSynchronously: boolean,
 * }} TranslateContext
 */

/**
 * @typedef {import('./hooks.js').AsyncLoaderHooks} AsyncLoaderHooks
 * @typedef {import('./hooks.js').AsyncLoaderHooksOnLoaderHookWorker} AsyncLoaderHooksOnLoaderHookWorker
 * @typedef {import('./hooks.js').AsyncLoaderHooksProxiedToLoaderHookWorker} AsyncLoaderHooksProxiedToLoaderHookWorker
 */

/**
 * This class covers the base machinery of module loading. There are two types of loader hooks:
 * 1. Asynchronous loader hooks, which are run in a separate loader hook worker thread.
 *    This is configured in #asyncLoaderHooks.
 * 2. Synchronous loader hooks, which are run in-thread. This is shared with the CJS loader and is
 *    stored in the cross-module syncResolveHooks and syncLoadHooks arrays.
 */
class ModuleLoader {
  /**
   * The conditions for resolving packages if `--conditions` is not used.
   */
  #defaultConditions = getDefaultConditions();

  /**
   * Registry of resolved specifiers
   */
  #resolveCache = new ResolveCache();

  /**
   * Registry of loaded modules, akin to `require.cache`
   */
  loadCache = new LoadCache();

  /**
   * @see {AsyncLoaderHooks.isForAsyncLoaderHookWorker}
   * Shortcut to this.#asyncLoaderHooks.isForAsyncLoaderHookWorker.
   */
  isForAsyncLoaderHookWorker = false;

  /**
   * Asynchronous loader hooks to pass requests to.
   *
   * Note that this value _MUST_ be set with `#setAsyncLoaderHooks`
   * because it needs to copy `#asyncLoaderHooks.isForAsyncLoaderHookWorker`
   * to this property.
   * TODO(joyeecheung): this was a legacy of the previous setup of import.meta.resolve
   * configuration; put this information in the environment directly instead.
   *
   * When the ModuleLoader is created on a loader hook thread, this is
   * {@link AsyncLoaderHooksOnLoaderHookWorker}, and its methods directly call out
   * to loader methods. Otherwise, this is {@link AsyncLoaderHooksProxiedToLoaderHookWorker},
   * and its methods post messages to the loader thread and possibly block on it.
   * @see {ModuleLoader.#setAsyncLoaderHooks}
   * @type {AsyncLoaderHooks}
   */
  #asyncLoaderHooks;

  constructor(asyncLoaderHooks) {
    this.#setAsyncLoaderHooks(asyncLoaderHooks);
  }

  /**
   * Change the currently activate async loader hooks for this module
   * loader to be the provided `AsyncLoaderHooks`.
   *
   * If present, this class customizes its core functionality to the
   * `AsyncLoaderHooks` object, including registration, loading, and resolving.
   * There are some responsibilities that this class _always_ takes
   * care of, like validating outputs, so that the AsyncLoaderHooks object
   * does not have to do so.
   *
   * Calling this function alters how modules are loaded and should be
   * invoked with care.
   * @param {AsyncLoaderHooks} asyncLoaderHooks
   */
  #setAsyncLoaderHooks(asyncLoaderHooks) {
    this.#asyncLoaderHooks = asyncLoaderHooks;
    if (asyncLoaderHooks) {
      this.isForAsyncLoaderHookWorker = asyncLoaderHooks.isForAsyncLoaderHookWorker;
    } else {
      this.isForAsyncLoaderHookWorker = false;
    }
  }

  /**
   *
   * @param {string} source Source code of the module.
   * @param {string} url URL of the module.
   * @param {{ isMain?: boolean }|undefined} context - context object containing module metadata.
   * @returns {object} The module wrap object.
   */
  createModuleWrap(source, url, context = kEmptyObject) {
    return compileSourceTextModule(url, source, kUser, context);
  }

  /**
   *
   * @param {string} url URL of the module.
   * @param {object} wrap Module wrap object.
   * @param {boolean} isEntryPoint Whether the module is the entry point.
   * @returns {Promise<object>} The module object.
   */
  async executeModuleJob(url, wrap, isEntryPoint = false) {
    const module = await onImport.tracePromise(async () => {
      const job = new ModuleJob(this, url, undefined, wrap, kEvaluationPhase, false, false, kImportInImportedESM);
      this.loadCache.set(url, undefined, job);
      const { module } = await job.run(isEntryPoint);
      return module;
    }, {
      __proto__: null,
      parentURL: '<eval>',
      url,
    });

    return {
      __proto__: null,
      namespace: module.getNamespace(),
      module,
    };
  }

  /**
   *
   * @param {string} source Source code of the module.
   * @param {string} url URL of the module.
   * @param {boolean} isEntryPoint Whether the module is the entry point.
   * @returns {Promise<object>} The module object.
   */
  eval(source, url, isEntryPoint = false) {
    const context = isEntryPoint ? { isMain: true } : undefined;
    const wrap = this.createModuleWrap(source, url, context);
    return this.executeModuleJob(url, wrap, isEntryPoint);
  }

  /**
   * This constructs (creates, instantiates and evaluates) a module graph that
   * is require()'d.
   * @param {CJSModule} mod CJS module wrapper of the ESM.
   * @param {string} filename Resolved filename of the module being require()'d
   * @param {string} source Source code. TODO(joyeecheung): pass the raw buffer.
   * @param {string} isMain Whether this module is a main module.
   * @param {CJSModule|undefined} parent Parent module, if any.
   * @returns {{wrap: ModuleWrap, namespace: import('internal/modules/esm/utils').ModuleNamespaceObject}}
   */
  importSyncForRequire(mod, filename, source, isMain, parent) {
    const url = pathToFileURL(filename).href;
    if (!getOptionValue('--require-module')) {
      throw new ERR_REQUIRE_ESM(url, true);
    }

    let job = this.loadCache.get(url, kImplicitTypeAttribute);
    // This module job is already created:
    // 1. If it was loaded by `require()` before, at this point the instantiation
    //    is already completed and we can check whether it is in a cycle
    //    (in that case the module status is kEvaluating), and whether the
    //    required graph is synchronous.
    // 2. If it was loaded by `import` before, only allow it if it's already evaluated
    //    to forbid cycles.
    //    TODO(joyeecheung): ensure that imported synchronous graphs are evaluated
    //    synchronously so that any previously imported synchronous graph is already
    //    evaluated at this point.
    // TODO(joyeecheung): add something similar to CJS loader's requireStack to help
    // debugging the problematic links in the graph for import.
    debug('importSyncForRequire', parent?.filename, '->', filename, job);
    if (job !== undefined) {
      mod[kRequiredModuleSymbol] = job.module;
      const parentFilename = urlToFilename(parent?.filename);
      // This race should only be possible on the loader hook thread. See https://github.com/nodejs/node/issues/59666
      if (!job.module) {
        throw new ERR_REQUIRE_ESM_RACE_CONDITION(filename, parentFilename, this.isForAsyncLoaderHookWorker);
      }
      const status = job.module.getStatus();
      debug('Module status', job, status);
      // hasAsyncGraph is available after module been instantiated.
      if (status >= kInstantiated) {
        job.throwIfAsyncGraph(parent);
      }
      if (status === kEvaluated) {
        return { wrap: job.module, namespace: job.module.getNamespace() };
      } else if (status === kInstantiated) {
        // When it's an async job cached by another import request,
        // which has finished linking but has not started its
        // evaluation because the async run() task would be later
        // in line. Then start the evaluation now with runSync(), which
        // is guaranteed to finish by the time the other run() get to it,
        // and the other task would just get the cached evaluation results,
        // similar to what would happen when both are async.
        mod[kRequiredModuleSymbol] = job.module;
        const { namespace } = job.runSync(parent);
        return { wrap: job.module, namespace: namespace || job.module.getNamespace() };
      } else if (status === kErrored) {
        // If the module was previously imported and errored, throw the error.
        throw job.module.getError();
      }
      // When the cached async job have already encountered a linking
      // error that gets wrapped into a rejection, but is still later
      // in line to throw on it, just unwrap and throw the linking error
      // from require().
      if (job.instantiated) {
        throwIfPromiseRejected(job.instantiated);
      }
      if (status !== kEvaluating) {
        assert(status === kUninstantiated, `Unexpected module status ${status}`);
        // If we get here, either there's a race where the job is still being instantiated
        // by an in-flight import(), or the cached module previously encountered an
        // instantiation error during a prior load (e.g. due to a mismatched import).
        // TODO(joyeecheung): the current check is too broad. We should attempt to
        // get the potential instantiation error and throw it.
        throw new ERR_REQUIRE_ESM_RACE_CONDITION(filename, parentFilename, false);
      }
      let message = `Cannot require() ES Module ${filename} in a cycle.`;
      if (parentFilename) {
        message += ` (from ${parentFilename})`;
      }
      message += ' A cycle involving require(esm) is not allowed to maintain ';
      message += 'invariants mandated by the ECMAScript specification. ';
      message += 'Try making at least part of the dependency in the graph lazily loaded.';
      throw new ERR_REQUIRE_CYCLE_MODULE(message);

    }
    // TODO(joyeecheung): refactor this so that we pre-parse in C++ and hit the
    // cache here, or use a carrier object to carry the compiled module script
    // into the constructor to ensure cache hit.
    const wrap = compileSourceTextModule(url, source, kUser);
    const inspectBrk = (isMain && getOptionValue('--inspect-brk'));

    job = new ModuleJobSync(this, url, kEmptyObject, wrap, kEvaluationPhase, isMain, inspectBrk,
                            kImportInRequiredESM);
    this.loadCache.set(url, kImplicitTypeAttribute, job);
    mod[kRequiredModuleSymbol] = job.module;
    return { wrap: job.module, namespace: job.runSync(parent).namespace };
  }

  /**
   * Load and evaluate the entry point module.
   * The graph is always loaded and linked synchronously. When it contains no top-level
   * await it is evaluated synchronously too, so startup creates no promises at all.
   * When it does contain top-level await, evaluation falls back to `job.run()` here,
   * reusing the job that was just built rather than making the caller re-enter the
   * loader and redo the resolution and linking for the same module.
   * @param {string} url The URL of the entry point module.
   * @returns {ModuleJobSync|undefined} The job still needing asynchronous evaluation when
   *   the graph has top-level await, otherwise `undefined` - the module is then already
   *   fully evaluated and no promise was ever created.
   */
  importForEntryPoint(url) {
    return onImport.traceSync(() => {
      const request = { specifier: url, phase: kEvaluationPhase, attributes: kEmptyObject, __proto__: null };
      const job = this.getOrCreateModuleJob(undefined, request, kImportInImportedESM);
      job.instantiateSync();
      if (job.module.hasAsyncGraph) {
        // Hand the job back so the caller can evaluate it through the ordinary
        // asynchronous entry point machinery, without re-resolving and re-linking
        // the module the way re-entering `import()` would.
        return job;
      }
      globalThis[entry_point_module_private_symbol] = job.module;
      job.runSync();
      return undefined;
    }, {
      __proto__: null,
      parentURL: undefined,
      url,
    });
  }

  /**
   * Check invariants on a cached module job when require()'d from ESM.
   * @param {string} specifier The first parameter of require().
   * @param {string} url URL of the module being required.
   * @param {string|undefined} parentURL URL of the module calling require().
   * @param {ModuleJobBase} job The cached module job.
   */
  #checkCachedJobForRequireESM(specifier, url, parentURL, job) {
    // This race should only be possible on the loader hook thread. See https://github.com/nodejs/node/issues/59666
    if (!job.module) {
      throw new ERR_REQUIRE_ESM_RACE_CONDITION(url, parentURL, this.isForAsyncLoaderHookWorker);
    }
    // This module is being evaluated, which means it's imported in a previous link
    // in a cycle.
    if (job.module.getStatus() === kEvaluating) {
      const parentFilename = urlToFilename(parentURL);
      let message = `Cannot import Module ${specifier} in a cycle.`;
      if (parentFilename) {
        message += ` (from ${parentFilename})`;
      }
      throw new ERR_REQUIRE_CYCLE_MODULE(message);
    }

    // Otherwise the module could be imported before but the evaluation may be already
    // completed (e.g. the require call is lazy) so it's okay. We will return the
    // job and check asynchronicity of the entire graph later, before the
    // graph is evaluated.
  }

  /**
   * Load a module and translate it into a ModuleWrap for require(esm).
   * This is run synchronously, and the translator always return a ModuleWrap synchronously.
   * @param {ResolveResult} resolveResult Result from the resolve step.
   * @param {object} loadContext See {@link load}
   * @param {string|undefined} parentURL URL of the parent module. Undefined if it's the entry point.
   * @param {ModuleRequest} request Module request.
   * @returns {ModuleWrap}
   */
  loadAndTranslateForImportInRequiredESM(resolveResult, loadContext, parentURL, request) {
    const { url } = resolveResult;
    const loadResult = this.#loadSync(url, loadContext);
    // Use the synchronous commonjs translator which can deal with cycles.
    const formatFromLoad = loadResult.format;
    const translatorKey = (formatFromLoad === 'commonjs' || formatFromLoad === 'commonjs-typescript') ?
      'commonjs-sync' : formatFromLoad;
    const translateContext = {
      ...resolveResult,
      ...loadResult,
      translatorKey,
      __proto__: null,
    };
    const wrap = this.#translate(url, translateContext, parentURL);
    assert(wrap instanceof ModuleWrap, `Translator used for require(${url}) should not be async`);

    const cjsModule = wrap[imported_cjs_symbol];
    if (cjsModule) {
      // Check if the ESM initiating import CJS is being required by the same CJS module.
      if (cjsModule?.[kIsExecuting]) {
        const parentFilename = urlToFilename(parentURL);
        let message = `Cannot import CommonJS Module ${request.specifier} in a cycle.`;
        if (parentFilename) {
          message += ` (from ${parentFilename})`;
        }
        throw new ERR_REQUIRE_CYCLE_MODULE(message);
      }
    }

    return wrap;
  }

  /**
   * Translate a loaded module source into a ModuleWrap. This is run synchronously,
   * but the translator may return the ModuleWrap in a Promise.
   * @param {string} url URL of the module to be translated.
   * @param {TranslateContext} translateContext Context for the translator
   * @param {string|undefined} parentURL URL of the module initiating the module loading for the first time.
   *   Undefined if it's the entry point.
   * @returns {ModuleWrap}
   */
  #translate(url, translateContext, parentURL) {
    const { translatorKey, format } = translateContext;
    this.validateLoadResult(url, format);
    const translator = translators.get(translatorKey);

    if (!translator) {
      throw new ERR_UNKNOWN_MODULE_FORMAT(translatorKey, url);
    }

    const result = FunctionPrototypeCall(translator, this, url, translateContext, parentURL);
    assert(result instanceof ModuleWrap, `The ${format} module returned is not a ModuleWrap`);
    if (format === 'commonjs' || format === 'commonjs-sync' || format === 'require-commonjs') {
      result.isCommonJS = true;
    }
    return result;
  }

  /**
   * Load a module and translate it into a ModuleWrap for require() in imported CJS.
   * This is run synchronously, and the translator always return a ModuleWrap synchronously.
   * @param {ResolveResult} resolveResult Result from the resolve step.
   * @param {object} loadContext See {@link load}
   * @param {string|undefined} parentURL URL of the parent module. Undefined if it's the entry point.
   * @returns {ModuleWrap}
   */
  loadAndTranslateForRequireInImportedCJS(resolveResult, loadContext, parentURL) {
    const { url } = resolveResult;
    const loadResult = this.#loadSync(url, loadContext);
    const formatFromLoad = loadResult.format;

    if (formatFromLoad === 'wasm') {  // require(wasm) is not supported.
      throw new ERR_UNKNOWN_MODULE_FORMAT(formatFromLoad, url);
    }

    if (formatFromLoad === 'module' || formatFromLoad === 'module-typescript') {
      if (!getOptionValue('--require-module')) {
        throw new ERR_REQUIRE_ESM(url, true);
      }
    }

    let translatorKey = formatFromLoad;
    if (formatFromLoad === 'commonjs') {
      translatorKey = 'require-commonjs';
    }
    if (formatFromLoad === 'commonjs-typescript') {
      translatorKey = 'require-commonjs-typescript';
    }

    const translateContext = {
      ...resolveResult,
      ...loadResult,
      translatorKey,
      __proto__: null,
    };
    const wrap = this.#translate(url, translateContext, parentURL);
    assert(wrap instanceof ModuleWrap, `Translator used for require(${url}) should not be async`);
    return wrap;
  }

  /**
   * Load a module and translate it into a ModuleWrap for ordinary imported ESM.
   * This may be run asynchronously if there are asynchronous module loader hooks registered.
   * @param {ResolveResult} resolveResult Result from the resolve step.
   * @param {object} loadContext See {@link load}
   * @param {string|undefined} parentURL URL of the parent module. Undefined if it's the entry point.
   * @returns {Promise<ModuleWrap>|ModuleWrap}
   */
  loadAndTranslate(resolveResult, loadContext, parentURL) {
    const { url } = resolveResult;
    const maybePromise = this.load(url, loadContext);
    const afterLoad = (loadResult) => {
      const translateContext = {
        ...resolveResult,
        ...loadResult,
        translatorKey: loadResult.format,
        __proto__: null,
      };
      return this.#translate(url, translateContext, parentURL);
    };
    if (isPromise(maybePromise)) {
      return PromisePrototypeThen(maybePromise, afterLoad);
    }
    return afterLoad(maybePromise);
  }

  /**
   * Given a resolved module request, obtain a ModuleJobBase from it - if it's already cached,
   * return the cached ModuleJobBase. Otherwise, load its source and translate it into a ModuleWrap first.
   * This runs synchronously. On any thread that is not an async loader hook worker thread,
   * the module should be linked by the time this returns. Otherwise it may still have
   * pending module requests.
   * @param {string} parentURL See {@link getOrCreateModuleJob}
   * @param {ResolveResult} resolveResult
   * @param {ModuleRequest} request Module request.
   * @param {ModuleRequestType} requestType Type of the module request.
   * @returns {ModuleJobBase} The (possibly pending) module job
   */
  #getOrCreateModuleJobAfterResolve(parentURL, resolveResult, request, requestType) {
    const { url, format } = resolveResult;

    if (process.env.WATCH_REPORT_DEPENDENCIES && process.send) {
      const type = requestType === kRequireInImportedCJS ? 'require' : 'import';
      process.send({ [`watch:${type}`]: [url] });
    }
    // Relay Events from worker to main thread
    if (process.env.WATCH_REPORT_DEPENDENCIES && !process.send) {
      const { isMainThread } = internalBinding('worker');
      if (!isMainThread) {
        const { parentPort } = require('worker_threads');
        if (parentPort) {
          parentPort.postMessage({ 'watch:import': [url] });
        }
      }
    }

    // TODO(joyeecheung): update the module requests to use importAttributes as property names.
    const importAttributes = resolveResult.importAttributes ?? request.attributes;
    let job = this.loadCache.get(url, importAttributes.type);

    if (job !== undefined) {
      if (requestType === kImportInRequiredESM) {
        this.#checkCachedJobForRequireESM(request.specifier, url, parentURL, job);
      }
      job.ensurePhase(request.phase, requestType);
      return job;
    }

    const context = { format, importAttributes, __proto__: null };

    let moduleOrModulePromise;
    if (requestType === kRequireInImportedCJS) {
      moduleOrModulePromise = this.loadAndTranslateForRequireInImportedCJS(resolveResult, context, parentURL);
    } else if (requestType === kImportInRequiredESM) {
      moduleOrModulePromise = this.loadAndTranslateForImportInRequiredESM(resolveResult, context, parentURL, request);
    } else {
      moduleOrModulePromise = this.loadAndTranslate(resolveResult, context, parentURL);
    }

    if (requestType === kImportInRequiredESM || requestType === kRequireInImportedCJS ||
        !this.isForAsyncLoaderHookWorker) {
      assert(moduleOrModulePromise instanceof ModuleWrap, `Expected ModuleWrap for loading ${url}`);
    }

    const isMain = (parentURL === undefined);
    const inspectBrk = (isMain && getOptionValue('--inspect-brk'));
    // Use ModuleJobSync for kImportInRequiredESM, which must always be linked synchronously,
    // and whenever we're on the main thread (not the async loader hook worker),
    // except for kRequireInImportedCJS (TODO: consolidate that case too).
    // TODO(joyeecheung): use ModuleJobSync for kRequireInImportedCJS too.
    const ModuleJobCtor = (requestType === kImportInRequiredESM ||
                           (!this.isForAsyncLoaderHookWorker && requestType !== kRequireInImportedCJS)) ?
      ModuleJobSync : ModuleJob;
    job = new ModuleJobCtor(
      this,
      url,
      importAttributes,
      moduleOrModulePromise,
      request.phase,
      isMain,
      inspectBrk,
      requestType,
    );

    this.loadCache.set(url, importAttributes.type, job);

    return job;
  }

  /**
   * Get a (possibly not yet fully linked) module job from the cache, or create one and return its Promise.
   * @param {string} [parentURL] The URL of the module where the module request is initiated.
   *   It's undefined if it's from the root module.
   * @param {ModuleRequest} request Module request.
   * @param {ModuleRequestType} requestType Type of the module request.
   * @returns {Promise<ModuleJobBase>|ModuleJobBase}
   */
  getOrCreateModuleJob(parentURL, request, requestType) {
    let maybePromise;
    if (!this.isForAsyncLoaderHookWorker ||
        requestType === kRequireInImportedCJS || requestType === kImportInRequiredESM) {
      // On the main thread, always resolve synchronously;
      // `resolveSync` coordinates with the async loader hook worker if needed.
      // On the async loader hook worker, require(esm) and require() in imported CJS
      // must still be resolved synchronously.
      maybePromise = this.resolveSync(parentURL, request);
      assert(!isPromise(maybePromise));
    } else {
      maybePromise = this.#resolve(parentURL, request);
    }

    const afterResolve = (resolveResult) => {
      return this.#getOrCreateModuleJobAfterResolve(parentURL, resolveResult, request, requestType);
    };

    if (isPromise(maybePromise)) {
      return PromisePrototypeThen(maybePromise, afterResolve);
    }
    return afterResolve(maybePromise);
  }

  /**
   * This method is usually called indirectly as part of the loading processes.
   * Use directly with caution.
   * @param {string} specifier The first parameter of an `import()` expression.
   * @param {string} parentURL Path of the parent importing the module.
   * @param {Record<string, string>} importAttributes Validations for the
   *   module import.
   * @param {number} [phase] The phase of the import.
   * @param {boolean} [isEntryPoint] Whether this is the realm-level entry point.
   * @returns {Promise<ModuleExports>}
   */
  async import(specifier, parentURL, importAttributes, phase = kEvaluationPhase, isEntryPoint = false) {
    return onImport.tracePromise(async () => {
      const request = { specifier, phase, attributes: importAttributes, __proto__: null };
      let moduleJob;
      try {
        moduleJob = await this.getOrCreateModuleJob(parentURL, request);
      } catch (e) {
        if (e?.code === 'ERR_ASYNC_LOADER_REQUEST_NEVER_SETTLED') {
          return new Promise(() => {});
        }
        throw e;
      }
      if (phase === kSourcePhase) {
        const module = await moduleJob.modulePromise;
        return module.getModuleSourceObject();
      }
      const { module } = await moduleJob.run(isEntryPoint);
      return module.getNamespace();
    }, {
      __proto__: null,
      parentURL,
      url: specifier,
    });
  }

  /**
   * @see {@link AsyncLoaderHooks.register}
   * @returns {any}
   */
  register(specifier, parentURL, data, transferList, isInternal) {
    if (!this.#asyncLoaderHooks) {
      // On the loader hook worker thread, the #asyncLoaderHooks must already have been initialized
      // to be an instance of AsyncLoaderHooksOnLoaderHookWorker, so this branch can only ever
      // be hit on a non-loader-hook thread that will talk to the loader hook worker thread.
      const { AsyncLoaderHooksProxiedToLoaderHookWorker } = require('internal/modules/esm/hooks');
      this.#setAsyncLoaderHooks(new AsyncLoaderHooksProxiedToLoaderHookWorker());
    }
    return this.#asyncLoaderHooks.register(`${specifier}`, `${parentURL}`, data, transferList, isInternal);
  }

  /**
   * Resolve a module request to a URL identifying the location of the module. Handles customization hooks,
   * if any.
   * @param {string} [parentURL] The URL of the module where the module request is initiated.
   *   It's undefined if it's from the root module.
   * @param {ModuleRequest} request Module request.
   * @returns {Promise<ResolveResult>|ResolveResult}
   */
  #resolve(parentURL, request) {
    if (this.isForAsyncLoaderHookWorker) {
      const specifier = `${request.specifier}`;
      const importAttributes = request.attributes ?? kEmptyObject;
      // TODO(joyeecheung): invoke the synchronous hooks in the default step on loader thread.
      return this.#asyncLoaderHooks.resolve(specifier, parentURL, importAttributes);
    }

    return this.resolveSync(parentURL, request);
  }

  /**
   * Either return a cached resolution, or perform the default resolution which is synchronous, and
   * cache the result.
   * @param {string} specifier See {@link resolve}.
   * @param {{ parentURL?: string, importAttributes: ImportAttributes, conditions?: string[]}} context
   * @returns {{ format: string, url: string }}
   */
  #cachedDefaultResolve(specifier, context) {
    const { parentURL, importAttributes } = context;
    const requestKey = this.#resolveCache.serializeKey(specifier, importAttributes);
    const cachedResult = this.#resolveCache.get(requestKey, parentURL);
    if (cachedResult != null) {
      return cachedResult;
    }

    const result = defaultResolve(specifier, context);
    this.#resolveCache.set(requestKey, parentURL, result);
    return result;
  }

  /**
   * This is the default resolve step for module.registerHooks(), which incorporates asynchronous hooks
   * from module.register() which are run in a blocking fashion for it to be synchronous.
   * @param {{isResolvedByDefaultResolve: boolean}} out Output object to track whether the default resolve was used
   *   without polluting the user-visible resolve result.
   * @param {string|URL} specifier See {@link resolveSync}.
   * @param {{ parentURL?: string, importAttributes: ImportAttributes, conditions?: string[]}} context
   *   See {@link resolveSync}.
   * @returns {{ format: string, url: string }}
   */
  #resolveAndMaybeBlockOnLoaderThread(out, specifier, context) {
    if (this.#asyncLoaderHooks?.resolveSync) {
      return this.#asyncLoaderHooks.resolveSync(specifier, context.parentURL, context.importAttributes);
    }
    out.isResolvedByDefaultResolve = true;
    return this.#cachedDefaultResolve(specifier, context);
  }

  /**
   * Similar to {@link resolve}, but the results are always synchronously returned. If there are any
   * asynchronous resolve hooks from module.register(), it will block until the results are returned
   * from the loader thread for this to be synchronous.
   * This is here to support `import.meta.resolve()`, `require()` in imported CJS, and
   * `module.registerHooks()` hooks.
   * @param {string} [parentURL] See {@link resolve}.
   * @param {ModuleRequest} request See {@link resolve}.
   * @param {boolean} [shouldSkipSyncHooks] Whether to skip the synchronous hooks registered by module.registerHooks().
   *   This is used to maintain compatibility for the re-invented require.resolve (in imported CJS customized
   *   by module.register()`) which invokes the CJS resolution separately from the hook chain.
   * @returns {ResolveResult}
   */
  resolveSync(parentURL, request, shouldSkipSyncHooks = false) {
    const specifier = `${request.specifier}`;
    const importAttributes = request.attributes ?? kEmptyObject;
    // Use an output parameter to track the state and avoid polluting the user-visible resolve results.
    const out = { isResolvedByDefaultResolve: false, __proto__: null };

    let result;
    let isResolvedBySyncHooks = false;
    if (!shouldSkipSyncHooks && syncResolveHooks.length) {
      // Has module.registerHooks() hooks, chain the asynchronous hooks in the default step.
      result = resolveWithSyncHooks(specifier, parentURL, importAttributes, this.#defaultConditions,
                                    this.#resolveAndMaybeBlockOnLoaderThread.bind(this, out));
      // If the default step ran, sync hooks did not short-circuit the resolution.
      isResolvedBySyncHooks = !out.isResolvedByDefaultResolve;
    } else {
      const context = {
        ...request,
        conditions: this.#defaultConditions,
        parentURL,
        importAttributes,
        __proto__: null,
      };
      result = this.#resolveAndMaybeBlockOnLoaderThread(out, specifier, context);
    }
    result.isResolvedBySyncHooks = isResolvedBySyncHooks;
    return result;
  }

  /**
   * Provide source that is understood by one of Node's translators. Handles customization hooks,
   * if any.
   * @typedef {{
   *   format: ModuleFormat,
   *   source: ModuleSource,
   *   responseURL?: string,
   *   isSourceLoadedSynchronously: boolean,
   * }} LoadResult
   * @param {string} url The URL of the module to be loaded.
   * @param {object} context Metadata about the module
   * @returns {Promise<LoadResult> | LoadResult}}
   */
  load(url, context) {
    if (this.isForAsyncLoaderHookWorker) {
      // TODO(joyeecheung): invoke the synchronous hooks in the default step on loader thread.
      return this.#asyncLoaderHooks.load(url, context);
    }
    return this.#loadSync(url, context);
  }

  /**
   * This is the default load step for module.registerHooks(), which incorporates asynchronous hooks
   * from module.register() which are run in a blocking fashion for it to be synchronous.
   * @param {{isSourceLoadedSynchronously: boolean}} out
   *   Output object to track whether the source was loaded synchronously without polluting
   *   the user-visible load result.
   * @param {string} url See {@link load}
   * @param {object} context See {@link load}
   * @returns {{ format: ModuleFormat, source: ModuleSource }}
   */
  #loadAndMaybeBlockOnLoaderThread(out, url, context) {
    if (this.#asyncLoaderHooks?.loadSync) {
      out.isSourceLoadedSynchronously = false;
      return this.#asyncLoaderHooks.loadSync(url, context);
    }
    out.isSourceLoadedSynchronously = true;
    return defaultLoadSync(url, context);
  }

  /**
   * Similar to {@link load} but this is always run synchronously. If there are asynchronous hooks
   * from module.register(), this blocks on the loader thread for it to return synchronously.
   *
   * This is here to support `require()` in imported CJS and `module.registerHooks()` hooks.
   * @param {string} url See {@link load}
   * @param {object} [context] See {@link load}
   * @returns {LoadResult}
   */
  #loadSync(url, context) {
    // Use an output parameter to track the state and avoid polluting the user-visible resolve results.
    const out = {
      isSourceLoadedSynchronously: true,
      __proto__: null,
    };
    let result;
    if (syncLoadHooks.length) {
      // Has module.registerHooks() hooks, chain the asynchronous hooks in the default step.
      // TODO(joyeecheung): construct the ModuleLoadContext in the loaders directly instead
      // of converting them from plain objects in the hooks.
      result = loadWithSyncHooks(
        url,
        context.format,
        context.importAttributes,
        this.#defaultConditions,
        this.#loadAndMaybeBlockOnLoaderThread.bind(this, out),
        validateLoadSloppy,
      );
    } else {
      result = this.#loadAndMaybeBlockOnLoaderThread(out, url, context);
    }
    result.isSourceLoadedSynchronously = out.isSourceLoadedSynchronously;
    return result;
  }

  validateLoadResult(url, format) {
    if (format == null) {
      throwUnknownModuleFormat(url, format);
    }
  }

  /**
   * Block until the async loader hooks have been initialized.
   *
   * No-op when no hooks have been supplied.
   */
  waitForAsyncLoaderHookInitialization() {
    this.#asyncLoaderHooks?.waitForLoaderHookInitialization();
  }
}
ObjectSetPrototypeOf(ModuleLoader.prototype, null);

let emittedLoaderFlagWarning = 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 {AsyncLoaderHooksOnLoaderHookWorker|undefined} [asyncLoaderHooks]
 *   Only provided when run on the loader hook thread.
 * @returns {ModuleLoader}
 */
function createModuleLoader(asyncLoaderHooks) {
  // Don't spawn a new loader hook worker if we are already in a loader hook worker to avoid infinite recursion.
  if (shouldSpawnLoaderHookWorker()) {
    assert(asyncLoaderHooks === undefined, 'asyncLoaderHooks should only be provided on the loader hook thread itself');
    const userLoaderPaths = getOptionValue('--experimental-loader');
    if (userLoaderPaths.length > 0) {
      if (!emittedLoaderFlagWarning) {
        const readableURIEncode = (string) => ArrayPrototypeReduce(
          [
            [/'/g, '%27'], // We need to URL-encode the single quote as it's the delimiter for the --import flag.
            [/%22/g, '"'], // We can decode the double quotes to improve readability.
            [/%2F/ig, '/'], // We can decode the slashes to improve readability.
          ],
          (str, { 0: regex, 1: replacement }) => RegExpPrototypeSymbolReplace(hardenRegExp(regex), str, replacement),
          encodeURIComponent(string));
        process.emitWarning(
          '`--experimental-loader` may be removed in the future; instead use `register()`:\n' +
          `--import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; ${ArrayPrototypeJoin(
            ArrayPrototypeMap(userLoaderPaths, (loader) => `register(${readableURIEncode(JSONStringify(loader))}, pathToFileURL("./"))`),
            '; ',
          )};'`,
          'ExperimentalWarning',
        );
        emittedLoaderFlagWarning = true;
      }
      const { AsyncLoaderHooksProxiedToLoaderHookWorker } = require('internal/modules/esm/hooks');
      asyncLoaderHooks = new AsyncLoaderHooksProxiedToLoaderHookWorker();
    }
  }

  return new ModuleLoader(asyncLoaderHooks);
}

let allowImportMetaResolveParentURL;
/**
 * This is only called from the native ImportMetaObjectInitialize function to set up import.meta.resolve
 * when import.meta.resolve is accessed for the first time in a module.
 * @param {ModuleLoader} loader The cascaded loader to use. Bound when this function gets passed to native land.
 * @param {string} moduleURL URL of the module accessing import.meta
 * @returns {function(string, URL['href']=): string} The import.meta.resolve function
 */
function createImportMetaResolve(loader, moduleURL) {
  /**
   * @param {string} specifier The module specifier to resolve.
   * @param {URL['href']} [parentURL] Optional parent URL to resolve against. Ignored unless
   *   `--experimental-import-meta-resolve` is enabled.
   * @returns {string}
   */
  return function resolve(specifier, parentURL) {
    // The second argument is ignored unless --experimental-import-meta-resolve is enabled.
    // Even then, if it's not provided, parentURL defaults to the url of the module accessing
    // import.meta.resolve.
    allowImportMetaResolveParentURL ??= getOptionValue('--experimental-import-meta-resolve');
    parentURL = allowImportMetaResolveParentURL ? (parentURL ?? moduleURL) : moduleURL;

    let url;
    try {
      ({ url } = loader.resolveSync(parentURL, { specifier, __proto__: null }));
      return url;
    } catch (error) {
      switch (error?.code) {
        case 'ERR_UNSUPPORTED_DIR_IMPORT':
        case 'ERR_MODULE_NOT_FOUND':
          ({ url } = error);
          if (url) {
            return url;
          }
      }
      throw error;
    }
  };
}

let cascadedLoader;
/**
 * This is a singleton ESM loader that integrates the loader hooks, if any.
 * It it used by other internal built-ins when they need to load user-land ESM code
 * while also respecting hooks.
 * When built-ins need access to this loader, they should do
 * require('internal/module/esm/loader').getOrInitializeCascadedLoader()
 * lazily only right before the loader is actually needed, and don't do it
 * in the top-level, to avoid circular dependencies.
 * @param {AsyncLoaderHooksOnLoaderHookWorker|undefined} [asyncLoaderHooks]
 *   Only provided when run on the loader hook thread.
 * @returns {ModuleLoader}
 */
function getOrInitializeCascadedLoader(asyncLoaderHooks) {
  if (!cascadedLoader) {
    cascadedLoader = createModuleLoader(asyncLoaderHooks);
    // import.meta.resolve is not allowed in the async loader hook worker thread.
    // So only set up the import.meta.resolve initializer when we are initializing
    // the non-loader-hook-thread cascaded loader. When the native land doesn't see it,
    // it knows the loader is running on the loader hook thread.
    if (!(asyncLoaderHooks?.isForAsyncLoaderHookWorker)) {
      setImportMetaResolveInitializer(createImportMetaResolve.bind(null, cascadedLoader));
    }
  }
  return cascadedLoader;
}

function isCascadedLoaderInitialized() {
  return cascadedLoader !== undefined;
}

/**
 * Register a single loader programmatically.
 * @param {string|URL} specifier
 * @param {string|URL} [parentURL] Base to use when resolving `specifier`; optional if
 *   `specifier` is absolute. Same as `options.parentUrl`, just inline
 * @param {object} [options] Additional options to apply, described below.
 * @param {string|URL} [options.parentURL] Base to use when resolving `specifier`
 * @param {any} [options.data] Arbitrary data passed to the loader's `initialize` hook
 * @param {any[]} [options.transferList] Objects in `data` that are changing ownership
 * @returns {void} We want to reserve the return value for potential future extension of the API.
 * @example
 * ```js
 * register('./myLoader.js');
 * register('ts-node/esm', { parentURL: import.meta.url });
 * register('./myLoader.js', { parentURL: import.meta.url });
 * register('ts-node/esm', import.meta.url);
 * register('./myLoader.js', import.meta.url);
 * register(new URL('./myLoader.js', import.meta.url));
 * register('./myLoader.js', {
 *   parentURL: import.meta.url,
 *   data: { banana: 'tasty' },
 * });
 * register('./myLoader.js', {
 *   parentURL: import.meta.url,
 *   data: someArrayBuffer,
 *   transferList: [someArrayBuffer],
 * });
 * ```
 */
const emitRegisterDeprecation = getDeprecationWarningEmitter(
  'DEP0205',
  '`module.register()` is deprecated. Use `module.registerHooks()` instead.',
  undefined,
  false,
);

function register(specifier, parentURL = undefined, options) {
  emitRegisterDeprecation();

  if (parentURL != null && typeof parentURL === 'object' && !isURL(parentURL)) {
    options = parentURL;
    parentURL = options.parentURL;
  }
  getOrInitializeCascadedLoader().register(
    specifier,
    parentURL ?? 'data:',
    options?.data,
    options?.transferList,
  );
}

module.exports = {
  createModuleLoader,
  getOrInitializeCascadedLoader,
  isCascadedLoaderInitialized,
  register,
};