• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3const {
4  ArrayPrototypeMap,
5  JSONParse,
6  ObjectCreate,
7  ObjectKeys,
8  ObjectGetOwnPropertyDescriptor,
9  ObjectPrototypeHasOwnProperty,
10  RegExpPrototypeTest,
11  SafeMap,
12  StringPrototypeMatch,
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');
33// Since the CJS module cache is mutable, which leads to memory leaks when
34// modules are deleted, we use a WeakMap so that the source map cache will
35// be purged automatically:
36const cjsSourceMapCache = new IterableWeakMap();
37// The esm cache is not mutable, so we can use a Map without memory concerns:
38const esmSourceMapCache = new SafeMap();
39const { fileURLToPath, pathToFileURL, URL } = require('internal/url');
40let SourceMap;
41
42let sourceMapsEnabled;
43function getSourceMapsEnabled() {
44  if (sourceMapsEnabled === undefined) {
45    setSourceMapsEnabled(getOptionValue('--enable-source-maps'));
46  }
47  return sourceMapsEnabled;
48}
49
50function setSourceMapsEnabled(val) {
51  validateBoolean(val, 'val');
52
53  const {
54    setSourceMapsEnabled,
55    setPrepareStackTraceCallback
56  } = internalBinding('errors');
57  setSourceMapsEnabled(val);
58  if (val) {
59    const {
60      prepareStackTrace
61    } = require('internal/source_map/prepare_stack_trace');
62    setPrepareStackTraceCallback(prepareStackTrace);
63  } else if (sourceMapsEnabled !== undefined) {
64    // Reset prepare stack trace callback only when disabling source maps.
65    const {
66      prepareStackTrace,
67    } = require('internal/errors');
68    setPrepareStackTraceCallback(prepareStackTrace);
69  }
70
71  sourceMapsEnabled = val;
72}
73
74function maybeCacheSourceMap(filename, content, cjsModuleInstance) {
75  const sourceMapsEnabled = getSourceMapsEnabled();
76  if (!(process.env.NODE_V8_COVERAGE || sourceMapsEnabled)) return;
77  try {
78    filename = normalizeReferrerURL(filename);
79  } catch (err) {
80    // This is most likely an [eval]-wrapper, which is currently not
81    // supported.
82    debug(err.stack);
83    return;
84  }
85  const match = StringPrototypeMatch(
86    content,
87    /\/[*/]#\s+sourceMappingURL=(?<sourceMappingURL>[^\s]+)/
88  );
89  if (match) {
90    const data = dataFromUrl(filename, match.groups.sourceMappingURL);
91    const url = data ? null : match.groups.sourceMappingURL;
92    if (cjsModuleInstance) {
93      cjsSourceMapCache.set(cjsModuleInstance, {
94        filename,
95        lineLengths: lineLengths(content),
96        data,
97        url
98      });
99    } else {
100      // If there is no cjsModuleInstance assume we are in a
101      // "modules/esm" context.
102      esmSourceMapCache.set(filename, {
103        lineLengths: lineLengths(content),
104        data,
105        url
106      });
107    }
108  }
109}
110
111function dataFromUrl(sourceURL, sourceMappingURL) {
112  try {
113    const url = new URL(sourceMappingURL);
114    switch (url.protocol) {
115      case 'data:':
116        return sourceMapFromDataUrl(sourceURL, url.pathname);
117      default:
118        debug(`unknown protocol ${url.protocol}`);
119        return null;
120    }
121  } catch (err) {
122    debug(err.stack);
123    // If no scheme is present, we assume we are dealing with a file path.
124    const mapURL = new URL(sourceMappingURL, sourceURL).href;
125    return sourceMapFromFile(mapURL);
126  }
127}
128
129// Cache the length of each line in the file that a source map was extracted
130// from. This allows translation from byte offset V8 coverage reports,
131// to line/column offset Source Map V3.
132function lineLengths(content) {
133  // We purposefully keep \r as part of the line-length calculation, in
134  // cases where there is a \r\n separator, so that this can be taken into
135  // account in coverage calculations.
136  return ArrayPrototypeMap(StringPrototypeSplit(content, /\n|\u2028|\u2029/), (line) => {
137    return line.length;
138  });
139}
140
141function sourceMapFromFile(mapURL) {
142  try {
143    const content = fs.readFileSync(fileURLToPath(mapURL), 'utf8');
144    const data = JSONParse(content);
145    return sourcesToAbsolute(mapURL, data);
146  } catch (err) {
147    debug(err.stack);
148    return null;
149  }
150}
151
152// data:[<mediatype>][;base64],<data> see:
153// https://tools.ietf.org/html/rfc2397#section-2
154function sourceMapFromDataUrl(sourceURL, url) {
155  const { 0: format, 1: data } = StringPrototypeSplit(url, ',');
156  const splitFormat = StringPrototypeSplit(format, ';');
157  const contentType = splitFormat[0];
158  const base64 = splitFormat[splitFormat.length - 1] === 'base64';
159  if (contentType === 'application/json') {
160    const decodedData = base64 ?
161      Buffer.from(data, 'base64').toString('utf8') : data;
162    try {
163      const parsedData = JSONParse(decodedData);
164      return sourcesToAbsolute(sourceURL, parsedData);
165    } catch (err) {
166      debug(err.stack);
167      return null;
168    }
169  } else {
170    debug(`unknown content-type ${contentType}`);
171    return null;
172  }
173}
174
175// If the sources are not absolute URLs after prepending of the "sourceRoot",
176// the sources are resolved relative to the SourceMap (like resolving script
177// src in a html document).
178function sourcesToAbsolute(baseURL, data) {
179  data.sources = data.sources.map((source) => {
180    source = (data.sourceRoot || '') + source;
181    return new URL(source, baseURL).href;
182  });
183  // The sources array is now resolved to absolute URLs, sourceRoot should
184  // be updated to noop.
185  data.sourceRoot = '';
186  return data;
187}
188
189// WARNING: The `sourceMapCacheToObject` and `appendCJSCache` run during
190// shutdown. In particular, they also run when Workers are terminated, making
191// it important that they do not call out to any user-provided code, including
192// built-in prototypes that might have been tampered with.
193
194// Get serialized representation of source-map cache, this is used
195// to persist a cache of source-maps to disk when NODE_V8_COVERAGE is enabled.
196function sourceMapCacheToObject() {
197  const obj = ObjectCreate(null);
198
199  for (const { 0: k, 1: v } of esmSourceMapCache) {
200    obj[k] = v;
201  }
202
203  appendCJSCache(obj);
204
205  if (ObjectKeys(obj).length === 0) {
206    return undefined;
207  }
208  return obj;
209}
210
211function appendCJSCache(obj) {
212  for (const value of cjsSourceMapCache) {
213    obj[ObjectGetValueSafe(value, 'filename')] = {
214      lineLengths: ObjectGetValueSafe(value, 'lineLengths'),
215      data: ObjectGetValueSafe(value, 'data'),
216      url: ObjectGetValueSafe(value, 'url')
217    };
218  }
219}
220
221function findSourceMap(sourceURL) {
222  if (!RegExpPrototypeTest(/^\w+:\/\//, sourceURL)) {
223    sourceURL = pathToFileURL(sourceURL).href;
224  }
225  if (!SourceMap) {
226    SourceMap = require('internal/source_map/source_map').SourceMap;
227  }
228  let sourceMap = esmSourceMapCache.get(sourceURL);
229  if (sourceMap === undefined) {
230    for (const value of cjsSourceMapCache) {
231      const filename = ObjectGetValueSafe(value, 'filename');
232      if (sourceURL === filename) {
233        sourceMap = {
234          data: ObjectGetValueSafe(value, 'data')
235        };
236      }
237    }
238  }
239  if (sourceMap && sourceMap.data) {
240    return new SourceMap(sourceMap.data);
241  }
242  return undefined;
243}
244
245module.exports = {
246  findSourceMap,
247  getSourceMapsEnabled,
248  setSourceMapsEnabled,
249  maybeCacheSourceMap,
250  sourceMapCacheToObject,
251};
252