mirror of
https://github.com/electron/electron.git
synced 2026-04-10 03:01:51 -04:00
Adds a GN typescript_check template that runs tsgo --noEmit over a
tsconfig and writes a stamp file on success, wired into BUILD.gn as
electron_lib_typecheck. electron_js2c depends on it so a broken type
in lib/ fails 'e build'.
tsgo (@typescript/native-preview, the Go-based TypeScript compiler
preview) runs the full lib/ typecheck in ~400ms, about 6x faster than
tsc 6.0.2 (~2.3s). ts-loader previously typechecked implicitly inside
webpack and was removed in the esbuild migration, so this restores
typecheck coverage that was briefly absent on the bundle build path.
Because tsgo has no 'ignoreDiagnostics' option like ts-loader's, the
previously-silenced TS6059 and TS1111 errors needed to be fixed
properly:
- tsconfig.electron.json drops 'rootDir: "lib"'. It was only
meaningful for emit, and the TS 6 implicit rootDir inference plus
the @node/* path alias was pulling Node's internal .js files
(specifically internal/url.js with its #searchParams brand check)
into the program.
- tsconfig.json drops the @node/* path alias entirely. Every call
site that used 'as typeof import("@node/lib/...")' is replaced
with narrow structural types declared once in an ambient
NodeInternalModules interface in typings/internal-ambient.d.ts.
__non_webpack_require__ becomes an overloaded function that picks
the right return type from a string-literal id argument, so call
sites no longer need 'as' casts.
- tsconfig.default_app.json: moduleResolution 'node' -> 'bundler'
(TS 6 deprecates 'node').
- typings/internal-ambient.d.ts: 'declare module NodeJS { }' ->
'declare namespace NodeJS { }' (TS 6 rejects the module keyword
for non-external declarations).
spec/ts-smoke/runner.js now invokes tsgo's bin instead of resolving
the 'typescript' package. The 'tsc' npm script is repointed from
'tsc' to 'tsgo' so the existing CI step
'node script/yarn.js tsc -p tsconfig.script.json' continues to run.
A small script/typecheck.js wrapper runs tsgo and writes the stamp
file for GN; the typescript_check template invokes it via a new
'tsc-check' npm script.
305 lines
7.9 KiB
TypeScript
305 lines
7.9 KiB
TypeScript
import minimist = require('minimist');
|
|
import streamChain = require('stream-chain');
|
|
import streamJson = require('stream-json');
|
|
import { ignore as streamJsonIgnore } from 'stream-json/filters/Ignore';
|
|
import { streamArray as streamJsonStreamArray } from 'stream-json/streamers/StreamArray';
|
|
|
|
import * as childProcess from 'node:child_process';
|
|
import * as fs from 'node:fs';
|
|
import * as os from 'node:os';
|
|
import * as path from 'node:path';
|
|
|
|
import { chunkFilenames, findMatchingFiles } from './lib/utils';
|
|
|
|
const SOURCE_ROOT = path.normalize(path.dirname(__dirname));
|
|
const LLVM_BIN = path.resolve(
|
|
SOURCE_ROOT,
|
|
'..',
|
|
'third_party',
|
|
'llvm-build',
|
|
'Release+Asserts',
|
|
'bin'
|
|
);
|
|
const PLATFORM = os.platform();
|
|
|
|
type SpawnAsyncResult = {
|
|
stdout: string;
|
|
stderr: string;
|
|
status: number | null;
|
|
};
|
|
|
|
class ErrorWithExitCode extends Error {
|
|
exitCode: number;
|
|
|
|
constructor (message: string, exitCode: number) {
|
|
super(message);
|
|
this.exitCode = exitCode;
|
|
}
|
|
}
|
|
|
|
async function spawnAsync (
|
|
command: string,
|
|
args: string[],
|
|
options?: childProcess.SpawnOptionsWithoutStdio | undefined
|
|
): Promise<SpawnAsyncResult> {
|
|
return new Promise((resolve, reject) => {
|
|
try {
|
|
const stdio = { stdout: '', stderr: '' };
|
|
const spawned = childProcess.spawn(command, args, options || {});
|
|
|
|
spawned.stdout.on('data', (data) => {
|
|
stdio.stdout += data;
|
|
});
|
|
|
|
spawned.stderr.on('data', (data) => {
|
|
stdio.stderr += data;
|
|
});
|
|
|
|
spawned.on('exit', (code) => resolve({ ...stdio, status: code }));
|
|
spawned.on('error', (err) => reject(err));
|
|
} catch (err) {
|
|
reject(err);
|
|
}
|
|
});
|
|
}
|
|
|
|
function getDepotToolsEnv (): NodeJS.ProcessEnv {
|
|
let depotToolsEnv;
|
|
|
|
const findDepotToolsOnPath = () => {
|
|
const result = childProcess.spawnSync(
|
|
PLATFORM === 'win32' ? 'where' : 'which',
|
|
['gclient']
|
|
);
|
|
|
|
if (result.status === 0) {
|
|
return process.env;
|
|
}
|
|
};
|
|
|
|
const checkForBuildTools = () => {
|
|
const result = childProcess.spawnSync(
|
|
'electron-build-tools',
|
|
['show', 'env', '--json'],
|
|
{ shell: true }
|
|
);
|
|
|
|
if (result.status === 0) {
|
|
return {
|
|
...process.env,
|
|
...JSON.parse(result.stdout.toString().trim())
|
|
};
|
|
}
|
|
};
|
|
|
|
try {
|
|
depotToolsEnv = findDepotToolsOnPath();
|
|
if (!depotToolsEnv) depotToolsEnv = checkForBuildTools();
|
|
} catch {}
|
|
|
|
if (!depotToolsEnv) {
|
|
throw new Error("Couldn't find depot_tools, ensure it's on your PATH");
|
|
}
|
|
|
|
if (!('CHROMIUM_BUILDTOOLS_PATH' in depotToolsEnv)) {
|
|
throw new Error(
|
|
'CHROMIUM_BUILDTOOLS_PATH environment variable must be set'
|
|
);
|
|
}
|
|
|
|
return depotToolsEnv;
|
|
}
|
|
|
|
async function runClangTidy (
|
|
outDir: string,
|
|
filenames: string[],
|
|
checks: string = '',
|
|
jobs: number = 1,
|
|
fix: boolean = false
|
|
): Promise<boolean> {
|
|
const cmd = path.resolve(LLVM_BIN, 'clang-tidy');
|
|
const args = [`-p=${outDir}`, "-header-filter=''"];
|
|
|
|
if (!process.env.CI) args.push('--use-color');
|
|
if (fix) args.push('--fix');
|
|
if (checks) args.push(`--checks=${checks}`);
|
|
|
|
// Remove any files that aren't in the compilation database to prevent
|
|
// errors from cluttering up the output. Since the compilation DB is hundreds
|
|
// of megabytes, this is done with streaming to not hold it all in memory.
|
|
const filterCompilationDatabase = (): Promise<string[]> => {
|
|
const compiledFilenames: string[] = [];
|
|
|
|
return new Promise((resolve) => {
|
|
const pipeline = streamChain.chain([
|
|
fs.createReadStream(path.resolve(outDir, 'compile_commands.json')),
|
|
streamJson.parser(),
|
|
streamJsonIgnore({ filter: /\bcommand\b/i }),
|
|
streamJsonStreamArray(),
|
|
({ value: { file, directory } }) => {
|
|
const filename = path.resolve(directory, file);
|
|
return filenames.includes(filename) ? filename : null;
|
|
}
|
|
]);
|
|
|
|
pipeline.on('data', (data) => compiledFilenames.push(data));
|
|
pipeline.on('end', () => resolve(compiledFilenames));
|
|
});
|
|
};
|
|
|
|
// clang-tidy can figure out the file from a short relative filename, so
|
|
// to get the most bang for the buck on the command line, let's trim the
|
|
// filenames to the minimum so that we can fit more per invocation
|
|
filenames = (await filterCompilationDatabase()).map((filename) =>
|
|
path.relative(SOURCE_ROOT, filename)
|
|
);
|
|
|
|
if (filenames.length === 0) {
|
|
throw new Error('No filenames to run');
|
|
}
|
|
|
|
const commandLength =
|
|
cmd.length + args.reduce((length, arg) => length + arg.length, 0);
|
|
|
|
const results: boolean[] = [];
|
|
const asyncWorkers = [];
|
|
const chunkedFilenames: string[][] = [];
|
|
|
|
const filesPerWorker = Math.ceil(filenames.length / jobs);
|
|
|
|
for (let i = 0; i < jobs; i++) {
|
|
chunkedFilenames.push(
|
|
...chunkFilenames(filenames.splice(0, filesPerWorker), commandLength)
|
|
);
|
|
}
|
|
|
|
const worker = async () => {
|
|
let filenames = chunkedFilenames.shift();
|
|
|
|
while (filenames?.length) {
|
|
results.push(
|
|
await spawnAsync(cmd, [...args, ...filenames], {}).then((result) => {
|
|
console.log(result.stdout);
|
|
|
|
if (result.status !== 0) {
|
|
console.error(result.stderr);
|
|
}
|
|
|
|
// On a clean run there's nothing on stdout. A run with warnings-only
|
|
// will have a status code of zero, but there's output on stdout
|
|
return result.status === 0 && result.stdout.length === 0;
|
|
})
|
|
);
|
|
|
|
filenames = chunkedFilenames.shift();
|
|
}
|
|
};
|
|
|
|
for (let i = 0; i < jobs; i++) {
|
|
asyncWorkers.push(worker());
|
|
}
|
|
|
|
try {
|
|
await Promise.all(asyncWorkers);
|
|
return results.every((x) => x);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function parseCommandLine () {
|
|
const showUsage = (arg?: string) : boolean => {
|
|
if (!arg || arg.startsWith('-')) {
|
|
console.log(
|
|
'Usage: script/run-clang-tidy.ts [-h|--help] [--jobs|-j] ' +
|
|
'[--fix] [--checks] --out-dir OUTDIR [file1 file2]'
|
|
);
|
|
process.exit(0);
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
const opts = minimist(process.argv.slice(2), {
|
|
boolean: ['fix', 'help'],
|
|
string: ['checks', 'out-dir'],
|
|
default: { jobs: 1 },
|
|
alias: { help: 'h', jobs: 'j' },
|
|
stopEarly: true,
|
|
unknown: showUsage
|
|
});
|
|
|
|
if (opts.help) showUsage();
|
|
|
|
if (!opts['out-dir']) {
|
|
console.log('--out-dir is a required argument');
|
|
process.exit(0);
|
|
}
|
|
|
|
return opts;
|
|
}
|
|
|
|
async function main (): Promise<boolean> {
|
|
const opts = parseCommandLine();
|
|
const outDir = path.resolve(opts['out-dir']);
|
|
|
|
if (!fs.existsSync(outDir)) {
|
|
throw new Error("Output directory doesn't exist");
|
|
} else {
|
|
// Make sure the compile_commands.json file is up-to-date
|
|
const env = getDepotToolsEnv();
|
|
|
|
const result = childProcess.spawnSync(
|
|
'gn',
|
|
['gen', '.', '--export-compile-commands'],
|
|
{ cwd: outDir, env, shell: true }
|
|
);
|
|
|
|
if (result.status !== 0) {
|
|
if (result.error) {
|
|
console.error(result.error.message);
|
|
} else {
|
|
console.error(result.stderr.toString());
|
|
}
|
|
|
|
throw new ErrorWithExitCode(
|
|
'Failed to automatically generate compile_commands.json for ' +
|
|
'output directory',
|
|
2
|
|
);
|
|
}
|
|
}
|
|
|
|
const filenames = [];
|
|
|
|
if (opts._.length > 0) {
|
|
if (opts._.some((filename) => filename.endsWith('.h'))) {
|
|
throw new ErrorWithExitCode(
|
|
'Filenames must be for translation units, not headers', 3
|
|
);
|
|
}
|
|
|
|
filenames.push(...opts._.map((filename) => path.resolve(filename)));
|
|
} else {
|
|
filenames.push(
|
|
...(await findMatchingFiles(
|
|
path.resolve(SOURCE_ROOT, 'shell'),
|
|
(filename: string) => /.*\.(?:cc|mm)$/.test(filename)
|
|
))
|
|
);
|
|
}
|
|
|
|
return runClangTidy(outDir, filenames, opts.checks, opts.jobs, opts.fix);
|
|
}
|
|
|
|
if (require.main === module) {
|
|
main()
|
|
.then((success) => {
|
|
process.exit(success ? 0 : 1);
|
|
})
|
|
.catch((err: ErrorWithExitCode) => {
|
|
console.error(`ERROR: ${err.message}`);
|
|
process.exit(err.exitCode || 1);
|
|
});
|
|
}
|