187 lines
5.6 KiB
TypeScript
Raw Normal View History

/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {utf8Encode} from '../util';
// https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit
const VERSION = 3;
const JS_B64_PREFIX = '# sourceMappingURL=data:application/json;base64,';
type Segment = {
col0: number,
sourceUrl?: string,
sourceLine0?: number,
sourceCol0?: number,
};
export type SourceMap = {
version: number,
file?: string,
sourceRoot: string,
sources: string[],
sourcesContent: (string | null)[],
mappings: string,
};
export class SourceMapGenerator {
private sourcesContent: Map<string, string|null> = new Map();
private lines: Segment[][] = [];
private lastCol0: number = 0;
private hasMappings = false;
constructor(private file: string|null = null) {}
// The content is `null` when the content is expected to be loaded using the URL
addSource(url: string, content: string|null = null): this {
if (!this.sourcesContent.has(url)) {
this.sourcesContent.set(url, content);
}
return this;
}
addLine(): this {
this.lines.push([]);
this.lastCol0 = 0;
return this;
}
addMapping(col0: number, sourceUrl?: string, sourceLine0?: number, sourceCol0?: number): this {
if (!this.currentLine) {
throw new Error(`A line must be added before mappings can be added`);
}
if (sourceUrl != null && !this.sourcesContent.has(sourceUrl)) {
throw new Error(`Unknown source file "${sourceUrl}"`);
}
if (col0 == null) {
throw new Error(`The column in the generated code must be provided`);
}
if (col0 < this.lastCol0) {
throw new Error(`Mapping should be added in output order`);
}
if (sourceUrl && (sourceLine0 == null || sourceCol0 == null)) {
throw new Error(`The source location must be provided when a source url is provided`);
}
this.hasMappings = true;
this.lastCol0 = col0;
this.currentLine.push({col0, sourceUrl, sourceLine0, sourceCol0});
return this;
}
private get currentLine(): Segment[]|null { return this.lines.slice(-1)[0]; }
toJSON(): SourceMap|null {
if (!this.hasMappings) {
return null;
}
const sourcesIndex = new Map<string, number>();
const sources: string[] = [];
const sourcesContent: (string | null)[] = [];
Array.from(this.sourcesContent.keys()).forEach((url: string, i: number) => {
sourcesIndex.set(url, i);
sources.push(url);
sourcesContent.push(this.sourcesContent.get(url) || null);
});
let mappings: string = '';
let lastCol0: number = 0;
let lastSourceIndex: number = 0;
let lastSourceLine0: number = 0;
let lastSourceCol0: number = 0;
this.lines.forEach(segments => {
lastCol0 = 0;
mappings += segments
.map(segment => {
// zero-based starting column of the line in the generated code
let segAsStr = toBase64VLQ(segment.col0 - lastCol0);
lastCol0 = segment.col0;
if (segment.sourceUrl != null) {
// zero-based index into the “sources” list
segAsStr +=
toBase64VLQ(sourcesIndex.get(segment.sourceUrl) ! - lastSourceIndex);
lastSourceIndex = sourcesIndex.get(segment.sourceUrl) !;
// the zero-based starting line in the original source
segAsStr += toBase64VLQ(segment.sourceLine0 ! - lastSourceLine0);
lastSourceLine0 = segment.sourceLine0 !;
// the zero-based starting column in the original source
segAsStr += toBase64VLQ(segment.sourceCol0 ! - lastSourceCol0);
lastSourceCol0 = segment.sourceCol0 !;
}
return segAsStr;
})
.join(',');
mappings += ';';
});
mappings = mappings.slice(0, -1);
return {
'file': this.file || '',
'version': VERSION,
'sourceRoot': '',
'sources': sources,
'sourcesContent': sourcesContent,
'mappings': mappings,
};
}
toJsComment(): string {
return this.hasMappings ? '//' + JS_B64_PREFIX + toBase64String(JSON.stringify(this, null, 0)) :
'';
}
}
export function toBase64String(value: string): string {
let b64 = '';
value = utf8Encode(value);
for (let i = 0; i < value.length;) {
const i1 = value.charCodeAt(i++);
const i2 = value.charCodeAt(i++);
const i3 = value.charCodeAt(i++);
b64 += toBase64Digit(i1 >> 2);
b64 += toBase64Digit(((i1 & 3) << 4) | (isNaN(i2) ? 0 : i2 >> 4));
b64 += isNaN(i2) ? '=' : toBase64Digit(((i2 & 15) << 2) | (i3 >> 6));
b64 += isNaN(i2) || isNaN(i3) ? '=' : toBase64Digit(i3 & 63);
}
return b64;
}
function toBase64VLQ(value: number): string {
value = value < 0 ? ((-value) << 1) + 1 : value << 1;
let out = '';
do {
let digit = value & 31;
value = value >> 5;
if (value > 0) {
digit = digit | 32;
}
out += toBase64Digit(digit);
} while (value > 0);
return out;
}
const B64_DIGITS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
function toBase64Digit(value: number): string {
if (value < 0 || value >= 64) {
throw new Error(`Can only encode value in the range [0, 63]`);
}
return B64_DIGITS[value];
}