461 lines
22 KiB
TypeScript
461 lines
22 KiB
TypeScript
//
|
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//
|
|
|
|
///<reference path='harness.ts'/>
|
|
|
|
module Harness.SourceMapRecoder {
|
|
|
|
interface SourceMapSpanWithDecodeErrors {
|
|
sourceMapSpan: ts.SourceMapSpan;
|
|
decodeErrors: string[];
|
|
}
|
|
|
|
module SourceMapDecoder {
|
|
var sourceMapMappings: string;
|
|
var sourceMapNames: string[];
|
|
var decodingIndex: number;
|
|
var prevNameIndex: number;
|
|
var decodeOfEncodedMapping: ts.SourceMapSpan;
|
|
var errorDecodeOfEncodedMapping: string;
|
|
|
|
export function initializeSourceMapDecoding(sourceMapData: ts.SourceMapData) {
|
|
sourceMapMappings = sourceMapData.sourceMapMappings;
|
|
sourceMapNames = sourceMapData.sourceMapNames;
|
|
decodingIndex = 0;
|
|
prevNameIndex = 0;
|
|
decodeOfEncodedMapping = {
|
|
emittedLine: 1,
|
|
emittedColumn: 1,
|
|
sourceLine: 1,
|
|
sourceColumn: 1,
|
|
sourceIndex: 0,
|
|
};
|
|
errorDecodeOfEncodedMapping = undefined;
|
|
}
|
|
|
|
function isSourceMappingSegmentEnd() {
|
|
if (decodingIndex == sourceMapMappings.length) {
|
|
return true;
|
|
}
|
|
|
|
if (sourceMapMappings.charAt(decodingIndex) == ',') {
|
|
return true;
|
|
}
|
|
|
|
if (sourceMapMappings.charAt(decodingIndex) == ';') {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
export function decodeNextEncodedSourceMapSpan() {
|
|
errorDecodeOfEncodedMapping = undefined;
|
|
|
|
function createErrorIfCondition(condition: boolean, errormsg: string) {
|
|
if (errorDecodeOfEncodedMapping) {
|
|
// there was existing error:
|
|
return true;
|
|
}
|
|
|
|
if (condition) {
|
|
errorDecodeOfEncodedMapping = errormsg;
|
|
}
|
|
|
|
return condition;
|
|
}
|
|
|
|
function base64VLQFormatDecode() {
|
|
function base64FormatDecode() {
|
|
return "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".indexOf(sourceMapMappings.charAt(decodingIndex));
|
|
}
|
|
|
|
var moreDigits = true;
|
|
var shiftCount = 0;
|
|
var value = 0;
|
|
|
|
for (; moreDigits; decodingIndex++) {
|
|
if (createErrorIfCondition(decodingIndex >= sourceMapMappings.length, "Error in decoding base64VLQFormatDecode, past the mapping string")) {
|
|
return;
|
|
}
|
|
|
|
// 6 digit number
|
|
var currentByte = base64FormatDecode();
|
|
|
|
// If msb is set, we still have more bits to continue
|
|
moreDigits = (currentByte & 32) != 0;
|
|
|
|
// least significant 5 bits are the next msbs in the final value.
|
|
value = value | ((currentByte & 31) << shiftCount);
|
|
shiftCount += 5;
|
|
}
|
|
|
|
// Least significant bit if 1 represents negative and rest of the msb is actual absolute value
|
|
if ((value & 1) == 0) {
|
|
// + number
|
|
value = value >> 1;
|
|
}
|
|
else {
|
|
// - number
|
|
value = value >> 1;
|
|
value = -value;
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
while (decodingIndex < sourceMapMappings.length) {
|
|
if (sourceMapMappings.charAt(decodingIndex) == ';') {
|
|
// New line
|
|
decodeOfEncodedMapping.emittedLine++;
|
|
decodeOfEncodedMapping.emittedColumn = 1;
|
|
decodingIndex++;
|
|
continue;
|
|
}
|
|
|
|
if (sourceMapMappings.charAt(decodingIndex) == ',') {
|
|
// Next entry is on same line - no action needed
|
|
decodingIndex++;
|
|
continue;
|
|
}
|
|
|
|
// Read the current span
|
|
// 1. Column offset from prev read jsColumn
|
|
decodeOfEncodedMapping.emittedColumn += base64VLQFormatDecode();
|
|
// Incorrect emittedColumn dont support this map
|
|
if (createErrorIfCondition(decodeOfEncodedMapping.emittedColumn < 1, "Invalid emittedColumn found")) {
|
|
return { error: errorDecodeOfEncodedMapping, sourceMapSpan: decodeOfEncodedMapping };
|
|
}
|
|
// Dont support reading mappings that dont have information about original source and its line numbers
|
|
if (createErrorIfCondition(isSourceMappingSegmentEnd(), "Unsupported Error Format: No entries after emitted column")) {
|
|
return { error: errorDecodeOfEncodedMapping, sourceMapSpan: decodeOfEncodedMapping };
|
|
}
|
|
|
|
// 2. Relative sourceIndex
|
|
decodeOfEncodedMapping.sourceIndex += base64VLQFormatDecode();
|
|
// Incorrect sourceIndex dont support this map
|
|
if (createErrorIfCondition(decodeOfEncodedMapping.sourceIndex < 0, "Invalid sourceIndex found")) {
|
|
return { error: errorDecodeOfEncodedMapping, sourceMapSpan: decodeOfEncodedMapping };
|
|
}
|
|
// Dont support reading mappings that dont have information about original source span
|
|
if (createErrorIfCondition(isSourceMappingSegmentEnd(), "Unsupported Error Format: No entries after sourceIndex")) {
|
|
return { error: errorDecodeOfEncodedMapping, sourceMapSpan: decodeOfEncodedMapping };
|
|
}
|
|
|
|
// 3. Relative sourceLine 0 based
|
|
decodeOfEncodedMapping.sourceLine += base64VLQFormatDecode();
|
|
// Incorrect sourceLine dont support this map
|
|
if (createErrorIfCondition(decodeOfEncodedMapping.sourceLine < 1, "Invalid sourceLine found")) {
|
|
return { error: errorDecodeOfEncodedMapping, sourceMapSpan: decodeOfEncodedMapping };
|
|
}
|
|
// Dont support reading mappings that dont have information about original source and its line numbers
|
|
if (createErrorIfCondition(isSourceMappingSegmentEnd(), "Unsupported Error Format: No entries after emitted Line")) {
|
|
return { error: errorDecodeOfEncodedMapping, sourceMapSpan: decodeOfEncodedMapping };
|
|
}
|
|
|
|
// 4. Relative sourceColumn 0 based
|
|
decodeOfEncodedMapping.sourceColumn += base64VLQFormatDecode();
|
|
// Incorrect sourceColumn dont support this map
|
|
if (createErrorIfCondition(decodeOfEncodedMapping.sourceColumn < 1, "Invalid sourceLine found")) {
|
|
return { error: errorDecodeOfEncodedMapping, sourceMapSpan: decodeOfEncodedMapping };
|
|
}
|
|
// 5. Check if there is name:
|
|
decodeOfEncodedMapping.nameIndex = -1;
|
|
if (!isSourceMappingSegmentEnd()) {
|
|
prevNameIndex += base64VLQFormatDecode();
|
|
decodeOfEncodedMapping.nameIndex = prevNameIndex;
|
|
// Incorrect nameIndex dont support this map
|
|
if (createErrorIfCondition(decodeOfEncodedMapping.nameIndex < 0 || decodeOfEncodedMapping.nameIndex >= sourceMapNames.length, "Invalid name index for the source map entry")) {
|
|
return { error: errorDecodeOfEncodedMapping, sourceMapSpan: decodeOfEncodedMapping };
|
|
}
|
|
}
|
|
// Dont support reading mappings that dont have information about original source and its line numbers
|
|
if (createErrorIfCondition(!isSourceMappingSegmentEnd(), "Unsupported Error Format: There are more entries after " + (decodeOfEncodedMapping.nameIndex == -1 ? "sourceColumn" : "nameIndex"))) {
|
|
return { error: errorDecodeOfEncodedMapping, sourceMapSpan: decodeOfEncodedMapping };
|
|
}
|
|
|
|
// Populated the entry
|
|
return { error: errorDecodeOfEncodedMapping, sourceMapSpan: decodeOfEncodedMapping };
|
|
}
|
|
|
|
createErrorIfCondition(true, "No encoded entry found");
|
|
}
|
|
|
|
export function hasCompletedDecoding() {
|
|
return decodingIndex === sourceMapMappings.length;
|
|
}
|
|
|
|
export function getRemainingDecodeString() {
|
|
return sourceMapMappings.substr(decodingIndex);
|
|
}
|
|
}
|
|
|
|
module SourceMapSpanWriter {
|
|
var sourceMapRecoder: Compiler.WriterAggregator;
|
|
var sourceMapSources: string[];
|
|
var sourceMapNames: string[];
|
|
|
|
var jsFile: Compiler.GeneratedFile;
|
|
var jsLineMap: number[];
|
|
var tsCode: string;
|
|
var tsLineMap: number[];
|
|
|
|
var spansOnSingleLine: SourceMapSpanWithDecodeErrors[];
|
|
var prevWrittenSourcePos: number;
|
|
var prevWrittenJsLine: number;
|
|
var spanMarkerContinues: boolean;
|
|
|
|
export function intializeSourceMapSpanWriter(sourceMapRecordWriter: Compiler.WriterAggregator, sourceMapData: ts.SourceMapData, currentJsFile: Compiler.GeneratedFile) {
|
|
sourceMapRecoder = sourceMapRecordWriter;
|
|
sourceMapSources = sourceMapData.sourceMapSources;
|
|
sourceMapNames = sourceMapData.sourceMapNames;
|
|
|
|
jsFile = currentJsFile;
|
|
jsLineMap = ts.getLineStarts(jsFile.code);
|
|
|
|
spansOnSingleLine = [];
|
|
prevWrittenSourcePos = 0;
|
|
prevWrittenJsLine = 0;
|
|
spanMarkerContinues = false;
|
|
|
|
SourceMapDecoder.initializeSourceMapDecoding(sourceMapData);
|
|
|
|
sourceMapRecoder.WriteLine("===================================================================");
|
|
sourceMapRecoder.WriteLine("JsFile: " + sourceMapData.sourceMapFile);
|
|
sourceMapRecoder.WriteLine("mapUrl: " + sourceMapData.jsSourceMappingURL);
|
|
sourceMapRecoder.WriteLine("sourceRoot: " + sourceMapData.sourceMapSourceRoot);
|
|
sourceMapRecoder.WriteLine("sources: " + sourceMapData.sourceMapSources);
|
|
sourceMapRecoder.WriteLine("===================================================================");
|
|
}
|
|
|
|
function getSourceMapSpanString(mapEntry: ts.SourceMapSpan, getAbsentNameIndex?: boolean) {
|
|
var mapString = "Emitted(" + mapEntry.emittedLine + ", " + mapEntry.emittedColumn + ") Source(" + mapEntry.sourceLine + ", " + mapEntry.sourceColumn + ") + SourceIndex(" + mapEntry.sourceIndex + ")";
|
|
if (mapEntry.nameIndex >= 0 && mapEntry.nameIndex < sourceMapNames.length) {
|
|
mapString += " name (" + sourceMapNames[mapEntry.nameIndex] + ")";
|
|
}
|
|
else {
|
|
if (mapEntry.nameIndex != -1 || getAbsentNameIndex) {
|
|
mapString += " nameIndex (" + mapEntry.nameIndex + ")";
|
|
}
|
|
}
|
|
|
|
return mapString;
|
|
}
|
|
|
|
export function recordSourceMapSpan(sourceMapSpan: ts.SourceMapSpan) {
|
|
// verify the decoded span is same as the new span
|
|
var decodeResult = SourceMapDecoder.decodeNextEncodedSourceMapSpan();
|
|
var decodedErrors: string[];
|
|
if (decodeResult.error
|
|
|| decodeResult.sourceMapSpan.emittedLine != sourceMapSpan.emittedLine
|
|
|| decodeResult.sourceMapSpan.emittedColumn != sourceMapSpan.emittedColumn
|
|
|| decodeResult.sourceMapSpan.sourceLine != sourceMapSpan.sourceLine
|
|
|| decodeResult.sourceMapSpan.sourceColumn != sourceMapSpan.sourceColumn
|
|
|| decodeResult.sourceMapSpan.sourceIndex != sourceMapSpan.sourceIndex
|
|
|| decodeResult.sourceMapSpan.nameIndex != sourceMapSpan.nameIndex) {
|
|
if (decodeResult.error) {
|
|
decodedErrors = ["!!^^ !!^^ There was decoding error in the sourcemap at this location: " + decodeResult.error];
|
|
}
|
|
else {
|
|
decodedErrors = ["!!^^ !!^^ The decoded span from sourcemap's mapping entry does not match what was encoded for this span:"];
|
|
}
|
|
decodedErrors.push("!!^^ !!^^ Decoded span from sourcemap's mappings entry: " + getSourceMapSpanString(decodeResult.sourceMapSpan, /*getAbsentNameIndex*/ true) + " Span encoded by the emitter:" + getSourceMapSpanString(sourceMapSpan, /*getAbsentNameIndex*/ true));
|
|
}
|
|
|
|
if (spansOnSingleLine.length && spansOnSingleLine[0].sourceMapSpan.emittedLine !== sourceMapSpan.emittedLine) {
|
|
// On different line from the one that we have been recording till now,
|
|
writeRecordedSpans();
|
|
spansOnSingleLine = [{ sourceMapSpan: sourceMapSpan, decodeErrors: decodedErrors }];
|
|
}
|
|
else {
|
|
spansOnSingleLine.push({ sourceMapSpan: sourceMapSpan, decodeErrors: decodedErrors });
|
|
}
|
|
}
|
|
|
|
export function recordNewSourceFileSpan(sourceMapSpan: ts.SourceMapSpan, newSourceFileCode: string) {
|
|
assert.isTrue(spansOnSingleLine.length == 0 || spansOnSingleLine[0].sourceMapSpan.emittedLine !== sourceMapSpan.emittedLine, "new file source map span should be on new line. We currently handle only that scenario");
|
|
recordSourceMapSpan(sourceMapSpan);
|
|
|
|
assert.isTrue(spansOnSingleLine.length === 1);
|
|
sourceMapRecoder.WriteLine("-------------------------------------------------------------------");
|
|
sourceMapRecoder.WriteLine("emittedFile:" + jsFile.fileName);
|
|
sourceMapRecoder.WriteLine("sourceFile:" + sourceMapSources[spansOnSingleLine[0].sourceMapSpan.sourceIndex]);
|
|
sourceMapRecoder.WriteLine("-------------------------------------------------------------------");
|
|
|
|
tsLineMap = ts.getLineStarts(newSourceFileCode);
|
|
tsCode = newSourceFileCode;
|
|
prevWrittenSourcePos = 0;
|
|
}
|
|
|
|
export function close() {
|
|
// Write the lines pending on the single line
|
|
writeRecordedSpans();
|
|
|
|
if (!SourceMapDecoder.hasCompletedDecoding()) {
|
|
sourceMapRecoder.WriteLine("!!!! **** There are more source map entries in the sourceMap's mapping than what was encoded");
|
|
sourceMapRecoder.WriteLine("!!!! **** Remaining decoded string: " + SourceMapDecoder.getRemainingDecodeString());
|
|
|
|
}
|
|
|
|
// write remaining js lines
|
|
writeJsFileLines(jsLineMap.length);
|
|
}
|
|
|
|
function getTextOfLine(line: number, lineMap: number[], code: string) {
|
|
var startPos = lineMap[line];
|
|
var endPos = lineMap[line + 1];
|
|
return code.substring(startPos, endPos);
|
|
}
|
|
|
|
function writeJsFileLines(endJsLine: number) {
|
|
for (; prevWrittenJsLine < endJsLine; prevWrittenJsLine++) {
|
|
sourceMapRecoder.Write(">>>" + getTextOfLine(prevWrittenJsLine, jsLineMap, jsFile.code));
|
|
}
|
|
}
|
|
|
|
function writeRecordedSpans() {
|
|
function getMarkerId(markerIndex: number) {
|
|
var markerId = "";
|
|
if (spanMarkerContinues) {
|
|
assert.isTrue(markerIndex === 0);
|
|
markerId = "1->";
|
|
}
|
|
else {
|
|
var markerId = "" + (markerIndex + 1);
|
|
if (markerId.length < 2) {
|
|
markerId = markerId + " ";
|
|
}
|
|
markerId += ">";
|
|
}
|
|
return markerId;
|
|
}
|
|
|
|
var prevEmittedCol: number;
|
|
function iterateSpans(fn: (currentSpan: SourceMapSpanWithDecodeErrors, index: number) => void) {
|
|
prevEmittedCol = 1;
|
|
for (var i = 0; i < spansOnSingleLine.length; i++) {
|
|
fn(spansOnSingleLine[i], i);
|
|
prevEmittedCol = spansOnSingleLine[i].sourceMapSpan.emittedColumn;
|
|
}
|
|
}
|
|
|
|
function writeSourceMapIndent(indentLength: number, indentPrefix: string) {
|
|
sourceMapRecoder.Write(indentPrefix);
|
|
for (var i = 1; i < indentLength; i++) {
|
|
sourceMapRecoder.Write(" ");
|
|
}
|
|
}
|
|
|
|
function writeSourceMapMarker(currentSpan: SourceMapSpanWithDecodeErrors, index: number, endColumn = currentSpan.sourceMapSpan.emittedColumn, endContinues?: boolean) {
|
|
var markerId = getMarkerId(index);
|
|
markerIds.push(markerId);
|
|
|
|
writeSourceMapIndent(prevEmittedCol, markerId);
|
|
|
|
for (var i = prevEmittedCol; i < endColumn; i++) {
|
|
sourceMapRecoder.Write("^");
|
|
}
|
|
if (endContinues) {
|
|
sourceMapRecoder.Write("->");
|
|
}
|
|
sourceMapRecoder.WriteLine("");
|
|
spanMarkerContinues = endContinues;
|
|
}
|
|
|
|
function writeSourceMapSourceText(currentSpan: SourceMapSpanWithDecodeErrors, index: number) {
|
|
var sourcePos = tsLineMap[currentSpan.sourceMapSpan.sourceLine - 1] + (currentSpan.sourceMapSpan.sourceColumn - 1);
|
|
var sourceText = "";
|
|
if (prevWrittenSourcePos < sourcePos) {
|
|
// Position that goes forward, get text
|
|
sourceText = tsCode.substring(prevWrittenSourcePos, sourcePos);
|
|
}
|
|
|
|
if (currentSpan.decodeErrors) {
|
|
// If there are decode errors, write
|
|
for (var i = 0; i < currentSpan.decodeErrors.length; i++) {
|
|
writeSourceMapIndent(prevEmittedCol, markerIds[index]);
|
|
sourceMapRecoder.WriteLine(currentSpan.decodeErrors[i]);
|
|
}
|
|
}
|
|
|
|
var tsCodeLineMap = ts.getLineStarts(sourceText);
|
|
for (var i = 0; i < tsCodeLineMap.length; i++) {
|
|
writeSourceMapIndent(prevEmittedCol, i == 0 ? markerIds[index] : " >");
|
|
sourceMapRecoder.Write(getTextOfLine(i, tsCodeLineMap, sourceText));
|
|
if (i == tsCodeLineMap.length - 1) {
|
|
sourceMapRecoder.WriteLine("");
|
|
}
|
|
}
|
|
|
|
prevWrittenSourcePos = sourcePos;
|
|
}
|
|
|
|
function writeSpanDetails(currentSpan: SourceMapSpanWithDecodeErrors, index: number) {
|
|
sourceMapRecoder.WriteLine(markerIds[index] + getSourceMapSpanString(currentSpan.sourceMapSpan));
|
|
}
|
|
|
|
if (spansOnSingleLine.length) {
|
|
var currentJsLine = spansOnSingleLine[0].sourceMapSpan.emittedLine;
|
|
|
|
// Write js line
|
|
writeJsFileLines(currentJsLine);
|
|
|
|
// Emit markers
|
|
var markerIds: string[] = [];
|
|
iterateSpans(writeSourceMapMarker);
|
|
|
|
var jsFileText = getTextOfLine(currentJsLine, jsLineMap, jsFile.code);
|
|
if (prevEmittedCol < jsFileText.length) {
|
|
// There is remaining text on this line that will be part of next source span so write marker that continues
|
|
writeSourceMapMarker(undefined, spansOnSingleLine.length, /*endColumn*/ jsFileText.length, /*endContinues*/ true);
|
|
}
|
|
|
|
// Emit Source text
|
|
iterateSpans(writeSourceMapSourceText);
|
|
|
|
// Emit column number etc
|
|
iterateSpans(writeSpanDetails);
|
|
|
|
sourceMapRecoder.WriteLine("---");
|
|
}
|
|
}
|
|
}
|
|
|
|
export function getSourceMapRecord(sourceMapDataList: ts.SourceMapData[], program: ts.Program, jsFiles: Compiler.GeneratedFile[]) {
|
|
var sourceMapRecoder = new Compiler.WriterAggregator();
|
|
|
|
for (var i = 0; i < sourceMapDataList.length; i++) {
|
|
var sourceMapData = sourceMapDataList[i];
|
|
var prevSourceFile: ts.SourceFile = null;
|
|
|
|
SourceMapSpanWriter.intializeSourceMapSpanWriter(sourceMapRecoder, sourceMapData, jsFiles[i]);
|
|
for (var j = 0; j < sourceMapData.sourceMapDecodedMappings.length; j++) {
|
|
var decodedSourceMapping = sourceMapData.sourceMapDecodedMappings[j];
|
|
var currentSourceFile = program.getSourceFile(sourceMapData.inputSourceFileNames[decodedSourceMapping.sourceIndex]);
|
|
if (currentSourceFile != prevSourceFile) {
|
|
SourceMapSpanWriter.recordNewSourceFileSpan(decodedSourceMapping, currentSourceFile.text);
|
|
prevSourceFile = currentSourceFile;
|
|
}
|
|
else {
|
|
SourceMapSpanWriter.recordSourceMapSpan(decodedSourceMapping);
|
|
}
|
|
}
|
|
SourceMapSpanWriter.close();// If the last spans werent emitted, emit them
|
|
}
|
|
sourceMapRecoder.Close();
|
|
return sourceMapRecoder.lines.join('\r\n');
|
|
}
|
|
}
|