diff --git a/.eslintignore b/.eslintignore index 1d3d4b8a6b80..a5e66a653443 100644 --- a/.eslintignore +++ b/.eslintignore @@ -10,7 +10,6 @@ coverage/** !.eslintrc.js -lighthouse-core/lib/cdt/generated/SourceMap.js lighthouse-core/scripts/legacy-javascript/variants third-party/** diff --git a/build/build-cdt-lib.js b/build/build-cdt-lib.js index 165e462ec694..fd260c3aaabc 100644 --- a/build/build-cdt-lib.js +++ b/build/build-cdt-lib.js @@ -4,7 +4,6 @@ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ - /* eslint-disable no-console */ import fs from 'fs'; @@ -13,97 +12,184 @@ import ts from 'typescript'; import {LH_ROOT} from '../root.js'; +/** + * @typedef Modification + * @property {string} input + * @property {string} output + * @property {string} template + * @property {Record} rawCodeToReplace Complicated expressions are hard detect with the TS lib, so instead use this to work with the raw code. + * @property {string[]} classesToRemove + * @property {string[]} methodsToRemove + * @property {string[]} variablesToRemove + */ + const outDir = `${LH_ROOT}/lighthouse-core/lib/cdt/generated`; -const files = { - 'node_modules/chrome-devtools-frontend/front_end/core/sdk/SourceMap.ts': 'SourceMap.js', -}; -console.log('making modifications ...'); +/** @type {Modification[]} */ +const modifications = [ + { + input: 'node_modules/chrome-devtools-frontend/front_end/core/sdk/SourceMap.ts', + output: `${outDir}/SourceMap.js`, + template: [ + 'const Common = require(\'../Common.js\');', + 'const Platform = require(\'../Platform.js\');', + '%sourceFilePrinted%', + 'module.exports = TextSourceMap;', + ].join('\n'), + rawCodeToReplace: { + /* Original: + + let url = Common.ParsedURL.ParsedURL.completeURL(this.#baseURL, href) || href; + const source = sourceMap.sourcesContent && sourceMap.sourcesContent[i]; + if (url === this.#compiledURLInternal && source) { + url = Common.ParsedURL.ParsedURL.concatenate(url, '? [sm]'); + } + if (this.#sourceInfos.has(url)) { + continue; + } + this.#sourceInfos.set(url, new TextSourceMap.SourceInfo(source || null, null)); + sourcesList.push(url); + ---- + If a source file is the same as the compiled url and there is a sourcesContent, + then `entry.sourceURL` (what is returned from .mappings) will have `? [sm]` appended. + This is useful in DevTools - to show that a sources panel tab not a real network resource - + but for us it is not wanted. The sizing function uses `entry.sourceURL` to index the byte + counts, and is further used in the details to specify a file within a source map. + */ + [`url = Common.ParsedURL.ParsedURL.concatenate(url, '? [sm]');`]: '', + // Use normal console.warn so we don't need to import CDT's logger. + 'Common.Console.Console.instance().warn': 'console.warn', + // Similar to the reason for removing `url += Common.UIString('? [sm]')`. + // The entries in `.mappings` should not have their url property modified. + 'Common.ParsedURL.ParsedURL.completeURL(this.#baseURL, href)': `''`, + // Replace i18n function with a very simple templating function. + 'i18n.i18n.getLocalizedString.bind(undefined, str_)': ( + /** @param {string} template @param {object} vars */ + function(template, vars) { + let result = template; + for (const [key, value] of Object.entries(vars)) { + result = result.replace(new RegExp('{' + key + '}'), value); + } + return result; + }).toString(), + // Add some types. + // eslint-disable-next-line max-len + 'mappings(): SourceMapEntry[] {': '/** @return {Array<{lineNumber: number, columnNumber: number, sourceURL?: string, sourceLineNumber: number, sourceColumnNumber: number, name?: string, lastColumnNumber?: number}>} */\nmappings(): SourceMapEntry[] {', + }, + classesToRemove: [], + methodsToRemove: [ + // Not needed. + 'load', + // Not needed. + 'sourceContentProvider', + ], + variablesToRemove: [ + 'Common', + 'CompilerSourceMappingContentProvider_js_1', + 'i18n', + 'i18nString', + 'PageResourceLoader_js_1', + 'Platform', + 'str_', + 'TextUtils', + 'UIStrings', + ], + }, + { + input: 'node_modules/chrome-devtools-frontend/front_end/core/common/ParsedURL.ts', + output: `${outDir}/ParsedURL.js`, + template: '%sourceFilePrinted%', + rawCodeToReplace: {}, + classesToRemove: [], + methodsToRemove: [ + // TODO: look into removing the `Common.ParsedURL.ParsedURL.completeURL` replacement above, + // which will also mean including all/most of these methods. + 'completeURL', + 'dataURLDisplayName', + 'domain', + 'encodedFromParentPathAndName', + 'encodedPathToRawPathString', + 'extractExtension', + 'extractName', + 'extractOrigin', + 'extractPath', + 'fromString', + 'isAboutBlank', + 'isBlobURL', + 'isDataURL', + 'isHttpOrHttps', + 'isValidUrlString', + 'join', + 'lastPathComponentWithFragment', + 'preEncodeSpecialCharactersInPath', + 'prepend', + 'rawPathToEncodedPathString', + 'rawPathToUrlString', + 'relativePathToUrlString', + 'removeWasmFunctionInfoFromURL', + 'securityOrigin', + 'slice', + 'sliceUrlToEncodedPathString', + 'split', + 'splitLineAndColumn', + 'substr', + 'substring', + 'toLowerCase', + 'trim', + 'urlFromParentUrlAndName', + 'urlRegex', + 'urlRegexInstance', + 'urlToRawPathString', + 'urlWithoutHash', + 'urlWithoutScheme', + ], + variablesToRemove: [ + 'Platform', + ], + }, +]; + +/** + * @param {string} code + * @param {string[]} codeFragments + */ +function assertPresence(code, codeFragments) { + for (const codeFragment of codeFragments) { + if (!code.includes(codeFragment)) { + throw new Error(`did not find expected code fragment: ${codeFragment}`); + } + } +} + +/** + * @param {Modification} modification + */ +function doModification(modification) { + const {rawCodeToReplace, classesToRemove, methodsToRemove, variablesToRemove} = modification; -for (const [inFilename, outFilename] of Object.entries(files)) { - const code = fs.readFileSync(inFilename, 'utf-8'); - const codeTranspiledToCommonJS = ts.transpileModule(code, { - compilerOptions: {module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2019}, - }).outputText; + const code = fs.readFileSync(modification.input, 'utf-8'); + assertPresence(code, Object.keys(rawCodeToReplace)); + + // First pass - do raw string replacements. + let modifiedCode = code; + for (const [code, replacement] of Object.entries(rawCodeToReplace)) { + modifiedCode = modifiedCode.replace(code, replacement); + } + + const codeTranspiledToCommonJS = ts.transpileModule(modifiedCode, { + compilerOptions: {module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022}, + }).outputText.replace(`"use strict";`, ''); const sourceFile = ts.createSourceFile('', codeTranspiledToCommonJS, - ts.ScriptTarget.ES2019, true, ts.ScriptKind.JS); + ts.ScriptTarget.ES2022, true, ts.ScriptKind.JS); const simplePrinter = ts.createPrinter({newLine: ts.NewLineKind.LineFeed}); - /** @type {string[]} */ - const classesToRemove = [ - // Currently empty. - ]; - const methodsToRemove = [ - // Not needed. - 'load', - // Not needed. - 'sourceContentProvider', - ]; - const variablesToRemove = [ - 'Common', - 'CompilerSourceMappingContentProvider_js_1', - 'i18n', - 'i18nString', - 'PageResourceLoader_js_1', - 'Platform', - 'str_', - 'TextUtils', - 'UIStrings', - ]; - const expressionsToRemove = [ - /* Original: - - let url = Common.ParsedURL.ParsedURL.completeURL(this.baseURL, href) || href; - const source = sourceMap.sourcesContent && sourceMap.sourcesContent[i]; - if (url === this.compiledURLInternal && source) { - url += '? [sm]'; - } - this.sourceInfos.set(url, new TextSourceMap.SourceInfo(source || null, null)); - sourcesList.push(url); - ---- - If a source file is the same as the compiled url and there is a sourcesContent, - then `entry.sourceURL` (what is returned from .mappings) will have `? [sm]` appended. - This is useful in DevTools - to show that a sources panel tab not a real network resource - - but for us it is not wanted. The sizing function uses `entry.sourceURL` to index the byte - counts, and is further used in the details to specify a file within a source map. - */ - `url += '? [sm]'`, - ]; - // Complicated expressions are hard detect with the TS lib, so instead work with the raw code. - const rawCodeToReplace = { - 'Common.Console.Console.instance().warn': 'console.warn', - // Similar to the reason for removing `url += Common.UIString('? [sm]')`. - // The entries in `.mappings` should not have their url property modified. - 'Common.ParsedURL.ParsedURL.completeURL(this.baseURL, href)': `''`, - // Replace i18n function with a very simple templating function. - 'i18n.i18n.getLocalizedString.bind(undefined, str_)': ( - /** @param {string} template @param {object} vars */ - function(template, vars) { - let result = template; - for (const [key, value] of Object.entries(vars)) { - result = result.replace(new RegExp('{' + key + '}'), value); - } - return result; - }).toString(), - // Add some types. - // eslint-disable-next-line max-len - 'mappings() {': '/** @return {Array<{lineNumber: number, columnNumber: number, sourceURL?: string, sourceLineNumber: number, sourceColumnNumber: number, name?: string, lastColumnNumber?: number}>} */\nmappings() {', - }; - - // Verify that all the above code is present. - const codeFragments = [ + // Second pass - use tsc to remove all references to certain variables. + assertPresence(codeTranspiledToCommonJS, [ ...classesToRemove, ...methodsToRemove, ...variablesToRemove, - ...expressionsToRemove, - ...Object.keys(rawCodeToReplace), - ]; - for (const codeFragment of codeFragments) { - if (!codeTranspiledToCommonJS.includes(codeFragment)) { - throw new Error(`did not find expected code fragment: ${codeFragment}`); - } - } + ]); const printer = ts.createPrinter({newLine: ts.NewLineKind.LineFeed}, { substituteNode(hint, node) { @@ -125,9 +211,6 @@ for (const [inFilename, outFilename] of Object.entries(files)) { if (classesToRemove.some(className => asString.includes(className))) { removeNode = true; } - if (expressionsToRemove.some(className => asString.includes(className))) { - removeNode = true; - } } if (ts.isVariableDeclarationList(node) && node.declarations.length === 1) { @@ -150,16 +233,15 @@ for (const [inFilename, outFilename] of Object.entries(files)) { sourceFilePrinted += printer.printNode(ts.EmitHint.Unspecified, node, sourceFile) + '\n'; }); - for (const [code, replacement] of Object.entries(rawCodeToReplace)) { - sourceFilePrinted = sourceFilePrinted.replace(code, replacement); - } - const modifiedFile = [ '// @ts-nocheck\n', '// generated by yarn build-cdt-lib\n', - 'const Platform = require(\'../Platform.js\');\n', - sourceFilePrinted, - 'module.exports = TextSourceMap;', + '/* eslint-disable */\n', + '"use strict";\n', + modification.template.replace('%sourceFilePrinted%', () => sourceFilePrinted), ].join(''); - fs.writeFileSync(`${outDir}/${outFilename}`, modifiedFile); + + fs.writeFileSync(modification.output, modifiedFile); } + +modifications.forEach(doModification); diff --git a/lighthouse-core/audits/byte-efficiency/unused-javascript.js b/lighthouse-core/audits/byte-efficiency/unused-javascript.js index a416f9300e02..669f70792ba5 100644 --- a/lighthouse-core/audits/byte-efficiency/unused-javascript.js +++ b/lighthouse-core/audits/byte-efficiency/unused-javascript.js @@ -133,7 +133,7 @@ class UnusedJavaScript extends ByteEfficiencyAudit { }) .filter(d => d.unused >= bundleSourceUnusedThreshold); - const commonSourcePrefix = commonPrefix([...bundle.map.sourceInfos.keys()]); + const commonSourcePrefix = commonPrefix(bundle.map.sourceURLs()); item.subItems = { type: 'subitems', items: topUnusedSourceSizes.map(({source, unused, total}) => { diff --git a/lighthouse-core/lib/cdt/Common.js b/lighthouse-core/lib/cdt/Common.js new file mode 100644 index 000000000000..d3bca66464fb --- /dev/null +++ b/lighthouse-core/lib/cdt/Common.js @@ -0,0 +1,13 @@ +// @ts-nocheck +/** + * @license Copyright 2022 The Lighthouse Authors. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ +'use strict'; + +const ParsedURL = require('./generated/ParsedURL.js'); + +module.exports = { + ParsedURL, +}; diff --git a/lighthouse-core/lib/cdt/Platform.js b/lighthouse-core/lib/cdt/Platform.js index 5e9911524e1d..c3cd9bb21d0f 100644 --- a/lighthouse-core/lib/cdt/Platform.js +++ b/lighthouse-core/lib/cdt/Platform.js @@ -52,4 +52,7 @@ module.exports = { lowerBound, upperBound, }, + DevToolsPath: { + EmptyUrlString: '', + }, }; diff --git a/lighthouse-core/lib/cdt/generated/ParsedURL.js b/lighthouse-core/lib/cdt/generated/ParsedURL.js new file mode 100644 index 000000000000..c966cd82e697 --- /dev/null +++ b/lighthouse-core/lib/cdt/generated/ParsedURL.js @@ -0,0 +1,178 @@ +// @ts-nocheck +// generated by yarn build-cdt-lib +/* eslint-disable */ +"use strict"; +/* + * 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. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ParsedURL = exports.normalizePath = void 0; +; +/** + * http://tools.ietf.org/html/rfc3986#section-5.2.4 + */ +function normalizePath(path) { + if (path.indexOf('..') === -1 && path.indexOf('.') === -1) { + return path; + } + // Remove leading slash (will be added back below) so we + // can handle all (including empty) segments consistently. + const segments = (path[0] === '/' ? path.substring(1) : path).split('/'); + const normalizedSegments = []; + for (const segment of segments) { + if (segment === '.') { + continue; + } + else if (segment === '..') { + normalizedSegments.pop(); + } + else { + normalizedSegments.push(segment); + } + } + let normalizedPath = normalizedSegments.join('/'); + if (path[0] === '/' && normalizedPath) { + normalizedPath = '/' + normalizedPath; + } + if (normalizedPath[normalizedPath.length - 1] !== '/' && + ((path[path.length - 1] === '/') || (segments[segments.length - 1] === '.') || + (segments[segments.length - 1] === '..'))) { + normalizedPath = normalizedPath + '/'; + } + return normalizedPath; +} +exports.normalizePath = normalizePath; +class ParsedURL { + isValid; + url; + scheme; + user; + host; + port; + path; + queryParams; + fragment; + folderPathComponents; + lastPathComponent; + blobInnerScheme; + #displayNameInternal; + #dataURLDisplayNameInternal; + constructor(url) { + this.isValid = false; + this.url = url; + this.scheme = ''; + this.user = ''; + this.host = ''; + this.port = ''; + this.path = ''; + this.queryParams = ''; + this.fragment = ''; + this.folderPathComponents = ''; + this.lastPathComponent = ''; + const isBlobUrl = this.url.startsWith('blob:'); + const urlToMatch = isBlobUrl ? url.substring(5) : url; + const match = urlToMatch.match(ParsedURL.urlRegex()); + if (match) { + this.isValid = true; + if (isBlobUrl) { + this.blobInnerScheme = match[2].toLowerCase(); + this.scheme = 'blob'; + } + else { + this.scheme = match[2].toLowerCase(); + } + this.user = match[3] ?? ''; + this.host = match[4] ?? ''; + this.port = match[5] ?? ''; + this.path = match[6] ?? '/'; + this.queryParams = match[7] ?? ''; + this.fragment = match[8] ?? ''; + } + else { + if (this.url.startsWith('data:')) { + this.scheme = 'data'; + return; + } + if (this.url.startsWith('blob:')) { + this.scheme = 'blob'; + return; + } + if (this.url === 'about:blank') { + this.scheme = 'about'; + return; + } + this.path = this.url; + } + const lastSlashIndex = this.path.lastIndexOf('/'); + if (lastSlashIndex !== -1) { + this.folderPathComponents = this.path.substring(0, lastSlashIndex); + this.lastPathComponent = this.path.substring(lastSlashIndex + 1); + } + else { + this.lastPathComponent = this.path; + } + } + static concatenate(devToolsPath, ...appendage) { + return devToolsPath.concat(...appendage); + } + static beginsWithWindowsDriveLetter(url) { + return /^[A-Za-z]:/.test(url); + } + static beginsWithScheme(url) { + return /^[A-Za-z][A-Za-z0-9+.-]*:/.test(url); + } + static isRelativeURL(url) { + return !this.beginsWithScheme(url) || this.beginsWithWindowsDriveLetter(url); + } + get displayName() { + if (this.#displayNameInternal) { + return this.#displayNameInternal; + } + if (this.isDataURL()) { + return this.dataURLDisplayName(); + } + if (this.isBlobURL()) { + return this.url; + } + if (this.isAboutBlank()) { + return this.url; + } + this.#displayNameInternal = this.lastPathComponent; + if (!this.#displayNameInternal) { + this.#displayNameInternal = (this.host || '') + '/'; + } + if (this.#displayNameInternal === '/') { + this.#displayNameInternal = this.url; + } + return this.#displayNameInternal; + } + static urlRegexInstance = null; +} +exports.ParsedURL = ParsedURL; + diff --git a/lighthouse-core/lib/cdt/generated/SourceMap.js b/lighthouse-core/lib/cdt/generated/SourceMap.js index deac103dc887..e2a91b6cbd32 100644 --- a/lighthouse-core/lib/cdt/generated/SourceMap.js +++ b/lighthouse-core/lib/cdt/generated/SourceMap.js @@ -1,7 +1,14 @@ // @ts-nocheck // generated by yarn build-cdt-lib -const Platform = require('../Platform.js'); +/* eslint-disable */ "use strict"; +const Common = require('../Common.js'); +const Platform = require('../Platform.js'); +// 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.TextSourceMap = exports.SourceMapEntry = exports.Offset = exports.Section = exports.SourceMapV3 = void 0; /* * Copyright (C) 2012 Google Inc. All rights reserved. * @@ -15,7 +22,7 @@ const Platform = require('../Platform.js'); * 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 + * * 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. * @@ -31,8 +38,6 @@ const Platform = require('../Platform.js'); * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.TextSourceMap = exports.SourceMapEntry = exports.Offset = exports.Section = exports.SourceMapV3 = void 0; ; ; ; @@ -43,21 +48,40 @@ exports.TextSourceMap = exports.SourceMapEntry = exports.Offset = exports.Sectio ; ; class SourceMapV3 { + version; + file; + sources; + sections; + mappings; + sourceRoot; + names; + sourcesContent; constructor() { } } exports.SourceMapV3 = SourceMapV3; class Section { + map; + offset; + url; constructor() { } } exports.Section = Section; class Offset { + line; + column; constructor() { } } exports.Offset = Offset; class SourceMapEntry { + lineNumber; + columnNumber; + sourceURL; + sourceLineNumber; + sourceColumnNumber; + name; constructor(lineNumber, columnNumber, sourceURL, sourceLineNumber, sourceColumnNumber, name) { this.lineNumber = lineNumber; this.columnNumber = columnNumber; @@ -81,20 +105,27 @@ for (let i = 0; i < base64Digits.length; ++i) { } const sourceMapToSourceList = new WeakMap(); class TextSourceMap { + #initiator; + #json; + #compiledURLInternal; + #sourceMappingURL; + #baseURL; + #mappingsInternal; + #sourceInfos; /** * Implements Source Map V3 model. See https://github.com/google/closure-compiler/wiki/Source-Maps * for format description. */ constructor(compiledURL, sourceMappingURL, payload, initiator) { - this.initiator = initiator; - this.json = payload; - this.compiledURLInternal = compiledURL; - this.sourceMappingURL = sourceMappingURL; - this.baseURL = sourceMappingURL.startsWith('data:') ? compiledURL : sourceMappingURL; - this.mappingsInternal = null; - this.sourceInfos = new Map(); - if (this.json.sections) { - const sectionWithURL = Boolean(this.json.sections.find(section => Boolean(section.url))); + this.#initiator = initiator; + this.#json = payload; + this.#compiledURLInternal = compiledURL; + this.#sourceMappingURL = sourceMappingURL; + this.#baseURL = (sourceMappingURL.startsWith('data:') ? compiledURL : sourceMappingURL); + this.#mappingsInternal = null; + this.#sourceInfos = new Map(); + if (this.#json.sections) { + const sectionWithURL = Boolean(this.#json.sections.find(section => Boolean(section.url))); if (sectionWithURL) { console.warn(`SourceMap "${sourceMappingURL}" contains unsupported "URL" field in one of its sections.`); } @@ -102,16 +133,16 @@ class TextSourceMap { this.eachSection(this.parseSources.bind(this)); } compiledURL() { - return this.compiledURLInternal; + return this.#compiledURLInternal; } url() { - return this.sourceMappingURL; + return this.#sourceMappingURL; } sourceURLs() { - return [...this.sourceInfos.keys()]; + return [...this.#sourceInfos.keys()]; } embeddedContentByURL(sourceURL) { - const entry = this.sourceInfos.get(sourceURL); + const entry = this.#sourceInfos.get(sourceURL); if (!entry) { return null; } @@ -123,52 +154,92 @@ class TextSourceMap { return index ? mappings[index - 1] : null; } sourceLineMapping(sourceURL, lineNumber, columnNumber) { - const mappings = this.reversedMappings(sourceURL); - const first = Platform.ArrayUtilities.lowerBound(mappings, lineNumber, lineComparator); - const last = Platform.ArrayUtilities.upperBound(mappings, lineNumber, lineComparator); - if (first >= mappings.length || mappings[first].sourceLineNumber !== lineNumber) { + 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 = mappings.slice(first, last); + const columnMappings = reverseMappings.slice(first, last); if (!columnMappings.length) { return null; } - const index = Platform.ArrayUtilities.lowerBound(columnMappings, columnNumber, (columnNumber, mapping) => columnNumber - mapping.sourceColumnNumber); - return index >= columnMappings.length ? columnMappings[columnMappings.length - 1] : columnMappings[index]; - function lineComparator(lineNumber, mapping) { - return lineNumber - mapping.sourceLineNumber; + 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; } } - findReverseEntries(sourceURL, lineNumber, columnNumber) { - const mappings = this.reversedMappings(sourceURL); - const endIndex = Platform.ArrayUtilities.upperBound(mappings, undefined, (unused, entry) => lineNumber - entry.sourceLineNumber || columnNumber - entry.sourceColumnNumber); + 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[startIndex - 1].sourceLineNumber === mappings[endIndex - 1].sourceLineNumber && - mappings[startIndex - 1].sourceColumnNumber === mappings[endIndex - 1].sourceColumnNumber) { + 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 mappings.slice(startIndex, endIndex); + 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() { - if (this.mappingsInternal === null) { - this.mappingsInternal = []; + mappings() { + if (this.#mappingsInternal === null) { + this.#mappingsInternal = []; this.eachSection(this.parseMap.bind(this)); - this.json = null; + this.#json = null; } - return /** @type {!Array} */ this.mappingsInternal; + return this.#mappingsInternal; } reversedMappings(sourceURL) { - const info = this.sourceInfos.get(sourceURL); + const info = this.#sourceInfos.get(sourceURL); if (!info) { return []; } const mappings = this.mappings(); if (info.reverseMappings === null) { - info.reverseMappings = mappings.filter(mapping => mapping.sourceURL === sourceURL).sort(sourceMappingComparator); + const indexes = Array(mappings.length).fill(0).map((_, i) => i); + info.reverseMappings = indexes.filter(i => mappings[i].sourceURL === sourceURL).sort(sourceMappingComparator); } return info.reverseMappings; - function sourceMappingComparator(a, b) { + function sourceMappingComparator(indexA, indexB) { + const a = mappings[indexA]; + const b = mappings[indexB]; if (a.sourceLineNumber !== b.sourceLineNumber) { return a.sourceLineNumber - b.sourceLineNumber; } @@ -182,30 +253,43 @@ mappings() { } } eachSection(callback) { - if (!this.json) { + if (!this.#json) { return; } - if (!this.json.sections) { - callback(this.json, 0, 0); + if (!this.#json.sections) { + callback(this.#json, 0, 0); return; } - for (const section of this.json.sections) { + for (const section of this.#json.sections) { callback(section.map, section.offset.line, section.offset.column); } } parseSources(sourceMap) { const sourcesList = []; - let sourceRoot = sourceMap.sourceRoot || ''; - if (sourceRoot && !sourceRoot.endsWith('/')) { - sourceRoot += '/'; - } + const sourceRoot = sourceMap.sourceRoot || Platform.DevToolsPath.EmptyUrlString; for (let i = 0; i < sourceMap.sources.length; ++i) { - const href = sourceRoot + sourceMap.sources[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. Appending 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 = Common.ParsedURL.ParsedURL.concatenate(sourceRoot, '/', href); + } + else { + href = Common.ParsedURL.ParsedURL.concatenate(sourceRoot, href); + } + } let url = '' || href; const source = sourceMap.sourcesContent && sourceMap.sourcesContent[i]; - if (url === this.compiledURLInternal && source) { + if (url === this.#compiledURLInternal && source) { } - this.sourceInfos.set(url, new TextSourceMap.SourceInfo(source || null, null)); + if (this.#sourceInfos.has(url)) { + continue; + } + this.#sourceInfos.set(url, new TextSourceMap.SourceInfo(source || null, null)); sourcesList.push(url); } sourceMapToSourceList.set(sourceMap, sourcesList); @@ -282,27 +366,31 @@ mappings() { return negative ? -result : result; } reverseMapTextRange(url, textRange) { - function comparator(position, mapping) { - if (position.lineNumber !== mapping.sourceLineNumber) { - return position.lineNumber - mapping.sourceLineNumber; + function comparator(position, mappingIndex) { + if (position.lineNumber !== mappings[mappingIndex].sourceLineNumber) { + return position.lineNumber - mappings[mappingIndex].sourceLineNumber; } - return position.columnNumber - mapping.sourceColumnNumber; + return position.columnNumber - mappings[mappingIndex].sourceColumnNumber; + } + const reverseMappings = this.reversedMappings(url); + const mappings = this.mappings(); + if (!reverseMappings.length) { + return null; } - const mappings = this.reversedMappings(url); - if (!mappings.length) { + const startIndex = Platform.ArrayUtilities.lowerBound(reverseMappings, { lineNumber: textRange.startLine, columnNumber: textRange.startColumn }, comparator); + const endIndex = Platform.ArrayUtilities.upperBound(reverseMappings, { lineNumber: textRange.endLine, columnNumber: textRange.endColumn }, comparator); + if (endIndex >= reverseMappings.length) { return null; } - const startIndex = Platform.ArrayUtilities.lowerBound(mappings, { lineNumber: textRange.startLine, columnNumber: textRange.startColumn }, comparator); - const endIndex = Platform.ArrayUtilities.upperBound(mappings, { lineNumber: textRange.endLine, columnNumber: textRange.endColumn }, comparator); - const startMapping = mappings[startIndex]; - const endMapping = mappings[endIndex]; + const startMapping = mappings[reverseMappings[startIndex]]; + const endMapping = mappings[reverseMappings[endIndex]]; return new TextUtils.TextRange.TextRange(startMapping.lineNumber, startMapping.columnNumber, endMapping.lineNumber, endMapping.columnNumber); } mapsOrigin() { const mappings = this.mappings(); if (mappings.length > 0) { const firstEntry = mappings[0]; - return (firstEntry === null || firstEntry === void 0 ? void 0 : firstEntry.lineNumber) === 0 || firstEntry.columnNumber === 0; + return firstEntry?.lineNumber === 0 || firstEntry.columnNumber === 0; } return false; } @@ -319,6 +407,8 @@ exports.TextSourceMap = TextSourceMap; // eslint-disable-next-line @typescript-eslint/naming-convention TextSourceMap._VLQ_CONTINUATION_MASK = 1 << 5; class StringCharIterator { + string; + position; constructor(string) { this.string = string; this.position = 0; @@ -335,6 +425,8 @@ exports.TextSourceMap = TextSourceMap; } TextSourceMap.StringCharIterator = StringCharIterator; class SourceInfo { + content; + reverseMappings; constructor(content, reverseMappings) { this.content = content; this.reverseMappings = reverseMappings; @@ -343,4 +435,5 @@ exports.TextSourceMap = TextSourceMap; TextSourceMap.SourceInfo = SourceInfo; })(TextSourceMap = exports.TextSourceMap || (exports.TextSourceMap = {})); + module.exports = TextSourceMap; \ No newline at end of file diff --git a/package.json b/package.json index c6dd3cbb5024..d01fda7a3af0 100644 --- a/package.json +++ b/package.json @@ -132,7 +132,7 @@ "archiver": "^3.0.0", "c8": "^7.4.0", "chalk": "^2.4.1", - "chrome-devtools-frontend": "1.0.922924", + "chrome-devtools-frontend": "1.0.1012379", "concurrently": "^6.4.0", "conventional-changelog-cli": "^2.1.1", "cpy": "^8.1.2", diff --git a/yarn.lock b/yarn.lock index acc12093c97f..1810beb29d3d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2557,10 +2557,10 @@ chownr@^1.1.1: resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== -chrome-devtools-frontend@1.0.922924: - version "1.0.922924" - resolved "https://registry.yarnpkg.com/chrome-devtools-frontend/-/chrome-devtools-frontend-1.0.922924.tgz#ef05c7f1af8045aa9b2b52e10525daf9d58e5721" - integrity sha512-Ha9vpN3uo7rQFFcZLOzR5WO6JrSpCqLYBo0cPMuXQpsn6bsnIfwtrQZVHPY7kDt2+c8O4m9jVt24XQrhKGngmw== +chrome-devtools-frontend@1.0.1012379: + version "1.0.1012379" + resolved "https://registry.yarnpkg.com/chrome-devtools-frontend/-/chrome-devtools-frontend-1.0.1012379.tgz#808a795f324c08d8d6322e93d910090e6d47c21d" + integrity sha512-U/cG/MzSrXoGhilVauVb9EjX1Iwlnlj7LpsyED+xWG90i/sBPdHtAI55qTGC6QP1/lCfFJYqoZR99yTa6QpUXQ== chrome-launcher@^0.15.1: version "0.15.1"