UNPKG

lighthouse

Version:

Automated auditing, performance metrics, and best practices for the web.

572 lines (568 loc) • 24.8 kB
// @ts-nocheck // generated by yarn build-cdt-lib /* eslint-disable */ "use strict"; const Common = require('../Common.js'); const Platform = require('../Platform.js'); const BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; const BASE64_CODES = new Uint8Array(123); for (let index = 0; index < BASE64_CHARS.length; ++index) { BASE64_CODES[BASE64_CHARS.charCodeAt(index)] = index; } // 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. Object.defineProperty(exports, "__esModule", { value: true }); exports.TokenIterator = exports.SourceMap = exports.SourceMapEntry = void 0; exports.parseSourceMap = parseSourceMap; /* * Copyright (C) 2012 Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * Neither the #name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ ; ; ; ; ; ; /** * Parses the {@link content} as JSON, ignoring BOM markers in the beginning, and * also handling the CORB bypass prefix correctly. * * @param content the string representation of a sourcemap. * @returns the {@link SourceMapV3} representation of the {@link content}. */ function parseSourceMap(content) { if (content.startsWith(')]}')) { content = content.substring(content.indexOf('\n')); } if (content.charCodeAt(0) === 0xFEFF) { // Strip BOM at the beginning before parsing the JSON. content = content.slice(1); } return JSON.parse(content); } class SourceMapEntry { lineNumber; columnNumber; sourceIndex; sourceURL; sourceLineNumber; sourceColumnNumber; name; constructor(lineNumber, columnNumber, sourceIndex, sourceURL, sourceLineNumber, sourceColumnNumber, name) { this.lineNumber = lineNumber; this.columnNumber = columnNumber; this.sourceIndex = sourceIndex; this.sourceURL = sourceURL; this.sourceLineNumber = sourceLineNumber; this.sourceColumnNumber = sourceColumnNumber; this.name = name; } static compare(entry1, entry2) { if (entry1.lineNumber !== entry2.lineNumber) { return entry1.lineNumber - entry2.lineNumber; } return entry1.columnNumber - entry2.columnNumber; } } exports.SourceMapEntry = SourceMapEntry; class SourceMap { #json; #compiledURLInternal; #sourceMappingURL; #baseURL; #mappingsInternal; #sourceInfos = []; #sourceInfoByURL = new Map(); #scopesInfo = null; /** * @param {string} compiledURL * @param {string} sourceMappingURL * @param {object} payload * Implements Source Map V3 model. See https://github.com/google/closure-compiler/wiki/Source-Maps * for format description. */ constructor(compiledURL, sourceMappingURL, payload) { this.#json = payload; this.#compiledURLInternal = compiledURL; this.#sourceMappingURL = sourceMappingURL; this.#baseURL = (Common.ParsedURL.schemeIs(sourceMappingURL, 'data:')) ? compiledURL : sourceMappingURL; this.#mappingsInternal = null; if ('sections' in this.#json) { if (this.#json.sections.find(section => 'url' in section)) { console.warn(`SourceMap "${sourceMappingURL}" contains unsupported "URL" field in one of its sections.`); } } this.eachSection(this.parseSources.bind(this)); } #sourceIndex(sourceURL) { return this.#sourceInfos.findIndex(info => info.sourceURL === sourceURL); } compiledURL() { return this.#compiledURLInternal; } url() { return this.#sourceMappingURL; } sourceURLs() { return [...this.#sourceInfoByURL.keys()]; } embeddedContentByURL(sourceURL) { const entry = this.#sourceInfoByURL.get(sourceURL); if (!entry) { return null; } return entry.content; } hasScopeInfo() { this.#ensureMappingsProcessed(); return this.#scopesInfo !== null; } findEntry(lineNumber, columnNumber, inlineFrameIndex) { this.#ensureMappingsProcessed(); if (inlineFrameIndex && this.#scopesInfo !== null) { // For inlineFrameIndex != 0 we use the callsite info for the corresponding inlining site. // Note that the callsite for "inlineFrameIndex" is actually in the previous frame. const { inlinedFunctions } = this.#scopesInfo.findInlinedFunctions(lineNumber, columnNumber); const { callsite } = inlinedFunctions[inlineFrameIndex - 1]; if (!callsite) { console.error('Malformed source map. Expected to have a callsite info for index', inlineFrameIndex); return null; } return { lineNumber, columnNumber, sourceIndex: callsite.sourceIndex, sourceURL: this.sourceURLs()[callsite.sourceIndex], sourceLineNumber: callsite.line, sourceColumnNumber: callsite.column, name: undefined, }; } const mappings = this.mappings(); const index = Platform.ArrayUtilities.upperBound(mappings, undefined, (unused, entry) => lineNumber - entry.lineNumber || columnNumber - entry.columnNumber); return index ? mappings[index - 1] : null; } findEntryRanges(lineNumber, columnNumber) { const mappings = this.mappings(); const endIndex = Platform.ArrayUtilities.upperBound(mappings, undefined, (unused, entry) => lineNumber - entry.lineNumber || columnNumber - entry.columnNumber); if (!endIndex) { // If the line and column are preceding all the entries, then there is nothing to map. return null; } // startIndex must be within mappings range because endIndex must be not falsy const startIndex = endIndex - 1; const sourceURL = mappings[startIndex].sourceURL; if (!sourceURL) { return null; } // Let us compute the range that contains the source position in the compiled code. const endLine = endIndex < mappings.length ? mappings[endIndex].lineNumber : 2 ** 31 - 1; const endColumn = endIndex < mappings.length ? mappings[endIndex].columnNumber : 2 ** 31 - 1; const range = new TextUtils.TextRange.TextRange(mappings[startIndex].lineNumber, mappings[startIndex].columnNumber, endLine, endColumn); // Now try to find the corresponding token in the original code. const reverseMappings = this.reversedMappings(sourceURL); const startSourceLine = mappings[startIndex].sourceLineNumber; const startSourceColumn = mappings[startIndex].sourceColumnNumber; const endReverseIndex = Platform.ArrayUtilities.upperBound(reverseMappings, undefined, (unused, i) => startSourceLine - mappings[i].sourceLineNumber || startSourceColumn - mappings[i].sourceColumnNumber); if (!endReverseIndex) { return null; } const endSourceLine = endReverseIndex < reverseMappings.length ? mappings[reverseMappings[endReverseIndex]].sourceLineNumber : 2 ** 31 - 1; const endSourceColumn = endReverseIndex < reverseMappings.length ? mappings[reverseMappings[endReverseIndex]].sourceColumnNumber : 2 ** 31 - 1; const sourceRange = new TextUtils.TextRange.TextRange(startSourceLine, startSourceColumn, endSourceLine, endSourceColumn); return { range, sourceRange, sourceURL }; } sourceLineMapping(sourceURL, lineNumber, columnNumber) { const mappings = this.mappings(); const reverseMappings = this.reversedMappings(sourceURL); const first = Platform.ArrayUtilities.lowerBound(reverseMappings, lineNumber, lineComparator); const last = Platform.ArrayUtilities.upperBound(reverseMappings, lineNumber, lineComparator); if (first >= reverseMappings.length || mappings[reverseMappings[first]].sourceLineNumber !== lineNumber) { return null; } const columnMappings = reverseMappings.slice(first, last); if (!columnMappings.length) { return null; } const index = Platform.ArrayUtilities.lowerBound(columnMappings, columnNumber, (columnNumber, i) => columnNumber - mappings[i].sourceColumnNumber); return index >= columnMappings.length ? mappings[columnMappings[columnMappings.length - 1]] : mappings[columnMappings[index]]; function lineComparator(lineNumber, i) { return lineNumber - mappings[i].sourceLineNumber; } } findReverseIndices(sourceURL, lineNumber, columnNumber) { const mappings = this.mappings(); const reverseMappings = this.reversedMappings(sourceURL); const endIndex = Platform.ArrayUtilities.upperBound(reverseMappings, undefined, (unused, i) => lineNumber - mappings[i].sourceLineNumber || columnNumber - mappings[i].sourceColumnNumber); let startIndex = endIndex; while (startIndex > 0 && mappings[reverseMappings[startIndex - 1]].sourceLineNumber === mappings[reverseMappings[endIndex - 1]].sourceLineNumber && mappings[reverseMappings[startIndex - 1]].sourceColumnNumber === mappings[reverseMappings[endIndex - 1]].sourceColumnNumber) { --startIndex; } return reverseMappings.slice(startIndex, endIndex); } findReverseEntries(sourceURL, lineNumber, columnNumber) { const mappings = this.mappings(); return this.findReverseIndices(sourceURL, lineNumber, columnNumber).map(i => mappings[i]); } findReverseRanges(sourceURL, lineNumber, columnNumber) { const mappings = this.mappings(); const indices = this.findReverseIndices(sourceURL, lineNumber, columnNumber); const ranges = []; for (let i = 0; i < indices.length; ++i) { const startIndex = indices[i]; // Merge adjacent ranges. let endIndex = startIndex + 1; while (i + 1 < indices.length && endIndex === indices[i + 1]) { ++endIndex; ++i; } // Source maps don't contain end positions for entries, but each entry is assumed to // span until the following entry. This doesn't work however in case of the last // entry, where there's no following entry. We also don't know the number of lines // and columns in the original source code (which might not be available at all), so // for that case we store the maximum signed 32-bit integer, which is definitely going // to be larger than any script we can process and can safely be serialized as part of // the skip list we send to V8 with `Debugger.stepOver` (http://crbug.com/1305956). const startLine = mappings[startIndex].lineNumber; const startColumn = mappings[startIndex].columnNumber; const endLine = endIndex < mappings.length ? mappings[endIndex].lineNumber : 2 ** 31 - 1; const endColumn = endIndex < mappings.length ? mappings[endIndex].columnNumber : 2 ** 31 - 1; ranges.push(new TextUtils.TextRange.TextRange(startLine, startColumn, endLine, endColumn)); } return ranges; } /** @return {Array<{lineNumber: number, columnNumber: number, sourceURL?: string, sourceLineNumber: number, sourceColumnNumber: number, name?: string, lastColumnNumber?: number}>} */ mappings() { this.#ensureMappingsProcessed(); return this.#mappingsInternal ?? []; } reversedMappings(sourceURL) { this.#ensureMappingsProcessed(); return this.#sourceInfoByURL.get(sourceURL)?.reverseMappings ?? []; } #ensureMappingsProcessed() { if (this.#mappingsInternal === null) { this.#mappingsInternal = []; try { this.eachSection(this.parseMap.bind(this)); } catch (e) { console.error('Failed to parse source map', e); this.#mappingsInternal = []; } // As per spec, mappings are not necessarily sorted. this.mappings().sort(SourceMapEntry.compare); this.#computeReverseMappings(this.#mappingsInternal); this.#json = null; } } #computeReverseMappings(mappings) { const reverseMappingsPerUrl = new Map(); for (let i = 0; i < mappings.length; i++) { const entryUrl = mappings[i].sourceURL; if (!entryUrl) { continue; } let reverseMap = reverseMappingsPerUrl.get(entryUrl); if (!reverseMap) { reverseMap = []; reverseMappingsPerUrl.set(entryUrl, reverseMap); } reverseMap.push(i); } for (const [url, reverseMap] of reverseMappingsPerUrl.entries()) { const info = this.#sourceInfoByURL.get(url); if (!info) { continue; } reverseMap.sort(sourceMappingComparator); info.reverseMappings = reverseMap; } function sourceMappingComparator(indexA, indexB) { const a = mappings[indexA]; const b = mappings[indexB]; return a.sourceLineNumber - b.sourceLineNumber || a.sourceColumnNumber - b.sourceColumnNumber || a.lineNumber - b.lineNumber || a.columnNumber - b.columnNumber; } } eachSection(callback) { if (!this.#json) { return; } if ('sections' in this.#json) { let sourcesIndex = 0; for (const section of this.#json.sections) { if ('map' in section) { callback(section.map, sourcesIndex, section.offset.line, section.offset.column); sourcesIndex += section.map.sources.length; } } } else { callback(this.#json, 0, 0, 0); } } parseSources(sourceMap) { const sourceRoot = sourceMap.sourceRoot ?? ''; const ignoreList = new Set(sourceMap.ignoreList ?? sourceMap.x_google_ignoreList); for (let i = 0; i < sourceMap.sources.length; ++i) { let href = sourceMap.sources[i]; // The source map v3 proposal says to prepend the sourceRoot to the source URL // and if the resulting URL is not absolute, then resolve the source URL against // the source map URL. Prepending the sourceRoot (if one exists) is not likely to // be meaningful or useful if the source URL is already absolute though. In this // case, use the source URL as is without prepending the sourceRoot. if (Common.ParsedURL.ParsedURL.isRelativeURL(href)) { if (sourceRoot && !sourceRoot.endsWith('/') && href && !href.startsWith('/')) { href = sourceRoot.concat('/', href); } else { href = sourceRoot.concat(href); } } const url = '' || href; const source = sourceMap.sourcesContent && sourceMap.sourcesContent[i]; const sourceInfo = { sourceURL: url, content: source ?? null, ignoreListHint: ignoreList.has(i), reverseMappings: null, }; this.#sourceInfos.push(sourceInfo); if (!this.#sourceInfoByURL.has(url)) { this.#sourceInfoByURL.set(url, sourceInfo); } } } parseMap(map, baseSourceIndex, baseLineNumber, baseColumnNumber) { let sourceIndex = baseSourceIndex; let lineNumber = baseLineNumber; let columnNumber = baseColumnNumber; let sourceLineNumber = 0; let sourceColumnNumber = 0; let nameIndex = 0; const names = map.names ?? []; const tokenIter = new TokenIterator(map.mappings); let sourceURL = this.#sourceInfos[sourceIndex].sourceURL; while (true) { if (tokenIter.peek() === ',') { tokenIter.next(); } else { while (tokenIter.peek() === ';') { lineNumber += 1; columnNumber = 0; tokenIter.next(); } if (!tokenIter.hasNext()) { break; } } columnNumber += tokenIter.nextVLQ(); if (!tokenIter.hasNext() || this.isSeparator(tokenIter.peek())) { this.mappings().push(new SourceMapEntry(lineNumber, columnNumber)); continue; } const sourceIndexDelta = tokenIter.nextVLQ(); if (sourceIndexDelta) { sourceIndex += sourceIndexDelta; sourceURL = this.#sourceInfos[sourceIndex].sourceURL; } sourceLineNumber += tokenIter.nextVLQ(); sourceColumnNumber += tokenIter.nextVLQ(); if (!tokenIter.hasNext() || this.isSeparator(tokenIter.peek())) { this.mappings().push(new SourceMapEntry(lineNumber, columnNumber, sourceIndex, sourceURL, sourceLineNumber, sourceColumnNumber)); continue; } nameIndex += tokenIter.nextVLQ(); this.mappings().push(new SourceMapEntry(lineNumber, columnNumber, sourceIndex, sourceURL, sourceLineNumber, sourceColumnNumber, names[nameIndex])); } if (false) { if (!this.#scopesInfo) { this.#scopesInfo = new SourceMapScopesInfo_js_1.SourceMapScopesInfo(this, [], []); } if (map.originalScopes && map.generatedRanges) { const { originalScopes, generatedRanges } = (0, SourceMapScopes_js_1.decodeScopes)(map, { line: baseLineNumber, column: baseColumnNumber }); this.#scopesInfo.addOriginalScopes(originalScopes); this.#scopesInfo.addGeneratedRanges(generatedRanges); } else if (map.x_com_bloomberg_sourcesFunctionMappings) { const originalScopes = this.parseBloombergScopes(map); this.#scopesInfo.addOriginalScopes(originalScopes); } else { // Keep the OriginalScope[] tree array consistent with sources. this.#scopesInfo.addOriginalScopes(new Array(map.sources.length)); } } } isSeparator(char) { return char === ',' || char === ';'; } mapsOrigin() { const mappings = this.mappings(); if (mappings.length > 0) { const firstEntry = mappings[0]; return firstEntry?.lineNumber === 0 || firstEntry.columnNumber === 0; } return false; } hasIgnoreListHint(sourceURL) { return this.#sourceInfoByURL.get(sourceURL)?.ignoreListHint ?? false; } /** * Returns a list of ranges in the generated script for original sources that * match a predicate. Each range is a [begin, end) pair, meaning that code at * the beginning location, up to but not including the end location, matches * the predicate. */ findRanges(predicate, options) { const mappings = this.mappings(); const ranges = []; if (!mappings.length) { return []; } let current = null; // If the first mapping isn't at the beginning of the original source, it's // up to the caller to decide if it should be considered matching the // predicate or not. By default, it's not. if ((mappings[0].lineNumber !== 0 || mappings[0].columnNumber !== 0) && options?.isStartMatching) { current = TextUtils.TextRange.TextRange.createUnboundedFromLocation(0, 0); ranges.push(current); } for (const { sourceURL, lineNumber, columnNumber } of mappings) { const ignoreListHint = sourceURL && predicate(sourceURL); if (!current && ignoreListHint) { current = TextUtils.TextRange.TextRange.createUnboundedFromLocation(lineNumber, columnNumber); ranges.push(current); continue; } if (current && !ignoreListHint) { current.endLine = lineNumber; current.endColumn = columnNumber; current = null; } } return ranges; } expandCallFrame(frame) { this.#ensureMappingsProcessed(); if (this.#scopesInfo === null) { return [frame]; } return this.#scopesInfo.expandCallFrame(frame); } resolveScopeChain(frame) { this.#ensureMappingsProcessed(); if (this.#scopesInfo === null) { return null; } return this.#scopesInfo.resolveMappedScopeChain(frame); } findOriginalFunctionName(position) { this.#ensureMappingsProcessed(); return this.#scopesInfo?.findOriginalFunctionName(position) ?? null; } } exports.SourceMap = SourceMap; const VLQ_BASE_SHIFT = 5; const VLQ_BASE_MASK = (1 << 5) - 1; const VLQ_CONTINUATION_MASK = 1 << 5; class TokenIterator { #string; #position; constructor(string) { this.#string = string; this.#position = 0; } next() { return this.#string.charAt(this.#position++); } /** Returns the unicode value of the next character and advances the iterator */ nextCharCode() { return this.#string.charCodeAt(this.#position++); } peek() { return this.#string.charAt(this.#position); } hasNext() { return this.#position < this.#string.length; } nextVLQ() { // Read unsigned value. let result = 0; let shift = 0; let digit = VLQ_CONTINUATION_MASK; while (digit & VLQ_CONTINUATION_MASK) { if (!this.hasNext()) { throw new Error('Unexpected end of input while decodling VLQ number!'); } const charCode = this.nextCharCode(); digit = BASE64_CODES[charCode]; if (charCode !== 65 /* 'A' */ && digit === 0) { throw new Error(`Unexpected char '${String.fromCharCode(charCode)}' encountered while decoding`); } result += (digit & VLQ_BASE_MASK) << shift; shift += VLQ_BASE_SHIFT; } // Fix the sign. const negative = result & 1; result >>= 1; return negative ? -result : result; } /** * @returns the next VLQ number without iterating further. Or returns null if * the iterator is at the end or it's not a valid number. */ peekVLQ() { const pos = this.#position; try { return this.nextVLQ(); } catch { return null; } finally { this.#position = pos; // Reset the iterator. } } } exports.TokenIterator = TokenIterator; module.exports = SourceMap; SourceMap.parseSourceMap = parseSourceMap;