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