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