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