mirror of
https://github.com/microsoft/TypeScript.git
synced 2025-12-12 03:20:56 -06:00
495 lines
20 KiB
TypeScript
495 lines
20 KiB
TypeScript
/// <reference path="checker.ts"/>
|
|
|
|
/* @internal */
|
|
namespace ts {
|
|
export interface SourceMapWriter {
|
|
/**
|
|
* Initialize the SourceMapWriter for a new output file.
|
|
*
|
|
* @param filePath The path to the generated output file.
|
|
* @param sourceMapFilePath The path to the output source map file.
|
|
* @param sourceFileOrBundle The input source file or bundle for the program.
|
|
*/
|
|
initialize(filePath: string, sourceMapFilePath: string, sourceFileOrBundle: SourceFile | Bundle): void;
|
|
|
|
/**
|
|
* Reset the SourceMapWriter to an empty state.
|
|
*/
|
|
reset(): void;
|
|
|
|
/**
|
|
* Set the current source file.
|
|
*
|
|
* @param sourceFile The source file.
|
|
*/
|
|
setSourceFile(sourceFile: SourceFile): void;
|
|
|
|
/**
|
|
* Emits a mapping.
|
|
*
|
|
* If the position is synthetic (undefined or a negative value), no mapping will be
|
|
* created.
|
|
*
|
|
* @param pos The position.
|
|
*/
|
|
emitPos(pos: number): void;
|
|
|
|
/**
|
|
* Emits a node with possible leading and trailing source maps.
|
|
*
|
|
* @param hint The current emit context
|
|
* @param node The node to emit.
|
|
* @param emitCallback The callback used to emit the node.
|
|
*/
|
|
emitNodeWithSourceMap(hint: EmitHint, node: Node, emitCallback: (hint: EmitHint, node: Node) => void): void;
|
|
|
|
/**
|
|
* Emits a token of a node node with possible leading and trailing source maps.
|
|
*
|
|
* @param node The node containing the token.
|
|
* @param token The token to emit.
|
|
* @param tokenStartPos The start pos of the token.
|
|
* @param emitCallback The callback used to emit the token.
|
|
*/
|
|
emitTokenWithSourceMap(node: Node, token: SyntaxKind, tokenStartPos: number, emitCallback: (token: SyntaxKind, tokenStartPos: number) => number): number;
|
|
|
|
/**
|
|
* Gets the text for the source map.
|
|
*/
|
|
getText(): string;
|
|
|
|
/**
|
|
* Gets the SourceMappingURL for the source map.
|
|
*/
|
|
getSourceMappingURL(): string;
|
|
|
|
/**
|
|
* Gets test data for source maps.
|
|
*/
|
|
getSourceMapData(): SourceMapData;
|
|
}
|
|
|
|
// Used for initialize lastEncodedSourceMapSpan and reset lastEncodedSourceMapSpan when updateLastEncodedAndRecordedSpans
|
|
const defaultLastEncodedSourceMapSpan: SourceMapSpan = {
|
|
emittedLine: 1,
|
|
emittedColumn: 1,
|
|
sourceLine: 1,
|
|
sourceColumn: 1,
|
|
sourceIndex: 0
|
|
};
|
|
|
|
export function createSourceMapWriter(host: EmitHost, writer: EmitTextWriter): SourceMapWriter {
|
|
const compilerOptions = host.getCompilerOptions();
|
|
const extendedDiagnostics = compilerOptions.extendedDiagnostics;
|
|
let currentSourceFile: SourceFile;
|
|
let currentSourceText: string;
|
|
let sourceMapDir: string; // The directory in which sourcemap will be
|
|
|
|
// Current source map file and its index in the sources list
|
|
let sourceMapSourceIndex: number;
|
|
|
|
// Last recorded and encoded spans
|
|
let lastRecordedSourceMapSpan: SourceMapSpan;
|
|
let lastEncodedSourceMapSpan: SourceMapSpan;
|
|
let lastEncodedNameIndex: number;
|
|
|
|
// Source map data
|
|
let sourceMapData: SourceMapData;
|
|
let disabled: boolean = !(compilerOptions.sourceMap || compilerOptions.inlineSourceMap);
|
|
|
|
return {
|
|
initialize,
|
|
reset,
|
|
getSourceMapData: () => sourceMapData,
|
|
setSourceFile,
|
|
emitPos,
|
|
emitNodeWithSourceMap,
|
|
emitTokenWithSourceMap,
|
|
getText,
|
|
getSourceMappingURL,
|
|
};
|
|
|
|
/**
|
|
* Initialize the SourceMapWriter for a new output file.
|
|
*
|
|
* @param filePath The path to the generated output file.
|
|
* @param sourceMapFilePath The path to the output source map file.
|
|
* @param sourceFileOrBundle The input source file or bundle for the program.
|
|
*/
|
|
function initialize(filePath: string, sourceMapFilePath: string, sourceFileOrBundle: SourceFile | Bundle) {
|
|
if (disabled) {
|
|
return;
|
|
}
|
|
|
|
if (sourceMapData) {
|
|
reset();
|
|
}
|
|
|
|
currentSourceFile = undefined;
|
|
currentSourceText = undefined;
|
|
|
|
// Current source map file and its index in the sources list
|
|
sourceMapSourceIndex = -1;
|
|
|
|
// Last recorded and encoded spans
|
|
lastRecordedSourceMapSpan = undefined;
|
|
lastEncodedSourceMapSpan = defaultLastEncodedSourceMapSpan;
|
|
lastEncodedNameIndex = 0;
|
|
|
|
// Initialize source map data
|
|
sourceMapData = {
|
|
sourceMapFilePath: sourceMapFilePath,
|
|
jsSourceMappingURL: !compilerOptions.inlineSourceMap ? getBaseFileName(normalizeSlashes(sourceMapFilePath)) : undefined,
|
|
sourceMapFile: getBaseFileName(normalizeSlashes(filePath)),
|
|
sourceMapSourceRoot: compilerOptions.sourceRoot || "",
|
|
sourceMapSources: [],
|
|
inputSourceFileNames: [],
|
|
sourceMapNames: [],
|
|
sourceMapMappings: "",
|
|
sourceMapSourcesContent: compilerOptions.inlineSources ? [] : undefined,
|
|
sourceMapDecodedMappings: []
|
|
};
|
|
|
|
// Normalize source root and make sure it has trailing "/" so that it can be used to combine paths with the
|
|
// relative paths of the sources list in the sourcemap
|
|
sourceMapData.sourceMapSourceRoot = ts.normalizeSlashes(sourceMapData.sourceMapSourceRoot);
|
|
if (sourceMapData.sourceMapSourceRoot.length && sourceMapData.sourceMapSourceRoot.charCodeAt(sourceMapData.sourceMapSourceRoot.length - 1) !== CharacterCodes.slash) {
|
|
sourceMapData.sourceMapSourceRoot += directorySeparator;
|
|
}
|
|
|
|
if (compilerOptions.mapRoot) {
|
|
sourceMapDir = normalizeSlashes(compilerOptions.mapRoot);
|
|
if (sourceFileOrBundle.kind === SyntaxKind.SourceFile) { // emitting single module file
|
|
// For modules or multiple emit files the mapRoot will have directory structure like the sources
|
|
// So if src\a.ts and src\lib\b.ts are compiled together user would be moving the maps into mapRoot\a.js.map and mapRoot\lib\b.js.map
|
|
sourceMapDir = getDirectoryPath(getSourceFilePathInNewDir(sourceFileOrBundle, host, sourceMapDir));
|
|
}
|
|
|
|
if (!isRootedDiskPath(sourceMapDir) && !isUrl(sourceMapDir)) {
|
|
// The relative paths are relative to the common directory
|
|
sourceMapDir = combinePaths(host.getCommonSourceDirectory(), sourceMapDir);
|
|
sourceMapData.jsSourceMappingURL = getRelativePathToDirectoryOrUrl(
|
|
getDirectoryPath(normalizePath(filePath)), // get the relative sourceMapDir path based on jsFilePath
|
|
combinePaths(sourceMapDir, sourceMapData.jsSourceMappingURL), // this is where user expects to see sourceMap
|
|
host.getCurrentDirectory(),
|
|
host.getCanonicalFileName,
|
|
/*isAbsolutePathAnUrl*/ true);
|
|
}
|
|
else {
|
|
sourceMapData.jsSourceMappingURL = combinePaths(sourceMapDir, sourceMapData.jsSourceMappingURL);
|
|
}
|
|
}
|
|
else {
|
|
sourceMapDir = getDirectoryPath(normalizePath(filePath));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reset the SourceMapWriter to an empty state.
|
|
*/
|
|
function reset() {
|
|
if (disabled) {
|
|
return;
|
|
}
|
|
|
|
currentSourceFile = undefined;
|
|
sourceMapDir = undefined;
|
|
sourceMapSourceIndex = undefined;
|
|
lastRecordedSourceMapSpan = undefined;
|
|
lastEncodedSourceMapSpan = undefined;
|
|
lastEncodedNameIndex = undefined;
|
|
sourceMapData = undefined;
|
|
}
|
|
|
|
// Encoding for sourcemap span
|
|
function encodeLastRecordedSourceMapSpan() {
|
|
if (!lastRecordedSourceMapSpan || lastRecordedSourceMapSpan === lastEncodedSourceMapSpan) {
|
|
return;
|
|
}
|
|
|
|
let prevEncodedEmittedColumn = lastEncodedSourceMapSpan.emittedColumn;
|
|
// Line/Comma delimiters
|
|
if (lastEncodedSourceMapSpan.emittedLine === lastRecordedSourceMapSpan.emittedLine) {
|
|
// Emit comma to separate the entry
|
|
if (sourceMapData.sourceMapMappings) {
|
|
sourceMapData.sourceMapMappings += ",";
|
|
}
|
|
}
|
|
else {
|
|
// Emit line delimiters
|
|
for (let encodedLine = lastEncodedSourceMapSpan.emittedLine; encodedLine < lastRecordedSourceMapSpan.emittedLine; encodedLine++) {
|
|
sourceMapData.sourceMapMappings += ";";
|
|
}
|
|
prevEncodedEmittedColumn = 1;
|
|
}
|
|
|
|
// 1. Relative Column 0 based
|
|
sourceMapData.sourceMapMappings += base64VLQFormatEncode(lastRecordedSourceMapSpan.emittedColumn - prevEncodedEmittedColumn);
|
|
|
|
// 2. Relative sourceIndex
|
|
sourceMapData.sourceMapMappings += base64VLQFormatEncode(lastRecordedSourceMapSpan.sourceIndex - lastEncodedSourceMapSpan.sourceIndex);
|
|
|
|
// 3. Relative sourceLine 0 based
|
|
sourceMapData.sourceMapMappings += base64VLQFormatEncode(lastRecordedSourceMapSpan.sourceLine - lastEncodedSourceMapSpan.sourceLine);
|
|
|
|
// 4. Relative sourceColumn 0 based
|
|
sourceMapData.sourceMapMappings += base64VLQFormatEncode(lastRecordedSourceMapSpan.sourceColumn - lastEncodedSourceMapSpan.sourceColumn);
|
|
|
|
// 5. Relative namePosition 0 based
|
|
if (lastRecordedSourceMapSpan.nameIndex >= 0) {
|
|
Debug.assert(false, "We do not support name index right now, Make sure to update updateLastEncodedAndRecordedSpans when we start using this");
|
|
sourceMapData.sourceMapMappings += base64VLQFormatEncode(lastRecordedSourceMapSpan.nameIndex - lastEncodedNameIndex);
|
|
lastEncodedNameIndex = lastRecordedSourceMapSpan.nameIndex;
|
|
}
|
|
|
|
lastEncodedSourceMapSpan = lastRecordedSourceMapSpan;
|
|
sourceMapData.sourceMapDecodedMappings.push(lastEncodedSourceMapSpan);
|
|
}
|
|
|
|
/**
|
|
* Emits a mapping.
|
|
*
|
|
* If the position is synthetic (undefined or a negative value), no mapping will be
|
|
* created.
|
|
*
|
|
* @param pos The position.
|
|
*/
|
|
function emitPos(pos: number) {
|
|
if (disabled || positionIsSynthesized(pos)) {
|
|
return;
|
|
}
|
|
|
|
if (extendedDiagnostics) {
|
|
performance.mark("beforeSourcemap");
|
|
}
|
|
|
|
const sourceLinePos = getLineAndCharacterOfPosition(currentSourceFile, pos);
|
|
|
|
// Convert the location to be one-based.
|
|
sourceLinePos.line++;
|
|
sourceLinePos.character++;
|
|
|
|
const emittedLine = writer.getLine();
|
|
const emittedColumn = writer.getColumn();
|
|
|
|
// If this location wasn't recorded or the location in source is going backwards, record the span
|
|
if (!lastRecordedSourceMapSpan ||
|
|
lastRecordedSourceMapSpan.emittedLine !== emittedLine ||
|
|
lastRecordedSourceMapSpan.emittedColumn !== emittedColumn ||
|
|
(lastRecordedSourceMapSpan.sourceIndex === sourceMapSourceIndex &&
|
|
(lastRecordedSourceMapSpan.sourceLine > sourceLinePos.line ||
|
|
(lastRecordedSourceMapSpan.sourceLine === sourceLinePos.line && lastRecordedSourceMapSpan.sourceColumn > sourceLinePos.character)))) {
|
|
|
|
// Encode the last recordedSpan before assigning new
|
|
encodeLastRecordedSourceMapSpan();
|
|
|
|
// New span
|
|
lastRecordedSourceMapSpan = {
|
|
emittedLine: emittedLine,
|
|
emittedColumn: emittedColumn,
|
|
sourceLine: sourceLinePos.line,
|
|
sourceColumn: sourceLinePos.character,
|
|
sourceIndex: sourceMapSourceIndex
|
|
};
|
|
}
|
|
else {
|
|
// Take the new pos instead since there is no change in emittedLine and column since last location
|
|
lastRecordedSourceMapSpan.sourceLine = sourceLinePos.line;
|
|
lastRecordedSourceMapSpan.sourceColumn = sourceLinePos.character;
|
|
lastRecordedSourceMapSpan.sourceIndex = sourceMapSourceIndex;
|
|
}
|
|
|
|
if (extendedDiagnostics) {
|
|
performance.mark("afterSourcemap");
|
|
performance.measure("Source Map", "beforeSourcemap", "afterSourcemap");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Emits a node with possible leading and trailing source maps.
|
|
*
|
|
* @param hint A hint as to the intended usage of the node.
|
|
* @param node The node to emit.
|
|
* @param emitCallback The callback used to emit the node.
|
|
*/
|
|
function emitNodeWithSourceMap(hint: EmitHint, node: Node, emitCallback: (hint: EmitHint, node: Node) => void) {
|
|
if (disabled) {
|
|
return emitCallback(hint, node);
|
|
}
|
|
|
|
if (node) {
|
|
const emitNode = node.emitNode;
|
|
const emitFlags = emitNode && emitNode.flags;
|
|
const { pos, end } = emitNode && emitNode.sourceMapRange || node;
|
|
|
|
if (node.kind !== SyntaxKind.NotEmittedStatement
|
|
&& (emitFlags & EmitFlags.NoLeadingSourceMap) === 0
|
|
&& pos >= 0) {
|
|
emitPos(skipTrivia(currentSourceText, pos));
|
|
}
|
|
|
|
if (emitFlags & EmitFlags.NoNestedSourceMaps) {
|
|
disabled = true;
|
|
emitCallback(hint, node);
|
|
disabled = false;
|
|
}
|
|
else {
|
|
emitCallback(hint, node);
|
|
}
|
|
|
|
if (node.kind !== SyntaxKind.NotEmittedStatement
|
|
&& (emitFlags & EmitFlags.NoTrailingSourceMap) === 0
|
|
&& end >= 0) {
|
|
emitPos(end);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Emits a token of a node with possible leading and trailing source maps.
|
|
*
|
|
* @param node The node containing the token.
|
|
* @param token The token to emit.
|
|
* @param tokenStartPos The start pos of the token.
|
|
* @param emitCallback The callback used to emit the token.
|
|
*/
|
|
function emitTokenWithSourceMap(node: Node, token: SyntaxKind, tokenPos: number, emitCallback: (token: SyntaxKind, tokenStartPos: number) => number) {
|
|
if (disabled) {
|
|
return emitCallback(token, tokenPos);
|
|
}
|
|
|
|
const emitNode = node && node.emitNode;
|
|
const emitFlags = emitNode && emitNode.flags;
|
|
const range = emitNode && emitNode.tokenSourceMapRanges && emitNode.tokenSourceMapRanges[token];
|
|
|
|
tokenPos = skipTrivia(currentSourceText, range ? range.pos : tokenPos);
|
|
if ((emitFlags & EmitFlags.NoTokenLeadingSourceMaps) === 0 && tokenPos >= 0) {
|
|
emitPos(tokenPos);
|
|
}
|
|
|
|
tokenPos = emitCallback(token, tokenPos);
|
|
|
|
if (range) tokenPos = range.end;
|
|
if ((emitFlags & EmitFlags.NoTokenTrailingSourceMaps) === 0 && tokenPos >= 0) {
|
|
emitPos(tokenPos);
|
|
}
|
|
|
|
return tokenPos;
|
|
}
|
|
|
|
/**
|
|
* Set the current source file.
|
|
*
|
|
* @param sourceFile The source file.
|
|
*/
|
|
function setSourceFile(sourceFile: SourceFile) {
|
|
if (disabled) {
|
|
return;
|
|
}
|
|
|
|
currentSourceFile = sourceFile;
|
|
currentSourceText = currentSourceFile.text;
|
|
|
|
// Add the file to tsFilePaths
|
|
// If sourceroot option: Use the relative path corresponding to the common directory path
|
|
// otherwise source locations relative to map file location
|
|
const sourcesDirectoryPath = compilerOptions.sourceRoot ? host.getCommonSourceDirectory() : sourceMapDir;
|
|
|
|
const source = getRelativePathToDirectoryOrUrl(sourcesDirectoryPath,
|
|
currentSourceFile.fileName,
|
|
host.getCurrentDirectory(),
|
|
host.getCanonicalFileName,
|
|
/*isAbsolutePathAnUrl*/ true);
|
|
|
|
sourceMapSourceIndex = indexOf(sourceMapData.sourceMapSources, source);
|
|
if (sourceMapSourceIndex === -1) {
|
|
sourceMapSourceIndex = sourceMapData.sourceMapSources.length;
|
|
sourceMapData.sourceMapSources.push(source);
|
|
|
|
// The one that can be used from program to get the actual source file
|
|
sourceMapData.inputSourceFileNames.push(currentSourceFile.fileName);
|
|
|
|
if (compilerOptions.inlineSources) {
|
|
sourceMapData.sourceMapSourcesContent.push(currentSourceFile.text);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the text for the source map.
|
|
*/
|
|
function getText() {
|
|
if (disabled) {
|
|
return;
|
|
}
|
|
|
|
encodeLastRecordedSourceMapSpan();
|
|
|
|
return JSON.stringify({
|
|
version: 3,
|
|
file: sourceMapData.sourceMapFile,
|
|
sourceRoot: sourceMapData.sourceMapSourceRoot,
|
|
sources: sourceMapData.sourceMapSources,
|
|
names: sourceMapData.sourceMapNames,
|
|
mappings: sourceMapData.sourceMapMappings,
|
|
sourcesContent: sourceMapData.sourceMapSourcesContent,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Gets the SourceMappingURL for the source map.
|
|
*/
|
|
function getSourceMappingURL() {
|
|
if (disabled) {
|
|
return;
|
|
}
|
|
|
|
if (compilerOptions.inlineSourceMap) {
|
|
// Encode the sourceMap into the sourceMap url
|
|
const base64SourceMapText = convertToBase64(getText());
|
|
return sourceMapData.jsSourceMappingURL = `data:application/json;base64,${base64SourceMapText}`;
|
|
}
|
|
else {
|
|
return sourceMapData.jsSourceMappingURL;
|
|
}
|
|
}
|
|
}
|
|
|
|
const base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
|
|
function base64FormatEncode(inValue: number) {
|
|
if (inValue < 64) {
|
|
return base64Chars.charAt(inValue);
|
|
}
|
|
|
|
throw TypeError(inValue + ": not a 64 based value");
|
|
}
|
|
|
|
function base64VLQFormatEncode(inValue: number) {
|
|
// Add a new least significant bit that has the sign of the value.
|
|
// if negative number the least significant bit that gets added to the number has value 1
|
|
// else least significant bit value that gets added is 0
|
|
// eg. -1 changes to binary : 01 [1] => 3
|
|
// +1 changes to binary : 01 [0] => 2
|
|
if (inValue < 0) {
|
|
inValue = ((-inValue) << 1) + 1;
|
|
}
|
|
else {
|
|
inValue = inValue << 1;
|
|
}
|
|
|
|
// Encode 5 bits at a time starting from least significant bits
|
|
let encodedStr = "";
|
|
do {
|
|
let currentDigit = inValue & 31; // 11111
|
|
inValue = inValue >> 5;
|
|
if (inValue > 0) {
|
|
// There are still more digits to decode, set the msb (6th bit)
|
|
currentDigit = currentDigit | 32;
|
|
}
|
|
encodedStr = encodedStr + base64FormatEncode(currentDigit);
|
|
} while (inValue > 0);
|
|
|
|
return encodedStr;
|
|
}
|
|
} |