• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3// This is needed to avoid cycles in esm/resolve <-> cjs/loader
4require('internal/modules/cjs/loader');
5
6const {
7  Array,
8  ArrayIsArray,
9  ArrayPrototypeJoin,
10  ArrayPrototypePush,
11  FunctionPrototypeCall,
12  ObjectAssign,
13  ObjectCreate,
14  ObjectDefineProperty,
15  ObjectSetPrototypeOf,
16  RegExpPrototypeExec,
17  SafePromiseAllReturnArrayLike,
18  SafeWeakMap,
19  StringPrototypeSlice,
20  StringPrototypeToUpperCase,
21  globalThis,
22} = primordials;
23const { MessageChannel } = require('internal/worker/io');
24
25const {
26  ERR_LOADER_CHAIN_INCOMPLETE,
27  ERR_INTERNAL_ASSERTION,
28  ERR_INVALID_ARG_TYPE,
29  ERR_INVALID_ARG_VALUE,
30  ERR_INVALID_RETURN_PROPERTY_VALUE,
31  ERR_INVALID_RETURN_VALUE,
32  ERR_UNKNOWN_MODULE_FORMAT,
33} = require('internal/errors').codes;
34const { pathToFileURL, isURL, URL } = require('internal/url');
35const { emitExperimentalWarning } = require('internal/util');
36const {
37  isAnyArrayBuffer,
38  isArrayBufferView,
39} = require('internal/util/types');
40const {
41  validateObject,
42  validateString,
43} = require('internal/validators');
44const ModuleMap = require('internal/modules/esm/module_map');
45const ModuleJob = require('internal/modules/esm/module_job');
46
47const {
48  defaultResolve,
49  DEFAULT_CONDITIONS,
50} = require('internal/modules/esm/resolve');
51const {
52  initializeImportMeta,
53} = require('internal/modules/esm/initialize_import_meta');
54const { defaultLoad } = require('internal/modules/esm/load');
55const { translators } = require(
56  'internal/modules/esm/translators');
57const { getOptionValue } = require('internal/options');
58
59/**
60 * @typedef {object} ExportedHooks
61 * @property {Function} globalPreload Global preload hook.
62 * @property {Function} resolve Resolve hook.
63 * @property {Function} load Load hook.
64 */
65
66/**
67 * @typedef {Record<string, any>} ModuleExports
68 */
69
70/**
71 * @typedef {object} KeyedExports
72 * @property {ModuleExports} exports The contents of the module.
73 * @property {URL['href']} url The URL of the module.
74 */
75
76/**
77 * @typedef {object} KeyedHook
78 * @property {Function} fn The hook function.
79 * @property {URL['href']} url The URL of the module.
80 */
81
82/**
83 * @typedef {'builtin'|'commonjs'|'json'|'module'|'wasm'} ModuleFormat
84 */
85
86/**
87 * @typedef {ArrayBuffer|TypedArray|string} ModuleSource
88 */
89
90// [2] `validate...()`s throw the wrong error
91
92let emittedSpecifierResolutionWarning = false;
93
94/**
95 * A utility function to iterate through a hook chain, track advancement in the
96 * chain, and generate and supply the `next<HookName>` argument to the custom
97 * hook.
98 * @param {KeyedHook[]} chain The whole hook chain.
99 * @param {object} meta Properties that change as the current hook advances
100 * along the chain.
101 * @param {boolean} meta.chainFinished Whether the end of the chain has been
102 * reached AND invoked.
103 * @param {string} meta.hookErrIdentifier A user-facing identifier to help
104 *  pinpoint where an error occurred. Ex "file:///foo.mjs 'resolve'".
105 * @param {number} meta.hookIndex A non-negative integer tracking the current
106 * position in the hook chain.
107 * @param {string} meta.hookName The kind of hook the chain is (ex 'resolve')
108 * @param {boolean} meta.shortCircuited Whether a hook signaled a short-circuit.
109 * @param {(hookErrIdentifier, hookArgs) => void} validate A wrapper function
110 *  containing all validation of a custom loader hook's intermediary output. Any
111 *  validation within MUST throw.
112 * @returns {function next<HookName>(...hookArgs)} The next hook in the chain.
113 */
114function nextHookFactory(chain, meta, { validateArgs, validateOutput }) {
115  // First, prepare the current
116  const { hookName } = meta;
117  const {
118    fn: hook,
119    url: hookFilePath,
120  } = chain[meta.hookIndex];
121
122  // ex 'nextResolve'
123  const nextHookName = `next${
124    StringPrototypeToUpperCase(hookName[0]) +
125    StringPrototypeSlice(hookName, 1)
126  }`;
127
128  // When hookIndex is 0, it's reached the default, which does not call next()
129  // so feed it a noop that blows up if called, so the problem is obvious.
130  const generatedHookIndex = meta.hookIndex;
131  let nextNextHook;
132  if (meta.hookIndex > 0) {
133    // Now, prepare the next: decrement the pointer so the next call to the
134    // factory generates the next link in the chain.
135    meta.hookIndex--;
136
137    nextNextHook = nextHookFactory(chain, meta, { validateArgs, validateOutput });
138  } else {
139    // eslint-disable-next-line func-name-matching
140    nextNextHook = function chainAdvancedTooFar() {
141      throw new ERR_INTERNAL_ASSERTION(
142        `ESM custom loader '${hookName}' advanced beyond the end of the chain.`,
143      );
144    };
145  }
146
147  return ObjectDefineProperty(
148    async (arg0 = undefined, context) => {
149      // Update only when hook is invoked to avoid fingering the wrong filePath
150      meta.hookErrIdentifier = `${hookFilePath} '${hookName}'`;
151
152      validateArgs(`${meta.hookErrIdentifier} hook's ${nextHookName}()`, arg0, context);
153
154      const outputErrIdentifier = `${chain[generatedHookIndex].url} '${hookName}' hook's ${nextHookName}()`;
155
156      // Set when next<HookName> is actually called, not just generated.
157      if (generatedHookIndex === 0) { meta.chainFinished = true; }
158
159      if (context) { // `context` has already been validated, so no fancy check needed.
160        ObjectAssign(meta.context, context);
161      }
162
163      const output = await hook(arg0, meta.context, nextNextHook);
164
165      validateOutput(outputErrIdentifier, output);
166
167      if (output?.shortCircuit === true) { meta.shortCircuited = true; }
168      return output;
169
170    },
171    'name',
172    { __proto__: null, value: nextHookName },
173  );
174}
175
176/**
177 * An ESMLoader instance is used as the main entry point for loading ES modules.
178 * Currently, this is a singleton -- there is only one used for loading
179 * the main module and everything in its dependency graph.
180 */
181class ESMLoader {
182  #hooks = {
183    /**
184     * Prior to ESM loading. These are called once before any modules are started.
185     * @private
186     * @property {KeyedHook[]} globalPreload Last-in-first-out list of preload hooks.
187     */
188    globalPreload: [],
189
190    /**
191     * Phase 2 of 2 in ESM loading (phase 1 is below).
192     * @private
193     * @property {KeyedHook[]} load Last-in-first-out collection of loader hooks.
194     */
195    load: [
196      {
197        fn: defaultLoad,
198        url: 'node:internal/modules/esm/load',
199      },
200    ],
201
202    /**
203     * Phase 1 of 2 in ESM loading.
204     * @private
205     * @property {KeyedHook[]} resolve Last-in-first-out collection of resolve hooks.
206     */
207    resolve: [
208      {
209        fn: defaultResolve,
210        url: 'node:internal/modules/esm/resolve',
211      },
212    ],
213  };
214
215  #importMetaInitializer = initializeImportMeta;
216
217  /**
218   * Map of already-loaded CJS modules to use
219   */
220  cjsCache = new SafeWeakMap();
221
222  /**
223   * The index for assigning unique URLs to anonymous module evaluation
224   */
225  evalIndex = 0;
226
227  /**
228   * Registry of loaded modules, akin to `require.cache`
229   */
230  moduleMap = new ModuleMap();
231
232  /**
233   * Methods which translate input code or other information into ES modules
234   */
235  translators = translators;
236
237  constructor() {
238    if (getOptionValue('--experimental-loader').length > 0) {
239      emitExperimentalWarning('Custom ESM Loaders');
240    }
241    if (getOptionValue('--experimental-network-imports')) {
242      emitExperimentalWarning('Network Imports');
243    }
244    if (
245      !emittedSpecifierResolutionWarning &&
246      getOptionValue('--experimental-specifier-resolution') === 'node'
247    ) {
248      process.emitWarning(
249        'The Node.js specifier resolution flag is experimental. It could change or be removed at any time.',
250        'ExperimentalWarning',
251      );
252      emittedSpecifierResolutionWarning = true;
253    }
254  }
255
256  /**
257   *
258   * @param {ModuleExports} exports
259   * @returns {ExportedHooks}
260   */
261  static pluckHooks({
262    globalPreload,
263    resolve,
264    load,
265    // obsolete hooks:
266    dynamicInstantiate,
267    getFormat,
268    getGlobalPreloadCode,
269    getSource,
270    transformSource,
271  }) {
272    const obsoleteHooks = [];
273    const acceptedHooks = ObjectCreate(null);
274
275    if (getGlobalPreloadCode) {
276      globalPreload ??= getGlobalPreloadCode;
277
278      process.emitWarning(
279        'Loader hook "getGlobalPreloadCode" has been renamed to "globalPreload"',
280      );
281    }
282    if (dynamicInstantiate) ArrayPrototypePush(
283      obsoleteHooks,
284      'dynamicInstantiate',
285    );
286    if (getFormat) ArrayPrototypePush(
287      obsoleteHooks,
288      'getFormat',
289    );
290    if (getSource) ArrayPrototypePush(
291      obsoleteHooks,
292      'getSource',
293    );
294    if (transformSource) ArrayPrototypePush(
295      obsoleteHooks,
296      'transformSource',
297    );
298
299    if (obsoleteHooks.length) process.emitWarning(
300      `Obsolete loader hook(s) supplied and will be ignored: ${
301        ArrayPrototypeJoin(obsoleteHooks, ', ')
302      }`,
303      'DeprecationWarning',
304    );
305
306    if (globalPreload) {
307      acceptedHooks.globalPreload = globalPreload;
308    }
309    if (resolve) {
310      acceptedHooks.resolve = resolve;
311    }
312    if (load) {
313      acceptedHooks.load = load;
314    }
315
316    return acceptedHooks;
317  }
318
319  /**
320   * Collect custom/user-defined hook(s). After all hooks have been collected,
321   * calls global preload hook(s).
322   * @param {KeyedExports} customLoaders
323   *  A list of exports from user-defined loaders (as returned by
324   *  ESMLoader.import()).
325   */
326  addCustomLoaders(
327    customLoaders = [],
328  ) {
329    for (let i = 0; i < customLoaders.length; i++) {
330      const {
331        exports,
332        url,
333      } = customLoaders[i];
334      const {
335        globalPreload,
336        resolve,
337        load,
338      } = ESMLoader.pluckHooks(exports);
339
340      if (globalPreload) {
341        ArrayPrototypePush(
342          this.#hooks.globalPreload,
343          {
344            fn: globalPreload,
345            url,
346          },
347        );
348      }
349      if (resolve) {
350        ArrayPrototypePush(
351          this.#hooks.resolve,
352          {
353            fn: resolve,
354            url,
355          },
356        );
357      }
358      if (load) {
359        ArrayPrototypePush(
360          this.#hooks.load,
361          {
362            fn: load,
363            url,
364          },
365        );
366      }
367    }
368
369    this.preload();
370  }
371
372  async eval(
373    source,
374    url = pathToFileURL(`${process.cwd()}/[eval${++this.evalIndex}]`).href,
375  ) {
376    const evalInstance = (url) => {
377      const { ModuleWrap, callbackMap } = internalBinding('module_wrap');
378      const module = new ModuleWrap(url, undefined, source, 0, 0);
379      callbackMap.set(module, {
380        importModuleDynamically: (specifier, { url }, importAssertions) => {
381          return this.import(specifier, url, importAssertions);
382        },
383      });
384
385      return module;
386    };
387    const job = new ModuleJob(
388      this, url, undefined, evalInstance, false, false);
389    this.moduleMap.set(url, undefined, job);
390    const { module } = await job.run();
391
392    return {
393      namespace: module.getNamespace(),
394    };
395  }
396
397  /**
398   * Get a (possibly still pending) module job from the cache,
399   * or create one and return its Promise.
400   * @param {string} specifier The string after `from` in an `import` statement,
401   *                           or the first parameter of an `import()`
402   *                           expression
403   * @param {string | undefined} parentURL The URL of the module importing this
404   *                                     one, unless this is the Node.js entry
405   *                                     point.
406   * @param {Record<string, string>} importAssertions Validations for the
407   *                                                  module import.
408   * @returns {Promise<ModuleJob>} The (possibly pending) module job
409   */
410  async getModuleJob(specifier, parentURL, importAssertions) {
411    let importAssertionsForResolve;
412
413    // By default, `this.#hooks.load` contains just the Node default load hook
414    if (this.#hooks.load.length !== 1) {
415      // We can skip cloning if there are no user-provided loaders because
416      // the Node.js default resolve hook does not use import assertions.
417      importAssertionsForResolve = {
418        __proto__: null,
419        ...importAssertions,
420      };
421    }
422
423    const { format, url } =
424      await this.resolve(specifier, parentURL, importAssertionsForResolve);
425
426    let job = this.moduleMap.get(url, importAssertions.type);
427
428    // CommonJS will set functions for lazy job evaluation.
429    if (typeof job === 'function') {
430      this.moduleMap.set(url, undefined, job = job());
431    }
432
433    if (job === undefined) {
434      job = this.#createModuleJob(url, importAssertions, parentURL, format);
435    }
436
437    return job;
438  }
439
440  /**
441   * Create and cache an object representing a loaded module.
442   * @param {string} url The absolute URL that was resolved for this module
443   * @param {Record<string, string>} importAssertions Validations for the
444   *                                                  module import.
445   * @param {string} [parentURL] The absolute URL of the module importing this
446   *                             one, unless this is the Node.js entry point
447   * @param {string} [format] The format hint possibly returned by the
448   *                          `resolve` hook
449   * @returns {Promise<ModuleJob>} The (possibly pending) module job
450   */
451  #createModuleJob(url, importAssertions, parentURL, format) {
452    const moduleProvider = async (url, isMain) => {
453      const {
454        format: finalFormat,
455        responseURL,
456        source,
457      } = await this.load(url, {
458        format,
459        importAssertions,
460      });
461
462      const translator = translators.get(finalFormat);
463
464      if (!translator) {
465        throw new ERR_UNKNOWN_MODULE_FORMAT(finalFormat, responseURL);
466      }
467
468      return FunctionPrototypeCall(translator, this, responseURL, source, isMain);
469    };
470
471    const inspectBrk = (
472      parentURL === undefined &&
473      getOptionValue('--inspect-brk')
474    );
475
476    if (process.env.WATCH_REPORT_DEPENDENCIES && process.send) {
477      process.send({ 'watch:import': [url] });
478    }
479
480    const job = new ModuleJob(
481      this,
482      url,
483      importAssertions,
484      moduleProvider,
485      parentURL === undefined,
486      inspectBrk,
487    );
488
489    this.moduleMap.set(url, importAssertions.type, job);
490
491    return job;
492  }
493
494  /**
495   * This method is usually called indirectly as part of the loading processes.
496   * Internally, it is used directly to add loaders. Use directly with caution.
497   *
498   * This method must NOT be renamed: it functions as a dynamic import on a
499   * loader module.
500   * @param {string | string[]} specifiers Path(s) to the module.
501   * @param {string} parentURL Path of the parent importing the module.
502   * @param {Record<string, string>} importAssertions Validations for the
503   *                                                  module import.
504   * @returns {Promise<ExportedHooks | KeyedExports[]>}
505   *  A collection of module export(s) or a list of collections of module
506   *  export(s).
507   */
508  async import(specifiers, parentURL, importAssertions) {
509    // For loaders, `import` is passed multiple things to process, it returns a
510    // list pairing the url and exports collected. This is especially useful for
511    // error messaging, to identity from where an export came. But, in most
512    // cases, only a single url is being "imported" (ex `import()`), so there is
513    // only 1 possible url from which the exports were collected and it is
514    // already known to the caller. Nesting that in a list would only ever
515    // create redundant work for the caller, so it is later popped off the
516    // internal list.
517    const wasArr = ArrayIsArray(specifiers);
518    if (!wasArr) { specifiers = [specifiers]; }
519
520    const count = specifiers.length;
521    const jobs = new Array(count);
522
523    for (let i = 0; i < count; i++) {
524      jobs[i] = this.getModuleJob(specifiers[i], parentURL, importAssertions)
525        .then((job) => job.run())
526        .then(({ module }) => module.getNamespace());
527    }
528
529    const namespaces = await SafePromiseAllReturnArrayLike(jobs);
530
531    if (!wasArr) { return namespaces[0]; } // We can skip the pairing below
532
533    for (let i = 0; i < count; i++) {
534      namespaces[i] = {
535        __proto__: null,
536        url: specifiers[i],
537        exports: namespaces[i],
538      };
539    }
540
541    return namespaces;
542  }
543
544  /**
545   * Provide source that is understood by one of Node's translators.
546   *
547   * Internally, this behaves like a backwards iterator, wherein the stack of
548   * hooks starts at the top and each call to `nextLoad()` moves down 1 step
549   * until it reaches the bottom or short-circuits.
550   * @param {URL['href']} url The URL/path of the module to be loaded
551   * @param {object} context Metadata about the module
552   * @returns {{ format: ModuleFormat, source: ModuleSource }}
553   */
554  async load(url, context = {}) {
555    const chain = this.#hooks.load;
556    const meta = {
557      chainFinished: null,
558      context,
559      hookErrIdentifier: '',
560      hookIndex: chain.length - 1,
561      hookName: 'load',
562      shortCircuited: false,
563    };
564
565    const validateArgs = (hookErrIdentifier, nextUrl, ctx) => {
566      if (typeof nextUrl !== 'string') {
567        // non-strings can be coerced to a url string
568        // validateString() throws a less-specific error
569        throw new ERR_INVALID_ARG_TYPE(
570          `${hookErrIdentifier} url`,
571          'a url string',
572          nextUrl,
573        );
574      }
575
576      // Try to avoid expensive URL instantiation for known-good urls
577      if (!this.moduleMap.has(nextUrl)) {
578        try {
579          new URL(nextUrl);
580        } catch {
581          throw new ERR_INVALID_ARG_VALUE(
582            `${hookErrIdentifier} url`,
583            nextUrl,
584            'should be a url string',
585          );
586        }
587      }
588
589      if (ctx) validateObject(ctx, `${hookErrIdentifier} context`);
590    };
591    const validateOutput = (hookErrIdentifier, output) => {
592      if (typeof output !== 'object' || output === null) { // [2]
593        throw new ERR_INVALID_RETURN_VALUE(
594          'an object',
595          hookErrIdentifier,
596          output,
597        );
598      }
599    };
600
601    const nextLoad = nextHookFactory(chain, meta, { validateArgs, validateOutput });
602
603    const loaded = await nextLoad(url, context);
604    const { hookErrIdentifier } = meta; // Retrieve the value after all settled
605
606    validateOutput(hookErrIdentifier, loaded);
607
608    if (loaded?.shortCircuit === true) { meta.shortCircuited = true; }
609
610    if (!meta.chainFinished && !meta.shortCircuited) {
611      throw new ERR_LOADER_CHAIN_INCOMPLETE(hookErrIdentifier);
612    }
613
614    const {
615      format,
616      source,
617    } = loaded;
618    let responseURL = loaded.responseURL;
619
620    if (responseURL === undefined) {
621      responseURL = url;
622    }
623
624    let responseURLObj;
625    if (typeof responseURL === 'string') {
626      try {
627        responseURLObj = new URL(responseURL);
628      } catch {
629        // responseURLObj not defined will throw in next branch.
630      }
631    }
632
633    if (responseURLObj?.href !== responseURL) {
634      throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
635        'undefined or a fully resolved URL string',
636        hookErrIdentifier,
637        'responseURL',
638        responseURL,
639      );
640    }
641
642    if (format == null) {
643      const dataUrl = RegExpPrototypeExec(
644        /^data:([^/]+\/[^;,]+)(?:[^,]*?)(;base64)?,/,
645        url,
646      );
647
648      throw new ERR_UNKNOWN_MODULE_FORMAT(
649        dataUrl ? dataUrl[1] : format,
650        url);
651    }
652
653    if (typeof format !== 'string') { // [2]
654      throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
655        'a string',
656        hookErrIdentifier,
657        'format',
658        format,
659      );
660    }
661
662    if (
663      source != null &&
664      typeof source !== 'string' &&
665      !isAnyArrayBuffer(source) &&
666      !isArrayBufferView(source)
667    ) {
668      throw ERR_INVALID_RETURN_PROPERTY_VALUE(
669        'a string, an ArrayBuffer, or a TypedArray',
670        hookErrIdentifier,
671        'source',
672        source,
673      );
674    }
675
676    return {
677      __proto__: null,
678      format,
679      responseURL,
680      source,
681    };
682  }
683
684  preload() {
685    for (let i = this.#hooks.globalPreload.length - 1; i >= 0; i--) {
686      const channel = new MessageChannel();
687      const {
688        port1: insidePreload,
689        port2: insideLoader,
690      } = channel;
691
692      insidePreload.unref();
693      insideLoader.unref();
694
695      const {
696        fn: preload,
697        url: specifier,
698      } = this.#hooks.globalPreload[i];
699
700      const preloaded = preload({
701        port: insideLoader,
702      });
703
704      if (preloaded == null) { return; }
705
706      const hookErrIdentifier = `${specifier} globalPreload`;
707
708      if (typeof preloaded !== 'string') { // [2]
709        throw new ERR_INVALID_RETURN_VALUE(
710          'a string',
711          hookErrIdentifier,
712          preload,
713        );
714      }
715      const { compileFunction } = require('vm');
716      const preloadInit = compileFunction(
717        preloaded,
718        ['getBuiltin', 'port', 'setImportMetaCallback'],
719        {
720          filename: '<preload>',
721        },
722      );
723      const { BuiltinModule } = require('internal/bootstrap/loaders');
724      // We only allow replacing the importMetaInitializer during preload,
725      // after preload is finished, we disable the ability to replace it
726      //
727      // This exposes accidentally setting the initializer too late by
728      // throwing an error.
729      let finished = false;
730      let replacedImportMetaInitializer = false;
731      let next = this.#importMetaInitializer;
732      try {
733        // Calls the compiled preload source text gotten from the hook
734        // Since the parameters are named we use positional parameters
735        // see compileFunction above to cross reference the names
736        FunctionPrototypeCall(
737          preloadInit,
738          globalThis,
739          // Param getBuiltin
740          (builtinName) => {
741            if (BuiltinModule.canBeRequiredByUsers(builtinName) &&
742                BuiltinModule.canBeRequiredWithoutScheme(builtinName)) {
743              return require(builtinName);
744            }
745            throw new ERR_INVALID_ARG_VALUE('builtinName', builtinName);
746          },
747          // Param port
748          insidePreload,
749          // Param setImportMetaCallback
750          (fn) => {
751            if (finished || typeof fn !== 'function') {
752              throw new ERR_INVALID_ARG_TYPE('fn', fn);
753            }
754            replacedImportMetaInitializer = true;
755            const parent = next;
756            next = (meta, context) => {
757              return fn(meta, context, parent);
758            };
759          });
760      } finally {
761        finished = true;
762        if (replacedImportMetaInitializer) {
763          this.#importMetaInitializer = next;
764        }
765      }
766    }
767  }
768
769  importMetaInitialize(meta, context) {
770    this.#importMetaInitializer(meta, context);
771  }
772
773  /**
774   * Resolve the location of the module.
775   *
776   * Internally, this behaves like a backwards iterator, wherein the stack of
777   * hooks starts at the top and each call to `nextResolve()` moves down 1 step
778   * until it reaches the bottom or short-circuits.
779   * @param {string} originalSpecifier The specified URL path of the module to
780   *                                   be resolved.
781   * @param {string} [parentURL] The URL path of the module's parent.
782   * @param {ImportAssertions} importAssertions Assertions from the import
783   *                                              statement or expression.
784   * @returns {{ format: string, url: URL['href'] }}
785   */
786  async resolve(originalSpecifier, parentURL, importAssertions) {
787    const isMain = parentURL === undefined;
788
789    if (
790      !isMain &&
791      typeof parentURL !== 'string' &&
792      !isURL(parentURL)
793    ) {
794      throw new ERR_INVALID_ARG_TYPE(
795        'parentURL',
796        ['string', 'URL'],
797        parentURL,
798      );
799    }
800    const chain = this.#hooks.resolve;
801    const context = {
802      conditions: DEFAULT_CONDITIONS,
803      importAssertions,
804      parentURL,
805    };
806    const meta = {
807      chainFinished: null,
808      context,
809      hookErrIdentifier: '',
810      hookIndex: chain.length - 1,
811      hookName: 'resolve',
812      shortCircuited: false,
813    };
814
815    const validateArgs = (hookErrIdentifier, suppliedSpecifier, ctx) => {
816      validateString(
817        suppliedSpecifier,
818        `${hookErrIdentifier} specifier`,
819      ); // non-strings can be coerced to a url string
820
821      if (ctx) validateObject(ctx, `${hookErrIdentifier} context`);
822    };
823    const validateOutput = (hookErrIdentifier, output) => {
824      if (typeof output !== 'object' || output === null) { // [2]
825        throw new ERR_INVALID_RETURN_VALUE(
826          'an object',
827          hookErrIdentifier,
828          output,
829        );
830      }
831    };
832
833    const nextResolve = nextHookFactory(chain, meta, { validateArgs, validateOutput });
834
835    const resolution = await nextResolve(originalSpecifier, context);
836    const { hookErrIdentifier } = meta; // Retrieve the value after all settled
837
838    validateOutput(hookErrIdentifier, resolution);
839
840    if (resolution?.shortCircuit === true) { meta.shortCircuited = true; }
841
842    if (!meta.chainFinished && !meta.shortCircuited) {
843      throw new ERR_LOADER_CHAIN_INCOMPLETE(hookErrIdentifier);
844    }
845
846    const {
847      format,
848      url,
849    } = resolution;
850
851    if (
852      format != null &&
853      typeof format !== 'string' // [2]
854    ) {
855      throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
856        'a string',
857        hookErrIdentifier,
858        'format',
859        format,
860      );
861    }
862
863    if (typeof url !== 'string') {
864      // non-strings can be coerced to a url string
865      // validateString() throws a less-specific error
866      throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
867        'a url string',
868        hookErrIdentifier,
869        'url',
870        url,
871      );
872    }
873
874    // Try to avoid expensive URL instantiation for known-good urls
875    if (!this.moduleMap.has(url)) {
876      try {
877        new URL(url);
878      } catch {
879        throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
880          'a url string',
881          hookErrIdentifier,
882          'url',
883          url,
884        );
885      }
886    }
887
888    return {
889      __proto__: null,
890      format,
891      url,
892    };
893  }
894}
895
896ObjectSetPrototypeOf(ESMLoader.prototype, null);
897
898exports.ESMLoader = ESMLoader;
899