• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3const {
4  ArrayPrototypePush,
5  ArrayPrototypePushApply,
6  FunctionPrototypeCall,
7  Int32Array,
8  ObjectAssign,
9  ObjectDefineProperty,
10  ObjectSetPrototypeOf,
11  Promise,
12  ReflectSet,
13  SafeSet,
14  StringPrototypeSlice,
15  StringPrototypeStartsWith,
16  StringPrototypeToUpperCase,
17  globalThis,
18} = primordials;
19
20const {
21  Atomics: {
22    load: AtomicsLoad,
23    wait: AtomicsWait,
24    waitAsync: AtomicsWaitAsync,
25  },
26  SharedArrayBuffer,
27} = globalThis;
28
29const {
30  ERR_INTERNAL_ASSERTION,
31  ERR_INVALID_ARG_TYPE,
32  ERR_INVALID_ARG_VALUE,
33  ERR_INVALID_RETURN_PROPERTY_VALUE,
34  ERR_INVALID_RETURN_VALUE,
35  ERR_LOADER_CHAIN_INCOMPLETE,
36  ERR_METHOD_NOT_IMPLEMENTED,
37  ERR_UNKNOWN_BUILTIN_MODULE,
38  ERR_WORKER_UNSERIALIZABLE_ERROR,
39} = require('internal/errors').codes;
40const { URL } = require('internal/url');
41const { canParse: URLCanParse } = internalBinding('url');
42const { receiveMessageOnPort } = require('worker_threads');
43const {
44  isAnyArrayBuffer,
45  isArrayBufferView,
46} = require('internal/util/types');
47const {
48  validateObject,
49  validateString,
50} = require('internal/validators');
51const {
52  emitExperimentalWarning,
53  kEmptyObject,
54} = require('internal/util');
55
56const {
57  defaultResolve,
58  throwIfInvalidParentURL,
59} = require('internal/modules/esm/resolve');
60const {
61  getDefaultConditions,
62  loaderWorkerId,
63} = require('internal/modules/esm/utils');
64const { deserializeError } = require('internal/error_serdes');
65const {
66  SHARED_MEMORY_BYTE_LENGTH,
67  WORKER_TO_MAIN_THREAD_NOTIFICATION,
68} = require('internal/modules/esm/shared_constants');
69let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
70  debug = fn;
71});
72let importMetaInitializer;
73
74let importAssertionAlreadyWarned = false;
75
76function emitImportAssertionWarning() {
77  if (!importAssertionAlreadyWarned) {
78    importAssertionAlreadyWarned = true;
79    process.emitWarning('Use `importAttributes` instead of `importAssertions`', 'ExperimentalWarning');
80  }
81}
82
83function defineImportAssertionAlias(context) {
84  return ObjectDefineProperty(context, 'importAssertions', {
85    __proto__: null,
86    configurable: true,
87    get() {
88      emitImportAssertionWarning();
89      return this.importAttributes;
90    },
91    set(value) {
92      emitImportAssertionWarning();
93      return ReflectSet(this, 'importAttributes', value);
94    },
95  });
96}
97
98/**
99 * @typedef {object} ExportedHooks
100 * @property {Function} initialize Customizations setup hook.
101 * @property {Function} globalPreload Global preload hook.
102 * @property {Function} resolve Resolve hook.
103 * @property {Function} load Load hook.
104 */
105
106/**
107 * @typedef {object} KeyedHook
108 * @property {Function} fn The hook function.
109 * @property {URL['href']} url The URL of the module.
110 */
111
112// [2] `validate...()`s throw the wrong error
113
114class Hooks {
115  #chains = {
116    /**
117     * Prior to ESM loading. These are called once before any modules are started.
118     * @private
119     * @property {KeyedHook[]} globalPreload Last-in-first-out list of preload hooks.
120     */
121    globalPreload: [],
122
123    /**
124     * Phase 1 of 2 in ESM loading.
125     * The output of the `resolve` chain of hooks is passed into the `load` chain of hooks.
126     * @private
127     * @property {KeyedHook[]} resolve Last-in-first-out collection of resolve hooks.
128     */
129    resolve: [
130      {
131        fn: defaultResolve,
132        url: 'node:internal/modules/esm/resolve',
133      },
134    ],
135
136    /**
137     * Phase 2 of 2 in ESM loading.
138     * @private
139     * @property {KeyedHook[]} load Last-in-first-out collection of loader hooks.
140     */
141    load: [
142      {
143        fn: require('internal/modules/esm/load').defaultLoad,
144        url: 'node:internal/modules/esm/load',
145      },
146    ],
147  };
148
149  // Cache URLs we've already validated to avoid repeated validation
150  #validatedUrls = new SafeSet();
151
152  allowImportMetaResolve = false;
153
154  /**
155   * Import and register custom/user-defined module loader hook(s).
156   * @param {string} urlOrSpecifier
157   * @param {string} parentURL
158   * @param {any} [data] Arbitrary data to be passed from the custom
159   * loader (user-land) to the worker.
160   */
161  async register(urlOrSpecifier, parentURL, data) {
162    const moduleLoader = require('internal/process/esm_loader').esmLoader;
163    const keyedExports = await moduleLoader.import(
164      urlOrSpecifier,
165      parentURL,
166      kEmptyObject,
167    );
168    await this.addCustomLoader(urlOrSpecifier, keyedExports, data);
169  }
170
171  /**
172   * Collect custom/user-defined module loader hook(s).
173   * After all hooks have been collected, the global preload hook(s) must be initialized.
174   * @param {string} url Custom loader specifier
175   * @param {Record<string, unknown>} exports
176   * @param {any} [data] Arbitrary data to be passed from the custom loader (user-land)
177   * to the worker.
178   * @returns {any | Promise<any>} User data, ignored unless it's a promise, in which case it will be awaited.
179   */
180  addCustomLoader(url, exports, data) {
181    const {
182      globalPreload,
183      initialize,
184      resolve,
185      load,
186    } = pluckHooks(exports);
187
188    if (globalPreload && !initialize) {
189      emitExperimentalWarning(
190        '`globalPreload` is planned for removal in favor of `initialize`. `globalPreload`',
191      );
192      ArrayPrototypePush(this.#chains.globalPreload, { __proto__: null, fn: globalPreload, url });
193    }
194    if (resolve) {
195      const next = this.#chains.resolve[this.#chains.resolve.length - 1];
196      ArrayPrototypePush(this.#chains.resolve, { __proto__: null, fn: resolve, url, next });
197    }
198    if (load) {
199      const next = this.#chains.load[this.#chains.load.length - 1];
200      ArrayPrototypePush(this.#chains.load, { __proto__: null, fn: load, url, next });
201    }
202    return initialize?.(data);
203  }
204
205  /**
206   * Initialize `globalPreload` hooks.
207   */
208  initializeGlobalPreload() {
209    const preloadScripts = [];
210    for (let i = this.#chains.globalPreload.length - 1; i >= 0; i--) {
211      const { MessageChannel } = require('internal/worker/io');
212      const channel = new MessageChannel();
213      const {
214        port1: insidePreload,
215        port2: insideLoader,
216      } = channel;
217
218      insidePreload.unref();
219      insideLoader.unref();
220
221      const {
222        fn: preload,
223        url: specifier,
224      } = this.#chains.globalPreload[i];
225
226      const preloaded = preload({
227        port: insideLoader,
228      });
229
230      if (preloaded == null) { continue; }
231
232      if (typeof preloaded !== 'string') { // [2]
233        throw new ERR_INVALID_RETURN_VALUE(
234          'a string',
235          `${specifier} globalPreload`,
236          preload,
237        );
238      }
239
240      ArrayPrototypePush(preloadScripts, {
241        code: preloaded,
242        port: insidePreload,
243      });
244    }
245    return preloadScripts;
246  }
247
248  /**
249   * Resolve the location of the module.
250   *
251   * Internally, this behaves like a backwards iterator, wherein the stack of
252   * hooks starts at the top and each call to `nextResolve()` moves down 1 step
253   * until it reaches the bottom or short-circuits.
254   * @param {string} originalSpecifier The specified URL path of the module to
255   *                                   be resolved.
256   * @param {string} [parentURL] The URL path of the module's parent.
257   * @param {ImportAttributes} [importAttributes] Attributes from the import
258   *                                              statement or expression.
259   * @returns {Promise<{ format: string, url: URL['href'] }>}
260   */
261  async resolve(
262    originalSpecifier,
263    parentURL,
264    importAttributes = { __proto__: null },
265  ) {
266    throwIfInvalidParentURL(parentURL);
267
268    const chain = this.#chains.resolve;
269    const context = {
270      conditions: getDefaultConditions(),
271      importAttributes,
272      parentURL,
273    };
274    const meta = {
275      chainFinished: null,
276      context,
277      hookErrIdentifier: '',
278      hookName: 'resolve',
279      shortCircuited: false,
280    };
281
282    const validateArgs = (hookErrIdentifier, suppliedSpecifier, ctx) => {
283      validateString(
284        suppliedSpecifier,
285        `${hookErrIdentifier} specifier`,
286      ); // non-strings can be coerced to a URL string
287
288      if (ctx) { validateObject(ctx, `${hookErrIdentifier} context`); }
289    };
290    const validateOutput = (hookErrIdentifier, output) => {
291      if (typeof output !== 'object' || output === null) { // [2]
292        throw new ERR_INVALID_RETURN_VALUE(
293          'an object',
294          hookErrIdentifier,
295          output,
296        );
297      }
298    };
299
300    const nextResolve = nextHookFactory(chain[chain.length - 1], meta, { validateArgs, validateOutput });
301
302    const resolution = await nextResolve(originalSpecifier, context);
303    const { hookErrIdentifier } = meta; // Retrieve the value after all settled
304
305    validateOutput(hookErrIdentifier, resolution);
306
307    if (resolution?.shortCircuit === true) { meta.shortCircuited = true; }
308
309    if (!meta.chainFinished && !meta.shortCircuited) {
310      throw new ERR_LOADER_CHAIN_INCOMPLETE(hookErrIdentifier);
311    }
312
313    let resolvedImportAttributes;
314    const {
315      format,
316      url,
317    } = resolution;
318
319    if (typeof url !== 'string') {
320      // non-strings can be coerced to a URL string
321      // validateString() throws a less-specific error
322      throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
323        'a URL string',
324        hookErrIdentifier,
325        'url',
326        url,
327      );
328    }
329
330    // Avoid expensive URL instantiation for known-good URLs
331    if (!this.#validatedUrls.has(url)) {
332      // No need to convert to string, since the type is already validated
333      if (!URLCanParse(url)) {
334        throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
335          'a URL string',
336          hookErrIdentifier,
337          'url',
338          url,
339        );
340      }
341
342      this.#validatedUrls.add(url);
343    }
344
345    if (!('importAttributes' in resolution) && ('importAssertions' in resolution)) {
346      emitImportAssertionWarning();
347      resolvedImportAttributes = resolution.importAssertions;
348    } else {
349      resolvedImportAttributes = resolution.importAttributes;
350    }
351
352    if (
353      resolvedImportAttributes != null &&
354      typeof resolvedImportAttributes !== 'object'
355    ) {
356      throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
357        'an object',
358        hookErrIdentifier,
359        'importAttributes',
360        resolvedImportAttributes,
361      );
362    }
363
364    if (
365      format != null &&
366      typeof format !== 'string' // [2]
367    ) {
368      throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
369        'a string',
370        hookErrIdentifier,
371        'format',
372        format,
373      );
374    }
375
376    return {
377      __proto__: null,
378      format,
379      importAttributes: resolvedImportAttributes,
380      url,
381    };
382  }
383
384  resolveSync(_originalSpecifier, _parentURL, _importAttributes) {
385    throw new ERR_METHOD_NOT_IMPLEMENTED('resolveSync()');
386  }
387
388  /**
389   * Provide source that is understood by one of Node's translators.
390   *
391   * Internally, this behaves like a backwards iterator, wherein the stack of
392   * hooks starts at the top and each call to `nextLoad()` moves down 1 step
393   * until it reaches the bottom or short-circuits.
394   * @param {URL['href']} url The URL/path of the module to be loaded
395   * @param {object} context Metadata about the module
396   * @returns {Promise<{ format: ModuleFormat, source: ModuleSource }>}
397   */
398  async load(url, context = {}) {
399    const chain = this.#chains.load;
400    const meta = {
401      chainFinished: null,
402      context,
403      hookErrIdentifier: '',
404      hookName: 'load',
405      shortCircuited: false,
406    };
407
408    const validateArgs = (hookErrIdentifier, nextUrl, ctx) => {
409      if (typeof nextUrl !== 'string') {
410        // Non-strings can be coerced to a URL string
411        // validateString() throws a less-specific error
412        throw new ERR_INVALID_ARG_TYPE(
413          `${hookErrIdentifier} url`,
414          'a URL string',
415          nextUrl,
416        );
417      }
418
419      // Avoid expensive URL instantiation for known-good URLs
420      if (!this.#validatedUrls.has(nextUrl)) {
421        // No need to convert to string, since the type is already validated
422        if (!URLCanParse(nextUrl)) {
423          throw new ERR_INVALID_ARG_VALUE(
424            `${hookErrIdentifier} url`,
425            nextUrl,
426            'should be a URL string',
427          );
428        }
429
430        this.#validatedUrls.add(nextUrl);
431      }
432
433      if (ctx) { validateObject(ctx, `${hookErrIdentifier} context`); }
434    };
435    const validateOutput = (hookErrIdentifier, output) => {
436      if (typeof output !== 'object' || output === null) { // [2]
437        throw new ERR_INVALID_RETURN_VALUE(
438          'an object',
439          hookErrIdentifier,
440          output,
441        );
442      }
443    };
444
445    const nextLoad = nextHookFactory(chain[chain.length - 1], meta, { validateArgs, validateOutput });
446
447    const loaded = await nextLoad(url, defineImportAssertionAlias(context));
448    const { hookErrIdentifier } = meta; // Retrieve the value after all settled
449
450    validateOutput(hookErrIdentifier, loaded);
451
452    if (loaded?.shortCircuit === true) { meta.shortCircuited = true; }
453
454    if (!meta.chainFinished && !meta.shortCircuited) {
455      throw new ERR_LOADER_CHAIN_INCOMPLETE(hookErrIdentifier);
456    }
457
458    const {
459      format,
460      source,
461    } = loaded;
462    let responseURL = loaded.responseURL;
463
464    if (responseURL === undefined) {
465      responseURL = url;
466    }
467
468    let responseURLObj;
469    if (typeof responseURL === 'string') {
470      try {
471        responseURLObj = new URL(responseURL);
472      } catch {
473        // responseURLObj not defined will throw in next branch.
474      }
475    }
476
477    if (responseURLObj?.href !== responseURL) {
478      throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
479        'undefined or a fully resolved URL string',
480        hookErrIdentifier,
481        'responseURL',
482        responseURL,
483      );
484    }
485
486    if (format == null) {
487      require('internal/modules/esm/load').throwUnknownModuleFormat(url, format);
488    }
489
490    if (typeof format !== 'string') { // [2]
491      throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
492        'a string',
493        hookErrIdentifier,
494        'format',
495        format,
496      );
497    }
498
499    if (
500      source != null &&
501      typeof source !== 'string' &&
502      !isAnyArrayBuffer(source) &&
503      !isArrayBufferView(source)
504    ) {
505      throw ERR_INVALID_RETURN_PROPERTY_VALUE(
506        'a string, an ArrayBuffer, or a TypedArray',
507        hookErrIdentifier,
508        'source',
509        source,
510      );
511    }
512
513    return {
514      __proto__: null,
515      format,
516      responseURL,
517      source,
518    };
519  }
520
521  forceLoadHooks() {
522    // No-op
523  }
524
525  importMetaInitialize(meta, context, loader) {
526    importMetaInitializer ??= require('internal/modules/esm/initialize_import_meta').initializeImportMeta;
527    meta = importMetaInitializer(meta, context, loader);
528    return meta;
529  }
530}
531ObjectSetPrototypeOf(Hooks.prototype, null);
532
533/**
534 * There may be multiple instances of Hooks/HooksProxy, but there is only 1 Internal worker, so
535 * there is only 1 MessageChannel.
536 */
537let MessageChannel;
538class HooksProxy {
539  /**
540   * Shared memory. Always use Atomics method to read or write to it.
541   * @type {Int32Array}
542   */
543  #lock;
544  /**
545   * The InternalWorker instance, which lets us communicate with the loader thread.
546   */
547  #worker;
548
549  /**
550   * The last notification ID received from the worker. This is used to detect
551   * if the worker has already sent a notification before putting the main
552   * thread to sleep, to avoid a race condition.
553   * @type {number}
554   */
555  #workerNotificationLastId = 0;
556
557  /**
558   * Track how many async responses the main thread should expect.
559   * @type {number}
560   */
561  #numberOfPendingAsyncResponses = 0;
562
563  #isReady = false;
564
565  constructor() {
566    const { InternalWorker } = require('internal/worker');
567    MessageChannel ??= require('internal/worker/io').MessageChannel;
568
569    const lock = new SharedArrayBuffer(SHARED_MEMORY_BYTE_LENGTH);
570    this.#lock = new Int32Array(lock);
571
572    this.#worker = new InternalWorker(loaderWorkerId, {
573      stderr: false,
574      stdin: false,
575      stdout: false,
576      trackUnmanagedFds: false,
577      workerData: {
578        lock,
579      },
580    });
581    this.#worker.unref(); // ! Allows the process to eventually exit.
582    this.#worker.on('exit', process.exit);
583  }
584
585  waitForWorker() {
586    if (!this.#isReady) {
587      const { kIsOnline } = require('internal/worker');
588      if (!this.#worker[kIsOnline]) {
589        debug('wait for signal from worker');
590        AtomicsWait(this.#lock, WORKER_TO_MAIN_THREAD_NOTIFICATION, 0);
591        const response = this.#worker.receiveMessageSync();
592        if (response == null || response.message.status === 'exit') { return; }
593        const { preloadScripts } = this.#unwrapMessage(response);
594        this.#executePreloadScripts(preloadScripts);
595      }
596
597      this.#isReady = true;
598    }
599  }
600
601  /**
602   * Invoke a remote method asynchronously.
603   * @param {string} method Method to invoke
604   * @param {any[]} [transferList] Objects in `args` to be transferred
605   * @param  {any[]} args Arguments to pass to `method`
606   * @returns {Promise<any>}
607   */
608  async makeAsyncRequest(method, transferList, ...args) {
609    this.waitForWorker();
610
611    MessageChannel ??= require('internal/worker/io').MessageChannel;
612    const asyncCommChannel = new MessageChannel();
613
614    // Pass work to the worker.
615    debug('post async message to worker', { method, args, transferList });
616    const finalTransferList = [asyncCommChannel.port2];
617    if (transferList) {
618      ArrayPrototypePushApply(finalTransferList, transferList);
619    }
620    this.#worker.postMessage({
621      __proto__: null,
622      method, args,
623      port: asyncCommChannel.port2,
624    }, finalTransferList);
625
626    if (this.#numberOfPendingAsyncResponses++ === 0) {
627      // On the next lines, the main thread will await a response from the worker thread that might
628      // come AFTER the last task in the event loop has run its course and there would be nothing
629      // left keeping the thread alive (and once the main thread dies, the whole process stops).
630      // However we want to keep the process alive until the worker thread responds (or until the
631      // event loop of the worker thread is also empty), so we ref the worker until we get all the
632      // responses back.
633      this.#worker.ref();
634    }
635
636    let response;
637    do {
638      debug('wait for async response from worker', { method, args });
639      await AtomicsWaitAsync(this.#lock, WORKER_TO_MAIN_THREAD_NOTIFICATION, this.#workerNotificationLastId).value;
640      this.#workerNotificationLastId = AtomicsLoad(this.#lock, WORKER_TO_MAIN_THREAD_NOTIFICATION);
641
642      response = receiveMessageOnPort(asyncCommChannel.port1);
643    } while (response == null);
644    debug('got async response from worker', { method, args }, this.#lock);
645
646    if (--this.#numberOfPendingAsyncResponses === 0) {
647      // We got all the responses from the worker, its job is done (until next time).
648      this.#worker.unref();
649    }
650
651    const body = this.#unwrapMessage(response);
652    asyncCommChannel.port1.close();
653    return body;
654  }
655
656  /**
657   * Invoke a remote method synchronously.
658   * @param {string} method Method to invoke
659   * @param {any[]} [transferList] Objects in `args` to be transferred
660   * @param  {any[]} args Arguments to pass to `method`
661   * @returns {any}
662   */
663  makeSyncRequest(method, transferList, ...args) {
664    this.waitForWorker();
665
666    // Pass work to the worker.
667    debug('post sync message to worker', { method, args, transferList });
668    this.#worker.postMessage({ __proto__: null, method, args }, transferList);
669
670    let response;
671    do {
672      debug('wait for sync response from worker', { method, args });
673      // Sleep until worker responds.
674      AtomicsWait(this.#lock, WORKER_TO_MAIN_THREAD_NOTIFICATION, this.#workerNotificationLastId);
675      this.#workerNotificationLastId = AtomicsLoad(this.#lock, WORKER_TO_MAIN_THREAD_NOTIFICATION);
676
677      response = this.#worker.receiveMessageSync();
678    } while (response == null);
679    debug('got sync response from worker', { method, args });
680    if (response.message.status === 'never-settle') {
681      process.exit(13);
682    } else if (response.message.status === 'exit') {
683      process.exit(response.message.body);
684    }
685    return this.#unwrapMessage(response);
686  }
687
688  #unwrapMessage(response) {
689    if (response.message.status === 'never-settle') {
690      return new Promise(() => {});
691    }
692    const { status, body } = response.message;
693    if (status === 'error') {
694      if (body == null || typeof body !== 'object') { throw body; }
695      if (body.serializationFailed || body.serialized == null) {
696        throw ERR_WORKER_UNSERIALIZABLE_ERROR();
697      }
698
699      // eslint-disable-next-line no-restricted-syntax
700      throw deserializeError(body.serialized);
701    } else {
702      return body;
703    }
704  }
705
706  #importMetaInitializer = require('internal/modules/esm/initialize_import_meta').initializeImportMeta;
707
708  importMetaInitialize(meta, context, loader) {
709    this.#importMetaInitializer(meta, context, loader);
710  }
711
712  #executePreloadScripts(preloadScripts) {
713    for (let i = 0; i < preloadScripts.length; i++) {
714      const { code, port } = preloadScripts[i];
715      const { compileFunction } = require('vm');
716      const preloadInit = compileFunction(
717        code,
718        ['getBuiltin', 'port', 'setImportMetaCallback'],
719        {
720          filename: '<preload>',
721        },
722      );
723      let finished = false;
724      let replacedImportMetaInitializer = false;
725      let next = this.#importMetaInitializer;
726      const { BuiltinModule } = require('internal/bootstrap/realm');
727      // Calls the compiled preload source text gotten from the hook
728      // Since the parameters are named we use positional parameters
729      // see compileFunction above to cross reference the names
730      try {
731        FunctionPrototypeCall(
732          preloadInit,
733          globalThis,
734          // Param getBuiltin
735          (builtinName) => {
736            if (StringPrototypeStartsWith(builtinName, 'node:')) {
737              builtinName = StringPrototypeSlice(builtinName, 5);
738            } else if (!BuiltinModule.canBeRequiredWithoutScheme(builtinName)) {
739              throw new ERR_UNKNOWN_BUILTIN_MODULE(builtinName);
740            }
741            if (BuiltinModule.canBeRequiredByUsers(builtinName)) {
742              return require(builtinName);
743            }
744            throw new ERR_UNKNOWN_BUILTIN_MODULE(builtinName);
745          },
746          // Param port
747          port,
748          // setImportMetaCallback
749          (fn) => {
750            if (finished || typeof fn !== 'function') {
751              throw new ERR_INVALID_ARG_TYPE('fn', fn);
752            }
753            replacedImportMetaInitializer = true;
754            const parent = next;
755            next = (meta, context) => {
756              return fn(meta, context, parent);
757            };
758          },
759        );
760      } finally {
761        finished = true;
762        if (replacedImportMetaInitializer) {
763          this.#importMetaInitializer = next;
764        }
765      }
766    }
767  }
768}
769ObjectSetPrototypeOf(HooksProxy.prototype, null);
770
771/**
772 * A utility function to pluck the hooks from a user-defined loader.
773 * @param {import('./loader.js).ModuleExports} exports
774 * @returns {ExportedHooks}
775 */
776function pluckHooks({
777  globalPreload,
778  initialize,
779  resolve,
780  load,
781}) {
782  const acceptedHooks = { __proto__: null };
783
784  if (globalPreload) {
785    acceptedHooks.globalPreload = globalPreload;
786  }
787  if (resolve) {
788    acceptedHooks.resolve = resolve;
789  }
790  if (load) {
791    acceptedHooks.load = load;
792  }
793
794  if (initialize) {
795    acceptedHooks.initialize = initialize;
796  }
797
798  return acceptedHooks;
799}
800
801
802/**
803 * A utility function to iterate through a hook chain, track advancement in the
804 * chain, and generate and supply the `next<HookName>` argument to the custom
805 * hook.
806 * @param {Hook} current The (currently) first hook in the chain (this shifts
807 * on every call).
808 * @param {object} meta Properties that change as the current hook advances
809 * along the chain.
810 * @param {boolean} meta.chainFinished Whether the end of the chain has been
811 * reached AND invoked.
812 * @param {string} meta.hookErrIdentifier A user-facing identifier to help
813 *  pinpoint where an error occurred. Ex "file:///foo.mjs 'resolve'".
814 * @param {string} meta.hookName The kind of hook the chain is (ex 'resolve')
815 * @param {boolean} meta.shortCircuited Whether a hook signaled a short-circuit.
816 * @param {(hookErrIdentifier, hookArgs) => void} validate A wrapper function
817 *  containing all validation of a custom loader hook's intermediary output. Any
818 *  validation within MUST throw.
819 * @returns {function next<HookName>(...hookArgs)} The next hook in the chain.
820 */
821function nextHookFactory(current, meta, { validateArgs, validateOutput }) {
822  // First, prepare the current
823  const { hookName } = meta;
824  const {
825    fn: hook,
826    url: hookFilePath,
827    next,
828  } = current;
829
830  // ex 'nextResolve'
831  const nextHookName = `next${
832    StringPrototypeToUpperCase(hookName[0]) +
833    StringPrototypeSlice(hookName, 1)
834  }`;
835
836  let nextNextHook;
837  if (next) {
838    nextNextHook = nextHookFactory(next, meta, { validateArgs, validateOutput });
839  } else {
840    // eslint-disable-next-line func-name-matching
841    nextNextHook = function chainAdvancedTooFar() {
842      throw new ERR_INTERNAL_ASSERTION(
843        `ESM custom loader '${hookName}' advanced beyond the end of the chain.`,
844      );
845    };
846  }
847
848  return ObjectDefineProperty(
849    async (arg0 = undefined, context) => {
850      // Update only when hook is invoked to avoid fingering the wrong filePath
851      meta.hookErrIdentifier = `${hookFilePath} '${hookName}'`;
852
853      validateArgs(`${meta.hookErrIdentifier} hook's ${nextHookName}()`, arg0, context);
854
855      const outputErrIdentifier = `${hookFilePath} '${hookName}' hook's ${nextHookName}()`;
856
857      // Set when next<HookName> is actually called, not just generated.
858      if (!next) { meta.chainFinished = true; }
859
860      if (context) { // `context` has already been validated, so no fancy check needed.
861        ObjectAssign(meta.context, context);
862      }
863
864      const output = await hook(arg0, meta.context, nextNextHook);
865      validateOutput(outputErrIdentifier, output);
866
867      if (output?.shortCircuit === true) { meta.shortCircuited = true; }
868
869      return output;
870    },
871    'name',
872    { __proto__: null, value: nextHookName },
873  );
874}
875
876
877exports.Hooks = Hooks;
878exports.HooksProxy = HooksProxy;
879