1namespace Harness.SourceMapRecorder { 2 3 interface SourceMapSpanWithDecodeErrors { 4 sourceMapSpan: ts.Mapping; 5 decodeErrors: string[] | undefined; 6 } 7 8 namespace SourceMapDecoder { 9 let sourceMapMappings: string; 10 let decodingIndex: number; 11 let mappings: ts.MappingsDecoder | undefined; 12 13 export interface DecodedMapping { 14 sourceMapSpan: ts.Mapping; 15 error?: string; 16 } 17 18 export function initializeSourceMapDecoding(sourceMap: ts.RawSourceMap) { 19 decodingIndex = 0; 20 sourceMapMappings = sourceMap.mappings; 21 mappings = ts.decodeMappings(sourceMap.mappings); 22 } 23 24 export function decodeNextEncodedSourceMapSpan(): DecodedMapping { 25 if (!mappings) return ts.Debug.fail("not initialized"); 26 const result = mappings.next(); 27 if (result.done) return { error: mappings.error || "No encoded entry found", sourceMapSpan: mappings.state }; 28 return { sourceMapSpan: result.value }; 29 } 30 31 export function hasCompletedDecoding() { 32 if (!mappings) return ts.Debug.fail("not initialized"); 33 return mappings.pos === sourceMapMappings.length; 34 } 35 36 export function getRemainingDecodeString() { 37 return sourceMapMappings.substr(decodingIndex); 38 } 39 } 40 41 namespace SourceMapSpanWriter { 42 let sourceMapRecorder: Compiler.WriterAggregator; 43 let sourceMapSources: string[]; 44 let sourceMapNames: string[] | null | undefined; 45 46 let jsFile: documents.TextDocument; 47 let jsLineMap: readonly number[]; 48 let tsCode: string; 49 let tsLineMap: number[]; 50 51 let spansOnSingleLine: SourceMapSpanWithDecodeErrors[]; 52 let prevWrittenSourcePos: number; 53 let nextJsLineToWrite: number; 54 let spanMarkerContinues: boolean; 55 56 export function initializeSourceMapSpanWriter(sourceMapRecordWriter: Compiler.WriterAggregator, sourceMap: ts.RawSourceMap, currentJsFile: documents.TextDocument) { 57 sourceMapRecorder = sourceMapRecordWriter; 58 sourceMapSources = sourceMap.sources; 59 sourceMapNames = sourceMap.names; 60 61 jsFile = currentJsFile; 62 jsLineMap = jsFile.lineStarts; 63 64 spansOnSingleLine = []; 65 prevWrittenSourcePos = 0; 66 nextJsLineToWrite = 0; 67 spanMarkerContinues = false; 68 69 SourceMapDecoder.initializeSourceMapDecoding(sourceMap); 70 sourceMapRecorder.WriteLine("==================================================================="); 71 sourceMapRecorder.WriteLine("JsFile: " + sourceMap.file); 72 sourceMapRecorder.WriteLine("mapUrl: " + ts.tryGetSourceMappingURL(ts.getLineInfo(jsFile.text, jsLineMap))); 73 sourceMapRecorder.WriteLine("sourceRoot: " + sourceMap.sourceRoot); 74 sourceMapRecorder.WriteLine("sources: " + sourceMap.sources); 75 if (sourceMap.sourcesContent) { 76 sourceMapRecorder.WriteLine("sourcesContent: " + JSON.stringify(sourceMap.sourcesContent)); 77 } 78 sourceMapRecorder.WriteLine("==================================================================="); 79 } 80 81 function getSourceMapSpanString(mapEntry: ts.Mapping, getAbsentNameIndex?: boolean) { 82 let mapString = "Emitted(" + (mapEntry.generatedLine + 1) + ", " + (mapEntry.generatedCharacter + 1) + ")"; 83 if (ts.isSourceMapping(mapEntry)) { 84 mapString += " Source(" + (mapEntry.sourceLine + 1) + ", " + (mapEntry.sourceCharacter + 1) + ") + SourceIndex(" + mapEntry.sourceIndex + ")"; 85 if (mapEntry.nameIndex! >= 0 && mapEntry.nameIndex! < sourceMapNames!.length) { 86 mapString += " name (" + sourceMapNames![mapEntry.nameIndex!] + ")"; 87 } 88 else { 89 if ((mapEntry.nameIndex && mapEntry.nameIndex !== -1) || getAbsentNameIndex) { 90 mapString += " nameIndex (" + mapEntry.nameIndex + ")"; 91 } 92 } 93 } 94 95 return mapString; 96 } 97 98 export function recordSourceMapSpan(sourceMapSpan: ts.Mapping) { 99 // verify the decoded span is same as the new span 100 const decodeResult = SourceMapDecoder.decodeNextEncodedSourceMapSpan(); 101 let decodeErrors: string[] | undefined; 102 if (typeof decodeResult.error === "string" || !ts.sameMapping(decodeResult.sourceMapSpan, sourceMapSpan)) { 103 if (decodeResult.error) { 104 decodeErrors = ["!!^^ !!^^ There was decoding error in the sourcemap at this location: " + decodeResult.error]; 105 } 106 else { 107 decodeErrors = ["!!^^ !!^^ The decoded span from sourcemap's mapping entry does not match what was encoded for this span:"]; 108 } 109 decodeErrors.push("!!^^ !!^^ Decoded span from sourcemap's mappings entry: " + getSourceMapSpanString(decodeResult.sourceMapSpan, /*getAbsentNameIndex*/ true) + " Span encoded by the emitter:" + getSourceMapSpanString(sourceMapSpan, /*getAbsentNameIndex*/ true)); 110 } 111 112 if (spansOnSingleLine.length && spansOnSingleLine[0].sourceMapSpan.generatedLine !== sourceMapSpan.generatedLine) { 113 // On different line from the one that we have been recording till now, 114 writeRecordedSpans(); 115 spansOnSingleLine = []; 116 } 117 spansOnSingleLine.push({ sourceMapSpan, decodeErrors }); 118 } 119 120 export function recordNewSourceFileSpan(sourceMapSpan: ts.Mapping, newSourceFileCode: string) { 121 let continuesLine = false; 122 if (spansOnSingleLine.length > 0 && spansOnSingleLine[0].sourceMapSpan.generatedCharacter === sourceMapSpan.generatedLine) { 123 writeRecordedSpans(); 124 spansOnSingleLine = []; 125 nextJsLineToWrite--; // walk back one line to reprint the line 126 continuesLine = true; 127 } 128 129 recordSourceMapSpan(sourceMapSpan); 130 131 assert.isTrue(spansOnSingleLine.length === 1); 132 sourceMapRecorder.WriteLine("-------------------------------------------------------------------"); 133 sourceMapRecorder.WriteLine("emittedFile:" + jsFile.file + (continuesLine ? ` (${sourceMapSpan.generatedLine + 1}, ${sourceMapSpan.generatedCharacter + 1})` : "")); 134 sourceMapRecorder.WriteLine("sourceFile:" + sourceMapSources[spansOnSingleLine[0].sourceMapSpan.sourceIndex!]); 135 sourceMapRecorder.WriteLine("-------------------------------------------------------------------"); 136 137 tsLineMap = ts.computeLineStarts(newSourceFileCode); 138 tsCode = newSourceFileCode; 139 prevWrittenSourcePos = 0; 140 } 141 142 export function close() { 143 // Write the lines pending on the single line 144 writeRecordedSpans(); 145 146 if (!SourceMapDecoder.hasCompletedDecoding()) { 147 sourceMapRecorder.WriteLine("!!!! **** There are more source map entries in the sourceMap's mapping than what was encoded"); 148 sourceMapRecorder.WriteLine("!!!! **** Remaining decoded string: " + SourceMapDecoder.getRemainingDecodeString()); 149 150 } 151 152 // write remaining js lines 153 writeJsFileLines(jsLineMap.length); 154 } 155 156 function getTextOfLine(line: number, lineMap: readonly number[], code: string) { 157 const startPos = lineMap[line]; 158 const endPos = lineMap[line + 1]; 159 const text = code.substring(startPos, endPos); 160 return line === 0 ? Utils.removeByteOrderMark(text) : text; 161 } 162 163 function writeJsFileLines(endJsLine: number) { 164 for (; nextJsLineToWrite < endJsLine; nextJsLineToWrite++) { 165 sourceMapRecorder.Write(">>>" + getTextOfLine(nextJsLineToWrite, jsLineMap, jsFile.text)); 166 } 167 } 168 169 function writeRecordedSpans() { 170 const markerIds: string[] = []; 171 172 function getMarkerId(markerIndex: number) { 173 let markerId = ""; 174 if (spanMarkerContinues) { 175 assert.isTrue(markerIndex === 0); 176 markerId = "1->"; 177 } 178 else { 179 markerId = "" + (markerIndex + 1); 180 if (markerId.length < 2) { 181 markerId = markerId + " "; 182 } 183 markerId += ">"; 184 } 185 return markerId; 186 } 187 188 let prevEmittedCol!: number; 189 function iterateSpans(fn: (currentSpan: SourceMapSpanWithDecodeErrors, index: number) => void) { 190 prevEmittedCol = 0; 191 for (let i = 0; i < spansOnSingleLine.length; i++) { 192 fn(spansOnSingleLine[i], i); 193 prevEmittedCol = spansOnSingleLine[i].sourceMapSpan.generatedCharacter; 194 } 195 } 196 197 function writeSourceMapIndent(indentLength: number, indentPrefix: string) { 198 sourceMapRecorder.Write(indentPrefix); 199 for (let i = 0; i < indentLength; i++) { 200 sourceMapRecorder.Write(" "); 201 } 202 } 203 204 function writeSourceMapMarker(currentSpan: SourceMapSpanWithDecodeErrors, index: number, endColumn = currentSpan.sourceMapSpan.generatedCharacter, endContinues = false) { 205 const markerId = getMarkerId(index); 206 markerIds.push(markerId); 207 208 writeSourceMapIndent(prevEmittedCol, markerId); 209 210 for (let i = prevEmittedCol; i < endColumn; i++) { 211 sourceMapRecorder.Write("^"); 212 } 213 if (endContinues) { 214 sourceMapRecorder.Write("->"); 215 } 216 sourceMapRecorder.WriteLine(""); 217 spanMarkerContinues = endContinues; 218 } 219 220 function writeSourceMapSourceText(currentSpan: SourceMapSpanWithDecodeErrors, index: number) { 221 const sourcePos = tsLineMap[currentSpan.sourceMapSpan.sourceLine!] + (currentSpan.sourceMapSpan.sourceCharacter!); 222 let sourceText = ""; 223 if (prevWrittenSourcePos < sourcePos) { 224 // Position that goes forward, get text 225 sourceText = tsCode.substring(prevWrittenSourcePos, sourcePos); 226 } 227 228 if (currentSpan.decodeErrors) { 229 // If there are decode errors, write 230 for (const decodeError of currentSpan.decodeErrors) { 231 writeSourceMapIndent(prevEmittedCol, markerIds[index]); 232 sourceMapRecorder.WriteLine(decodeError); 233 } 234 } 235 236 const tsCodeLineMap = ts.computeLineStarts(sourceText); 237 for (let i = 0; i < tsCodeLineMap.length; i++) { 238 writeSourceMapIndent(prevEmittedCol, i === 0 ? markerIds[index] : " >"); 239 sourceMapRecorder.Write(getTextOfLine(i, tsCodeLineMap, sourceText)); 240 if (i === tsCodeLineMap.length - 1) { 241 sourceMapRecorder.WriteLine(""); 242 } 243 } 244 245 prevWrittenSourcePos = sourcePos; 246 } 247 248 function writeSpanDetails(currentSpan: SourceMapSpanWithDecodeErrors, index: number) { 249 sourceMapRecorder.WriteLine(markerIds[index] + getSourceMapSpanString(currentSpan.sourceMapSpan)); 250 } 251 252 if (spansOnSingleLine.length) { 253 const currentJsLine = spansOnSingleLine[0].sourceMapSpan.generatedLine; 254 255 // Write js line 256 writeJsFileLines(currentJsLine + 1); 257 258 // Emit markers 259 iterateSpans(writeSourceMapMarker); 260 261 const jsFileText = getTextOfLine(currentJsLine + 1, jsLineMap, jsFile.text); 262 if (prevEmittedCol < jsFileText.length - 1) { 263 // There is remaining text on this line that will be part of next source span so write marker that continues 264 writeSourceMapMarker(/*currentSpan*/ undefined!, spansOnSingleLine.length, /*endColumn*/ jsFileText.length - 1, /*endContinues*/ true); // TODO: GH#18217 265 } 266 267 // Emit Source text 268 iterateSpans(writeSourceMapSourceText); 269 270 // Emit column number etc 271 iterateSpans(writeSpanDetails); 272 273 sourceMapRecorder.WriteLine("---"); 274 } 275 } 276 } 277 278 export function getSourceMapRecord(sourceMapDataList: readonly ts.SourceMapEmitResult[], program: ts.Program, jsFiles: readonly documents.TextDocument[], declarationFiles: readonly documents.TextDocument[]) { 279 const sourceMapRecorder = new Compiler.WriterAggregator(); 280 281 for (let i = 0; i < sourceMapDataList.length; i++) { 282 const sourceMapData = sourceMapDataList[i]; 283 let prevSourceFile: ts.SourceFile | undefined; 284 let currentFile: documents.TextDocument; 285 if (ts.isDeclarationFileName(sourceMapData.sourceMap.file)) { 286 if (sourceMapDataList.length > jsFiles.length) { 287 currentFile = declarationFiles[Math.floor(i / 2)]; // When both kinds of source map are present, they alternate js/dts 288 } 289 else { 290 currentFile = declarationFiles[i]; 291 } 292 } 293 else { 294 if (sourceMapDataList.length > jsFiles.length) { 295 currentFile = jsFiles[Math.floor(i / 2)]; 296 } 297 else { 298 currentFile = jsFiles[i]; 299 } 300 } 301 302 SourceMapSpanWriter.initializeSourceMapSpanWriter(sourceMapRecorder, sourceMapData.sourceMap, currentFile); 303 const mapper = ts.decodeMappings(sourceMapData.sourceMap.mappings); 304 for (let iterResult = mapper.next(); !iterResult.done; iterResult = mapper.next()) { 305 const decodedSourceMapping = iterResult.value; 306 const currentSourceFile = ts.isSourceMapping(decodedSourceMapping) 307 ? program.getSourceFile(sourceMapData.inputSourceFileNames[decodedSourceMapping.sourceIndex]) 308 : undefined; 309 if (currentSourceFile !== prevSourceFile) { 310 if (currentSourceFile) { 311 SourceMapSpanWriter.recordNewSourceFileSpan(decodedSourceMapping, currentSourceFile.text); 312 } 313 prevSourceFile = currentSourceFile; 314 } 315 else { 316 SourceMapSpanWriter.recordSourceMapSpan(decodedSourceMapping); 317 } 318 } 319 SourceMapSpanWriter.close(); // If the last spans werent emitted, emit them 320 } 321 sourceMapRecorder.Close(); 322 return sourceMapRecorder.lines.join("\r\n"); 323 } 324 325 export function getSourceMapRecordWithSystem(sys: ts.System, sourceMapFile: string) { 326 const sourceMapRecorder = new Compiler.WriterAggregator(); 327 let prevSourceFile: documents.TextDocument | undefined; 328 const files = new ts.Map<string, documents.TextDocument>(); 329 const sourceMap = ts.tryParseRawSourceMap(sys.readFile(sourceMapFile, "utf8")!); 330 if (sourceMap) { 331 const mapDirectory = ts.getDirectoryPath(sourceMapFile); 332 const sourceRoot = sourceMap.sourceRoot ? ts.getNormalizedAbsolutePath(sourceMap.sourceRoot, mapDirectory) : mapDirectory; 333 const generatedAbsoluteFilePath = ts.getNormalizedAbsolutePath(sourceMap.file, mapDirectory); 334 const sourceFileAbsolutePaths = sourceMap.sources.map(source => ts.getNormalizedAbsolutePath(source, sourceRoot)); 335 const currentFile = getFile(generatedAbsoluteFilePath); 336 337 SourceMapSpanWriter.initializeSourceMapSpanWriter(sourceMapRecorder, sourceMap, currentFile); 338 const mapper = ts.decodeMappings(sourceMap.mappings); 339 for (let iterResult = mapper.next(); !iterResult.done; iterResult = mapper.next()) { 340 const decodedSourceMapping = iterResult.value; 341 const currentSourceFile = ts.isSourceMapping(decodedSourceMapping) 342 ? getFile(sourceFileAbsolutePaths[decodedSourceMapping.sourceIndex]) 343 : undefined; 344 if (currentSourceFile !== prevSourceFile) { 345 if (currentSourceFile) { 346 SourceMapSpanWriter.recordNewSourceFileSpan(decodedSourceMapping, currentSourceFile.text); 347 } 348 prevSourceFile = currentSourceFile; 349 } 350 else { 351 SourceMapSpanWriter.recordSourceMapSpan(decodedSourceMapping); 352 } 353 } 354 SourceMapSpanWriter.close(); // If the last spans werent emitted, emit them 355 } 356 sourceMapRecorder.Close(); 357 return sourceMapRecorder.lines.join("\r\n"); 358 359 function getFile(path: string) { 360 const existing = files.get(path); 361 if (existing) return existing; 362 const value = new documents.TextDocument(path, sys.readFile(path, "utf8")!); 363 files.set(path, value); 364 return value; 365 } 366 } 367} 368