• 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  FunctionPrototypeBind,
8  ObjectSetPrototypeOf,
9  RegExpPrototypeExec,
10  SafeWeakMap,
11  StringPrototypeStartsWith,
12  globalThis,
13} = primordials;
14
15const {
16  ERR_INVALID_ARG_TYPE,
17  ERR_INVALID_ARG_VALUE,
18  ERR_INVALID_MODULE_SPECIFIER,
19  ERR_INVALID_RETURN_PROPERTY,
20  ERR_INVALID_RETURN_PROPERTY_VALUE,
21  ERR_INVALID_RETURN_VALUE,
22  ERR_UNKNOWN_MODULE_FORMAT
23} = require('internal/errors').codes;
24const { URL, pathToFileURL, isURLInstance } = require('internal/url');
25const ModuleMap = require('internal/modules/esm/module_map');
26const ModuleJob = require('internal/modules/esm/module_job');
27
28const {
29  defaultResolve,
30  DEFAULT_CONDITIONS,
31} = require('internal/modules/esm/resolve');
32const { defaultGetFormat } = require('internal/modules/esm/get_format');
33const { defaultGetSource } = require(
34  'internal/modules/esm/get_source');
35const { defaultTransformSource } = require(
36  'internal/modules/esm/transform_source');
37const { translators } = require(
38  'internal/modules/esm/translators');
39const { getOptionValue } = require('internal/options');
40
41/* A Loader instance is used as the main entry point for loading ES modules.
42 * Currently, this is a singleton -- there is only one used for loading
43 * the main module and everything in its dependency graph. */
44class Loader {
45  constructor() {
46    // Methods which translate input code or other information
47    // into es modules
48    this.translators = translators;
49
50    // Registry of loaded modules, akin to `require.cache`
51    this.moduleMap = new ModuleMap();
52
53    // Map of already-loaded CJS modules to use
54    this.cjsCache = new SafeWeakMap();
55
56    // This hook is called before the first root module is imported. It's a
57    // function that returns a piece of code that runs as a sloppy-mode script.
58    // The script may evaluate to a function that can be called with a
59    // `getBuiltin` helper that can be used to retrieve builtins.
60    // If the hook returns `null` instead of a source string, it opts out of
61    // running any preload code.
62    // The preload code runs as soon as the hook module has finished evaluating.
63    this._getGlobalPreloadCode = null;
64    // The resolver has the signature
65    //   (specifier : string, parentURL : string, defaultResolve)
66    //       -> Promise<{ url : string }>
67    // where defaultResolve is ModuleRequest.resolve (having the same
68    // signature itself).
69    this._resolve = defaultResolve;
70    // This hook is called after the module is resolved but before a translator
71    // is chosen to load it; the format returned by this function is the name
72    // of a translator.
73    this._getFormat = defaultGetFormat;
74    // This hook is called just before the source code of an ES module file
75    // is loaded.
76    this._getSource = defaultGetSource;
77    // This hook is called just after the source code of an ES module file
78    // is loaded, but before anything is done with the string.
79    this._transformSource = defaultTransformSource;
80    // The index for assigning unique URLs to anonymous module evaluation
81    this.evalIndex = 0;
82  }
83
84  async resolve(specifier, parentURL) {
85    const isMain = parentURL === undefined;
86    if (!isMain && typeof parentURL !== 'string' && !isURLInstance(parentURL))
87      throw new ERR_INVALID_ARG_TYPE('parentURL', ['string', 'URL'], parentURL);
88
89    const resolveResponse = await this._resolve(
90      specifier, { parentURL, conditions: DEFAULT_CONDITIONS }, defaultResolve);
91    if (typeof resolveResponse !== 'object') {
92      throw new ERR_INVALID_RETURN_VALUE(
93        'object', 'loader resolve', resolveResponse);
94    }
95
96    const { url } = resolveResponse;
97    if (typeof url !== 'string') {
98      throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
99        'string', 'loader resolve', 'url', url);
100    }
101    return url;
102  }
103
104  async getFormat(url) {
105    const getFormatResponse = await this._getFormat(
106      url, {}, defaultGetFormat);
107    if (typeof getFormatResponse !== 'object') {
108      throw new ERR_INVALID_RETURN_VALUE(
109        'object', 'loader getFormat', getFormatResponse);
110    }
111
112    const { format } = getFormatResponse;
113    if (format === null) {
114      const dataUrl = RegExpPrototypeExec(
115        /^data:([^/]+\/[^;,]+)(?:[^,]*?)(;base64)?,/,
116        url,
117      );
118      throw new ERR_INVALID_MODULE_SPECIFIER(
119        url,
120        dataUrl ? `has an unsupported MIME type "${dataUrl[1]}"` : '');
121    }
122    if (typeof format !== 'string') {
123      throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
124        'string', 'loader getFormat', 'format', format);
125    }
126
127    if (format === 'builtin') {
128      return format;
129    }
130
131    if (this._resolve !== defaultResolve) {
132      try {
133        new URL(url);
134      } catch {
135        throw new ERR_INVALID_RETURN_PROPERTY(
136          'url', 'loader resolve', 'url', url
137        );
138      }
139    }
140
141    if (this._resolve === defaultResolve &&
142      !StringPrototypeStartsWith(url, 'file:') &&
143      !StringPrototypeStartsWith(url, 'data:')
144    ) {
145      throw new ERR_INVALID_RETURN_PROPERTY(
146        'file: or data: url', 'loader resolve', 'url', url
147      );
148    }
149
150    return format;
151  }
152
153  async eval(
154    source,
155    url = pathToFileURL(`${process.cwd()}/[eval${++this.evalIndex}]`).href
156  ) {
157    const evalInstance = (url) => {
158      const { ModuleWrap, callbackMap } = internalBinding('module_wrap');
159      const module = new ModuleWrap(url, undefined, source, 0, 0);
160      callbackMap.set(module, {
161        importModuleDynamically: (specifier, { url }) => {
162          return this.import(specifier, url);
163        }
164      });
165
166      return module;
167    };
168    const job = new ModuleJob(this, url, evalInstance, false, false);
169    this.moduleMap.set(url, job);
170    const { module } = await job.run();
171    return {
172      namespace: module.getNamespace(),
173    };
174  }
175
176  async import(specifier, parent) {
177    const job = await this.getModuleJob(specifier, parent);
178    const { module } = await job.run();
179    return module.getNamespace();
180  }
181
182  hook(hooks) {
183    const {
184      resolve,
185      dynamicInstantiate,
186      getFormat,
187      getSource,
188      transformSource,
189      getGlobalPreloadCode,
190    } = hooks;
191
192    // Use .bind() to avoid giving access to the Loader instance when called.
193    if (resolve !== undefined)
194      this._resolve = FunctionPrototypeBind(resolve, null);
195    if (dynamicInstantiate !== undefined) {
196      process.emitWarning(
197        'The dynamicInstantiate loader hook has been removed.');
198    }
199    if (getFormat !== undefined) {
200      this._getFormat = FunctionPrototypeBind(getFormat, null);
201    }
202    if (getSource !== undefined) {
203      this._getSource = FunctionPrototypeBind(getSource, null);
204    }
205    if (transformSource !== undefined) {
206      this._transformSource = FunctionPrototypeBind(transformSource, null);
207    }
208    if (getGlobalPreloadCode !== undefined) {
209      this._getGlobalPreloadCode =
210        FunctionPrototypeBind(getGlobalPreloadCode, null);
211    }
212  }
213
214  runGlobalPreloadCode() {
215    if (!this._getGlobalPreloadCode) {
216      return;
217    }
218    const preloadCode = this._getGlobalPreloadCode();
219    if (preloadCode === null) {
220      return;
221    }
222
223    if (typeof preloadCode !== 'string') {
224      throw new ERR_INVALID_RETURN_VALUE(
225        'string', 'loader getGlobalPreloadCode', preloadCode);
226    }
227    const { compileFunction } = require('vm');
228    const preloadInit = compileFunction(preloadCode, ['getBuiltin'], {
229      filename: '<preload>',
230    });
231    const { NativeModule } = require('internal/bootstrap/loaders');
232
233    preloadInit.call(globalThis, (builtinName) => {
234      if (NativeModule.canBeRequiredByUsers(builtinName)) {
235        return require(builtinName);
236      }
237      throw new ERR_INVALID_ARG_VALUE('builtinName', builtinName);
238    });
239  }
240
241  async getModuleJob(specifier, parentURL) {
242    const url = await this.resolve(specifier, parentURL);
243    const format = await this.getFormat(url);
244    let job = this.moduleMap.get(url);
245    // CommonJS will set functions for lazy job evaluation.
246    if (typeof job === 'function')
247      this.moduleMap.set(url, job = job());
248    if (job !== undefined)
249      return job;
250
251    if (!translators.has(format))
252      throw new ERR_UNKNOWN_MODULE_FORMAT(format);
253
254    const loaderInstance = translators.get(format);
255
256    const inspectBrk = parentURL === undefined &&
257        format === 'module' && getOptionValue('--inspect-brk');
258    job = new ModuleJob(this, url, loaderInstance, parentURL === undefined,
259                        inspectBrk);
260    this.moduleMap.set(url, job);
261    return job;
262  }
263}
264
265ObjectSetPrototypeOf(Loader.prototype, null);
266
267exports.Loader = Loader;
268