• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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