223 строки
6.7 KiB
JavaScript
223 строки
6.7 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright 2020 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
/* eslint-disable no-console */
|
|
|
|
import fs from 'fs';
|
|
|
|
import ts from 'typescript';
|
|
|
|
import {LH_ROOT} from '../shared/root.js';
|
|
|
|
/**
|
|
* @typedef Modification
|
|
* @property {string} input
|
|
* @property {string} output
|
|
* @property {string} template
|
|
* @property {Record<string, string>} 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}/core/lib/cdt/generated`;
|
|
|
|
/** @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 = SourceMap;',
|
|
'SourceMap.parseSourceMap = parseSourceMap;',
|
|
].join('\n'),
|
|
rawCodeToReplace: {
|
|
// Use normal console.warn so we don't need to import CDT's logger.
|
|
'Common.Console.Console.instance().warn': 'console.warn',
|
|
// The entries in `.mappings` should not have their url property modified.
|
|
// 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.
|
|
'Common.ParsedURL.ParsedURL.completeURL(this.#baseURL, href)': `''`,
|
|
// 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.
|
|
'compatibleForURL',
|
|
'load',
|
|
'reverseMapTextRanges',
|
|
],
|
|
variablesToRemove: [
|
|
'Common',
|
|
'Platform',
|
|
'TextUtils',
|
|
],
|
|
},
|
|
{
|
|
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;
|
|
|
|
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.ES2022, true, ts.ScriptKind.JS);
|
|
const simplePrinter = ts.createPrinter({newLine: ts.NewLineKind.LineFeed});
|
|
|
|
// Second pass - use tsc to remove all references to certain variables.
|
|
assertPresence(codeTranspiledToCommonJS, [
|
|
...classesToRemove,
|
|
...methodsToRemove,
|
|
...variablesToRemove,
|
|
]);
|
|
|
|
const printer = ts.createPrinter({newLine: ts.NewLineKind.LineFeed}, {
|
|
substituteNode(hint, node) {
|
|
let removeNode = false;
|
|
|
|
if (ts.isMethodDeclaration(node) &&
|
|
// Make typescript happy.
|
|
!ts.isComputedPropertyName(node.name) &&
|
|
methodsToRemove.includes(node.name.text)) {
|
|
removeNode = true;
|
|
}
|
|
|
|
if (ts.isClassDeclaration(node) && node.name && classesToRemove.includes(node.name.text)) {
|
|
removeNode = true;
|
|
}
|
|
|
|
if (ts.isExpressionStatement(node)) {
|
|
const asString = simplePrinter.printNode(ts.EmitHint.Unspecified, node, sourceFile);
|
|
if (classesToRemove.some(className => asString.includes(className))) {
|
|
removeNode = true;
|
|
}
|
|
}
|
|
|
|
if (ts.isVariableDeclarationList(node) && node.declarations.length === 1) {
|
|
// @ts-expect-error: is read-only, but whatever.
|
|
node.declarations =
|
|
node.declarations.filter(d => !variablesToRemove.includes(d.name.getText()));
|
|
if (node.declarations.length === 0) removeNode = true;
|
|
}
|
|
|
|
if (removeNode) {
|
|
return ts.factory.createNotEmittedStatement(node);
|
|
}
|
|
|
|
return node;
|
|
},
|
|
});
|
|
|
|
let sourceFilePrinted = '';
|
|
sourceFile.forEachChild(node => {
|
|
sourceFilePrinted += printer.printNode(ts.EmitHint.Unspecified, node, sourceFile) + '\n';
|
|
});
|
|
|
|
const content = modification.template.replace('%sourceFilePrinted%', () => sourceFilePrinted);
|
|
writeGeneratedFile(modification.output, content);
|
|
}
|
|
|
|
/**
|
|
* @param {string} outputPath
|
|
* @param {string} contents
|
|
*/
|
|
function writeGeneratedFile(outputPath, contents) {
|
|
const modifiedFile = [
|
|
'// @ts-nocheck\n',
|
|
'// generated by yarn build-cdt-lib\n',
|
|
'/* eslint-disable */\n',
|
|
'"use strict";\n',
|
|
contents,
|
|
].join('');
|
|
|
|
fs.writeFileSync(outputPath, modifiedFile);
|
|
}
|
|
|
|
modifications.forEach(doModification);
|