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