1'use strict'; 2 3const { 4 JSONParse, 5 ObjectCreate, 6 ObjectKeys, 7 ObjectGetOwnPropertyDescriptor, 8 ObjectPrototypeHasOwnProperty, 9 Map, 10 MapPrototypeEntries, 11 WeakMap, 12 WeakMapPrototypeGet, 13 uncurryThis, 14} = primordials; 15 16const MapIteratorNext = uncurryThis(MapPrototypeEntries(new Map()).next); 17 18function ObjectGetValueSafe(obj, key) { 19 const desc = ObjectGetOwnPropertyDescriptor(obj, key); 20 return ObjectPrototypeHasOwnProperty(desc, 'value') ? desc.value : undefined; 21} 22 23// See https://sourcemaps.info/spec.html for SourceMap V3 specification. 24const { Buffer } = require('buffer'); 25let debug = require('internal/util/debuglog').debuglog('source_map', (fn) => { 26 debug = fn; 27}); 28const { dirname, resolve } = require('path'); 29const fs = require('fs'); 30const { getOptionValue } = require('internal/options'); 31const { 32 normalizeReferrerURL, 33} = require('internal/modules/cjs/helpers'); 34// For cjs, since Module._cache is exposed to users, we use a WeakMap 35// keyed on module, facilitating garbage collection. 36const cjsSourceMapCache = new WeakMap(); 37// The esm cache is not exposed to users, so we can use a Map keyed 38// on filenames. 39const esmSourceMapCache = new Map(); 40const { fileURLToPath, URL } = require('url'); 41let Module; 42let SourceMap; 43 44let experimentalSourceMaps; 45function maybeCacheSourceMap(filename, content, cjsModuleInstance) { 46 if (experimentalSourceMaps === undefined) { 47 experimentalSourceMaps = getOptionValue('--enable-source-maps'); 48 } 49 if (!(process.env.NODE_V8_COVERAGE || experimentalSourceMaps)) return; 50 let basePath; 51 try { 52 filename = normalizeReferrerURL(filename); 53 basePath = dirname(fileURLToPath(filename)); 54 } catch (err) { 55 // This is most likely an [eval]-wrapper, which is currently not 56 // supported. 57 debug(err.stack); 58 return; 59 } 60 61 const match = content.match(/\/[*/]#\s+sourceMappingURL=(?<sourceMappingURL>[^\s]+)/); 62 if (match) { 63 const data = dataFromUrl(basePath, match.groups.sourceMappingURL); 64 const url = data ? null : match.groups.sourceMappingURL; 65 if (cjsModuleInstance) { 66 if (!Module) Module = require('internal/modules/cjs/loader').Module; 67 cjsSourceMapCache.set(cjsModuleInstance, { 68 filename, 69 lineLengths: lineLengths(content), 70 data, 71 url 72 }); 73 } else { 74 // If there is no cjsModuleInstance assume we are in a 75 // "modules/esm" context. 76 esmSourceMapCache.set(filename, { 77 lineLengths: lineLengths(content), 78 data, 79 url 80 }); 81 } 82 } 83} 84 85function dataFromUrl(basePath, sourceMappingURL) { 86 try { 87 const url = new URL(sourceMappingURL); 88 switch (url.protocol) { 89 case 'data:': 90 return sourceMapFromDataUrl(basePath, url.pathname); 91 default: 92 debug(`unknown protocol ${url.protocol}`); 93 return null; 94 } 95 } catch (err) { 96 debug(err.stack); 97 // If no scheme is present, we assume we are dealing with a file path. 98 const sourceMapFile = resolve(basePath, sourceMappingURL); 99 return sourceMapFromFile(sourceMapFile); 100 } 101} 102 103// Cache the length of each line in the file that a source map was extracted 104// from. This allows translation from byte offset V8 coverage reports, 105// to line/column offset Source Map V3. 106function lineLengths(content) { 107 // We purposefully keep \r as part of the line-length calculation, in 108 // cases where there is a \r\n separator, so that this can be taken into 109 // account in coverage calculations. 110 return content.split(/\n|\u2028|\u2029/).map((line) => { 111 return line.length; 112 }); 113} 114 115function sourceMapFromFile(sourceMapFile) { 116 try { 117 const content = fs.readFileSync(sourceMapFile, 'utf8'); 118 const data = JSONParse(content); 119 return sourcesToAbsolute(dirname(sourceMapFile), data); 120 } catch (err) { 121 debug(err.stack); 122 return null; 123 } 124} 125 126// data:[<mediatype>][;base64],<data> see: 127// https://tools.ietf.org/html/rfc2397#section-2 128function sourceMapFromDataUrl(basePath, url) { 129 const [format, data] = url.split(','); 130 const splitFormat = format.split(';'); 131 const contentType = splitFormat[0]; 132 const base64 = splitFormat[splitFormat.length - 1] === 'base64'; 133 if (contentType === 'application/json') { 134 const decodedData = base64 ? 135 Buffer.from(data, 'base64').toString('utf8') : data; 136 try { 137 const parsedData = JSONParse(decodedData); 138 return sourcesToAbsolute(basePath, parsedData); 139 } catch (err) { 140 debug(err.stack); 141 return null; 142 } 143 } else { 144 debug(`unknown content-type ${contentType}`); 145 return null; 146 } 147} 148 149// If the sources are not absolute URLs after prepending of the "sourceRoot", 150// the sources are resolved relative to the SourceMap (like resolving script 151// src in a html document). 152function sourcesToAbsolute(base, data) { 153 data.sources = data.sources.map((source) => { 154 source = (data.sourceRoot || '') + source; 155 if (!/^[\\/]/.test(source[0])) { 156 source = resolve(base, source); 157 } 158 if (!source.startsWith('file://')) source = `file://${source}`; 159 return source; 160 }); 161 // The sources array is now resolved to absolute URLs, sourceRoot should 162 // be updated to noop. 163 data.sourceRoot = ''; 164 return data; 165} 166 167// Move source map from garbage collected module to alternate key. 168function rekeySourceMap(cjsModuleInstance, newInstance) { 169 const sourceMap = cjsSourceMapCache.get(cjsModuleInstance); 170 if (sourceMap) { 171 cjsSourceMapCache.set(newInstance, sourceMap); 172 } 173} 174 175// WARNING: The `sourceMapCacheToObject` and `appendCJSCache` run during 176// shutdown. In particular, they also run when Workers are terminated, making 177// it important that they do not call out to any user-provided code, including 178// built-in prototypes that might have been tampered with. 179 180// Get serialized representation of source-map cache, this is used 181// to persist a cache of source-maps to disk when NODE_V8_COVERAGE is enabled. 182function sourceMapCacheToObject() { 183 const obj = ObjectCreate(null); 184 185 const it = MapPrototypeEntries(esmSourceMapCache); 186 let entry; 187 while (!(entry = MapIteratorNext(it)).done) { 188 const k = entry.value[0]; 189 const v = entry.value[1]; 190 obj[k] = v; 191 } 192 193 appendCJSCache(obj); 194 195 if (ObjectKeys(obj).length === 0) { 196 return undefined; 197 } 198 return obj; 199} 200 201// Since WeakMap can't be iterated over, we use Module._cache's 202// keys to facilitate Source Map serialization. 203// 204// TODO(bcoe): this means we don't currently serialize source-maps attached 205// to error instances, only module instances. 206function appendCJSCache(obj) { 207 if (!Module) return; 208 const cjsModuleCache = ObjectGetValueSafe(Module, '_cache'); 209 const cjsModules = ObjectKeys(cjsModuleCache); 210 for (let i = 0; i < cjsModules.length; i++) { 211 const key = cjsModules[i]; 212 const module = ObjectGetValueSafe(cjsModuleCache, key); 213 const value = WeakMapPrototypeGet(cjsSourceMapCache, module); 214 if (value) { 215 // This is okay because `obj` has a null prototype. 216 obj[`file://${key}`] = { 217 lineLengths: ObjectGetValueSafe(value, 'lineLengths'), 218 data: ObjectGetValueSafe(value, 'data'), 219 url: ObjectGetValueSafe(value, 'url') 220 }; 221 } 222 } 223} 224 225// Attempt to lookup a source map, which is either attached to a file URI, or 226// keyed on an error instance. 227// TODO(bcoe): once WeakRefs are available in Node.js, refactor to drop 228// requirement of error parameter. 229function findSourceMap(uri, error) { 230 if (!Module) Module = require('internal/modules/cjs/loader').Module; 231 if (!SourceMap) { 232 SourceMap = require('internal/source_map/source_map').SourceMap; 233 } 234 let sourceMap = cjsSourceMapCache.get(Module._cache[uri]); 235 if (!uri.startsWith('file://')) uri = normalizeReferrerURL(uri); 236 if (sourceMap === undefined) { 237 sourceMap = esmSourceMapCache.get(uri); 238 } 239 if (sourceMap === undefined) { 240 const candidateSourceMap = cjsSourceMapCache.get(error); 241 if (candidateSourceMap && uri === candidateSourceMap.filename) { 242 sourceMap = candidateSourceMap; 243 } 244 } 245 if (sourceMap && sourceMap.data) { 246 return new SourceMap(sourceMap.data); 247 } 248 return undefined; 249} 250 251module.exports = { 252 findSourceMap, 253 maybeCacheSourceMap, 254 rekeySourceMap, 255 sourceMapCacheToObject, 256}; 257