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