UNPKG

chrome-devtools-frontend

Version:
367 lines (310 loc) • 10.8 kB
// Copyright 2021 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as SDK from '../core/sdk/sdk.js'; const base64Digits = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; export function encodeVlq(n: number): string { // Set the sign bit as the least significant bit. n = n >= 0 ? 2 * n : 1 - 2 * n; // Encode into a base64 run. let result = ''; while (true) { // Extract the lowest 5 bits and remove them from the number. const digit = n & 0x1f; n >>= 5; // Is there anything more left to encode? if (n === 0) { // We are done encoding, finish the run. result += base64Digits[digit]; break; } else { // There is still more encode, so add the digit and the continuation bit. result += base64Digits[0x20 + digit]; } } return result; } export function encodeVlqList(list: number[]) { return list.map(encodeVlq).join(''); } // Encode array mappings of the form "compiledLine:compiledColumn => srcFile:srcLine:srcColumn@name" // as a source map. export function encodeSourceMap(textMap: string[], sourceRoot?: string): SDK.SourceMap.SourceMapV3Object { let mappings = ''; const sources: string[] = []; const names: string[] = []; let sourcesContent: Array<null|string>|undefined; const state = { line: -1, column: 0, srcFile: 0, srcLine: 0, srcColumn: 0, srcName: 0, }; for (const mapping of textMap) { let match = mapping.match(/^(\d+):(\d+)(?:\s*=>\s*([^:]+):(\d+):(\d+)(?:@(\S+))?)?$/); if (!match) { match = mapping.match(/^([^:]+):\s*(.+)$/); if (!match) { throw new Error(`Cannot parse mapping "${mapping}"`); } (sourcesContent = sourcesContent ?? [])[getOrAddString(sources, match[1])] = match[2]; continue; } const lastState = Object.assign({}, state); state.line = Number(match[1]); state.column = Number(match[2]); const hasSource = match[3] !== undefined; const hasName = hasSource && (match[6] !== undefined); if (hasSource) { state.srcFile = getOrAddString(sources, match[3]); state.srcLine = Number(match[4]); state.srcColumn = Number(match[5]); if (hasName) { state.srcName = getOrAddString(names, match[6]); } } if (state.line < lastState.line) { throw new Error('Line numbers must be increasing'); } const isNewLine = state.line !== lastState.line; if (isNewLine) { // Fixup for the first line mapping. if (lastState.line === -1) { lastState.line = 0; } // Insert semicolons for all the new lines. mappings += ';'.repeat(state.line - lastState.line); // Reset the compiled code column counter. lastState.column = 0; } else { mappings += ','; } // Encode the mapping and add it to the list of mappings. const toEncode = [state.column - lastState.column]; if (hasSource) { toEncode.push( state.srcFile - lastState.srcFile, state.srcLine - lastState.srcLine, state.srcColumn - lastState.srcColumn); if (hasName) { toEncode.push(state.srcName - lastState.srcName); } } mappings += encodeVlqList(toEncode); } const sourceMapV3: SDK.SourceMap.SourceMapV3 = {version: 3, mappings, sources, names}; if (sourceRoot !== undefined) { sourceMapV3.sourceRoot = sourceRoot; } if (sourcesContent !== undefined) { for (let i = 0; i < sources.length; ++i) { if (typeof sourcesContent[i] !== 'string') { sourcesContent[i] = null; } } sourceMapV3.sourcesContent = sourcesContent; } return sourceMapV3; function getOrAddString(array: string[], s: string) { const index = array.indexOf(s); if (index >= 0) { return index; } array.push(s); return array.length - 1; } } export class OriginalScopeBuilder { #encodedScope = ''; #lastLine = 0; #lastKind = 0; readonly #names: string[]; /** The 'names' field of the SourceMap. The builder will modify it. */ constructor(names: string[]) { this.#names = names; } start( line: number, column: number, options?: {name?: string, kind?: string, isStackFrame?: boolean, variables?: string[]}): this { if (this.#encodedScope !== '') { this.#encodedScope += ','; } const lineDiff = line - this.#lastLine; this.#lastLine = line; let flags = 0; const nameIdxAndKindIdx: number[] = []; if (options?.name) { flags |= SDK.SourceMapScopes.EncodedOriginalScopeFlag.HAS_NAME; nameIdxAndKindIdx.push(this.#nameIdx(options.name)); } if (options?.kind) { flags |= SDK.SourceMapScopes.EncodedOriginalScopeFlag.HAS_KIND; nameIdxAndKindIdx.push(this.#encodeKind(options?.kind)); } if (options?.isStackFrame) { flags |= SDK.SourceMapScopes.EncodedOriginalScopeFlag.IS_STACK_FRAME; } this.#encodedScope += encodeVlqList([lineDiff, column, flags, ...nameIdxAndKindIdx]); if (options?.variables) { this.#encodedScope += encodeVlqList(options.variables.map(variable => this.#nameIdx(variable))); } return this; } end(line: number, column: number): this { if (this.#encodedScope !== '') { this.#encodedScope += ','; } const lineDiff = line - this.#lastLine; this.#lastLine = line; this.#encodedScope += encodeVlqList([lineDiff, column]); return this; } build(): string { const result = this.#encodedScope; this.#lastLine = 0; this.#encodedScope = ''; return result; } #encodeKind(kind: string): number { const kindIdx = this.#nameIdx(kind); const encodedIdx = kindIdx - this.#lastKind; this.#lastKind = kindIdx; return encodedIdx; } #nameIdx(name: string): number { let idx = this.#names.indexOf(name); if (idx < 0) { idx = this.#names.length; this.#names.push(name); } return idx; } } export class GeneratedRangeBuilder { #encodedRange = ''; #state = { line: 0, column: 0, defSourceIdx: 0, defScopeIdx: 0, callsiteSourceIdx: 0, callsiteLine: 0, callsiteColumn: 0, }; readonly #names: string[]; /** The 'names' field of the SourceMap. The builder will modify it. */ constructor(names: string[]) { this.#names = names; } start(line: number, column: number, options?: { isStackFrame?: boolean, isHidden?: boolean, definition?: {sourceIdx: number, scopeIdx: number}, callsite?: {sourceIdx: number, line: number, column: number}, bindings?: Array<string|undefined|Array<{line: number, column: number, name: string|undefined}>>, }): this { this.#emitLineSeparator(line); this.#emitItemSepratorIfRequired(); const emittedColumn = column - (this.#state.line === line ? this.#state.column : 0); this.#encodedRange += encodeVlq(emittedColumn); this.#state.line = line; this.#state.column = column; let flags = 0; if (options?.definition) { flags |= SDK.SourceMapScopes.EncodedGeneratedRangeFlag.HAS_DEFINITION; } if (options?.callsite) { flags |= SDK.SourceMapScopes.EncodedGeneratedRangeFlag.HAS_CALLSITE; } if (options?.isStackFrame) { flags |= SDK.SourceMapScopes.EncodedGeneratedRangeFlag.IS_STACK_FRAME; } if (options?.isHidden) { flags |= SDK.SourceMapScopes.EncodedGeneratedRangeFlag.IS_HIDDEN; } this.#encodedRange += encodeVlq(flags); if (options?.definition) { const {sourceIdx, scopeIdx} = options.definition; this.#encodedRange += encodeVlq(sourceIdx - this.#state.defSourceIdx); const emittedScopeIdx = scopeIdx - (this.#state.defSourceIdx === sourceIdx ? this.#state.defScopeIdx : 0); this.#encodedRange += encodeVlq(emittedScopeIdx); this.#state.defSourceIdx = sourceIdx; this.#state.defScopeIdx = scopeIdx; } if (options?.callsite) { const {sourceIdx, line, column} = options.callsite; this.#encodedRange += encodeVlq(sourceIdx - this.#state.callsiteSourceIdx); const emittedLine = line - (this.#state.callsiteSourceIdx === sourceIdx ? this.#state.callsiteLine : 0); this.#encodedRange += encodeVlq(emittedLine); const emittedColumn = column - (this.#state.callsiteLine === line ? this.#state.callsiteColumn : 0); this.#encodedRange += encodeVlq(emittedColumn); this.#state.callsiteSourceIdx = sourceIdx; this.#state.callsiteLine = line; this.#state.callsiteColumn = column; } for (const bindings of options?.bindings ?? []) { if (bindings === undefined || typeof bindings === 'string') { this.#encodedRange += encodeVlq(this.#nameIdx(bindings)); continue; } this.#encodedRange += encodeVlq(-bindings.length); this.#encodedRange += encodeVlq(this.#nameIdx(bindings[0].name)); if (bindings[0].line !== line || bindings[0].column !== column) { throw new Error('First binding line/column must match the range start line/column'); } for (let i = 1; i < bindings.length; ++i) { const {line, column, name} = bindings[i]; const emittedLine = line - bindings[i - 1].line; const emittedColumn = column - (line === bindings[i - 1].line ? bindings[i - 1].column : 0); this.#encodedRange += encodeVlq(emittedLine); this.#encodedRange += encodeVlq(emittedColumn); this.#encodedRange += encodeVlq(this.#nameIdx(name)); } } return this; } end(line: number, column: number): this { this.#emitLineSeparator(line); this.#emitItemSepratorIfRequired(); const emittedColumn = column - (this.#state.line === line ? this.#state.column : 0); this.#encodedRange += encodeVlq(emittedColumn); this.#state.line = line; this.#state.column = column; return this; } #emitLineSeparator(line: number): void { for (let i = this.#state.line; i < line; ++i) { this.#encodedRange += ';'; } } #emitItemSepratorIfRequired(): void { if (this.#encodedRange !== '' && this.#encodedRange[this.#encodedRange.length - 1] !== ';') { this.#encodedRange += ','; } } #nameIdx(name?: string): number { if (name === undefined) { return -1; } let idx = this.#names.indexOf(name); if (idx < 0) { idx = this.#names.length; this.#names.push(name); } return idx; } build(): string { const result = this.#encodedRange; this.#state = { line: 0, column: 0, defSourceIdx: 0, defScopeIdx: 0, callsiteSourceIdx: 0, callsiteLine: 0, callsiteColumn: 0, }; this.#encodedRange = ''; return result; } }