2017-01-27 14:23:12 -08:00
|
|
|
/**
|
|
|
|
* @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
|
|
|
|
*/
|
|
|
|
|
2017-03-14 17:13:39 -07:00
|
|
|
import {utf8Encode} from '../util';
|
2017-03-14 09:16:15 -07:00
|
|
|
|
2017-01-27 14:23:12 -08:00
|
|
|
// 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[],
|
2017-03-24 09:59:58 -07:00
|
|
|
sourcesContent: (string | null)[],
|
2017-01-27 14:23:12 -08:00
|
|
|
mappings: string,
|
|
|
|
};
|
|
|
|
|
|
|
|
export class SourceMapGenerator {
|
2017-03-24 09:59:58 -07:00
|
|
|
private sourcesContent: Map<string, string|null> = new Map();
|
2017-01-27 14:23:12 -08:00
|
|
|
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[] = [];
|
2017-03-24 09:59:58 -07:00
|
|
|
const sourcesContent: (string | null)[] = [];
|
2017-01-27 14:23:12 -08:00
|
|
|
|
|
|
|
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 +=
|
2017-03-24 09:59:58 -07:00
|
|
|
toBase64VLQ(sourcesIndex.get(segment.sourceUrl) ! - lastSourceIndex);
|
|
|
|
lastSourceIndex = sourcesIndex.get(segment.sourceUrl) !;
|
2017-01-27 14:23:12 -08:00
|
|
|
// the zero-based starting line in the original source
|
2017-03-24 09:59:58 -07:00
|
|
|
segAsStr += toBase64VLQ(segment.sourceLine0 ! - lastSourceLine0);
|
|
|
|
lastSourceLine0 = segment.sourceLine0 !;
|
2017-01-27 14:23:12 -08:00
|
|
|
// the zero-based starting column in the original source
|
2017-03-24 09:59:58 -07:00
|
|
|
segAsStr += toBase64VLQ(segment.sourceCol0 ! - lastSourceCol0);
|
|
|
|
lastSourceCol0 = segment.sourceCol0 !;
|
2017-01-27 14:23:12 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
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 = '';
|
2017-03-14 09:16:15 -07:00
|
|
|
value = utf8Encode(value);
|
2017-01-27 14:23:12 -08:00
|
|
|
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];
|
|
|
|
}
|