• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3const {
4  ArrayPrototypeForEach,
5  ArrayPrototypeMap,
6  Boolean,
7  JSONParse,
8  ObjectGetPrototypeOf,
9  ObjectPrototypeHasOwnProperty,
10  ObjectKeys,
11  SafeArrayIterator,
12  SafeMap,
13  SafeSet,
14  StringPrototypeReplaceAll,
15  StringPrototypeSlice,
16  StringPrototypeStartsWith,
17  SyntaxErrorPrototype,
18  globalThis: { WebAssembly },
19} = primordials;
20
21let _TYPES = null;
22function lazyTypes() {
23  if (_TYPES !== null) return _TYPES;
24  return _TYPES = require('internal/util/types');
25}
26
27const { readFileSync } = require('fs');
28const { extname, isAbsolute } = require('path');
29const {
30  hasEsmSyntax,
31  loadBuiltinModule,
32  stripBOM,
33} = require('internal/modules/cjs/helpers');
34const {
35  Module: CJSModule,
36  cjsParseCache,
37} = require('internal/modules/cjs/loader');
38const internalURLModule = require('internal/url');
39const createDynamicModule = require(
40  'internal/modules/esm/create_dynamic_module');
41const { fileURLToPath, URL } = require('url');
42let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
43  debug = fn;
44});
45const { emitExperimentalWarning } = require('internal/util');
46const {
47  ERR_UNKNOWN_BUILTIN_MODULE,
48  ERR_INVALID_RETURN_PROPERTY_VALUE,
49} = require('internal/errors').codes;
50const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache');
51const moduleWrap = internalBinding('module_wrap');
52const { ModuleWrap } = moduleWrap;
53const asyncESM = require('internal/process/esm_loader');
54const { emitWarningSync } = require('internal/process/warning');
55const { TextDecoder } = require('internal/encoding');
56
57let cjsParse;
58async function initCJSParse() {
59  if (typeof WebAssembly === 'undefined') {
60    cjsParse = require('internal/deps/cjs-module-lexer/lexer').parse;
61  } else {
62    const { parse, init } =
63        require('internal/deps/cjs-module-lexer/dist/lexer');
64    try {
65      await init();
66      cjsParse = parse;
67    } catch {
68      cjsParse = require('internal/deps/cjs-module-lexer/lexer').parse;
69    }
70  }
71}
72
73const translators = new SafeMap();
74exports.translators = translators;
75exports.enrichCJSError = enrichCJSError;
76
77let DECODER = null;
78function assertBufferSource(body, allowString, hookName) {
79  if (allowString && typeof body === 'string') {
80    return;
81  }
82  const { isArrayBufferView, isAnyArrayBuffer } = lazyTypes();
83  if (isArrayBufferView(body) || isAnyArrayBuffer(body)) {
84    return;
85  }
86  throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
87    `${allowString ? 'string, ' : ''}array buffer, or typed array`,
88    hookName,
89    'source',
90    body,
91  );
92}
93
94function stringify(body) {
95  if (typeof body === 'string') return body;
96  assertBufferSource(body, false, 'transformSource');
97  DECODER = DECODER === null ? new TextDecoder() : DECODER;
98  return DECODER.decode(body);
99}
100
101function errPath(url) {
102  const parsed = new URL(url);
103  if (parsed.protocol === 'file:') {
104    return fileURLToPath(parsed);
105  }
106  return url;
107}
108
109async function importModuleDynamically(specifier, { url }, assertions) {
110  return asyncESM.esmLoader.import(specifier, url, assertions);
111}
112
113// Strategy for loading a standard JavaScript module.
114translators.set('module', async function moduleStrategy(url, source, isMain) {
115  assertBufferSource(source, true, 'load');
116  source = stringify(source);
117  maybeCacheSourceMap(url, source);
118  debug(`Translating StandardModule ${url}`);
119  const module = new ModuleWrap(url, undefined, source, 0, 0);
120  moduleWrap.callbackMap.set(module, {
121    initializeImportMeta: (meta, wrap) => this.importMetaInitialize(meta, { url }),
122    importModuleDynamically,
123  });
124  return module;
125});
126
127/**
128 * @param {Error | any} err
129 * @param {string} [content] Content of the file, if known.
130 * @param {string} [filename] Useful only if `content` is unknown.
131 */
132function enrichCJSError(err, content, filename) {
133  if (err != null && ObjectGetPrototypeOf(err) === SyntaxErrorPrototype &&
134      hasEsmSyntax(content || readFileSync(filename, 'utf-8'))) {
135    // Emit the warning synchronously because we are in the middle of handling
136    // a SyntaxError that will throw and likely terminate the process before an
137    // asynchronous warning would be emitted.
138    emitWarningSync(
139      'To load an ES module, set "type": "module" in the package.json or use ' +
140      'the .mjs extension.',
141    );
142  }
143}
144
145// Strategy for loading a node-style CommonJS module
146const isWindows = process.platform === 'win32';
147translators.set('commonjs', async function commonjsStrategy(url, source,
148                                                            isMain) {
149  debug(`Translating CJSModule ${url}`);
150
151  let filename = internalURLModule.fileURLToPath(new URL(url));
152  if (isWindows)
153    filename = StringPrototypeReplaceAll(filename, '/', '\\');
154
155  if (!cjsParse) await initCJSParse();
156  const { module, exportNames } = cjsPreparseModuleExports(filename);
157  const namesWithDefault = exportNames.has('default') ?
158    [...exportNames] : ['default', ...exportNames];
159
160  return new ModuleWrap(url, undefined, namesWithDefault, function() {
161    debug(`Loading CJSModule ${url}`);
162
163    let exports;
164    if (asyncESM.esmLoader.cjsCache.has(module)) {
165      exports = asyncESM.esmLoader.cjsCache.get(module);
166      asyncESM.esmLoader.cjsCache.delete(module);
167    } else {
168      try {
169        exports = CJSModule._load(filename, undefined, isMain);
170      } catch (err) {
171        enrichCJSError(err, undefined, filename);
172        throw err;
173      }
174    }
175
176    for (const exportName of exportNames) {
177      if (!ObjectPrototypeHasOwnProperty(exports, exportName) ||
178          exportName === 'default')
179        continue;
180      // We might trigger a getter -> dont fail.
181      let value;
182      try {
183        value = exports[exportName];
184      } catch {
185        // Continue regardless of error.
186      }
187      this.setExport(exportName, value);
188    }
189    this.setExport('default', exports);
190  });
191});
192
193function cjsPreparseModuleExports(filename) {
194  let module = CJSModule._cache[filename];
195  if (module) {
196    const cached = cjsParseCache.get(module);
197    if (cached)
198      return { module, exportNames: cached.exportNames };
199  }
200  const loaded = Boolean(module);
201  if (!loaded) {
202    module = new CJSModule(filename);
203    module.filename = filename;
204    module.paths = CJSModule._nodeModulePaths(module.path);
205    CJSModule._cache[filename] = module;
206  }
207
208  let source;
209  try {
210    source = readFileSync(filename, 'utf8');
211  } catch {
212    // Continue regardless of error.
213  }
214
215  let exports, reexports;
216  try {
217    ({ exports, reexports } = cjsParse(source || ''));
218  } catch {
219    exports = [];
220    reexports = [];
221  }
222
223  const exportNames = new SafeSet(new SafeArrayIterator(exports));
224
225  // Set first for cycles.
226  cjsParseCache.set(module, { source, exportNames, loaded });
227
228  if (reexports.length) {
229    module.filename = filename;
230    module.paths = CJSModule._nodeModulePaths(module.path);
231  }
232  ArrayPrototypeForEach(reexports, (reexport) => {
233    let resolved;
234    try {
235      resolved = CJSModule._resolveFilename(reexport, module);
236    } catch {
237      return;
238    }
239    const ext = extname(resolved);
240    if ((ext === '.js' || ext === '.cjs' || !CJSModule._extensions[ext]) &&
241        isAbsolute(resolved)) {
242      const { exportNames: reexportNames } = cjsPreparseModuleExports(resolved);
243      for (const name of reexportNames)
244        exportNames.add(name);
245    }
246  });
247
248  return { module, exportNames };
249}
250
251// Strategy for loading a node builtin CommonJS module that isn't
252// through normal resolution
253translators.set('builtin', async function builtinStrategy(url) {
254  debug(`Translating BuiltinModule ${url}`);
255  // Slice 'node:' scheme
256  const id = StringPrototypeSlice(url, 5);
257  const module = loadBuiltinModule(id, url);
258  if (!StringPrototypeStartsWith(url, 'node:') || !module) {
259    throw new ERR_UNKNOWN_BUILTIN_MODULE(url);
260  }
261  debug(`Loading BuiltinModule ${url}`);
262  return module.getESMFacade();
263});
264
265// Strategy for loading a JSON file
266translators.set('json', async function jsonStrategy(url, source) {
267  emitExperimentalWarning('Importing JSON modules');
268  assertBufferSource(source, true, 'load');
269  debug(`Loading JSONModule ${url}`);
270  const pathname = StringPrototypeStartsWith(url, 'file:') ?
271    fileURLToPath(url) : null;
272  let modulePath;
273  let module;
274  if (pathname) {
275    modulePath = isWindows ?
276      StringPrototypeReplaceAll(pathname, '/', '\\') : pathname;
277    module = CJSModule._cache[modulePath];
278    if (module && module.loaded) {
279      const exports = module.exports;
280      return new ModuleWrap(url, undefined, ['default'], function() {
281        this.setExport('default', exports);
282      });
283    }
284  }
285  source = stringify(source);
286  if (pathname) {
287    // A require call could have been called on the same file during loading and
288    // that resolves synchronously. To make sure we always return the identical
289    // export, we have to check again if the module already exists or not.
290    module = CJSModule._cache[modulePath];
291    if (module && module.loaded) {
292      const exports = module.exports;
293      return new ModuleWrap(url, undefined, ['default'], function() {
294        this.setExport('default', exports);
295      });
296    }
297  }
298  try {
299    const exports = JSONParse(stripBOM(source));
300    module = {
301      exports,
302      loaded: true,
303    };
304  } catch (err) {
305    // TODO (BridgeAR): We could add a NodeCore error that wraps the JSON
306    // parse error instead of just manipulating the original error message.
307    // That would allow to add further properties and maybe additional
308    // debugging information.
309    err.message = errPath(url) + ': ' + err.message;
310    throw err;
311  }
312  if (pathname) {
313    CJSModule._cache[modulePath] = module;
314  }
315  return new ModuleWrap(url, undefined, ['default'], function() {
316    debug(`Parsing JSONModule ${url}`);
317    this.setExport('default', module.exports);
318  });
319});
320
321// Strategy for loading a wasm module
322translators.set('wasm', async function(url, source) {
323  emitExperimentalWarning('Importing WebAssembly modules');
324
325  assertBufferSource(source, false, 'load');
326
327  debug(`Translating WASMModule ${url}`);
328
329  let compiled;
330  try {
331    compiled = await WebAssembly.compile(source);
332  } catch (err) {
333    err.message = errPath(url) + ': ' + err.message;
334    throw err;
335  }
336
337  const imports =
338      ArrayPrototypeMap(WebAssembly.Module.imports(compiled),
339                        ({ module }) => module);
340  const exports =
341    ArrayPrototypeMap(WebAssembly.Module.exports(compiled),
342                      ({ name }) => name);
343
344  return createDynamicModule(imports, exports, url, (reflect) => {
345    const { exports } = new WebAssembly.Instance(compiled, reflect.imports);
346    for (const expt of ObjectKeys(exports))
347      reflect.exports[expt].set(exports[expt]);
348  }).module;
349});
350