• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3const {
4  JSONParse,
5  ObjectCreate,
6  ObjectKeys,
7  ObjectGetOwnPropertyDescriptor,
8  ObjectPrototypeHasOwnProperty,
9  Map,
10  MapPrototypeEntries,
11  WeakMap,
12  WeakMapPrototypeGet,
13  uncurryThis,
14} = primordials;
15
16const MapIteratorNext = uncurryThis(MapPrototypeEntries(new Map()).next);
17
18function ObjectGetValueSafe(obj, key) {
19  const desc = ObjectGetOwnPropertyDescriptor(obj, key);
20  return ObjectPrototypeHasOwnProperty(desc, 'value') ? desc.value : undefined;
21}
22
23// See https://sourcemaps.info/spec.html for SourceMap V3 specification.
24const { Buffer } = require('buffer');
25let debug = require('internal/util/debuglog').debuglog('source_map', (fn) => {
26  debug = fn;
27});
28const { dirname, resolve } = require('path');
29const fs = require('fs');
30const { getOptionValue } = require('internal/options');
31const {
32  normalizeReferrerURL,
33} = require('internal/modules/cjs/helpers');
34// For cjs, since Module._cache is exposed to users, we use a WeakMap
35// keyed on module, facilitating garbage collection.
36const cjsSourceMapCache = new WeakMap();
37// The esm cache is not exposed to users, so we can use a Map keyed
38// on filenames.
39const esmSourceMapCache = new Map();
40const { fileURLToPath, URL } = require('url');
41let Module;
42let SourceMap;
43
44let experimentalSourceMaps;
45function maybeCacheSourceMap(filename, content, cjsModuleInstance) {
46  if (experimentalSourceMaps === undefined) {
47    experimentalSourceMaps = getOptionValue('--enable-source-maps');
48  }
49  if (!(process.env.NODE_V8_COVERAGE || experimentalSourceMaps)) return;
50  let basePath;
51  try {
52    filename = normalizeReferrerURL(filename);
53    basePath = dirname(fileURLToPath(filename));
54  } catch (err) {
55    // This is most likely an [eval]-wrapper, which is currently not
56    // supported.
57    debug(err.stack);
58    return;
59  }
60
61  const match = content.match(/\/[*/]#\s+sourceMappingURL=(?<sourceMappingURL>[^\s]+)/);
62  if (match) {
63    const data = dataFromUrl(basePath, match.groups.sourceMappingURL);
64    const url = data ? null : match.groups.sourceMappingURL;
65    if (cjsModuleInstance) {
66      if (!Module) Module = require('internal/modules/cjs/loader').Module;
67      cjsSourceMapCache.set(cjsModuleInstance, {
68        filename,
69        lineLengths: lineLengths(content),
70        data,
71        url
72      });
73    } else {
74      // If there is no cjsModuleInstance assume we are in a
75      // "modules/esm" context.
76      esmSourceMapCache.set(filename, {
77        lineLengths: lineLengths(content),
78        data,
79        url
80      });
81    }
82  }
83}
84
85function dataFromUrl(basePath, sourceMappingURL) {
86  try {
87    const url = new URL(sourceMappingURL);
88    switch (url.protocol) {
89      case 'data:':
90        return sourceMapFromDataUrl(basePath, url.pathname);
91      default:
92        debug(`unknown protocol ${url.protocol}`);
93        return null;
94    }
95  } catch (err) {
96    debug(err.stack);
97    // If no scheme is present, we assume we are dealing with a file path.
98    const sourceMapFile = resolve(basePath, sourceMappingURL);
99    return sourceMapFromFile(sourceMapFile);
100  }
101}
102
103// Cache the length of each line in the file that a source map was extracted
104// from. This allows translation from byte offset V8 coverage reports,
105// to line/column offset Source Map V3.
106function lineLengths(content) {
107  // We purposefully keep \r as part of the line-length calculation, in
108  // cases where there is a \r\n separator, so that this can be taken into
109  // account in coverage calculations.
110  return content.split(/\n|\u2028|\u2029/).map((line) => {
111    return line.length;
112  });
113}
114
115function sourceMapFromFile(sourceMapFile) {
116  try {
117    const content = fs.readFileSync(sourceMapFile, 'utf8');
118    const data = JSONParse(content);
119    return sourcesToAbsolute(dirname(sourceMapFile), data);
120  } catch (err) {
121    debug(err.stack);
122    return null;
123  }
124}
125
126// data:[<mediatype>][;base64],<data> see:
127// https://tools.ietf.org/html/rfc2397#section-2
128function sourceMapFromDataUrl(basePath, url) {
129  const [format, data] = url.split(',');
130  const splitFormat = format.split(';');
131  const contentType = splitFormat[0];
132  const base64 = splitFormat[splitFormat.length - 1] === 'base64';
133  if (contentType === 'application/json') {
134    const decodedData = base64 ?
135      Buffer.from(data, 'base64').toString('utf8') : data;
136    try {
137      const parsedData = JSONParse(decodedData);
138      return sourcesToAbsolute(basePath, parsedData);
139    } catch (err) {
140      debug(err.stack);
141      return null;
142    }
143  } else {
144    debug(`unknown content-type ${contentType}`);
145    return null;
146  }
147}
148
149// If the sources are not absolute URLs after prepending of the "sourceRoot",
150// the sources are resolved relative to the SourceMap (like resolving script
151// src in a html document).
152function sourcesToAbsolute(base, data) {
153  data.sources = data.sources.map((source) => {
154    source = (data.sourceRoot || '') + source;
155    if (!/^[\\/]/.test(source[0])) {
156      source = resolve(base, source);
157    }
158    if (!source.startsWith('file://')) source = `file://${source}`;
159    return source;
160  });
161  // The sources array is now resolved to absolute URLs, sourceRoot should
162  // be updated to noop.
163  data.sourceRoot = '';
164  return data;
165}
166
167// Move source map from garbage collected module to alternate key.
168function rekeySourceMap(cjsModuleInstance, newInstance) {
169  const sourceMap = cjsSourceMapCache.get(cjsModuleInstance);
170  if (sourceMap) {
171    cjsSourceMapCache.set(newInstance, sourceMap);
172  }
173}
174
175// WARNING: The `sourceMapCacheToObject` and `appendCJSCache` run during
176// shutdown. In particular, they also run when Workers are terminated, making
177// it important that they do not call out to any user-provided code, including
178// built-in prototypes that might have been tampered with.
179
180// Get serialized representation of source-map cache, this is used
181// to persist a cache of source-maps to disk when NODE_V8_COVERAGE is enabled.
182function sourceMapCacheToObject() {
183  const obj = ObjectCreate(null);
184
185  const it = MapPrototypeEntries(esmSourceMapCache);
186  let entry;
187  while (!(entry = MapIteratorNext(it)).done) {
188    const k = entry.value[0];
189    const v = entry.value[1];
190    obj[k] = v;
191  }
192
193  appendCJSCache(obj);
194
195  if (ObjectKeys(obj).length === 0) {
196    return undefined;
197  }
198  return obj;
199}
200
201// Since WeakMap can't be iterated over, we use Module._cache's
202// keys to facilitate Source Map serialization.
203//
204// TODO(bcoe): this means we don't currently serialize source-maps attached
205// to error instances, only module instances.
206function appendCJSCache(obj) {
207  if (!Module) return;
208  const cjsModuleCache = ObjectGetValueSafe(Module, '_cache');
209  const cjsModules = ObjectKeys(cjsModuleCache);
210  for (let i = 0; i < cjsModules.length; i++) {
211    const key = cjsModules[i];
212    const module = ObjectGetValueSafe(cjsModuleCache, key);
213    const value = WeakMapPrototypeGet(cjsSourceMapCache, module);
214    if (value) {
215      // This is okay because `obj` has a null prototype.
216      obj[`file://${key}`] = {
217        lineLengths: ObjectGetValueSafe(value, 'lineLengths'),
218        data: ObjectGetValueSafe(value, 'data'),
219        url: ObjectGetValueSafe(value, 'url')
220      };
221    }
222  }
223}
224
225// Attempt to lookup a source map, which is either attached to a file URI, or
226// keyed on an error instance.
227// TODO(bcoe): once WeakRefs are available in Node.js, refactor to drop
228// requirement of error parameter.
229function findSourceMap(uri, error) {
230  if (!Module) Module = require('internal/modules/cjs/loader').Module;
231  if (!SourceMap) {
232    SourceMap = require('internal/source_map/source_map').SourceMap;
233  }
234  let sourceMap = cjsSourceMapCache.get(Module._cache[uri]);
235  if (!uri.startsWith('file://')) uri = normalizeReferrerURL(uri);
236  if (sourceMap === undefined) {
237    sourceMap = esmSourceMapCache.get(uri);
238  }
239  if (sourceMap === undefined) {
240    const candidateSourceMap = cjsSourceMapCache.get(error);
241    if (candidateSourceMap && uri === candidateSourceMap.filename) {
242      sourceMap = candidateSourceMap;
243    }
244  }
245  if (sourceMap && sourceMap.data) {
246    return new SourceMap(sourceMap.data);
247  }
248  return undefined;
249}
250
251module.exports = {
252  findSourceMap,
253  maybeCacheSourceMap,
254  rekeySourceMap,
255  sourceMapCacheToObject,
256};
257