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