• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3const {
4  ArrayPrototypeMap,
5  JSONParse,
6  ObjectCreate,
7  ObjectKeys,
8  ObjectGetOwnPropertyDescriptor,
9  ObjectPrototypeHasOwnProperty,
10  RegExpPrototypeExec,
11  RegExpPrototypeSymbolSplit,
12  SafeMap,
13  StringPrototypeSplit,
14} = primordials;
15
16function ObjectGetValueSafe(obj, key) {
17  const desc = ObjectGetOwnPropertyDescriptor(obj, key);
18  return ObjectPrototypeHasOwnProperty(desc, 'value') ? desc.value : undefined;
19}
20
21// See https://sourcemaps.info/spec.html for SourceMap V3 specification.
22const { Buffer } = require('buffer');
23let debug = require('internal/util/debuglog').debuglog('source_map', (fn) => {
24  debug = fn;
25});
26const fs = require('fs');
27const { getOptionValue } = require('internal/options');
28const { IterableWeakMap } = require('internal/util/iterable_weak_map');
29const {
30  normalizeReferrerURL,
31} = require('internal/modules/cjs/helpers');
32const { validateBoolean } = require('internal/validators');
33const { setMaybeCacheGeneratedSourceMap } = internalBinding('errors');
34
35// Since the CJS module cache is mutable, which leads to memory leaks when
36// modules are deleted, we use a WeakMap so that the source map cache will
37// be purged automatically:
38const cjsSourceMapCache = new IterableWeakMap();
39// The esm cache is not mutable, so we can use a Map without memory concerns:
40const esmSourceMapCache = new SafeMap();
41// The generated sources is not mutable, so we can use a Map without memory concerns:
42const generatedSourceMapCache = new SafeMap();
43const kLeadingProtocol = /^\w+:\/\//;
44const kSourceMappingURLMagicComment = /\/[*/]#\s+sourceMappingURL=(?<sourceMappingURL>[^\s]+)/g;
45const kSourceURLMagicComment = /\/[*/]#\s+sourceURL=(?<sourceURL>[^\s]+)/g;
46
47const { fileURLToPath, pathToFileURL, URL } = require('internal/url');
48let SourceMap;
49
50let sourceMapsEnabled;
51function getSourceMapsEnabled() {
52  if (sourceMapsEnabled === undefined) {
53    setSourceMapsEnabled(getOptionValue('--enable-source-maps'));
54  }
55  return sourceMapsEnabled;
56}
57
58function setSourceMapsEnabled(val) {
59  validateBoolean(val, 'val');
60
61  const {
62    setSourceMapsEnabled,
63    setPrepareStackTraceCallback,
64  } = internalBinding('errors');
65  setSourceMapsEnabled(val);
66  if (val) {
67    const {
68      prepareStackTrace,
69    } = require('internal/source_map/prepare_stack_trace');
70    setPrepareStackTraceCallback(prepareStackTrace);
71  } else if (sourceMapsEnabled !== undefined) {
72    // Reset prepare stack trace callback only when disabling source maps.
73    const {
74      prepareStackTrace,
75    } = require('internal/errors');
76    setPrepareStackTraceCallback(prepareStackTrace);
77  }
78
79  sourceMapsEnabled = val;
80}
81
82function extractSourceURLMagicComment(content) {
83  let match;
84  let matchSourceURL;
85  // A while loop is used here to get the last occurrence of sourceURL.
86  // This is needed so that we don't match sourceURL in string literals.
87  while ((match = RegExpPrototypeExec(kSourceURLMagicComment, content))) {
88    matchSourceURL = match;
89  }
90  if (matchSourceURL == null) {
91    return null;
92  }
93  let sourceURL = matchSourceURL.groups.sourceURL;
94  if (sourceURL != null && RegExpPrototypeExec(kLeadingProtocol, sourceURL) === null) {
95    sourceURL = pathToFileURL(sourceURL).href;
96  }
97  return sourceURL;
98}
99
100function extractSourceMapURLMagicComment(content) {
101  let match;
102  let lastMatch;
103  // A while loop is used here to get the last occurrence of sourceMappingURL.
104  // This is needed so that we don't match sourceMappingURL in string literals.
105  while ((match = RegExpPrototypeExec(kSourceMappingURLMagicComment, content))) {
106    lastMatch = match;
107  }
108  if (lastMatch == null) {
109    return null;
110  }
111  return lastMatch.groups.sourceMappingURL;
112}
113
114function maybeCacheSourceMap(filename, content, cjsModuleInstance, isGeneratedSource, sourceURL, sourceMapURL) {
115  const sourceMapsEnabled = getSourceMapsEnabled();
116  if (!(process.env.NODE_V8_COVERAGE || sourceMapsEnabled)) return;
117  try {
118    filename = normalizeReferrerURL(filename);
119  } catch (err) {
120    // This is most likely an invalid filename in sourceURL of [eval]-wrapper.
121    debug(err);
122    return;
123  }
124
125  if (sourceMapURL === undefined) {
126    sourceMapURL = extractSourceMapURLMagicComment(content);
127  }
128
129  // Bail out when there is no source map url.
130  if (typeof sourceMapURL !== 'string') {
131    return;
132  }
133
134  if (sourceURL === undefined) {
135    sourceURL = extractSourceURLMagicComment(content);
136  }
137
138  const data = dataFromUrl(filename, sourceMapURL);
139  const url = data ? null : sourceMapURL;
140  if (cjsModuleInstance) {
141    cjsSourceMapCache.set(cjsModuleInstance, {
142      filename,
143      lineLengths: lineLengths(content),
144      data,
145      url,
146      sourceURL,
147    });
148  } else if (isGeneratedSource) {
149    const entry = {
150      lineLengths: lineLengths(content),
151      data,
152      url,
153      sourceURL,
154    };
155    generatedSourceMapCache.set(filename, entry);
156    if (sourceURL) {
157      generatedSourceMapCache.set(sourceURL, entry);
158    }
159  } else {
160    // If there is no cjsModuleInstance and is not generated source assume we are in a
161    // "modules/esm" context.
162    const entry = {
163      lineLengths: lineLengths(content),
164      data,
165      url,
166      sourceURL,
167    };
168    esmSourceMapCache.set(filename, entry);
169    if (sourceURL) {
170      esmSourceMapCache.set(sourceURL, entry);
171    }
172  }
173}
174
175function maybeCacheGeneratedSourceMap(content) {
176  const sourceMapsEnabled = getSourceMapsEnabled();
177  if (!(process.env.NODE_V8_COVERAGE || sourceMapsEnabled)) return;
178
179  const sourceURL = extractSourceURLMagicComment(content);
180  if (sourceURL === null) {
181    return;
182  }
183  try {
184    maybeCacheSourceMap(sourceURL, content, null, true, sourceURL);
185  } catch (err) {
186    // This can happen if the filename is not a valid URL.
187    // If we fail to cache the source map, we should not fail the whole process.
188    debug(err);
189  }
190}
191setMaybeCacheGeneratedSourceMap(maybeCacheGeneratedSourceMap);
192
193function dataFromUrl(sourceURL, sourceMappingURL) {
194  try {
195    const url = new URL(sourceMappingURL);
196    switch (url.protocol) {
197      case 'data:':
198        return sourceMapFromDataUrl(sourceURL, url.pathname);
199      default:
200        debug(`unknown protocol ${url.protocol}`);
201        return null;
202    }
203  } catch (err) {
204    debug(err);
205    // If no scheme is present, we assume we are dealing with a file path.
206    const mapURL = new URL(sourceMappingURL, sourceURL).href;
207    return sourceMapFromFile(mapURL);
208  }
209}
210
211// Cache the length of each line in the file that a source map was extracted
212// from. This allows translation from byte offset V8 coverage reports,
213// to line/column offset Source Map V3.
214function lineLengths(content) {
215  // We purposefully keep \r as part of the line-length calculation, in
216  // cases where there is a \r\n separator, so that this can be taken into
217  // account in coverage calculations.
218  return ArrayPrototypeMap(RegExpPrototypeSymbolSplit(/\n|\u2028|\u2029/, content), (line) => {
219    return line.length;
220  });
221}
222
223function sourceMapFromFile(mapURL) {
224  try {
225    const content = fs.readFileSync(fileURLToPath(mapURL), 'utf8');
226    const data = JSONParse(content);
227    return sourcesToAbsolute(mapURL, data);
228  } catch (err) {
229    debug(err);
230    return null;
231  }
232}
233
234// data:[<mediatype>][;base64],<data> see:
235// https://tools.ietf.org/html/rfc2397#section-2
236function sourceMapFromDataUrl(sourceURL, url) {
237  const { 0: format, 1: data } = StringPrototypeSplit(url, ',');
238  const splitFormat = StringPrototypeSplit(format, ';');
239  const contentType = splitFormat[0];
240  const base64 = splitFormat[splitFormat.length - 1] === 'base64';
241  if (contentType === 'application/json') {
242    const decodedData = base64 ?
243      Buffer.from(data, 'base64').toString('utf8') : data;
244    try {
245      const parsedData = JSONParse(decodedData);
246      return sourcesToAbsolute(sourceURL, parsedData);
247    } catch (err) {
248      debug(err);
249      return null;
250    }
251  } else {
252    debug(`unknown content-type ${contentType}`);
253    return null;
254  }
255}
256
257// If the sources are not absolute URLs after prepending of the "sourceRoot",
258// the sources are resolved relative to the SourceMap (like resolving script
259// src in a html document).
260function sourcesToAbsolute(baseURL, data) {
261  data.sources = data.sources.map((source) => {
262    source = (data.sourceRoot || '') + source;
263    return new URL(source, baseURL).href;
264  });
265  // The sources array is now resolved to absolute URLs, sourceRoot should
266  // be updated to noop.
267  data.sourceRoot = '';
268  return data;
269}
270
271// WARNING: The `sourceMapCacheToObject` and `appendCJSCache` run during
272// shutdown. In particular, they also run when Workers are terminated, making
273// it important that they do not call out to any user-provided code, including
274// built-in prototypes that might have been tampered with.
275
276// Get serialized representation of source-map cache, this is used
277// to persist a cache of source-maps to disk when NODE_V8_COVERAGE is enabled.
278function sourceMapCacheToObject() {
279  const obj = ObjectCreate(null);
280
281  for (const { 0: k, 1: v } of esmSourceMapCache) {
282    obj[k] = v;
283  }
284
285  appendCJSCache(obj);
286
287  if (ObjectKeys(obj).length === 0) {
288    return undefined;
289  }
290  return obj;
291}
292
293function appendCJSCache(obj) {
294  for (const value of cjsSourceMapCache) {
295    obj[ObjectGetValueSafe(value, 'filename')] = {
296      lineLengths: ObjectGetValueSafe(value, 'lineLengths'),
297      data: ObjectGetValueSafe(value, 'data'),
298      url: ObjectGetValueSafe(value, 'url'),
299    };
300  }
301}
302
303function findSourceMap(sourceURL) {
304  if (RegExpPrototypeExec(kLeadingProtocol, sourceURL) === null) {
305    sourceURL = pathToFileURL(sourceURL).href;
306  }
307  if (!SourceMap) {
308    SourceMap = require('internal/source_map/source_map').SourceMap;
309  }
310  let sourceMap = esmSourceMapCache.get(sourceURL) ?? generatedSourceMapCache.get(sourceURL);
311  if (sourceMap === undefined) {
312    for (const value of cjsSourceMapCache) {
313      const filename = ObjectGetValueSafe(value, 'filename');
314      const cachedSourceURL = ObjectGetValueSafe(value, 'sourceURL');
315      if (sourceURL === filename || sourceURL === cachedSourceURL) {
316        sourceMap = {
317          data: ObjectGetValueSafe(value, 'data'),
318        };
319      }
320    }
321  }
322  if (sourceMap && sourceMap.data) {
323    return new SourceMap(sourceMap.data);
324  }
325  return undefined;
326}
327
328module.exports = {
329  findSourceMap,
330  getSourceMapsEnabled,
331  setSourceMapsEnabled,
332  maybeCacheSourceMap,
333  sourceMapCacheToObject,
334};
335