-
Notifications
You must be signed in to change notification settings - Fork 30.1k
/
source_map_cache.js
385 lines (347 loc) Β· 12.7 KB
/
source_map_cache.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
'use strict';
const {
ArrayPrototypePush,
JSONParse,
RegExpPrototypeExec,
SafeMap,
StringPrototypeCodePointAt,
StringPrototypeSplit,
} = primordials;
// See https://sourcemaps.info/spec.html for SourceMap V3 specification.
const { Buffer } = require('buffer');
let debug = require('internal/util/debuglog').debuglog('source_map', (fn) => {
debug = fn;
});
const { validateBoolean } = require('internal/validators');
const {
setSourceMapsEnabled: setSourceMapsNative,
} = internalBinding('errors');
const {
defaultPrepareStackTrace,
setInternalPrepareStackTrace,
} = require('internal/errors');
const { getLazy } = require('internal/util');
const getModuleSourceMapCache = getLazy(() => {
const { SourceMapCacheMap } = require('internal/source_map/source_map_cache_map');
return new SourceMapCacheMap();
});
// The generated source module/script instance is not accessible, so we can use
// a Map without memory concerns. Separate generated source entries with the module
// source entries to avoid overriding the module source entries with arbitrary
// source url magic comments.
const generatedSourceMapCache = new SafeMap();
const kLeadingProtocol = /^\w+:\/\//;
const kSourceMappingURLMagicComment = /\/[*/]#\s+sourceMappingURL=(?<sourceMappingURL>[^\s]+)/g;
const kSourceURLMagicComment = /\/[*/]#\s+sourceURL=(?<sourceURL>[^\s]+)/g;
const { isAbsolute } = require('path');
const { fileURLToPath, pathToFileURL, URL, URLParse } = require('internal/url');
let SourceMap;
// This is configured with --enable-source-maps during pre-execution.
let sourceMapsEnabled = false;
function getSourceMapsEnabled() {
return sourceMapsEnabled;
}
/**
* Enables or disables source maps programmatically.
* @param {boolean} val
*/
function setSourceMapsEnabled(val) {
validateBoolean(val, 'val');
setSourceMapsNative(val);
if (val) {
const {
prepareStackTraceWithSourceMaps,
} = require('internal/source_map/prepare_stack_trace');
setInternalPrepareStackTrace(prepareStackTraceWithSourceMaps);
} else if (sourceMapsEnabled !== undefined) {
// Reset prepare stack trace callback only when disabling source maps.
setInternalPrepareStackTrace(defaultPrepareStackTrace);
}
sourceMapsEnabled = val;
}
/**
* Extracts the source url from the content if present. For example
* //# sourceURL=file:///path/to/file
*
* Read more at: https://tc39.es/source-map-spec/#linking-evald-code-to-named-generated-code
* @param {string} content - source content
* @returns {string | null} source url or null if not present
*/
function extractSourceURLMagicComment(content) {
let match;
let matchSourceURL;
// A while loop is used here to get the last occurrence of sourceURL.
// This is needed so that we don't match sourceURL in string literals.
while ((match = RegExpPrototypeExec(kSourceURLMagicComment, content))) {
matchSourceURL = match;
}
if (matchSourceURL == null) {
return null;
}
let sourceURL = matchSourceURL.groups.sourceURL;
if (sourceURL != null && RegExpPrototypeExec(kLeadingProtocol, sourceURL) === null) {
sourceURL = pathToFileURL(sourceURL).href;
}
return sourceURL;
}
/**
* Extracts the source map url from the content if present. For example
* //# sourceMappingURL=file:///path/to/file
*
* Read more at: https://tc39.es/source-map-spec/#linking-generated-code
* @param {string} content - source content
* @returns {string | null} source map url or null if not present
*/
function extractSourceMapURLMagicComment(content) {
let match;
let lastMatch;
// A while loop is used here to get the last occurrence of sourceMappingURL.
// This is needed so that we don't match sourceMappingURL in string literals.
while ((match = RegExpPrototypeExec(kSourceMappingURLMagicComment, content))) {
lastMatch = match;
}
if (lastMatch == null) {
return null;
}
return lastMatch.groups.sourceMappingURL;
}
/**
* Caches the source map if it is present in the content, with the given filename, moduleInstance, and sourceURL.
* @param {string} filename - the actual filename
* @param {string} content - the actual source content
* @param {import('internal/modules/cjs/loader').Module | ModuleWrap} moduleInstance - a module instance that
* associated with the source, once this is reclaimed, the source map entry will be removed from the cache
* @param {boolean} isGeneratedSource - if the source was generated and evaluated with the global eval
* @param {string | undefined} sourceURL - the source url
* @param {string | undefined} sourceMapURL - the source map url
*/
function maybeCacheSourceMap(filename, content, moduleInstance, isGeneratedSource, sourceURL, sourceMapURL) {
const sourceMapsEnabled = getSourceMapsEnabled();
if (!(process.env.NODE_V8_COVERAGE || sourceMapsEnabled)) return;
const { normalizeReferrerURL } = require('internal/modules/helpers');
filename = normalizeReferrerURL(filename);
if (filename === undefined) {
// This is most likely an invalid filename in sourceURL of [eval]-wrapper.
return;
}
if (sourceMapURL === undefined) {
sourceMapURL = extractSourceMapURLMagicComment(content);
}
// Bail out when there is no source map url.
if (typeof sourceMapURL !== 'string') {
return;
}
// FIXME: callers should obtain sourceURL from v8 and pass it
// rather than leaving it undefined and extract by regex.
if (sourceURL === undefined) {
sourceURL = extractSourceURLMagicComment(content);
}
const data = dataFromUrl(filename, sourceMapURL);
// `data` could be null if the source map is invalid.
// In this case, create a cache entry with null data with source url for test coverage.
const entry = {
__proto__: null,
lineLengths: lineLengths(content),
data,
// Save the source map url if it is not a data url.
sourceMapURL: data ? null : sourceMapURL,
sourceURL,
};
if (isGeneratedSource) {
generatedSourceMapCache.set(filename, entry);
if (sourceURL) {
generatedSourceMapCache.set(sourceURL, entry);
}
return;
}
// If it is not a generated source, we assume we are in a "cjs/esm"
// context.
const keys = sourceURL ? [filename, sourceURL] : [filename];
getModuleSourceMapCache().set(keys, entry, moduleInstance);
}
/**
* Caches the source map if it is present in the eval'd source.
* @param {string} content - the eval'd source code
*/
function maybeCacheGeneratedSourceMap(content) {
const sourceMapsEnabled = getSourceMapsEnabled();
if (!(process.env.NODE_V8_COVERAGE || sourceMapsEnabled)) return;
const sourceURL = extractSourceURLMagicComment(content);
if (sourceURL === null) {
return;
}
try {
maybeCacheSourceMap(sourceURL, content, null, true, sourceURL);
} catch (err) {
// This can happen if the filename is not a valid URL.
// If we fail to cache the source map, we should not fail the whole process.
debug(err);
}
}
/**
* Resolves source map payload data from the source url and source map url.
* If the source map url is a data url, the data is returned.
* Otherwise the source map url is resolved to a file path and the file is read.
* @param {string} sourceURL - url of the source file
* @param {string} sourceMappingURL - url of the source map
* @returns {object} deserialized source map JSON object
*/
function dataFromUrl(sourceURL, sourceMappingURL) {
const url = URLParse(sourceMappingURL);
if (url != null) {
switch (url.protocol) {
case 'data:':
return sourceMapFromDataUrl(sourceURL, url.pathname);
default:
debug(`unknown protocol ${url.protocol}`);
return null;
}
}
const mapURL = new URL(sourceMappingURL, sourceURL).href;
return sourceMapFromFile(mapURL);
}
// Cache the length of each line in the file that a source map was extracted
// from. This allows translation from byte offset V8 coverage reports,
// to line/column offset Source Map V3.
function lineLengths(content) {
const contentLength = content.length;
const output = [];
let lineLength = 0;
for (let i = 0; i < contentLength; i++, lineLength++) {
const codePoint = StringPrototypeCodePointAt(content, i);
// We purposefully keep \r as part of the line-length calculation, in
// cases where there is a \r\n separator, so that this can be taken into
// account in coverage calculations.
// codepoints for \n (new line), \u2028 (line separator) and \u2029 (paragraph separator)
if (codePoint === 10 || codePoint === 0x2028 || codePoint === 0x2029) {
ArrayPrototypePush(output, lineLength);
lineLength = -1; // To not count the matched codePoint such as \n character
}
}
ArrayPrototypePush(output, lineLength);
return output;
}
/**
* Read source map from file.
* @param {string} mapURL - file url of the source map
* @returns {object} deserialized source map JSON object
*/
function sourceMapFromFile(mapURL) {
try {
const fs = require('fs');
const content = fs.readFileSync(fileURLToPath(mapURL), 'utf8');
const data = JSONParse(content);
return sourcesToAbsolute(mapURL, data);
} catch (err) {
debug(err);
return null;
}
}
// data:[<mediatype>][;base64],<data> see:
// https://tools.ietf.org/html/rfc2397#section-2
function sourceMapFromDataUrl(sourceURL, url) {
const { 0: format, 1: data } = StringPrototypeSplit(url, ',');
const splitFormat = StringPrototypeSplit(format, ';');
const contentType = splitFormat[0];
const base64 = splitFormat[splitFormat.length - 1] === 'base64';
if (contentType === 'application/json') {
const decodedData = base64 ?
Buffer.from(data, 'base64').toString('utf8') : data;
try {
const parsedData = JSONParse(decodedData);
return sourcesToAbsolute(sourceURL, parsedData);
} catch (err) {
// TODO(legendecas): warn about invalid source map JSON string.
// But it could be verbose.
debug(err);
return null;
}
} else {
debug(`unknown content-type ${contentType}`);
return null;
}
}
// If the sources are not absolute URLs after prepending of the "sourceRoot",
// the sources are resolved relative to the SourceMap (like resolving script
// src in a html document).
// If the sources are absolute paths, the sources are converted to absolute file URLs.
function sourcesToAbsolute(baseURL, data) {
data.sources = data.sources.map((source) => {
source = (data.sourceRoot || '') + source;
if (isAbsolute(source)) {
return pathToFileURL(source).href;
}
return new URL(source, baseURL).href;
});
// The sources array is now resolved to absolute URLs, sourceRoot should
// be updated to noop.
data.sourceRoot = '';
return data;
}
// WARNING: The `sourceMapCacheToObject` runs during shutdown. In particular,
// it also runs when Workers are terminated, making it important that it does
// not call out to any user-provided code, including built-in prototypes that
// might have been tampered with.
// Get serialized representation of source-map cache, this is used
// to persist a cache of source-maps to disk when NODE_V8_COVERAGE is enabled.
function sourceMapCacheToObject() {
const moduleSourceMapCache = getModuleSourceMapCache();
if (moduleSourceMapCache.size === 0) {
return undefined;
}
const obj = { __proto__: null };
for (const { 0: k, 1: v } of moduleSourceMapCache) {
obj[k] = {
__proto__: null,
lineLengths: v.lineLengths,
data: v.data,
url: v.sourceMapURL,
};
}
return obj;
}
/**
* Find a source map for a given actual source URL or path.
*
* This function may be invoked from user code or test runner, this must not throw
* any exceptions.
* @param {string} sourceURL - actual source URL or path
* @returns {import('internal/source_map/source_map').SourceMap | undefined} a source map or undefined if not found
*/
function findSourceMap(sourceURL) {
if (typeof sourceURL !== 'string') {
return undefined;
}
// No source maps for builtin modules.
if (sourceURL.startsWith('node:')) {
return undefined;
}
SourceMap ??= require('internal/source_map/source_map').SourceMap;
try {
if (RegExpPrototypeExec(kLeadingProtocol, sourceURL) === null) {
// If the sourceURL is an invalid path, this will throw an error.
sourceURL = pathToFileURL(sourceURL).href;
}
const entry = getModuleSourceMapCache().get(sourceURL) ?? generatedSourceMapCache.get(sourceURL);
if (entry?.data == null) {
return undefined;
}
let sourceMap = entry.sourceMap;
if (sourceMap === undefined) {
sourceMap = new SourceMap(entry.data, { lineLengths: entry.lineLengths });
entry.sourceMap = sourceMap;
}
return sourceMap;
} catch (err) {
debug(err);
return undefined;
}
}
module.exports = {
findSourceMap,
getSourceMapsEnabled,
setSourceMapsEnabled,
maybeCacheSourceMap,
maybeCacheGeneratedSourceMap,
sourceMapCacheToObject,
};