1'use strict';
2
3// This is needed to avoid cycles in esm/resolve <-> cjs/loader
4require('internal/modules/cjs/loader');
5
6const {
7  ArrayPrototypeJoin,
8  ArrayPrototypeMap,
9  ArrayPrototypeReduce,
10  FunctionPrototypeCall,
11  JSONStringify,
12  ObjectSetPrototypeOf,
13  RegExpPrototypeSymbolReplace,
14  SafeWeakMap,
15  encodeURIComponent,
16  hardenRegExp,
17} = primordials;
18
19const {
20  ERR_UNKNOWN_MODULE_FORMAT,
21} = require('internal/errors').codes;
22const { getOptionValue } = require('internal/options');
23const { pathToFileURL, isURL } = require('internal/url');
24const { emitExperimentalWarning } = require('internal/util');
25const {
26  getDefaultConditions,
27} = require('internal/modules/esm/utils');
28let defaultResolve, defaultLoad, importMetaInitializer;
29
30/**
31 * Lazy loads the module_map module and returns a new instance of ResolveCache.
32 * @returns {import('./module_map.js').ResolveCache')}
33 */
34function newResolveCache() {
35  const { ResolveCache } = require('internal/modules/esm/module_map');
36  return new ResolveCache();
37}
38
39/**
40 * Generate a load cache (to store the final result of a load-chain for a particular module).
41 * @returns {import('./module_map.js').LoadCache')}
42 */
43function newLoadCache() {
44  const { LoadCache } = require('internal/modules/esm/module_map');
45  return new LoadCache();
46}
47
48/**
49 * Lazy-load translators to avoid potentially unnecessary work at startup (ex if ESM is not used).
50 * @returns {import('./translators.js').Translators}
51 */
52function getTranslators() {
53  const { translators } = require('internal/modules/esm/translators');
54  return translators;
55}
56
57/**
58 * @type {HooksProxy}
59 * Multiple loader instances exist for various, specific reasons (see code comments at site).
60 * In order to maintain consistency, we use a single worker (sandbox), which must sit apart of an
61 * individual loader instance.
62 */
63let hooksProxy;
64
65/**
66 * @typedef {Record<string, any>} ModuleExports
67 */
68
69/**
70 * @typedef {'builtin'|'commonjs'|'json'|'module'|'wasm'} ModuleFormat
71 */
72
73/**
74 * @typedef {ArrayBuffer|TypedArray|string} ModuleSource
75 */
76
77let emittedSpecifierResolutionWarning = false;
78
79/**
80 * This class covers the base machinery of module loading. To add custom
81 * behavior you can pass a customizations object and this object will be
82 * used to do the loading/resolving/registration process.
83 */
84class ModuleLoader {
85  /**
86   * The conditions for resolving packages if `--conditions` is not used.
87   */
88  #defaultConditions = getDefaultConditions();
89
90  /**
91   * Map of already-loaded CJS modules to use
92   */
93  cjsCache = new SafeWeakMap();
94
95  /**
96   * The index for assigning unique URLs to anonymous module evaluation
97   */
98  evalIndex = 0;
99
100  /**
101   * Registry of resolved specifiers
102   */
103  #resolveCache = newResolveCache();
104
105  /**
106   * Registry of loaded modules, akin to `require.cache`
107   */
108  loadCache = newLoadCache();
109
110  /**
111   * Methods which translate input code or other information into ES modules
112   */
113  translators = getTranslators();
114
115  /**
116   * Truthy to allow the use of `import.meta.resolve`. This is needed
117   * currently because the `Hooks` class does not have `resolveSync`
118   * implemented and `import.meta.resolve` requires it.
119   */
120  allowImportMetaResolve;
121
122  /**
123   * Customizations to pass requests to.
124   *
125   * Note that this value _MUST_ be set with `setCustomizations`
126   * because it needs to copy `customizations.allowImportMetaResolve`
127   *  to this property and failure to do so will cause undefined
128   * behavior when invoking `import.meta.resolve`.
129   * @see {ModuleLoader.setCustomizations}
130   */
131  #customizations;
132
133  constructor(customizations) {
134    if (getOptionValue('--experimental-network-imports')) {
135      emitExperimentalWarning('Network Imports');
136    }
137    if (
138      !emittedSpecifierResolutionWarning &&
139      getOptionValue('--experimental-specifier-resolution') === 'node'
140    ) {
141      process.emitWarning(
142        'The Node.js specifier resolution flag is experimental. It could change or be removed at any time.',
143        'ExperimentalWarning',
144      );
145      emittedSpecifierResolutionWarning = true;
146    }
147    this.setCustomizations(customizations);
148  }
149
150  /**
151   * Change the currently activate customizations for this module
152   * loader to be the provided `customizations`.
153   *
154   * If present, this class customizes its core functionality to the
155   * `customizations` object, including registration, loading, and resolving.
156   * There are some responsibilities that this class _always_ takes
157   * care of, like validating outputs, so that the customizations object
158   * does not have to do so.
159   *
160   * The customizations object has the shape:
161   *
162   * ```ts
163   * interface LoadResult {
164   *   format: ModuleFormat;
165   *   source: ModuleSource;
166   * }
167   *
168   * interface ResolveResult {
169   *   format: string;
170   *   url: URL['href'];
171   * }
172   *
173   * interface Customizations {
174   *   allowImportMetaResolve: boolean;
175   *   load(url: string, context: object): Promise<LoadResult>
176   *   resolve(
177   *     originalSpecifier:
178   *     string, parentURL: string,
179   *     importAttributes: Record<string, string>
180   *   ): Promise<ResolveResult>
181   *   resolveSync(
182   *     originalSpecifier:
183   *     string, parentURL: string,
184   *     importAttributes: Record<string, string>
185   *   ) ResolveResult;
186   *   register(specifier: string, parentURL: string): any;
187   *   forceLoadHooks(): void;
188   * }
189   * ```
190   *
191   * Note that this class _also_ implements the `Customizations`
192   * interface, as does `CustomizedModuleLoader` and `Hooks`.
193   *
194   * Calling this function alters how modules are loaded and should be
195   * invoked with care.
196   * @param {object} customizations
197   */
198  setCustomizations(customizations) {
199    this.#customizations = customizations;
200    if (customizations) {
201      this.allowImportMetaResolve = customizations.allowImportMetaResolve;
202    } else {
203      this.allowImportMetaResolve = true;
204    }
205  }
206
207  async eval(
208    source,
209    url = pathToFileURL(`${process.cwd()}/[eval${++this.evalIndex}]`).href,
210  ) {
211    const evalInstance = (url) => {
212      const { ModuleWrap } = internalBinding('module_wrap');
213      const { registerModule } = require('internal/modules/esm/utils');
214      const module = new ModuleWrap(url, undefined, source, 0, 0);
215      registerModule(module, {
216        __proto__: null,
217        initializeImportMeta: (meta, wrap) => this.importMetaInitialize(meta, { url }),
218        importModuleDynamically: (specifier, { url }, importAttributes) => {
219          return this.import(specifier, url, importAttributes);
220        },
221      });
222
223      return module;
224    };
225    const ModuleJob = require('internal/modules/esm/module_job');
226    const job = new ModuleJob(
227      this, url, undefined, evalInstance, false, false);
228    this.loadCache.set(url, undefined, job);
229    const { module } = await job.run();
230
231    return {
232      namespace: module.getNamespace(),
233    };
234  }
235
236  /**
237   * Get a (possibly still pending) module job from the cache,
238   * or create one and return its Promise.
239   * @param {string} specifier The string after `from` in an `import` statement,
240   *                           or the first parameter of an `import()`
241   *                           expression
242   * @param {string | undefined} parentURL The URL of the module importing this
243   *                                     one, unless this is the Node.js entry
244   *                                     point.
245   * @param {Record<string, string>} importAttributes Validations for the
246   *                                                  module import.
247   * @returns {Promise<ModuleJob>} The (possibly pending) module job
248   */
249  async getModuleJob(specifier, parentURL, importAttributes) {
250    const resolveResult = await this.resolve(specifier, parentURL, importAttributes);
251    return this.getJobFromResolveResult(resolveResult, parentURL, importAttributes);
252  }
253
254  getJobFromResolveResult(resolveResult, parentURL, importAttributes) {
255    const { url, format } = resolveResult;
256    const resolvedImportAttributes = resolveResult.importAttributes ?? importAttributes;
257    let job = this.loadCache.get(url, resolvedImportAttributes.type);
258
259    // CommonJS will set functions for lazy job evaluation.
260    if (typeof job === 'function') {
261      this.loadCache.set(url, undefined, job = job());
262    }
263
264    if (job === undefined) {
265      job = this.#createModuleJob(url, resolvedImportAttributes, parentURL, format);
266    }
267
268    return job;
269  }
270
271  /**
272   * Create and cache an object representing a loaded module.
273   * @param {string} url The absolute URL that was resolved for this module
274   * @param {Record<string, string>} importAttributes Validations for the
275   *                                                  module import.
276   * @param {string} [parentURL] The absolute URL of the module importing this
277   *                             one, unless this is the Node.js entry point
278   * @param {string} [format] The format hint possibly returned by the
279   *                          `resolve` hook
280   * @returns {Promise<ModuleJob>} The (possibly pending) module job
281   */
282  #createModuleJob(url, importAttributes, parentURL, format) {
283    const moduleProvider = async (url, isMain) => {
284      const {
285        format: finalFormat,
286        responseURL,
287        source,
288      } = await this.load(url, {
289        format,
290        importAttributes,
291      });
292
293      const translator = getTranslators().get(finalFormat);
294
295      if (!translator) {
296        throw new ERR_UNKNOWN_MODULE_FORMAT(finalFormat, responseURL);
297      }
298
299      return FunctionPrototypeCall(translator, this, responseURL, source, isMain);
300    };
301
302    const inspectBrk = (
303      parentURL === undefined &&
304      getOptionValue('--inspect-brk')
305    );
306
307    if (process.env.WATCH_REPORT_DEPENDENCIES && process.send) {
308      process.send({ 'watch:import': [url] });
309    }
310
311    const ModuleJob = require('internal/modules/esm/module_job');
312    const job = new ModuleJob(
313      this,
314      url,
315      importAttributes,
316      moduleProvider,
317      parentURL === undefined,
318      inspectBrk,
319    );
320
321    this.loadCache.set(url, importAttributes.type, job);
322
323    return job;
324  }
325
326  /**
327   * This method is usually called indirectly as part of the loading processes.
328   * Use directly with caution.
329   * @param {string} specifier The first parameter of an `import()` expression.
330   * @param {string} parentURL Path of the parent importing the module.
331   * @param {Record<string, string>} importAttributes Validations for the
332   *                                                  module import.
333   * @returns {Promise<ModuleExports>}
334   */
335  async import(specifier, parentURL, importAttributes) {
336    const moduleJob = await this.getModuleJob(specifier, parentURL, importAttributes);
337    const { module } = await moduleJob.run();
338    return module.getNamespace();
339  }
340
341  /**
342   * @see {@link CustomizedModuleLoader.register}
343   */
344  register(specifier, parentURL, data, transferList) {
345    if (!this.#customizations) {
346      // `CustomizedModuleLoader` is defined at the bottom of this file and
347      // available well before this line is ever invoked. This is here in
348      // order to preserve the git diff instead of moving the class.
349      // eslint-disable-next-line no-use-before-define
350      this.setCustomizations(new CustomizedModuleLoader());
351    }
352    return this.#customizations.register(`${specifier}`, `${parentURL}`, data, transferList);
353  }
354
355  /**
356   * Resolve the location of the module.
357   * @param {string} originalSpecifier The specified URL path of the module to
358   *                                   be resolved.
359   * @param {string} [parentURL] The URL path of the module's parent.
360   * @param {ImportAttributes} importAttributes Attributes from the import
361   *                                            statement or expression.
362   * @returns {{ format: string, url: URL['href'] }}
363   */
364  resolve(originalSpecifier, parentURL, importAttributes) {
365    if (this.#customizations) {
366      return this.#customizations.resolve(originalSpecifier, parentURL, importAttributes);
367    }
368    const requestKey = this.#resolveCache.serializeKey(originalSpecifier, importAttributes);
369    const cachedResult = this.#resolveCache.get(requestKey, parentURL);
370    if (cachedResult != null) {
371      return cachedResult;
372    }
373    const result = this.defaultResolve(originalSpecifier, parentURL, importAttributes);
374    this.#resolveCache.set(requestKey, parentURL, result);
375    return result;
376  }
377
378  /**
379   * Just like `resolve` except synchronous. This is here specifically to support
380   * `import.meta.resolve` which must happen synchronously.
381   */
382  resolveSync(originalSpecifier, parentURL, importAttributes) {
383    if (this.#customizations) {
384      return this.#customizations.resolveSync(originalSpecifier, parentURL, importAttributes);
385    }
386    return this.defaultResolve(originalSpecifier, parentURL, importAttributes);
387  }
388
389  /**
390   * Our `defaultResolve` is synchronous and can be used in both
391   * `resolve` and `resolveSync`. This function is here just to avoid
392   * repeating the same code block twice in those functions.
393   */
394  defaultResolve(originalSpecifier, parentURL, importAttributes) {
395    defaultResolve ??= require('internal/modules/esm/resolve').defaultResolve;
396
397    const context = {
398      __proto__: null,
399      conditions: this.#defaultConditions,
400      importAttributes,
401      parentURL,
402    };
403
404    return defaultResolve(originalSpecifier, context);
405  }
406
407  /**
408   * Provide source that is understood by one of Node's translators.
409   * @param {URL['href']} url The URL/path of the module to be loaded
410   * @param {object} [context] Metadata about the module
411   * @returns {Promise<{ format: ModuleFormat, source: ModuleSource }>}
412   */
413  async load(url, context) {
414    defaultLoad ??= require('internal/modules/esm/load').defaultLoad;
415    const result = this.#customizations ?
416      await this.#customizations.load(url, context) :
417      await defaultLoad(url, context);
418    this.validateLoadResult(url, result?.format);
419    return result;
420  }
421
422  validateLoadResult(url, format) {
423    if (format == null) {
424      require('internal/modules/esm/load').throwUnknownModuleFormat(url, format);
425    }
426  }
427
428  importMetaInitialize(meta, context) {
429    if (this.#customizations) {
430      return this.#customizations.importMetaInitialize(meta, context, this);
431    }
432    importMetaInitializer ??= require('internal/modules/esm/initialize_import_meta').initializeImportMeta;
433    meta = importMetaInitializer(meta, context, this);
434    return meta;
435  }
436
437  /**
438   * No-op when no hooks have been supplied.
439   */
440  forceLoadHooks() {
441    this.#customizations?.forceLoadHooks();
442  }
443}
444ObjectSetPrototypeOf(ModuleLoader.prototype, null);
445
446class CustomizedModuleLoader {
447
448  allowImportMetaResolve = true;
449
450  /**
451   * Instantiate a module loader that uses user-provided custom loader hooks.
452   */
453  constructor() {
454    getHooksProxy();
455  }
456
457  /**
458   * Register some loader specifier.
459   * @param {string} originalSpecifier The specified URL path of the loader to
460   *                                   be registered.
461   * @param {string} parentURL The parent URL from where the loader will be
462   *                           registered if using it package name as specifier
463   * @param {any} [data] Arbitrary data to be passed from the custom loader
464   * (user-land) to the worker.
465   * @param {any[]} [transferList] Objects in `data` that are changing ownership
466   * @returns {{ format: string, url: URL['href'] }}
467   */
468  register(originalSpecifier, parentURL, data, transferList) {
469    return hooksProxy.makeSyncRequest('register', transferList, originalSpecifier, parentURL, data);
470  }
471
472  /**
473   * Resolve the location of the module.
474   * @param {string} originalSpecifier The specified URL path of the module to
475   *                                   be resolved.
476   * @param {string} [parentURL] The URL path of the module's parent.
477   * @param {ImportAttributes} importAttributes Attributes from the import
478   *                                            statement or expression.
479   * @returns {{ format: string, url: URL['href'] }}
480   */
481  resolve(originalSpecifier, parentURL, importAttributes) {
482    return hooksProxy.makeAsyncRequest('resolve', undefined, originalSpecifier, parentURL, importAttributes);
483  }
484
485  resolveSync(originalSpecifier, parentURL, importAttributes) {
486    // This happens only as a result of `import.meta.resolve` calls, which must be sync per spec.
487    return hooksProxy.makeSyncRequest('resolve', undefined, originalSpecifier, parentURL, importAttributes);
488  }
489
490  /**
491   * Provide source that is understood by one of Node's translators.
492   * @param {URL['href']} url The URL/path of the module to be loaded
493   * @param {object} [context] Metadata about the module
494   * @returns {Promise<{ format: ModuleFormat, source: ModuleSource }>}
495   */
496  load(url, context) {
497    return hooksProxy.makeAsyncRequest('load', undefined, url, context);
498  }
499
500  importMetaInitialize(meta, context, loader) {
501    hooksProxy.importMetaInitialize(meta, context, loader);
502  }
503
504  forceLoadHooks() {
505    hooksProxy.waitForWorker();
506  }
507}
508
509let emittedLoaderFlagWarning = false;
510/**
511 * A loader instance is used as the main entry point for loading ES modules. Currently, this is a singleton; there is
512 * only one used for loading the main module and everything in its dependency graph, though separate instances of this
513 * class might be instantiated as part of bootstrap for other purposes.
514 * @param {boolean} useCustomLoadersIfPresent If the user has provided loaders via the --loader flag, use them.
515 * @returns {ModuleLoader}
516 */
517function createModuleLoader(useCustomLoadersIfPresent = true) {
518  let customizations = null;
519  if (useCustomLoadersIfPresent &&
520      // Don't spawn a new worker if we're already in a worker thread created by instantiating CustomizedModuleLoader;
521      // doing so would cause an infinite loop.
522      !require('internal/modules/esm/utils').isLoaderWorker()) {
523    const userLoaderPaths = getOptionValue('--experimental-loader');
524    if (userLoaderPaths.length > 0) {
525      if (!emittedLoaderFlagWarning) {
526        const readableURIEncode = (string) => ArrayPrototypeReduce(
527          [
528            [/'/g, '%27'], // We need to URL-encode the single quote as it's the delimiter for the --import flag.
529            [/%22/g, '"'], // We can decode the double quotes to improve readability.
530            [/%2F/ig, '/'], // We can decode the slashes to improve readability.
531          ],
532          (str, { 0: regex, 1: replacement }) => RegExpPrototypeSymbolReplace(hardenRegExp(regex), str, replacement),
533          encodeURIComponent(string));
534        process.emitWarning(
535          '`--experimental-loader` may be removed in the future; instead use `register()`:\n' +
536          `--import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; ${ArrayPrototypeJoin(
537            ArrayPrototypeMap(userLoaderPaths, (loader) => `register(${readableURIEncode(JSONStringify(loader))}, pathToFileURL("./"))`),
538            '; ',
539          )};'`,
540          'ExperimentalWarning',
541        );
542        emittedLoaderFlagWarning = true;
543      }
544      customizations = new CustomizedModuleLoader();
545    }
546  }
547
548  return new ModuleLoader(customizations);
549}
550
551
552/**
553 * Get the HooksProxy instance. If it is not defined, then create a new one.
554 * @returns {HooksProxy}
555 */
556function getHooksProxy() {
557  if (!hooksProxy) {
558    const { HooksProxy } = require('internal/modules/esm/hooks');
559    hooksProxy = new HooksProxy();
560  }
561
562  return hooksProxy;
563}
564
565/**
566 * Register a single loader programmatically.
567 * @param {string|import('url').URL} specifier
568 * @param {string|import('url').URL} [parentURL] Base to use when resolving `specifier`; optional if
569 * `specifier` is absolute. Same as `options.parentUrl`, just inline
570 * @param {object} [options] Additional options to apply, described below.
571 * @param {string|import('url').URL} [options.parentURL] Base to use when resolving `specifier`
572 * @param {any} [options.data] Arbitrary data passed to the loader's `initialize` hook
573 * @param {any[]} [options.transferList] Objects in `data` that are changing ownership
574 * @returns {void} We want to reserve the return value for potential future extension of the API.
575 * @example
576 * ```js
577 * register('./myLoader.js');
578 * register('ts-node/esm', { parentURL: import.meta.url });
579 * register('./myLoader.js', { parentURL: import.meta.url });
580 * register('ts-node/esm', import.meta.url);
581 * register('./myLoader.js', import.meta.url);
582 * register(new URL('./myLoader.js', import.meta.url));
583 * register('./myLoader.js', {
584 *   parentURL: import.meta.url,
585 *   data: { banana: 'tasty' },
586 * });
587 * register('./myLoader.js', {
588 *   parentURL: import.meta.url,
589 *   data: someArrayBuffer,
590 *   transferList: [someArrayBuffer],
591 * });
592 * ```
593 */
594function register(specifier, parentURL = undefined, options) {
595  const moduleLoader = require('internal/process/esm_loader').esmLoader;
596  if (parentURL != null && typeof parentURL === 'object' && !isURL(parentURL)) {
597    options = parentURL;
598    parentURL = options.parentURL;
599  }
600  moduleLoader.register(
601    specifier,
602    parentURL ?? 'data:',
603    options?.data,
604    options?.transferList,
605  );
606}
607
608module.exports = {
609  createModuleLoader,
610  getHooksProxy,
611  register,
612};
613