diff --git a/.gitignore b/.gitignore index 0e64e1e1..4c687958 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ third_party .disable-auto-updates /*.yml generated.env.sh +dist +build-tools-spec* \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..79a831cb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "cSpell.words": [ + "gerrit", + "osid", + "REAPI", + "SDKROOT", + "SISO" + ] +} \ No newline at end of file diff --git a/package.json b/package.json index bcf94429..5ca95e36 100644 --- a/package.json +++ b/package.json @@ -4,16 +4,19 @@ "engines": { "node": ">= 18.0.0" }, + "type": "module", "main": "null", "private": true, "scripts": { "lint:markdown": "electron-markdownlint \"**/*.md\"", - "lint:js": "prettier --check \"src/**/*.js\" \"tests/*.js\" \"src/e\"", + "lint:js": "prettier --check \"src/**/*.{js,ts}\" \"tests/*.{js,ts}\"", "lint": "npm run lint:js && npm run lint:markdown", - "prettier:write": "prettier --write \"src/**/*.js\" \"tests/*.js\" \"src/e\"", + "prettier:write": "prettier --write \"src/**/*.{js,ts}\" \"tests/*.{js,ts}\"", "prepare": "husky", + "pretest": "tsc", "test": "nyc --reporter=lcov --reporter=text-summary vitest run --reporter=verbose --exclude tests/bootstrap.spec.mjs", - "test:all": "nyc --reporter=lcov --reporter=text-summary vitest run --reporter=verbose" + "test:all": "nyc --reporter=lcov --reporter=text-summary vitest run --reporter=verbose", + "postinstall": "tsc" }, "repository": "https://github.com/electron/build-tools", "author": "Electron Authors", @@ -22,35 +25,43 @@ "@marshallofsound/chrome-cookies-secure": "^2.1.1", "@octokit/auth-oauth-device": "^3.1.1", "@octokit/rest": "^18.5.2", - "ajv": "^8.11.0", - "ajv-formats": "^2.1.1", - "chalk": "^2.4.1", + "@types/node": "^22.7.1", + "@types/progress": "^2.0.7", + "@types/semver": "^7.7.0", + "@types/tar": "^6.1.13", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "chalk": "^5.4.1", "command-exists": "^1.2.8", - "commander": "^9.0.0", + "commander": "^9.5.0", "debug": "^4.3.1", "extract-zip": "^2.0.1", "inquirer": "^8.2.4", "node-gyp": "^10.0.1", "open": "^6.4.0", - "path-key": "^3.1.0", "progress": "^2.0.3", "readline-sync": "^1.4.10", "semver": "^7.6.0", "tar": "^6.2.1", + "typescript": "^5.8.3", "vscode-uri": "^3.0.7", - "which": "^2.0.2", "yaml": "^2.4.5" }, "devDependencies": { "@electron/lint-roller": "^1.13.0", + "@tsconfig/node22": "^22.0.2", + "@types/command-exists": "^1.2.3", + "@types/inquirer": "^8.0.0", + "@types/readline-sync": "^1.4.8", "husky": "^9.1.6", + "json-schema-to-typescript": "^15.0.4", "lint-staged": "^15.2.10", "nyc": "^17.1.0", "prettier": "^3.3.3", "vitest": "^3.0.6" }, "lint-staged": { - "*.js": [ + "*.{js,ts}": [ "prettier --write" ], "e": [ diff --git a/src/download.js b/src/download.js deleted file mode 100644 index 5fffc4d0..00000000 --- a/src/download.js +++ /dev/null @@ -1,28 +0,0 @@ -const fs = require('fs'); -const stream = require('stream'); -const { pipeline } = require('stream/promises'); - -const { fatal } = require('./utils/logging'); -const { progressStream } = require('./utils/download'); - -const write = fs.createWriteStream(process.argv[3]); - -async function tryDownload(attemptsLeft = 3) { - const response = await fetch(process.argv[2]); - const total = parseInt(response.headers.get('content-length'), 10); - const progress = progressStream(total, '[:bar] :mbRateMB/s :percent :etas'); - - await pipeline( - stream.Readable.fromWeb(response.body), - ...(process.env.CI ? [write] : [progress, write]), - ).catch((err) => { - if (attemptsLeft === 0) { - return fatal(err); - } - - console.log('Download failed, trying', attemptsLeft, 'more times'); - tryDownload(attemptsLeft - 1); - }); -} - -tryDownload(); diff --git a/src/download.ts b/src/download.ts new file mode 100644 index 00000000..054cb74d --- /dev/null +++ b/src/download.ts @@ -0,0 +1,32 @@ +import fs from 'node:fs'; +import stream from 'node:stream'; +import { pipeline } from 'node:stream/promises'; + +import { fatal } from './utils/logging.js'; +import { progressStream } from './utils/download.js'; + +const write = fs.createWriteStream(process.argv[3]); + +async function tryDownload(attemptsLeft = 3) { + const response = await fetch(process.argv[2]); + const total = parseInt(response.headers.get('content-length') || '1', 10); + + let promise: Promise; + if (process.env.CI) { + promise = pipeline(stream.Readable.fromWeb(response.body!), write); + } else { + const progress = progressStream(total, '[:bar] :mbRateMB/s :percent :etas'); + promise = pipeline(stream.Readable.fromWeb(response.body!), progress, write); + } + + await promise.catch((err) => { + if (attemptsLeft === 0) { + return fatal(err); + } + + console.log('Download failed, trying', attemptsLeft, 'more times'); + tryDownload(attemptsLeft - 1); + }); +} + +tryDownload(); diff --git a/src/e-auto-update.js b/src/e-auto-update.ts similarity index 84% rename from src/e-auto-update.js rename to src/e-auto-update.ts index 8d68827c..60bc700d 100644 --- a/src/e-auto-update.js +++ b/src/e-auto-update.ts @@ -1,17 +1,17 @@ #!/usr/bin/env node -const chalk = require('chalk'); -const cp = require('child_process'); -const fs = require('fs'); -const path = require('path'); -const program = require('commander'); -const semver = require('semver'); +import chalk from 'chalk'; +import cp from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import { program } from 'commander'; +import semver from 'semver'; -const { color, fatal } = require('./utils/logging'); +import { color, fatal } from './utils/logging.js'; const BUILD_TOOLS_INSTALLER_MIN_VERSION = '1.1.0'; -const markerFilePath = path.join(__dirname, '..', '.disable-auto-updates'); +const markerFilePath = path.join(import.meta.dirname, '..', '.disable-auto-updates'); program .description('Check for build-tools updates or enable/disable automatic updates') @@ -27,7 +27,7 @@ program } console.log('Automatic updates enabled'); } catch (e) { - fatal(e); + fatal(e as Error); } }); @@ -39,7 +39,7 @@ program fs.closeSync(fs.openSync(markerFilePath, 'w')); console.log('Automatic updates disabled'); } catch (e) { - fatal(e); + fatal(e as Error); } }); @@ -72,7 +72,7 @@ function checkForUpdates() { ); try { - packageJson = JSON.parse(fs.readFileSync(buildToolsInstallerPackage)); + packageJson = JSON.parse(fs.readFileSync(buildToolsInstallerPackage, 'utf-8')); } catch { continue; } @@ -87,8 +87,8 @@ function checkForUpdates() { break; } - const execOpts = { cwd: path.resolve(__dirname, '..') }; - const git = (args) => cp.execSync(`git ${args}`, execOpts).toString('utf8').trim(); + const execOpts = { cwd: path.resolve(import.meta.dirname, '..') }; + const git = (args: string) => cp.execSync(`git ${args}`, execOpts).toString('utf8').trim(); const headCmd = 'rev-parse --verify HEAD'; const headBefore = git(headCmd); @@ -127,7 +127,7 @@ function checkForUpdates() { console.log('build-tools updated to latest version!'); } } catch (e) { - fatal(e); + fatal(e as Error); } } diff --git a/src/e-backport.js b/src/e-backport.ts similarity index 68% rename from src/e-backport.js rename to src/e-backport.ts index f5be5c92..9b98aa51 100644 --- a/src/e-backport.js +++ b/src/e-backport.ts @@ -1,15 +1,15 @@ #!/usr/bin/env node -const { Octokit } = require('@octokit/rest'); -const chalk = require('chalk').default; -const program = require('commander'); -const inquirer = require('inquirer'); -const path = require('path'); +import { Octokit } from '@octokit/rest'; +import chalk from 'chalk'; +import { program } from 'commander'; +import inquirer from 'inquirer'; +import path from 'node:path'; -const evmConfig = require('./evm-config'); -const { spawnSync } = require('./utils/depot-tools'); -const { getGitHubAuthToken } = require('./utils/github-auth'); -const { fatal } = require('./utils/logging'); +import * as evmConfig from './evm-config.js'; +import { depotSpawnSync } from './utils/depot-tools.js'; +import { getGitHubAuthToken } from './utils/github-auth.js'; +import { fatal } from './utils/logging.js'; program .arguments('[pr]') @@ -36,8 +36,8 @@ program } const targetBranches = pr.labels - .filter((label) => label.name.startsWith('needs-manual-bp/')) - .map((label) => label.name.substring(16)); + .filter((label) => label.name?.startsWith('needs-manual-bp/')) + .map((label) => label.name!.substring(16)); if (targetBranches.length === 0) { fatal('The given pull request is not needing any manual backports yet'); return; @@ -56,8 +56,8 @@ program const gitOpts = { cwd: path.resolve(config.root, 'src', 'electron'), stdio: 'pipe', - }; - const result = spawnSync(config, 'git', ['status', '--porcelain'], gitOpts); + } as const; + const result = depotSpawnSync(config, 'git', ['status', '--porcelain'], gitOpts); if (result.status !== 0 || result.stdout.toString().trim().length !== 0) { fatal( "Your current git working directory is not clean, we won't erase your local changes. Clean it up and try again", @@ -65,15 +65,21 @@ program return; } - spawnSync(config, 'git', ['checkout', targetBranch], gitOpts, 'Failed to checkout base branch'); - spawnSync( + depotSpawnSync( + config, + 'git', + ['checkout', targetBranch], + gitOpts, + 'Failed to checkout base branch', + ); + depotSpawnSync( config, 'git', ['pull', 'origin', targetBranch], gitOpts, 'Failed to update base branch', ); - spawnSync( + depotSpawnSync( config, 'git', ['fetch', 'origin', pr.base.ref], @@ -82,8 +88,8 @@ program ); const manualBpBranch = `manual-bp/${user.login}/pr/${prNumber}/branch/${targetBranch}`; - spawnSync(config, 'git', ['branch', '-D', manualBpBranch], gitOpts); - spawnSync( + depotSpawnSync(config, 'git', ['branch', '-D', manualBpBranch], gitOpts); + depotSpawnSync( config, 'git', ['checkout', '-b', manualBpBranch], @@ -91,13 +97,21 @@ program `Failed to checkout new branch "${manualBpBranch}"`, ); - spawnSync(config, 'yarn', ['install'], gitOpts, `Failed to do "yarn install" on new branch`); + depotSpawnSync( + config, + 'yarn', + ['install'], + gitOpts, + `Failed to do "yarn install" on new branch`, + ); - const cherryPickResult = spawnSync(config, 'git', ['cherry-pick', pr.merge_commit_sha], { + const cherryPickResult = depotSpawnSync(config, 'git', ['cherry-pick', pr.merge_commit_sha], { cwd: gitOpts.cwd, }); - const pushCommand = chalk.yellow(!!config.remotes.electron.fork ? 'git push fork' : 'git push'); + const pushCommand = chalk.yellow( + !!config.remotes?.electron.fork ? 'git push fork' : 'git push', + ); const cherryPickCommand = chalk.yellow('git cherry-pick --continue'); const prCommand = chalk.yellow(`e pr --backport ${prNumber}`); diff --git a/src/e-build.js b/src/e-build.ts similarity index 63% rename from src/e-build.js rename to src/e-build.ts index d918f17f..ce8920ad 100644 --- a/src/e-build.js +++ b/src/e-build.ts @@ -1,19 +1,21 @@ #!/usr/bin/env node -const fs = require('fs'); -const os = require('os'); -const path = require('path'); -const program = require('commander'); - -const evmConfig = require('./evm-config'); -const { color, fatal } = require('./utils/logging'); -const depot = require('./utils/depot-tools'); -const { ensureDir } = require('./utils/paths'); -const reclient = require('./utils/reclient'); -const siso = require('./utils/siso'); -const { ensureSDK, ensureSDKAndSymlink } = require('./utils/sdk'); - -function getGNArgs(config) { +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { program } from 'commander'; + +import * as evmConfig from './evm-config.js'; +import { color, fatal } from './utils/logging.js'; +import { depotExecFileSync, depotPath, ensureDepotTools } from './utils/depot-tools.js'; +import { downloadAndPrepareRBECredentialHelper, ensureHelperAuth } from './utils/reclient.js'; +import { ensureBackendStarlark, sisoFlags } from './utils/siso.js'; +import { ensureSDK, ensureSDKAndSymlink } from './utils/sdk.js'; +import { EVMBaseElectronConfiguration } from './evm-config.schema.js'; +import { ensureDir } from './utils/paths.js'; +import { ExecFileSyncOptions } from 'node:child_process'; + +function getGNArgs(config: EVMBaseElectronConfiguration): string { const configArgs = config.gen.args; if (process.platform === 'darwin') { @@ -33,20 +35,20 @@ function getGNArgs(config) { return configArgs.join(os.EOL); } -function runGNGen(config) { - depot.ensure(); +function runGNGen(config: EVMBaseElectronConfiguration): void { + ensureDepotTools(); const gnBasename = os.platform() === 'win32' ? 'gn.bat' : 'gn'; - const gnPath = path.resolve(depot.path, gnBasename); + const gnPath = path.resolve(depotPath, gnBasename); const gnArgs = getGNArgs(config); const argsFile = path.resolve(evmConfig.outDir(config), 'args.gn'); ensureDir(evmConfig.outDir(config)); fs.writeFileSync(argsFile, gnArgs, { encoding: 'utf8' }); const execArgs = ['gen', `out/${config.gen.out}`]; const execOpts = { cwd: path.resolve(config.root, 'src') }; - depot.execFileSync(config, gnPath, execArgs, execOpts); + depotExecFileSync(config, gnPath, execArgs, execOpts); } -function ensureGNGen(config) { +function ensureGNGen(config: EVMBaseElectronConfiguration): void { const buildfile = path.resolve(evmConfig.outDir(config), 'build.ninja'); if (!fs.existsSync(buildfile)) return runGNGen(config); const argsFile = path.resolve(evmConfig.outDir(config), 'args.gn'); @@ -58,9 +60,9 @@ function ensureGNGen(config) { } } -function runNinja(config, target, ninjaArgs) { - if (reclient.usingRemote && config.remoteBuild !== 'none') { - reclient.auth(config); +function runNinja(config: EVMBaseElectronConfiguration, target: string, ninjaArgs: string[]) { + if (config.remoteBuild !== 'none') { + ensureHelperAuth(config); // Autoninja sets this absurdly high, we take it down a notch if ( @@ -68,32 +70,32 @@ function runNinja(config, target, ninjaArgs) { !ninjaArgs.find((arg) => /^-j[0-9]+$/.test(arg.trim())) && config.remoteBuild === 'reclient' ) { - ninjaArgs.push('-j', 200); + ninjaArgs.push('-j', '200'); } if (config.remoteBuild === 'siso') { - ninjaArgs.push(...siso.flags(config)); + ninjaArgs.push(...sisoFlags(config)); } } else { console.info(`${color.info} Building ${target} with remote execution disabled`); } - depot.ensure(config); + ensureDepotTools(); ensureGNGen(config); // Using remoteexec means that we need autoninja so that reproxy is started + stopped // correctly - const ninjaName = config.reclient !== 'none' ? 'autoninja' : 'ninja'; + const ninjaName = config.remoteBuild !== 'none' ? 'autoninja' : 'ninja'; const exec = os.platform() === 'win32' ? `${ninjaName}.bat` : ninjaName; const args = [...ninjaArgs, target]; - const opts = { + const opts: ExecFileSyncOptions = { cwd: evmConfig.outDir(config), }; - if (!reclient.usingRemote && config.reclient !== 'none') { - opts.env = { RBE_remote_disabled: true }; + if (config.remoteBuild !== 'none') { + opts.env = { RBE_remote_disabled: 'true' }; } - depot.execFileSync(config, exec, args, opts); + depotExecFileSync(config, exec, args, opts); } program @@ -107,19 +109,21 @@ program try { const config = evmConfig.current(); - reclient.usingRemote = options.remote; + if (!options.remote) { + // If --no-remote is set, we disable remote execution + config.remoteBuild = 'none'; + } const winToolchainOverride = process.env.ELECTRON_DEPOT_TOOLS_WIN_TOOLCHAIN; if (os.platform() === 'win32' && winToolchainOverride === '0') { - config.reclient = 'none'; - reclient.usingRemote = false; + config.remoteBuild = 'none'; console.warn( `${color.warn} Build without remote execution when defined ${color.config('ELECTRON_DEPOT_TOOLS_WIN_TOOLCHAIN=0')} in environment variables.`, ); } - reclient.downloadAndPrepareRBECredentialHelper(config); - await siso.ensureBackendStarlark(config); + downloadAndPrepareRBECredentialHelper(config); + await ensureBackendStarlark(config); if (process.platform === 'darwin') { ensureSDK(); @@ -133,7 +137,7 @@ program const buildTarget = options.target || evmConfig.getDefaultTarget(); runNinja(config, buildTarget, ninjaArgs); } catch (e) { - fatal(e); + fatal(e as Error); } }) .parse(process.argv); diff --git a/src/e-cherry-pick.js b/src/e-cherry-pick.ts similarity index 90% rename from src/e-cherry-pick.js rename to src/e-cherry-pick.ts index b799ae69..fb6c0ff4 100644 --- a/src/e-cherry-pick.js +++ b/src/e-cherry-pick.ts @@ -1,23 +1,25 @@ #!/usr/bin/env node -const d = require('debug')('build-tools:cherry-pick'); -const program = require('commander'); -const cp = require('child_process'); -const fs = require('fs'); -const path = require('path'); -const os = require('os'); -const { Octokit } = require('@octokit/rest'); - -const { getGerritPatchDetailsFromURL } = require('./utils/gerrit'); -const { getGitHubAuthToken } = require('./utils/github-auth'); -const { fatal, color } = require('./utils/logging'); +import debug from 'debug'; +import { program } from 'commander'; +import cp from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import os from 'node:os'; +import { Octokit } from '@octokit/rest'; + +import { getGerritPatchDetailsFromURL } from './utils/gerrit.js'; +import { getGitHubAuthToken } from './utils/github-auth.js'; +import { fatal, color } from './utils/logging.js'; + +const d = debug('build-tools:cherry-pick'); const ELECTRON_REPO_DATA = { owner: 'electron', repo: 'electron', }; -async function getPatchDetailsFromURL(urlStr, security) { +async function getPatchDetailsFromURL(urlStr: string, security: boolean) { const parsedUrl = new URL(urlStr); if (parsedUrl.host.endsWith('.googlesource.com')) { return await getGerritPatchDetailsFromURL(parsedUrl, security); @@ -30,7 +32,7 @@ async function getPatchDetailsFromURL(urlStr, security) { ); } -async function getGitHubPatchDetailsFromURL(gitHubUrl, security) { +async function getGitHubPatchDetailsFromURL(gitHubUrl: URL, security: boolean) { if (security) { fatal('GitHub cherry-picks can not be security backports currently'); } @@ -52,6 +54,8 @@ async function getGitHubPatchDetailsFromURL(gitHubUrl, security) { patchDirName: 'node', shortCommit, patch, + bugNumber: null, + cve: null, }; } @@ -92,7 +96,7 @@ program ); const patchName = `cherry-pick-${shortCommit}.patch`; - const commitMessage = /Subject: \[PATCH\] (.+?)^---$/ms.exec(patch)[1]; + const commitMessage = /Subject: \[PATCH\] (.+?)^---$/ms.exec(patch)?.[1]; const patchPath = `patches/${patchDirName}`; const targetBranches = [targetBranch, ...additionalBranches]; @@ -189,7 +193,7 @@ program if (error) { console.error(`${color.err} Failed to cherry-pick`); - fatal(error); + fatal(error as Error); } } }) diff --git a/src/e-debug.js b/src/e-debug.ts similarity index 71% rename from src/e-debug.js rename to src/e-debug.ts index ab966ce8..e1689072 100644 --- a/src/e-debug.js +++ b/src/e-debug.ts @@ -1,22 +1,23 @@ #!/usr/bin/env node -const childProcess = require('child_process'); -const path = require('path'); +import childProcess from 'node:child_process'; +import path from 'node:path'; -const { sync: commandExistsSync } = require('command-exists'); -const program = require('commander'); +import { sync as commandExistsSync } from 'command-exists'; +import { program } from 'commander'; -const evmConfig = require('./evm-config'); -const { fatal } = require('./utils/logging'); +import * as evmConfig from './evm-config.js'; +import { fatal } from './utils/logging.js'; +import { EVMBaseElectronConfiguration } from './evm-config.schema.js'; const opts = { encoding: 'utf8', stdio: 'inherit', -}; +} as const; program.description('Run the Electron build with a debugger (gdb or lldb)').action(debug); -function run_gdb(config) { +function run_gdb(config: EVMBaseElectronConfiguration): void { const electron = evmConfig.execOf(config); const gdbinit = path.resolve(config.root, 'src', 'tools', 'gdb', 'gdbinit'); const ex = `source ${gdbinit}`; @@ -24,7 +25,7 @@ function run_gdb(config) { childProcess.execFileSync('gdb', args, opts); } -function run_lldb(config) { +function run_lldb(config: EVMBaseElectronConfiguration): void { const electron = evmConfig.execOf(config); const lldbinit = path.resolve(config.root, 'src', 'tools', 'lldb', 'lldbinit.py'); const args = [ @@ -53,7 +54,7 @@ function debug() { ); } } catch (e) { - fatal(e); + fatal(e as Error); } } diff --git a/src/e-depot-tools.js b/src/e-depot-tools.ts similarity index 67% rename from src/e-depot-tools.js rename to src/e-depot-tools.ts index 52d64666..e13b091a 100644 --- a/src/e-depot-tools.js +++ b/src/e-depot-tools.ts @@ -1,12 +1,12 @@ #!/usr/bin/env node -const program = require('commander'); -const os = require('os'); +import { program } from 'commander'; +import os from 'node:os'; -const evmConfig = require('./evm-config'); -const { fatal } = require('./utils/logging'); -const depot = require('./utils/depot-tools'); -const reclient = require('./utils/reclient'); +import * as evmConfig from './evm-config.js'; +import { fatal } from './utils/logging.js'; +import { setDepotToolsAutoUpdate, ensureDepotTools, depotSpawnSync } from './utils/depot-tools.js'; +import * as reclient from './utils/reclient.js'; program .command('depot-tools') @@ -14,7 +14,7 @@ program .allowUnknownOption() .helpOption('\0') .action(() => { - depot.ensure(); + ensureDepotTools(); const args = process.argv.slice(2); if (args.length === 0) { @@ -27,21 +27,21 @@ program } const enable = args[1] === 'enable'; - depot.setAutoUpdate(enable); + setDepotToolsAutoUpdate(enable); return; } let cwd; if (args[0] === 'rbe') { - reclient.downloadAndPrepareRBECredentialHelper(evmConfig.current(), true); - args[0] = reclient.helperPath(evmConfig.current()); + reclient.downloadAndPrepareRBECredentialHelper(evmConfig.current()); + args[0] = reclient.getHelperPath(evmConfig.current()); } if (args[0] === '--') { args.shift(); } - const { status, error } = depot.spawnSync(evmConfig.maybeCurrent(), args[0], args.slice(1), { + const { status, error } = depotSpawnSync(evmConfig.maybeCurrent(), args[0], args.slice(1), { cwd, stdio: 'inherit', env: { @@ -55,7 +55,7 @@ program let errorMsg = `Failed to run command:`; if (status !== null) errorMsg += `\n Exit Code: "${status}"`; if (error) errorMsg += `\n ${error}`; - fatal(errorMsg, status); + fatal(errorMsg, status ?? 1); } process.exit(0); diff --git a/src/e-gh-auth.js b/src/e-gh-auth.ts similarity index 70% rename from src/e-gh-auth.js rename to src/e-gh-auth.ts index e3fb20d2..83c6321a 100644 --- a/src/e-gh-auth.js +++ b/src/e-gh-auth.ts @@ -1,10 +1,9 @@ #!/usr/bin/env node -const d = require('debug')('build-tools:gh-auth'); -const program = require('commander'); +import { program } from 'commander'; -const { createGitHubAuthToken } = require('./utils/github-auth'); -const { fatal } = require('./utils/logging'); +import { createGitHubAuthToken } from './utils/github-auth.js'; +import { fatal } from './utils/logging.js'; program .description('Generates a device auth token for the electron org that build-tools can use') @@ -18,9 +17,9 @@ program } else { console.log('Token:', token); } - } catch (err) { + } catch (e) { console.error('Failed to authenticate'); - fatal(err); + fatal(e as Error); } }) .parse(process.argv); diff --git a/src/e-init.js b/src/e-init.ts similarity index 83% rename from src/e-init.js rename to src/e-init.ts index d216c3b1..4e4b03b0 100644 --- a/src/e-init.js +++ b/src/e-init.ts @@ -1,18 +1,21 @@ #!/usr/bin/env node -const childProcess = require('child_process'); -const os = require('os'); -const fs = require('fs'); -const path = require('path'); -const { program, Option } = require('commander'); -const { URI } = require('vscode-uri'); - -const evmConfig = require('./evm-config'); -const { color, fatal } = require('./utils/logging'); -const { resolvePath, ensureDir } = require('./utils/paths'); -const depot = require('./utils/depot-tools'); -const { checkGlobalGitConfig } = require('./utils/git'); -const { ensureSDK } = require('./utils/sdk'); +import childProcess from 'node:child_process'; +import os from 'node:os'; +import fs from 'node:fs'; +import path from 'node:path'; +import { program, Option } from 'commander'; +import vscode from 'vscode-uri'; + +import * as evmConfig from './evm-config.js'; +import { color, fatal } from './utils/logging.js'; +import { resolvePath, ensureDir } from './utils/paths.js'; +import { depotSpawnSync, ensureDepotTools } from './utils/depot-tools.js'; +import { checkGlobalGitConfig } from './utils/git.js'; +import { ensureSDK } from './utils/sdk.js'; +import { EVMBaseElectronConfiguration } from './evm-config.schema.js'; + +const { URI } = vscode; // https://gn.googlesource.com/gn/+/main/docs/reference.md?pli=1#var_target_cpu const archOption = new Option( @@ -20,7 +23,7 @@ const archOption = new Option( 'Set the desired architecture for the build', ).choices(['x86', 'x64', 'arm', 'arm64']); -function createConfig(options) { +function createConfig(options: Record): EVMBaseElectronConfiguration { const root = resolvePath(options.root); const homedir = os.homedir(); @@ -57,7 +60,7 @@ function createConfig(options) { }; return { - $schema: URI.file(path.resolve(__dirname, '..', 'evm-config.schema.json')).toString(), + $schema: URI.file(path.resolve(import.meta.dirname, '..', 'evm-config.schema.json')).toString(), remoteBuild: options.remoteBuild, root, remotes: { @@ -77,9 +80,9 @@ function createConfig(options) { }; } -function runGClientConfig(config) { +function runGClientConfig(config: EVMBaseElectronConfiguration) { const { root } = config; - depot.ensure(); + ensureDepotTools(); const exec = 'gclient'; const args = [ 'config', @@ -92,10 +95,10 @@ function runGClientConfig(config) { cwd: root, shell: true, }; - depot.spawnSync(config, exec, args, opts, 'gclient config failed'); + depotSpawnSync(config, exec, args, opts, 'gclient config failed'); } -function ensureRoot(config, force) { +function ensureRoot(config: EVMBaseElectronConfiguration, force: boolean) { const { root } = config; ensureDir(root); @@ -180,12 +183,12 @@ program } // save the new config - evmConfig.save(name, config); + evmConfig.saveConfig(name, config); console.log(`New build config ${color.config(name)} created in ${color.path(filename)}`); // `e use` the new config - const e = path.resolve(__dirname, 'e'); - const opts = { stdio: 'inherit' }; + const e = path.resolve(import.meta.dirname, 'e'); + const opts = { stdio: 'inherit' } as const; childProcess.execFileSync(process.execPath, [e, 'use', name], opts); // ensure macOS SDKs are loaded @@ -213,7 +216,7 @@ program childProcess.execFileSync(process.execPath, [e, 'build'], opts); } } catch (e) { - fatal(e); + fatal(e as Error); } }) .parse(process.argv); diff --git a/src/e-open.js b/src/e-open.ts similarity index 75% rename from src/e-open.js rename to src/e-open.ts index 79046af5..88760972 100644 --- a/src/e-open.js +++ b/src/e-open.ts @@ -1,21 +1,21 @@ #!/usr/bin/env node -const cp = require('child_process'); -const path = require('path'); -const program = require('commander'); +import cp from 'node:child_process'; +import path from 'node:path'; +import { program } from 'commander'; -const evmConfig = require('./evm-config.js'); -const { color, fatal } = require('./utils/logging'); +import * as evmConfig from './evm-config.js'; +import { color, fatal } from './utils/logging.js'; // 'feat: added foo (#1234)' --> 1234 -function getPullNumberFromSubject(subject) { +function getPullNumberFromSubject(subject: string): number | null { const pullNumberRegex = /^.*\s\(#(\d+)\)$/; const match = subject.match(pullNumberRegex); return match ? Number.parseInt(match[1]) : null; } // abbrev-sha1 --> { pullNumber, sha1 } -function getCommitInfo(object) { +function getCommitInfo(object: string): { pullNumber: number | null; sha1: string | null } { let pullNumber = null; let sha1 = null; @@ -24,7 +24,7 @@ function getCommitInfo(object) { const opts = { cwd: path.resolve(evmConfig.current().root, 'src', 'electron'), encoding: 'utf8', - }; + } as const; const result = cp.spawnSync(cmd, args, opts); if (result.status === 0) { @@ -37,7 +37,7 @@ function getCommitInfo(object) { } // ask GitHub for a commit's pull request URLs -async function getPullURLsFromGitHub(sha1) { +async function getPullURLsFromGitHub(sha1: string): Promise { const ret = []; const url = `https://api.github.com/repos/electron/electron/commits/${sha1}/pulls`; @@ -53,7 +53,7 @@ async function getPullURLsFromGitHub(sha1) { if (!response.ok) { fatal(`Could not open PR: ${url} got ${response.status}`); } - const data = await response.json(); + const data = (await response.json()) as { html_url: string }[]; ret.push(...(data || []).map((pull) => pull.html_url).filter((url) => !!url)); } catch (error) { console.log(color.err, error); @@ -63,9 +63,9 @@ async function getPullURLsFromGitHub(sha1) { } // get the pull request URLs for a git object or pull number -async function getPullURLs(ref) { +async function getPullURLs(ref: string): Promise { const { pullNumber, sha1 } = getCommitInfo(ref); - const makeURL = (num) => `https://github.com/electron/electron/pull/${num}`; + const makeURL = (num: number) => `https://github.com/electron/electron/pull/${num}`; if (pullNumber) { return [makeURL(pullNumber)]; @@ -84,7 +84,7 @@ async function getPullURLs(ref) { return []; } -async function doOpen(opts) { +async function doOpen(opts: { object: string; print: boolean }) { const urls = await getPullURLs(opts.object); if (urls.length === 0) { @@ -92,7 +92,7 @@ async function doOpen(opts) { return; } - const open = require('open'); + const { default: open } = await import('open'); for (const url of urls) { if (opts.print) { console.log(url); diff --git a/src/e-patches.js b/src/e-patches.ts similarity index 81% rename from src/e-patches.js rename to src/e-patches.ts index b7f9d9fc..af9b9d93 100644 --- a/src/e-patches.js +++ b/src/e-patches.ts @@ -1,12 +1,12 @@ #!/usr/bin/env node -const fs = require('fs'); -const path = require('path'); -const program = require('commander'); +import fs from 'node:fs'; +import path from 'node:path'; +import { program } from 'commander'; -const evmConfig = require('./evm-config.js'); -const depot = require('./utils/depot-tools'); -const { color, fatal } = require('./utils/logging'); +import * as evmConfig from './evm-config.js'; +import { depotExecFileSync } from './utils/depot-tools.js'; +import { color, fatal } from './utils/logging.js'; program .arguments('[target]') @@ -25,15 +25,16 @@ program const srcdir = path.resolve(config.root, 'src'); // build the list of targets - const targets = {}; + const targets: Record = {}; const patchesConfig = options.config; if (!fs.existsSync(patchesConfig)) throw `Config file '${patchesConfig}' not found`; - const configData = JSON.parse(fs.readFileSync(patchesConfig)); + const configData = JSON.parse(fs.readFileSync(patchesConfig, 'utf-8')); if (Array.isArray(configData)) { for (const target of configData) targets[path.basename(target.patch_dir)] = target; } else if (typeof configData == 'object') { - for (const [patch_dir, repo] of Object.entries(configData)) + for (const [patch_dir, repo] of Object.entries(configData) as [string, string][]) { targets[path.basename(patch_dir)] = { patch_dir, repo }; + } } if (options.listTargets) { @@ -49,7 +50,7 @@ program if (target === 'all') { const script = path.resolve(srcdir, 'electron', 'script', 'export_all_patches.py'); - depot.execFileSync(config, 'python3', [script, patchesConfig], { + depotExecFileSync(config, 'python3', [script, patchesConfig], { cwd: config.root, stdio: 'inherit', encoding: 'utf8', @@ -61,10 +62,10 @@ program cwd: path.resolve(config.root, targetConfig.repo), stdio: 'inherit', encoding: 'utf8', - }; + } as const; const args = [script, '--output', path.resolve(config.root, targetConfig.patch_dir)]; if (targetConfig.grep) args.push('--grep', targetConfig.grep); - depot.execFileSync(config, 'python3', args, opts); + depotExecFileSync(config, 'python3', args, opts); } else { console.log(`${color.err} Unrecognized target ${color.cmd(target)}.`); console.log( @@ -76,7 +77,7 @@ program fatal(`See ${color.path(patchesConfig)}`); } } catch (e) { - fatal(e); + fatal(e as Error); } }); diff --git a/src/e-pr.js b/src/e-pr.js deleted file mode 100644 index 1435e210..00000000 --- a/src/e-pr.js +++ /dev/null @@ -1,427 +0,0 @@ -#!/usr/bin/env node - -const childProcess = require('child_process'); -const fs = require('fs'); -const os = require('os'); -const path = require('path'); -const { Readable } = require('stream'); -const { pipeline } = require('stream/promises'); - -const extractZip = require('extract-zip'); -const querystring = require('querystring'); -const semver = require('semver'); -const open = require('open'); -const program = require('commander'); -const { Octokit } = require('@octokit/rest'); -const inquirer = require('inquirer'); - -const { progressStream } = require('./utils/download'); -const { getGitHubAuthToken } = require('./utils/github-auth'); -const { current } = require('./evm-config'); -const { color, fatal, logError } = require('./utils/logging'); - -const d = require('debug')('build-tools:pr'); - -// Adapted from https://github.com/electron/clerk -function findNoteInPRBody(body) { - const onelineMatch = /(?:(?:\r?\n)|^)notes: (.+?)(?:(?:\r?\n)|$)/gi.exec(body); - const multilineMatch = /(?:(?:\r?\n)Notes:(?:\r?\n+)((?:\*.+(?:(?:\r?\n)|$))+))/gi.exec(body); - - let notes = null; - if (onelineMatch && onelineMatch[1]) { - notes = onelineMatch[1]; - } else if (multilineMatch && multilineMatch[1]) { - notes = multilineMatch[1]; - } - - if (notes) { - // Remove the default PR template. - notes = notes.replace(//g, ''); - } - - return notes ? notes.trim() : notes; -} - -async function getPullRequestInfo(pullNumber) { - let notes = null; - let title = null; - - const url = `https://api.github.com/repos/electron/electron/pulls/${pullNumber}`; - const opts = { - responseType: 'json', - throwHttpErrors: false, - }; - try { - const response = await fetch(url, opts); - if (!response.ok) { - fatal(`Could not find PR: ${url} got ${response.status}`); - } - const data = await response.json(); - notes = findNoteInPRBody(data.body); - title = data.title; - } catch (error) { - console.log(color.err, error); - } - - return { - notes, - title, - }; -} - -function guessPRTarget(config) { - const electronDir = path.resolve(config.root, 'src', 'electron'); - if (process.cwd() !== electronDir) { - fatal(`You must be in an Electron repository to guess the default target PR branch`); - } - - let script = path.resolve(electronDir, 'script', 'lib', 'get-version.js'); - - if (process.platform === 'win32') { - script = script.replace(new RegExp(/\\/, 'g'), '\\\\'); - } - const version = childProcess - .execSync(`node -p "require('${script}').getElectronVersion()"`) - .toString() - .trim(); - - const latestVersion = childProcess - .execSync('git describe --tags `git rev-list --tags --max-count=1`') - .toString() - .trim(); - - // Nightlies are only released off of main, so we can safely make this assumption. - // However, if the nearest reachable tag from this commit is also the latest tag - // across all branches, and neither is a nightly, we're in the small time window - // between a stable release and the next nightly, and should also target main. - const inNightlyWindow = !version.includes('nightly') && version === latestVersion; - if (version.includes('nightly') || inNightlyWindow) return 'main'; - - const match = semver.valid(version); - if (match) { - return `${semver.major(match)}-x-y`; - } - - console.warn( - `Unable to guess default target PR branch -- generated version '${version}' should include 'nightly' or match ${versionPattern}`, - ); -} - -function guessPRSource(config) { - const command = 'git rev-parse --abbrev-ref HEAD'; - - const cwd = path.resolve(config.root, 'src', 'electron'); - const options = { cwd, encoding: 'utf8' }; - - try { - return childProcess.execSync(command, options).trim(); - } catch { - return 'main'; - } -} - -function pullRequestSource(source) { - const regexes = [ - /https:\/\/github.com\/(\S*)\/electron.git/, - /git@github.com:(\S*)\/electron.git/, - ]; - - const config = current(); - - if (config.remotes.electron.fork) { - const command = 'git remote get-url fork'; - const cwd = path.resolve(config.root, 'src', 'electron'); - const options = { cwd, encoding: 'utf8' }; - const remoteUrl = childProcess.execSync(command, options).trim(); - - for (const regex of regexes) { - if (regex.test(remoteUrl)) { - return `${regex.exec(remoteUrl)[1]}:${source}`; - } - } - } - - return source; -} - -program - .command('open', null, { isDefault: true }) - .description('Open a GitHub URL where you can PR your changes') - .option('-s, --source [source_branch]', 'Where the changes are coming from') - .option('-t, --target [target_branch]', 'Where the changes are going to') - .option('-b, --backport ', 'Pull request being backported') - .action(async (options) => { - const source = options.source || guessPRSource(current()); - const target = options.target || guessPRTarget(current()); - - if (!source) { - fatal(`'source' is required to create a PR`); - } else if (!target) { - fatal(`'target' is required to create a PR`); - } - - const repoBaseUrl = 'https://github.com/electron/electron'; - const comparePath = `${target}...${pullRequestSource(source)}`; - const queryParams = { expand: 1 }; - - if (!options.backport) { - const currentBranchResult = childProcess.spawnSync( - 'git', - ['rev-parse', '--abbrev-ref', 'HEAD'], - { - cwd: path.resolve(current().root, 'src', 'electron'), - }, - ); - const currentBranch = currentBranchResult.stdout.toString().trim(); - const manualBranchPattern = /^manual-bp\/[^\/]+\/pr\/([0-9]+)\/branch\/[^\/]+$/; - const manualBranchTarget = manualBranchPattern.exec(currentBranch); - if (manualBranchTarget) { - options.backport = manualBranchTarget[1]; - } - } - - if (options.backport) { - if (!/^\d+$/.test(options.backport)) { - fatal(`${options.backport} is not a valid GitHub backport number - try again`); - } - - const { notes, title } = await getPullRequestInfo(options.backport); - if (title) { - queryParams.title = title; - } - queryParams.body = `Backport of #${ - options.backport - }.\n\nSee that PR for details.\n\nNotes: ${notes || ''}`; - } - - return open(`${repoBaseUrl}/compare/${comparePath}?${querystring.stringify(queryParams)}`); - }); - -program - .command('download-dist ') - .description('Download a pull request dist') - .option( - '--platform [platform]', - 'Platform to download dist for. Defaults to current platform.', - process.platform, - ) - .option( - '--arch [arch]', - 'Architecture to download dist for. Defaults to current arch.', - process.arch, - ) - .option( - '-o, --output ', - 'Specify the output directory for downloaded artifacts. ' + - 'Defaults to ~/.electron_build_tools/artifacts/pr_{number}_{commithash}_{platform}_{arch}', - ) - .option( - '-s, --skip-confirmation', - 'Skip the confirmation prompt before downloading the dist.', - !!process.env.CI, - ) - .action(async (pullRequestNumber, options) => { - if (!pullRequestNumber) { - fatal(`Pull request number is required to download a PR`); - } - - d('checking auth...'); - const auth = await getGitHubAuthToken(['repo']); - const octokit = new Octokit({ auth }); - - d('fetching pr info...'); - let pullRequest; - try { - const { data } = await octokit.pulls.get({ - owner: 'electron', - repo: 'electron', - pull_number: pullRequestNumber, - }); - pullRequest = data; - } catch (error) { - console.error(`Failed to get pull request: ${error}`); - return; - } - - if (!options.skipConfirmation) { - const isElectronRepo = pullRequest.head.repo.full_name !== 'electron/electron'; - const { proceed } = await inquirer.prompt([ - { - type: 'confirm', - default: false, - name: 'proceed', - message: `You are about to download artifacts from: - -“${pullRequest.title} (#${pullRequest.number})” by ${pullRequest.user.login} -${pullRequest.head.repo.html_url}${isElectronRepo ? ' (fork)' : ''} -${pullRequest.state !== 'open' ? '\n❗❗❗ The pull request is closed, only proceed if you trust the source ❗❗❗\n' : ''} -Proceed?`, - }, - ]); - - if (!proceed) return; - } - - d('fetching workflow runs...'); - let workflowRuns; - try { - const { data } = await octokit.actions.listWorkflowRunsForRepo({ - owner: 'electron', - repo: 'electron', - branch: pullRequest.head.ref, - name: 'Build', - event: 'pull_request', - status: 'completed', - per_page: 10, - sort: 'created', - direction: 'desc', - }); - workflowRuns = data.workflow_runs; - } catch (error) { - console.error(`Failed to list workflow runs: ${error}`); - return; - } - - const latestBuildWorkflowRun = workflowRuns.find((run) => run.name === 'Build'); - if (!latestBuildWorkflowRun) { - fatal(`No 'Build' workflow runs found for pull request #${pullRequestNumber}`); - } - const shortCommitHash = latestBuildWorkflowRun.head_sha.substring(0, 7); - - d('fetching artifacts...'); - let artifacts; - try { - const { data } = await octokit.actions.listWorkflowRunArtifacts({ - owner: 'electron', - repo: 'electron', - run_id: latestBuildWorkflowRun.id, - }); - artifacts = data.artifacts; - } catch (error) { - console.error(`Failed to list artifacts: ${error}`); - return; - } - - const artifactPlatform = options.platform === 'win32' ? 'win' : options.platform; - const artifactName = `generated_artifacts_${artifactPlatform}_${options.arch}`; - const artifact = artifacts.find((artifact) => artifact.name === artifactName); - if (!artifact) { - console.error(`Failed to find artifact: ${artifactName}`); - return; - } - - let outputDir; - - if (options.output) { - outputDir = path.resolve(options.output); - - if (!(await fs.promises.stat(outputDir).catch(() => false))) { - fatal(`The output directory '${options.output}' does not exist`); - } - } else { - const artifactsDir = path.resolve(__dirname, '..', 'artifacts'); - const defaultDir = path.resolve( - artifactsDir, - `pr_${pullRequest.number}_${shortCommitHash}_${options.platform}_${options.arch}`, - ); - - // Clean up the directory if it exists - try { - await fs.promises.rm(defaultDir, { recursive: true, force: true }); - } catch (error) { - if (error.code !== 'ENOENT') { - throw error; - } - } - - // Create the directory - await fs.promises.mkdir(defaultDir, { recursive: true }); - - outputDir = defaultDir; - } - - console.log( - `Downloading artifact '${artifactName}' from pull request #${pullRequestNumber}...`, - ); - - // Download the artifact to a temporary directory - const tempDir = path.join(os.tmpdir(), 'electron-tmp'); - await fs.promises.rm(tempDir, { recursive: true, force: true }); - await fs.promises.mkdir(tempDir); - - const { url } = await octokit.actions.downloadArtifact.endpoint({ - owner: 'electron', - repo: 'electron', - artifact_id: artifact.id, - archive_format: 'zip', - }); - - const response = await fetch(url, { - headers: { - Authorization: `Bearer ${auth}`, - }, - }); - - if (!response.ok) { - fatal(`Could not find artifact: ${url} got ${response.status}`); - } - - const total = parseInt(response.headers.get('content-length'), 10); - const artifactDownloadStream = Readable.fromWeb(response.body); - - try { - const artifactZipPath = path.join(tempDir, `${artifactName}.zip`); - const artifactFileStream = fs.createWriteStream(artifactZipPath); - await pipeline( - artifactDownloadStream, - // Show download progress - ...(process.env.CI ? [] : [progressStream(total, '[:bar] :mbRateMB/s :percent :etas')]), - artifactFileStream, - ); - - // Extract artifact zip - d('unzipping artifact to %s', tempDir); - await extractZip(artifactZipPath, { dir: tempDir }); - - // Check if dist.zip exists within the extracted artifact - const distZipPath = path.join(tempDir, 'dist.zip'); - if (!(await fs.promises.stat(distZipPath).catch(() => false))) { - throw new Error(`dist.zip not found in build artifact.`); - } - - // Extract dist.zip - // NOTE: 'extract-zip' is used as it correctly extracts symlinks. - d('unzipping dist.zip to %s', outputDir); - await extractZip(distZipPath, { dir: outputDir }); - - const platformExecutables = { - win32: 'electron.exe', - darwin: 'Electron.app/', - linux: 'electron', - }; - - const executableName = platformExecutables[options.platform]; - if (!executableName) { - throw new Error(`Unable to find executable for platform '${options.platform}'`); - } - - const executablePath = path.join(outputDir, executableName); - if (!(await fs.promises.stat(executablePath).catch(() => false))) { - throw new Error(`${executableName} not found within dist.zip.`); - } - - console.log(`${color.success} Downloaded to ${outputDir}`); - } catch (error) { - logError(error); - process.exitCode = 1; // wait for cleanup - } finally { - // Cleanup temporary files - try { - await fs.promises.rm(tempDir, { recursive: true }); - } catch { - // ignore - } - } - }); - -program.parse(process.argv); diff --git a/src/e-pr.ts b/src/e-pr.ts new file mode 100644 index 00000000..e3da3497 --- /dev/null +++ b/src/e-pr.ts @@ -0,0 +1,444 @@ +#!/usr/bin/env node + +import childProcess from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import querystring from 'node:querystring'; +import { Readable } from 'node:stream'; +import { pipeline } from 'node:stream/promises'; + +import { program } from 'commander'; +import debug from 'debug'; +import extractZip from 'extract-zip'; +import semver from 'semver'; +import open from 'open'; +import { Octokit } from '@octokit/rest'; +import inquirer from 'inquirer'; + +import { progressStream } from './utils/download.js'; +import { getGitHubAuthToken } from './utils/github-auth.js'; +import * as evmConfig from './evm-config.js'; +import { color, fatal, logError } from './utils/logging.js'; +import { EVMBaseElectronConfiguration } from './evm-config.schema.js'; + +const d = debug('build-tools:pr'); + +// Adapted from https://github.com/electron/clerk +function findNoteInPRBody(body: string): string | null { + const onelineMatch = /(?:(?:\r?\n)|^)notes: (.+?)(?:(?:\r?\n)|$)/gi.exec(body); + const multilineMatch = /(?:(?:\r?\n)Notes:(?:\r?\n+)((?:\*.+(?:(?:\r?\n)|$))+))/gi.exec(body); + + let notes = null; + if (onelineMatch && onelineMatch[1]) { + notes = onelineMatch[1]; + } else if (multilineMatch && multilineMatch[1]) { + notes = multilineMatch[1]; + } + + if (notes) { + // Remove the default PR template. + notes = notes.replace(//g, ''); + } + + return notes ? notes.trim() : notes; +} + +async function getPullRequestInfo(pullNumber: number) { + let notes = null; + let title = null; + + const url = `https://api.github.com/repos/electron/electron/pulls/${pullNumber}`; + try { + const response = await fetch(url); + if (!response.ok) { + fatal(`Could not find PR: ${url} got ${response.status}`); + } + const data = (await response.json()) as { body: string; title: string }; + notes = findNoteInPRBody(data.body); + title = data.title; + } catch (error) { + console.log(color.err, error); + } + + return { + notes, + title, + }; +} + +function guessPRTarget(config: EVMBaseElectronConfiguration): string | null { + const electronDir = path.resolve(config.root, 'src', 'electron'); + if (process.cwd() !== electronDir) { + fatal(`You must be in an Electron repository to guess the default target PR branch`); + } + + let script = path.resolve(electronDir, 'script', 'lib', 'get-version.js'); + + if (process.platform === 'win32') { + script = script.replace(new RegExp(/\\/, 'g'), '\\\\'); + } + const version = childProcess + .execSync(`node -p "require('${script}').getElectronVersion()"`) + .toString() + .trim(); + + const latestVersion = childProcess + .execSync('git describe --tags `git rev-list --tags --max-count=1`') + .toString() + .trim(); + + // Nightlies are only released off of main, so we can safely make this assumption. + // However, if the nearest reachable tag from this commit is also the latest tag + // across all branches, and neither is a nightly, we're in the small time window + // between a stable release and the next nightly, and should also target main. + const inNightlyWindow = !version.includes('nightly') && version === latestVersion; + if (version.includes('nightly') || inNightlyWindow) return 'main'; + + const match = semver.valid(version); + if (match) { + return `${semver.major(match)}-x-y`; + } + + console.warn( + `Unable to guess default target PR branch -- generated version '${version}' should include 'nightly' or match V-x-y`, + ); + + return null; +} + +function guessPRSource(config: EVMBaseElectronConfiguration): string { + const command = 'git rev-parse --abbrev-ref HEAD'; + + const cwd = path.resolve(config.root, 'src', 'electron'); + const options = { cwd, encoding: 'utf8' } as const; + + try { + return childProcess.execSync(command, options).trim(); + } catch { + return 'main'; + } +} + +function pullRequestSource(source: string): string { + const regexes = [ + /https:\/\/github.com\/(\S*)\/electron.git/, + /git@github.com:(\S*)\/electron.git/, + ]; + + const config = evmConfig.current(); + + if (config.remotes?.electron.fork) { + const command = 'git remote get-url fork'; + const cwd = path.resolve(config.root, 'src', 'electron'); + const options = { cwd, encoding: 'utf8' } as const; + const remoteUrl = childProcess.execSync(command, options).trim(); + + for (const regex of regexes) { + if (regex.test(remoteUrl)) { + return `${regex.exec(remoteUrl)![1]}:${source}`; + } + } + } + + return source; +} + +program + .command('open', { isDefault: true }) + .description('Open a GitHub URL where you can PR your changes') + .option('-s, --source [source_branch]', 'Where the changes are coming from') + .option('-t, --target [target_branch]', 'Where the changes are going to') + .option('-b, --backport ', 'Pull request being backported') + .action(async (options) => { + const source = options.source || guessPRSource(evmConfig.current()); + const target = options.target || guessPRTarget(evmConfig.current()); + + if (!source) { + fatal(`'source' is required to create a PR`); + } else if (!target) { + fatal(`'target' is required to create a PR`); + } + + const repoBaseUrl = 'https://github.com/electron/electron'; + const comparePath = `${target}...${pullRequestSource(source)}`; + const queryParams = { + expand: 1, + title: undefined as string | undefined, + body: undefined as string | undefined, + }; + + if (!options.backport) { + const currentBranchResult = childProcess.spawnSync( + 'git', + ['rev-parse', '--abbrev-ref', 'HEAD'], + { + cwd: path.resolve(evmConfig.current().root, 'src', 'electron'), + }, + ); + const currentBranch = currentBranchResult.stdout.toString().trim(); + const manualBranchPattern = /^manual-bp\/[^\/]+\/pr\/([0-9]+)\/branch\/[^\/]+$/; + const manualBranchTarget = manualBranchPattern.exec(currentBranch); + if (manualBranchTarget) { + options.backport = manualBranchTarget[1]; + } + } + + if (options.backport) { + if (!/^\d+$/.test(options.backport)) { + fatal(`${options.backport} is not a valid GitHub backport number - try again`); + } + + const { notes, title } = await getPullRequestInfo(options.backport); + if (title) { + queryParams.title = title; + } + queryParams.body = `Backport of #${ + options.backport + }.\n\nSee that PR for details.\n\nNotes: ${notes || ''}`; + } + + await open(`${repoBaseUrl}/compare/${comparePath}?${querystring.stringify(queryParams)}`); + }); + +program + .command('download-dist ') + .description('Download a pull request dist') + .option( + '--platform [platform]', + 'Platform to download dist for. Defaults to current platform.', + process.platform, + ) + .option( + '--arch [arch]', + 'Architecture to download dist for. Defaults to current arch.', + process.arch, + ) + .option( + '-o, --output ', + 'Specify the output directory for downloaded artifacts. ' + + 'Defaults to ~/.electron_build_tools/artifacts/pr_{number}_{commithash}_{platform}_{arch}', + ) + .option( + '-s, --skip-confirmation', + 'Skip the confirmation prompt before downloading the dist.', + !!process.env.CI, + ) + .action( + async ( + pullRequestNumber: string, + options: { + platform: NodeJS.Platform; + arch: string; + output: string; + skipConfirmation: boolean; + }, + ) => { + if (!pullRequestNumber) { + fatal(`Pull request number is required to download a PR`); + } + + d('checking auth...'); + const auth = await getGitHubAuthToken(['repo']); + const octokit = new Octokit({ auth }); + + d('fetching pr info...'); + let pullRequest; + try { + const { data } = await octokit.pulls.get({ + owner: 'electron', + repo: 'electron', + pull_number: parseInt(pullRequestNumber, 10), + }); + pullRequest = data; + } catch (error) { + console.error(`Failed to get pull request: ${error}`); + return; + } + + if (!options.skipConfirmation) { + const isElectronRepo = pullRequest.head.repo.full_name !== 'electron/electron'; + const { proceed } = await inquirer.prompt([ + { + type: 'confirm', + default: false, + name: 'proceed', + message: `You are about to download artifacts from: + +“${pullRequest.title} (#${pullRequest.number})” by ${pullRequest.user?.login || 'unknown'} +${pullRequest.head.repo.html_url}${isElectronRepo ? ' (fork)' : ''} +${pullRequest.state !== 'open' ? '\n❗❗❗ The pull request is closed, only proceed if you trust the source ❗❗❗\n' : ''} +Proceed?`, + }, + ]); + + if (!proceed) return; + } + + d('fetching workflow runs...'); + let workflowRuns; + try { + const { data } = await octokit.actions.listWorkflowRunsForRepo({ + owner: 'electron', + repo: 'electron', + branch: pullRequest.head.ref, + name: 'Build', + event: 'pull_request', + status: 'completed', + per_page: 10, + sort: 'created', + direction: 'desc', + }); + workflowRuns = data.workflow_runs; + } catch (error) { + console.error(`Failed to list workflow runs: ${error}`); + return; + } + + const latestBuildWorkflowRun = workflowRuns.find((run) => run.name === 'Build'); + if (!latestBuildWorkflowRun) { + fatal(`No 'Build' workflow runs found for pull request #${pullRequestNumber}`); + } + const shortCommitHash = latestBuildWorkflowRun.head_sha.substring(0, 7); + + d('fetching artifacts...'); + let artifacts; + try { + const { data } = await octokit.actions.listWorkflowRunArtifacts({ + owner: 'electron', + repo: 'electron', + run_id: latestBuildWorkflowRun.id, + }); + artifacts = data.artifacts; + } catch (error) { + console.error(`Failed to list artifacts: ${error}`); + return; + } + + const artifactPlatform = options.platform === 'win32' ? 'win' : options.platform; + const artifactName = `generated_artifacts_${artifactPlatform}_${options.arch}`; + const artifact = artifacts.find((artifact) => artifact.name === artifactName); + if (!artifact) { + console.error(`Failed to find artifact: ${artifactName}`); + return; + } + + let outputDir; + + if (options.output) { + outputDir = path.resolve(options.output); + + if (!(await fs.promises.stat(outputDir).catch(() => false))) { + fatal(`The output directory '${options.output}' does not exist`); + } + } else { + const artifactsDir = path.resolve(import.meta.dirname, '..', 'artifacts'); + const defaultDir = path.resolve( + artifactsDir, + `pr_${pullRequest.number}_${shortCommitHash}_${options.platform}_${options.arch}`, + ); + + // Clean up the directory if it exists + try { + await fs.promises.rm(defaultDir, { recursive: true, force: true }); + } catch (error) { + if ((error as any).code !== 'ENOENT') { + throw error; + } + } + + // Create the directory + await fs.promises.mkdir(defaultDir, { recursive: true }); + + outputDir = defaultDir; + } + + console.log( + `Downloading artifact '${artifactName}' from pull request #${pullRequestNumber}...`, + ); + + // Download the artifact to a temporary directory + const tempDir = path.join(os.tmpdir(), 'electron-tmp'); + await fs.promises.rm(tempDir, { recursive: true, force: true }); + await fs.promises.mkdir(tempDir); + + const { url } = await octokit.actions.downloadArtifact.endpoint({ + owner: 'electron', + repo: 'electron', + artifact_id: artifact.id, + archive_format: 'zip', + }); + + const response = await fetch(url, { + headers: { + Authorization: `Bearer ${auth}`, + }, + }); + + if (!response.ok || !response.body) { + fatal(`Could not find artifact: ${url} got ${response.status}`); + } + + const total = parseInt(response.headers.get('content-length') || '', 10); + const artifactDownloadStream = Readable.fromWeb(response.body); + + try { + const artifactZipPath = path.join(tempDir, `${artifactName}.zip`); + const artifactFileStream = fs.createWriteStream(artifactZipPath); + if (process.env.CI) { + await pipeline(artifactDownloadStream, artifactFileStream); + } else { + await pipeline( + artifactDownloadStream, + progressStream(total, '[:bar] :mbRateMB/s :percent :etas'), + artifactFileStream, + ); + } + + // Extract artifact zip + d('unzipping artifact to %s', tempDir); + await extractZip(artifactZipPath, { dir: tempDir }); + + // Check if dist.zip exists within the extracted artifact + const distZipPath = path.join(tempDir, 'dist.zip'); + if (!(await fs.promises.stat(distZipPath).catch(() => false))) { + throw new Error(`dist.zip not found in build artifact.`); + } + + // Extract dist.zip + // NOTE: 'extract-zip' is used as it correctly extracts symlinks. + d('unzipping dist.zip to %s', outputDir); + await extractZip(distZipPath, { dir: outputDir }); + + const platformExecutables: Partial> = { + win32: 'electron.exe', + darwin: 'Electron.app/', + linux: 'electron', + }; + + const executableName = platformExecutables[options.platform]; + if (!executableName) { + throw new Error(`Unable to find executable for platform '${options.platform}'`); + } + + const executablePath = path.join(outputDir, executableName); + if (!(await fs.promises.stat(executablePath).catch(() => false))) { + throw new Error(`${executableName} not found within dist.zip.`); + } + + console.log(`${color.success} Downloaded to ${outputDir}`); + } catch (error) { + logError(error as Error); + process.exitCode = 1; // wait for cleanup + } finally { + // Cleanup temporary files + try { + await fs.promises.rm(tempDir, { recursive: true }); + } catch { + // ignore + } + } + }, + ); + +program.parse(process.argv); diff --git a/src/e-rcv.js b/src/e-rcv.ts similarity index 80% rename from src/e-rcv.js rename to src/e-rcv.ts index 974111cc..004c31dd 100644 --- a/src/e-rcv.js +++ b/src/e-rcv.ts @@ -1,18 +1,20 @@ #!/usr/bin/env node -const { Octokit } = require('@octokit/rest'); -const chalk = require('chalk').default; -const program = require('commander'); -const fs = require('fs'); -const inquirer = require('inquirer'); -const path = require('path'); -const os = require('os'); - -const evmConfig = require('./evm-config'); -const { spawnSync } = require('./utils/depot-tools'); -const { getGerritPatchDetailsFromURL } = require('./utils/gerrit'); -const { getGitHubAuthToken } = require('./utils/github-auth'); -const { color, fatal } = require('./utils/logging'); +import { Octokit } from '@octokit/rest'; +import chalk from 'chalk'; +import { program } from 'commander'; +import fs from 'node:fs'; +import inquirer from 'inquirer'; +import path from 'node:path'; +import os from 'node:os'; + +import * as evmConfig from './evm-config.js'; +import { depotSpawnSync } from './utils/depot-tools.js'; +import { getGerritPatchDetailsFromURL } from './utils/gerrit.js'; +import { getGitHubAuthToken } from './utils/github-auth.js'; +import { color, fatal } from './utils/logging.js'; +import { EVMBaseElectronConfiguration } from './evm-config.schema.js'; +import { SpawnSyncOptions } from 'node:child_process'; const ELECTRON_BOT_EMAIL = 'electron-bot@users.noreply.github.com'; const ELECTRON_BOT_NAME = 'Electron Bot'; @@ -23,25 +25,25 @@ const ELECTRON_REPO_DATA = { const DEPS_REGEX = new RegExp(`chromium_version':\n +'(.+?)',`, 'm'); const CL_REGEX = /https:\/\/chromium-review\.googlesource\.com\/c\/chromium\/src\/\+\/(\d+)/; -async function getChromiumVersion(octokit, ref) { +async function getChromiumVersion(octokit: Octokit, ref: string) { const { data } = await octokit.repos.getContent({ ...ELECTRON_REPO_DATA, path: 'DEPS', ref, }); - if (!data.content) { + if (!('content' in data) || !data.content) { fatal('Could not read content of PR'); return; } - const [, version] = DEPS_REGEX.exec(Buffer.from(data.content, 'base64').toString('utf8')); + const [, version] = DEPS_REGEX.exec(Buffer.from(data.content, 'base64').toString('utf8')) ?? []; - return version; + return version || null; } // Copied from https://github.com/electron/electron/blob/3a3595f2af59cb08fb09e3e2e4b7cdf713db2b27/script/release/notes/notes.ts#L605-L623 -const compareChromiumVersions = (v1, v2) => { +const compareChromiumVersions = (v1: string, v2: string): 1 | -1 | 0 => { const [split1, split2] = [v1.split('.'), v2.split('.')]; if (split1.length !== split2.length) { @@ -61,8 +63,13 @@ const compareChromiumVersions = (v1, v2) => { return 0; }; -function gitCommit(config, commitMessage, opts, fatalMessage) { - spawnSync( +function gitCommit( + config: EVMBaseElectronConfiguration, + commitMessage: string, + opts: SpawnSyncOptions, + fatalMessage: string, +) { + depotSpawnSync( config, 'git', [ @@ -89,7 +96,6 @@ program const prNumber = parseInt(prNumberStr, 10); if (isNaN(prNumber) || `${prNumber}` !== prNumberStr) { fatal(`rcv requires a PR number, "${prNumberStr}" was provided`); - return; } const octokit = new Octokit({ @@ -101,15 +107,19 @@ program }); if (!pr.merge_commit_sha) { fatal('No merge SHA available on PR'); - return; } const initialVersion = await getChromiumVersion(octokit, pr.base.sha); + if (!initialVersion) { + fatal('Could not find initial Chromium version in PR base'); + } const newVersion = await getChromiumVersion(octokit, pr.head.sha); + if (!newVersion) { + fatal('Could not find new Chromium version in PR head'); + } if (initialVersion === newVersion) { fatal('Does not look like a Chromium roll PR'); - return; } // Versions in the roll PR might span multiple milestones @@ -123,7 +133,7 @@ program await fetch( `https://chromiumdash.appspot.com/fetch_releases?channel=Canary&platform=Linux,Mac,Win32,Windows&milestone=${milestone}&num=1000`, ) - .then((resp) => resp.json()) + .then((resp) => resp.json() as Promise<{ version: string }[]>) .then((versions) => versions.map(({ version }) => version)), ); chromiumVersions.push(...milestoneVersions); @@ -173,8 +183,8 @@ program GIT_COMMITTER_EMAIL: ELECTRON_BOT_EMAIL, GIT_COMMITTER_NAME: ELECTRON_BOT_NAME, }, - }; - const gitStatusResult = spawnSync(config, 'git', ['status', '--porcelain'], spawnOpts); + } as const; + const gitStatusResult = depotSpawnSync(config, 'git', ['status', '--porcelain'], spawnOpts); if (gitStatusResult.status !== 0 || gitStatusResult.stdout.toString().trim().length !== 0) { fatal( "Your current git working directory is not clean, we won't erase your local changes. Clean it up and try again", @@ -197,18 +207,24 @@ program targetSha = mergeCommit.parents[0].sha; } - spawnSync( + depotSpawnSync( config, 'git', ['fetch', 'origin', targetSha], spawnOpts, 'Failed to fetch upstream base', ); - spawnSync(config, 'git', ['checkout', targetSha], spawnOpts, 'Failed to checkout base commit'); + depotSpawnSync( + config, + 'git', + ['checkout', targetSha], + spawnOpts, + 'Failed to checkout base commit', + ); const rcvBranch = `rcv/pr/${prNumber}/version/${chromiumVersionStr}`; - spawnSync(config, 'git', ['branch', '-D', rcvBranch], spawnOpts); - spawnSync( + depotSpawnSync(config, 'git', ['branch', '-D', rcvBranch], spawnOpts); + depotSpawnSync( config, 'git', ['checkout', '-b', rcvBranch], @@ -216,7 +232,13 @@ program `Failed to checkout new branch "${rcvBranch}"`, ); - spawnSync(config, 'yarn', ['install'], spawnOpts, 'Failed to do "yarn install" on new branch'); + depotSpawnSync( + config, + 'yarn', + ['install'], + spawnOpts, + 'Failed to do "yarn install" on new branch', + ); // Update the Chromium version in DEPS const regexToReplace = new RegExp(`(chromium_version':\n +').+?',`, 'gm'); @@ -225,7 +247,7 @@ program await fs.promises.writeFile(path.resolve(spawnOpts.cwd, 'DEPS'), newContent, 'utf8'); // Make a commit with this change - spawnSync(config, 'git', ['add', 'DEPS'], spawnOpts, 'Failed to add DEPS file for commit'); + depotSpawnSync(config, 'git', ['add', 'DEPS'], spawnOpts, 'Failed to add DEPS file for commit'); gitCommit( config, `chore: bump chromium to ${chromiumVersionStr}`, @@ -247,13 +269,15 @@ program if (clMatch) { const parsedUrl = new URL(clMatch[0]); - const { shortCommit: chromiumShortSha } = await getGerritPatchDetailsFromURL(parsedUrl); + const { shortCommit: chromiumShortSha } = await getGerritPatchDetailsFromURL( + parsedUrl, + false, + ); const { commits: chromiumCommits } = await fetch( `https://chromiumdash.appspot.com/fetch_commits?commit=${chromiumShortSha}`, - ).then((resp) => resp.json()); + ).then((resp) => resp.json() as Promise<{ commits: { earliest: string; time: number }[] }>); if (chromiumCommits.length !== 1) { fatal(`Expected to find exactly one commit for SHA "${chromiumShortSha}"`); - return; } // Grab the earliest Chromium version the CL was released in, and the merge time const { earliest, time } = chromiumCommits[0]; @@ -280,14 +304,14 @@ program } for (const commit of commitsToCherryPick) { - spawnSync( + depotSpawnSync( config, 'git', ['fetch', 'origin', commit.sha], spawnOpts, 'Failed to fetch commit to cherry-pick', ); - spawnSync( + depotSpawnSync( config, 'git', [ @@ -302,19 +326,24 @@ program } // Update filenames now that the commits have been cherry-picked - spawnSync( + depotSpawnSync( config, 'node', ['script/gen-hunspell-filenames.js'], spawnOpts, 'Failed to generate hunspell filenames', ); - const hunspellGitStatusResult = spawnSync(config, 'git', ['status', '--porcelain'], spawnOpts); + const hunspellGitStatusResult = depotSpawnSync( + config, + 'git', + ['status', '--porcelain'], + spawnOpts, + ); if ( hunspellGitStatusResult.status !== 0 || hunspellGitStatusResult.stdout.toString().trim().length !== 0 ) { - spawnSync( + depotSpawnSync( config, 'git', ['add', 'filenames.hunspell.gni'], @@ -329,19 +358,24 @@ program ); } - spawnSync( + depotSpawnSync( config, 'node', ['script/gen-libc++-filenames.js'], spawnOpts, 'Failed to generate libc++ filenames', ); - const genLibCxxStatusResult = spawnSync(config, 'git', ['status', '--porcelain'], spawnOpts); + const genLibCxxStatusResult = depotSpawnSync( + config, + 'git', + ['status', '--porcelain'], + spawnOpts, + ); if ( genLibCxxStatusResult.status !== 0 || genLibCxxStatusResult.stdout.toString().trim().length !== 0 ) { - spawnSync( + depotSpawnSync( config, 'git', ['add', 'filenames.libcxx.gni', 'filenames.libcxxabi.gni'], diff --git a/src/e-show.js b/src/e-show.ts similarity index 75% rename from src/e-show.js rename to src/e-show.ts index 640cd623..f25281f7 100755 --- a/src/e-show.js +++ b/src/e-show.ts @@ -1,18 +1,18 @@ #!/usr/bin/env node -const childProcess = require('child_process'); -const open = require('open'); -const os = require('os'); -const path = require('path'); -const program = require('commander'); +import childProcess, { ExecFileSyncOptions } from 'node:child_process'; +import { program } from 'commander'; +import os from 'node:os'; +import path from 'node:path'; -const evmConfig = require('./evm-config'); -const { color, fatal } = require('./utils/logging'); -const depot = require('./utils/depot-tools'); +import * as evmConfig from './evm-config.js'; +import { color, fatal } from './utils/logging.js'; +import { depotPath, depotOpts } from './utils/depot-tools.js'; +import { EVMBaseElectronConfiguration } from './evm-config.schema.js'; -function gitStatus(config) { +function gitStatus(config: EVMBaseElectronConfiguration): string { const exec = 'git'; - const opts = { + const opts: ExecFileSyncOptions = { cwd: path.resolve(config.root, 'src', 'electron'), encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], @@ -25,7 +25,7 @@ function gitStatus(config) { const outs = []; for (const args of switches) { try { - outs.push(childProcess.execFileSync(exec, args, opts)); + outs.push(childProcess.execFileSync(exec, args, opts) as string); } catch {} } return outs @@ -52,7 +52,7 @@ program const txt = parts.join(', '); if (txt) console.log(txt); } catch (e) { - fatal(e); + fatal(e as Error); } }); @@ -61,14 +61,14 @@ program .alias('ls') .description('Show installed build config') .action(() => { - let current; + let current: string; try { current = evmConfig.currentName(); } catch { // maybe there is no current config } try { - const names = evmConfig.names(); + const names = evmConfig.possibleNames(); if (names.length === 0) { console.log('No build configs found. (You can create one with `e init`)'); } else { @@ -78,14 +78,14 @@ program .forEach((name) => console.log(name)); } } catch (e) { - fatal(e); + fatal(e as Error); } }); program .command('depotdir') .description('Show path of the depot-tools directory') - .action(() => console.log(depot.path)); + .action(() => console.log(depotPath)); program .command('env') @@ -93,13 +93,13 @@ program .option('--json', 'Output as JSON') .action((options) => { try { - const { env } = depot.opts(evmConfig.current()); + const { env } = depotOpts(evmConfig.current()); // This command shows the difference between the current // process.env and the env that is needed for running commands - for (const key of Object.keys(env)) { - if (process.env[key] === env[key]) { - delete env[key]; + for (const key of Object.keys(env!)) { + if (process.env[key] === env![key]) { + delete env![key]; } } @@ -107,11 +107,12 @@ program console.log(JSON.stringify(env, null, 2)); } else { const exportKeyword = os.platform() === 'win32' ? 'set' : 'export'; - const logger = ([key, val]) => console.log(`${exportKeyword} ${key}=${val}`); - Object.entries(env).forEach(logger); + const logger = ([key, val]: [string, string | undefined]) => + console.log(`${exportKeyword} ${key}=${val}`); + Object.entries(env!).forEach(logger); } } catch (e) { - fatal(e); + fatal(e as Error); } }); @@ -123,7 +124,7 @@ program try { console.log(color.path(evmConfig.execOf(evmConfig.current()))); } catch (e) { - fatal(e); + fatal(e as Error); } }); @@ -134,7 +135,7 @@ program try { console.log(color.path(evmConfig.current().root)); } catch (e) { - fatal(e); + fatal(e as Error); } }); @@ -147,7 +148,7 @@ program name = name || 'electron'; console.log(color.path(path.resolve(root, 'src', name))); } catch (e) { - fatal(e); + fatal(e as Error); } }); @@ -163,7 +164,7 @@ program console.log(evmConfig.current().gen.out); } } catch (e) { - fatal(e); + fatal(e as Error); } }); diff --git a/src/e-sync.js b/src/e-sync.ts similarity index 58% rename from src/e-sync.js rename to src/e-sync.ts index a0f2ebb2..ca1c894e 100644 --- a/src/e-sync.js +++ b/src/e-sync.ts @@ -1,20 +1,20 @@ #!/usr/bin/env node -const cp = require('child_process'); -const path = require('path'); -const program = require('commander'); +import { execSync } from 'node:child_process'; +import path from 'node:path'; +import { program } from 'commander'; -const evmConfig = require('./evm-config'); -const { fatal } = require('./utils/logging'); -const { ensureDir } = require('./utils/paths'); -const depot = require('./utils/depot-tools'); -const { configureReclient } = require('./utils/setup-reclient-chromium'); -const { ensureSDK } = require('./utils/sdk'); +import * as evmConfig from './evm-config.js'; +import { fatal } from './utils/logging.js'; +import { ensureDir } from './utils/paths.js'; +import { ensureDepotTools, depotSpawnSync } from './utils/depot-tools.js'; +import { configureReclient } from './utils/setup-reclient-chromium.js'; +import { ensureSDK } from './utils/sdk.js'; -function setRemotes(cwd, repo) { +function setRemotes(cwd: string, repo: Record): void { // Confirm that cwd is the git root const gitRoot = path.normalize( - cp.execSync('git rev-parse --show-toplevel', { cwd }).toString().trim(), + execSync('git rev-parse --show-toplevel', { cwd }).toString().trim(), ); if (gitRoot !== cwd) { @@ -24,21 +24,21 @@ function setRemotes(cwd, repo) { for (const remote in repo) { // First check that the fork remote exists. if (remote === 'fork') { - const remotes = cp.execSync('git remote', { cwd }).toString().trim().split('\n'); + const remotes = execSync('git remote', { cwd }).toString().trim().split('\n'); // If we've not added the fork remote, add it instead of updating the url. if (!remotes.includes('fork')) { - cp.execSync(`git remote add ${remote} ${repo[remote]}`, { cwd }); + execSync(`git remote add ${remote} ${repo[remote]}`, { cwd }); break; } } - cp.execSync(`git remote set-url ${remote} ${repo[remote]}`, { cwd }); - cp.execSync(`git remote set-url --push ${remote} ${repo[remote]}`, { cwd }); + execSync(`git remote set-url ${remote} ${repo[remote]}`, { cwd }); + execSync(`git remote set-url --push ${remote} ${repo[remote]}`, { cwd }); } } -function runGClientSync(syncArgs, syncOpts) { +function runGClientSync(syncArgs: string[], syncOpts: { threeWay: boolean }): void { const config = evmConfig.current(); const srcdir = path.resolve(config.root, 'src'); ensureDir(srcdir); @@ -47,7 +47,7 @@ function runGClientSync(syncArgs, syncOpts) { ensureDir(config.env.GIT_CACHE_PATH); } - depot.ensure(); + ensureDepotTools(); if (process.platform === 'darwin') { ensureSDK(); @@ -68,12 +68,14 @@ function runGClientSync(syncArgs, syncOpts) { } : {}, }; - depot.spawnSync(config, exec, args, opts, 'gclient sync failed'); + depotSpawnSync(config, exec, args, opts, 'gclient sync failed'); // Only set remotes if we're building an Electron target. if (config.defaultTarget !== 'chrome') { const electronPath = path.resolve(srcdir, 'electron'); - setRemotes(electronPath, config.remotes.electron); + if (config.remotes?.electron) { + setRemotes(electronPath, config.remotes.electron); + } } } @@ -90,7 +92,7 @@ program const { threeWay } = options; runGClientSync(gclientArgs, { threeWay }); } catch (e) { - fatal(e); + fatal(e as Error); } }) .parse(process.argv); diff --git a/src/e-test.js b/src/e-test.ts similarity index 78% rename from src/e-test.js rename to src/e-test.ts index a5ada0c1..34fc7806 100644 --- a/src/e-test.js +++ b/src/e-test.ts @@ -1,14 +1,19 @@ #!/usr/bin/env node -const childProcess = require('child_process'); -const path = require('path'); -const program = require('commander'); +import childProcess from 'node:child_process'; +import path from 'node:path'; +import { program, Option } from 'commander'; -const evmConfig = require('./evm-config'); -const { ensureNodeHeaders } = require('./utils/headers'); -const { color, fatal } = require('./utils/logging'); +import * as evmConfig from './evm-config.js'; +import { ensureNodeHeaders } from './utils/headers.js'; +import { color, fatal } from './utils/logging.js'; +import { EVMBaseElectronConfiguration } from './evm-config.schema.js'; -function runSpecRunner(config, script, runnerArgs) { +function runSpecRunner( + config: EVMBaseElectronConfiguration, + script: string, + runnerArgs: string[], +): void { const exec = process.execPath; const args = [script, ...runnerArgs]; const opts = { @@ -18,7 +23,7 @@ function runSpecRunner(config, script, runnerArgs) { env: { ELECTRON_OUT_DIR: config.gen.out, npm_config_node_gyp: path.resolve( - __dirname, + import.meta.dirname, '..', 'node_modules', 'node-gyp', @@ -28,7 +33,7 @@ function runSpecRunner(config, script, runnerArgs) { ...process.env, ...config.env, }, - }; + } as const; console.log(color.childExec(exec, args, opts)); childProcess.execFileSync(exec, args, opts); } @@ -44,7 +49,7 @@ program 'Build test runner components (e.g. electron:node_headers) without remote execution', ) .addOption( - new program.Option( + new Option( '--runners ', 'A subset of tests to run - not used with either the node or nan specs', ).choices(['main', 'native']), @@ -75,7 +80,7 @@ program } runSpecRunner(config, script, specRunnerArgs); } catch (e) { - fatal(e); + fatal(e as Error); } }) .parse(process.argv); diff --git a/src/e.bat b/src/e.bat deleted file mode 100644 index 2b8d6554..00000000 --- a/src/e.bat +++ /dev/null @@ -1,6 +0,0 @@ -@echo off - -setlocal - -:: Defer Control. -node "%~dp0\e" %* diff --git a/src/e.js b/src/e.js new file mode 100644 index 00000000..de213c9c --- /dev/null +++ b/src/e.js @@ -0,0 +1,9 @@ +import path from 'node:path'; + +// Hacky shim to route src/e --> dist/e +process.argv[1] = process.argv[1].replace( + import.meta.dirname, + path.resolve(import.meta.dirname, '../dist'), +); + +import('../dist/e.js'); diff --git a/src/e b/src/e.ts similarity index 85% rename from src/e rename to src/e.ts index 8cf33af4..ccab60d7 100755 --- a/src/e +++ b/src/e.ts @@ -1,15 +1,16 @@ #!/usr/bin/env node -const cp = require('child_process'); -const fs = require('fs'); -const program = require('commander'); -const path = require('path'); +import cp from 'node:child_process'; +import fs from 'node:fs'; +import { program } from 'commander'; +import path from 'node:path'; -const evmConfig = require('./evm-config'); -const { color, fatal } = require('./utils/logging'); -const depot = require('./utils/depot-tools'); -const { refreshPathVariable } = require('./utils/refresh-path'); -const { ensureSDK } = require('./utils/sdk'); +import { ensureDepotTools, depotSpawnSync } from './utils/depot-tools.js'; +import { color, fatal } from './utils/logging.js'; +import { refreshPathVariable } from './utils/refresh-path.js'; +import { ensureSDK } from './utils/sdk.js'; + +import * as evmConfig from './evm-config.js'; // Refresh the PATH variable at the top of this shell so that retries in the same shell get the latest PATH variable refreshPathVariable(); @@ -20,7 +21,7 @@ function maybeCheckForUpdates() { // NB: send updater's stdout to stderr so its log messages are visible // but don't pollute stdout. For example, calling `FOO="$(e show exec)"` // should not get a FOO that includes "Checking for build-tools updates". - const disableAutoUpdatesFile = path.resolve(__dirname, '..', '.disable-auto-updates'); + const disableAutoUpdatesFile = path.resolve(import.meta.dirname, '..', '.disable-auto-updates'); if (fs.existsSync(disableAutoUpdatesFile)) { console.error(`${color.info} Auto-updates disabled - skipping update check`); return; @@ -36,7 +37,7 @@ function maybeCheckForUpdates() { // Don't check if we already checked recently const intervalHours = 4; - const updateCheckTSFile = path.resolve(__dirname, '..', '.update'); + const updateCheckTSFile = path.resolve(import.meta.dirname, '..', '.update'); const lastCheckEpochMsec = fs.existsSync(updateCheckTSFile) ? parseInt(fs.readFileSync(updateCheckTSFile, 'utf8'), 10) : 0; @@ -51,7 +52,7 @@ function maybeCheckForUpdates() { // but don't pollute stdout. For example, calling `FOO="$(e show exec)"` // should not get a FOO that includes "Checking for build-tools updates". cp.spawnSync(process.execPath, ['e-auto-update.js'], { - cwd: __dirname, + cwd: import.meta.dirname, stdio: [0, 2, 2], }); @@ -67,11 +68,11 @@ function maybeCheckForUpdates() { stdio: 'inherit', }, ); - process.exit(result.status); + process.exit(result.status ?? 1); } maybeCheckForUpdates(); -evmConfig.resetShouldWarn(); +evmConfig.warnAboutNextConfigChange(); program.description('Electron build tool').usage(' [commandArgs...]'); @@ -95,11 +96,11 @@ program .action((args) => { try { const exec = evmConfig.execOf(evmConfig.current()); - const opts = { stdio: 'inherit' }; + const opts = { stdio: 'inherit' } as const; console.log(color.childExec(exec, args, opts)); cp.execFileSync(exec, args, opts); } catch (e) { - fatal(e); + fatal(e as Error); } }) .on('--help', () => { @@ -121,11 +122,11 @@ program const opts = { env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' }, stdio: 'inherit', - }; + } as const; console.log(color.childExec(exec, args, opts)); cp.execFileSync(exec, args, opts); } catch (e) { - fatal(e); + fatal(e as Error); } }) .on('--help', () => { @@ -143,11 +144,11 @@ program .description('Use build config when running other `e` commands') .action((name) => { try { - evmConfig.setCurrent(name); + evmConfig.setCurrentConfig(name); console.log(`Now using config ${color.config(name)}`); process.exit(0); } catch (e) { - fatal(e); + fatal(e as Error); } }); @@ -157,11 +158,11 @@ program .description('Remove build config from list') .action((name) => { try { - evmConfig.remove(name); + evmConfig.removeConfig(name); console.log(`Removed config ${color.config(name)}`); process.exit(0); } catch (e) { - fatal(e); + fatal(e as Error); } }); @@ -203,7 +204,7 @@ program console.log(`${color.success} Sanitized contents of ${color.config(configName)}`); process.exit(0); } catch (e) { - fatal(e); + fatal(e as Error); } }); @@ -220,7 +221,7 @@ program fatal(`Must provide a command to 'e npm'`); } - const { status, error } = depot.spawnSync(evmConfig.current(), args[0], args.slice(1), { + const { status, error } = depotSpawnSync(evmConfig.current(), args[0], args.slice(1), { stdio: 'inherit', env: { ELECTRON_OVERRIDE_DIST_PATH: evmConfig.outDir(evmConfig.current()), @@ -231,7 +232,7 @@ program let errorMsg = `Failed to run command:`; if (status !== null) errorMsg += `\n Exit Code: "${status}"`; if (error) errorMsg += `\n ${error}`; - fatal(errorMsg, status); + fatal(errorMsg, status ?? 1); } process.exit(0); @@ -243,7 +244,7 @@ program "Launch a shell environment populated with build-tools' environment variables and context", ) .action(() => { - depot.ensure(); + ensureDepotTools(); if (!['linux', 'darwin'].includes(process.platform)) { fatal(`'e shell' is not supported on non-unix platforms`); @@ -254,14 +255,14 @@ program } console.info(`Launching build-tools shell with ${color.cmd(process.env.SHELL)}`); - const { status } = depot.spawnSync(evmConfig.current(), process.env.SHELL, [], { + const { status } = depotSpawnSync(evmConfig.current(), process.env.SHELL, [], { stdio: 'inherit', env: { ...process.env, SHELL_CONTEXT: evmConfig.currentName(), }, }); - process.exit(status); + process.exit(status ?? 1); }); program.on('--help', () => { diff --git a/src/evm-config.schema.d.ts b/src/evm-config.schema.d.ts new file mode 100644 index 00000000..4605115a --- /dev/null +++ b/src/evm-config.schema.d.ts @@ -0,0 +1,43 @@ +type EVMExtendsConfiguration = { + extends: string; +} & Partial; + +type EVMBaseElectronConfiguration = { + $schema: string; + root: string; + remoteBuild: 'reclient' | 'siso' | 'none'; + + defaultTarget?: string; + preserveSDK?: number; + execName?: string; + + rbeHelperPath?: string; + rbeServiceAddress?: string; + remotes?: { + electron: { + fork?: string; + origin: string; + }; + }; + gen: { + args: string[]; + out: string; + }; + env: { + GIT_CACHE_PATH?: string; + CHROMIUM_BUILDTOOLS_PATH: string; + [k: string]: string | undefined; + }; + configValidationLevel?: 'strict' | 'warn' | 'none'; +}; + +export type EVMMaybeOutdatedBaseElectronConfiguration = EVMBaseElectronConfiguration & { + preserveXcode?: number; + onlySdk?: boolean; + reclient?: 'remote_exec' | 'none'; + reclientHelperPath?: string; + reclientServiceAddress?: string; +}; + +export type EVMConfigurationSchema = EVMBaseElectronConfiguration | EVMExtendsConfiguration; +export type EVMResolvedConfiguration = EVMBaseElectronConfiguration; diff --git a/src/evm-config.js b/src/evm-config.ts similarity index 62% rename from src/evm-config.js rename to src/evm-config.ts index b843c3c1..4685e800 100644 --- a/src/evm-config.js +++ b/src/evm-config.ts @@ -1,51 +1,69 @@ -const fs = require('fs'); -const os = require('os'); -const path = require('path'); -const Ajv = require('ajv'); -const YAML = require('yaml'); -const { URI } = require('vscode-uri'); -const { color, fatal } = require('./utils/logging'); -const { ensureDir } = require('./utils/paths'); - -const configRoot = () => process.env.EVM_CONFIG || path.resolve(__dirname, '..', 'configs'); -const schema = require('../evm-config.schema.json'); -const ajv = require('ajv-formats')(new Ajv()); - -let shouldWarn = true; - -const resetShouldWarn = () => { - shouldWarn = true; +import { Ajv, AnySchema } from 'ajv'; +import ajvFormats from 'ajv-formats'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import YAML from 'yaml'; +import vscode from 'vscode-uri'; + +import { color, fatal } from './utils/logging.js'; +import { ensureDir } from './utils/paths.js'; +import { + EVMBaseElectronConfiguration, + EVMConfigurationSchema, + EVMMaybeOutdatedBaseElectronConfiguration, + EVMResolvedConfiguration, +} from './evm-config.schema.js'; + +const { URI } = vscode; + +const configRoot = () => + process.env.EVM_CONFIG || path.resolve(import.meta.dirname, '..', 'configs'); +const schema: AnySchema = JSON.parse( + fs.readFileSync(path.resolve(import.meta.dirname, '..', 'evm-config.schema.json'), 'utf8'), +); +const ajv = (ajvFormats as any)(new Ajv()); + +let warnAboutAutomaticConfigChanges = true; + +export const warnAboutNextConfigChange = () => { + warnAboutAutomaticConfigChanges = true; }; // If you want your shell sessions to each have different active configs, // try this in your ~/.profile or ~/.zshrc or ~/.bashrc: // export EVM_CURRENT_FILE="$(mktemp --tmpdir evm-current.XXXXXXXX.txt)" -const currentFiles = [ - process.env.EVM_CURRENT_FILE, - path.resolve(configRoot(), 'evm-current.txt'), -].filter(Boolean); +const currentFiles: string[] = [path.resolve(configRoot(), 'evm-current.txt')]; +if (process.env.EVM_CURRENT_FILE) { + currentFiles.unshift(process.env.EVM_CURRENT_FILE); +} -const getDefaultTarget = () => { +export function getDefaultTarget(): string { const name = getCurrentFileName(); const result = name ? sanitizeConfigWithName(name).defaultTarget : null; return result || 'electron'; -}; +} -function buildPath(name, suffix) { +function buildPath(name: string, suffix: string): string { return path.resolve(configRoot(), `evm.${name}.${suffix}`); } -function buildPathCandidates(name) { +function buildPathCandidates(name: string): string[] { const suffixes = ['json', 'yml', 'yaml']; return suffixes.map((suffix) => buildPath(name, suffix)); } -function mergeConfigs(target, source) { +function mergeConfigs(target: T, source: T): T { for (const key in source) { if (Array.isArray(target[key]) && Array.isArray(source[key])) { - target[key] = target[key].concat(source[key]); - } else if (typeof target[key] === 'object' && typeof source[key] === 'object') { + target[key] = target[key].concat(source[key]) as any; + } else if ( + typeof target[key] === 'object' && + typeof source[key] === 'object' && + target[key] !== null && + source[key] !== null + ) { target[key] = mergeConfigs(target[key], source[key]); } else { target[key] = source[key]; @@ -55,18 +73,18 @@ function mergeConfigs(target, source) { } // get the existing filename if it exists; otherwise the preferred name -function pathOf(name) { +export function pathOf(name: string): string { const files = buildPathCandidates(name).filter((file) => fs.existsSync(file)); const preferredFormat = process.env.EVM_FORMAT || 'json'; // yaml yml json return files[0] || buildPath(name, preferredFormat); } -function filenameToConfigName(filename) { +function filenameToConfigName(filename: string): string | null { const match = filename.match(/^evm\.(.*)\.(?:json|yml|yaml)$/); return match ? match[1] : null; } -function testConfigExists(name) { +function testConfigExists(name: string): void | never { if (!fs.existsSync(pathOf(name))) { fatal( `Build config ${color.config(name)} not found. (Tried ${buildPathCandidates(name) @@ -76,7 +94,7 @@ function testConfigExists(name) { } } -function save(name, o) { +export function saveConfig(name: string, o: EVMConfigurationSchema): void { ensureDir(configRoot()); const filename = pathOf(name); const isJSON = path.extname(filename) === '.json'; @@ -84,35 +102,38 @@ function save(name, o) { fs.writeFileSync(filename, txt); } -function setCurrent(name) { +export function setCurrentConfig(name: string): void { testConfigExists(name); try { - currentFiles.forEach((filename) => fs.writeFileSync(filename, `${name}\n`)); + for (const filename of currentFiles) { + fs.writeFileSync(filename, `${name}\n`); + } } catch (e) { - fatal(`Unable to set config ${color.config(name)}: `, e); + fatal(`Unable to set config ${color.config(name)}: ${e}`); } } -function names() { +export function possibleNames(): string[] { if (!fs.existsSync(configRoot())) return []; return fs .readdirSync(configRoot()) .map((filename) => filenameToConfigName(filename)) - .filter((name) => name) + .filter((name): name is string => name !== null) .sort(); } -function getCurrentFileName() { - return currentFiles.reduce((name, filename) => { +function getCurrentFileName(): string | null { + for (const filename of currentFiles) { try { - return name || fs.readFileSync(filename, { encoding: 'utf8' }).trim(); - } catch (e) { - return; + return fs.readFileSync(filename, 'utf-8').trim(); + } catch { + // Ignore } - }, null); + } + return null; } -function currentName() { +export function currentName(): string { // Return the contents of the first nonempty file in currentFiles. const name = getCurrentFileName(); @@ -120,11 +141,11 @@ function currentName() { fatal('No current build configuration.'); } -function outDir(config) { +export function outDir(config: EVMResolvedConfiguration): string { return path.resolve(config.root, 'src', 'out', config.gen.out); } -function execOf(config) { +export function execOf(config: EVMResolvedConfiguration): string { const execName = (config.execName || 'electron').toLowerCase(); const builddir = outDir(config); switch (os.type()) { @@ -138,16 +159,16 @@ function execOf(config) { } } -function maybeExtendConfig(config) { - if (config.extends) { +function maybeExtendConfig(config: EVMConfigurationSchema): EVMConfigurationSchema { + if ('extends' in config) { const deeperConfig = maybeExtendConfig(loadConfigFileRaw(config.extends)); - delete config.extends; - return mergeConfigs(config, deeperConfig); + const { extends: _, ...restConfig } = config; + return mergeConfigs(restConfig, deeperConfig) as EVMConfigurationSchema; } return config; } -function loadConfigFileRaw(name) { +function loadConfigFileRaw(name: string): EVMConfigurationSchema { const configPath = pathOf(name); if (!fs.existsSync(configPath)) { @@ -158,7 +179,7 @@ function loadConfigFileRaw(name) { return maybeExtendConfig(YAML.parse(configContents)); } -function validateConfig(config) { +export function validateConfig(config: Partial) { if (config.configValidationLevel === 'none') { return; } @@ -168,26 +189,32 @@ function validateConfig(config) { if (!validate(config)) { return validate.errors; } + return null; } -function setEnvVar(name, key, value) { - const config = loadConfigFileRaw(name); +export function setEnvVar(name: string, key: string, value: string): void { + const config = sanitizeConfigWithName(name); - config.env = config.env || {}; config.env[key] = value; - save(name, config); + saveConfig(name, config); } -function sanitizeConfig(name, config, overwrite = false) { - const changes = []; +export function sanitizeConfig( + name: string, + config: Partial, + overwrite = false, +): EVMBaseElectronConfiguration { + const changes: string[] = []; if (!config.configValidationLevel) { config.configValidationLevel = 'strict'; } if (!('$schema' in config)) { - config.$schema = URI.file(path.resolve(__dirname, '..', 'evm-config.schema.json')).toString(); + config.$schema = URI.file( + path.resolve(import.meta.dirname, '..', 'evm-config.schema.json'), + ).toString(); changes.push(`added missing property ${color.config('$schema')}`); } @@ -232,9 +259,10 @@ function sanitizeConfig(name, config, overwrite = false) { } } + config.gen ??= { args: [], out: 'Default' }; + config.gen.args ??= []; + if (config.remoteBuild !== 'none' && !hasRemoteExecGN) { - config.gen ??= {}; - config.gen.args ??= []; config.gen.args.push(remoteExecGnArg); changes.push(`added gn arg ${color.cmd(remoteExecGnArg)} needed by remoteexec`); } else if (config.remoteBuild === 'none' && hasRemoteExecGN) { @@ -243,8 +271,6 @@ function sanitizeConfig(name, config, overwrite = false) { } if (config.remoteBuild === 'siso' && !hasUseSisoGN) { - config.gen ??= {}; - config.gen.args ??= []; config.gen.args.push(useSisoGnArg); changes.push( `added gn arg ${color.cmd(useSisoGnArg)} needed by ${color.config('remoteBuild')} siso`, @@ -270,19 +296,23 @@ function sanitizeConfig(name, config, overwrite = false) { delete config.reclientServiceAddress; } - config.env ??= {}; + if (!config.root) { + fatal(`Config ${color.config(name)} is missing the required property ${color.config('root')}`); + } + + const toolsPath = path.resolve(config.root, 'src', 'buildtools'); + config.env ??= { CHROMIUM_BUILDTOOLS_PATH: toolsPath }; if (!config.env.CHROMIUM_BUILDTOOLS_PATH) { - const toolsPath = path.resolve(config.root, 'src', 'buildtools'); config.env.CHROMIUM_BUILDTOOLS_PATH = toolsPath; changes.push(`defined ${color.config('CHROMIUM_BUILDTOOLS_PATH')}`); } if (changes.length > 0) { if (overwrite) { - save(name, config); - } else if (shouldWarn) { - shouldWarn = false; + saveConfig(name, config as EVMBaseElectronConfiguration); + } else if (warnAboutAutomaticConfigChanges) { + warnAboutAutomaticConfigChanges = false; console.warn(`${color.warn} We've made these temporary changes to your configuration:`); console.warn(changes.map((change) => ` * ${change}`).join('\n')); console.warn(`Run ${color.cmd('e sanitize-config')} to make these changes permanent.`); @@ -303,14 +333,14 @@ function sanitizeConfig(name, config, overwrite = false) { } } - return config; + return config as EVMBaseElectronConfiguration; } -function sanitizeConfigWithName(name, overwrite = false) { +export function sanitizeConfigWithName(name: string, overwrite = false) { return sanitizeConfig(name, loadConfigFileRaw(name), overwrite); } -function remove(name) { +export function removeConfig(name: string): void { testConfigExists(name); let currentConfigName; @@ -327,26 +357,19 @@ function remove(name) { try { return fs.unlinkSync(filename); } catch (e) { - fatal(`Unable to remove config ${color.config(name)}: `, e); + fatal(`Unable to remove config ${color.config(name)}: ${e}`); } } -module.exports = { - getDefaultTarget, - current: () => sanitizeConfigWithName(currentName()), - maybeCurrent: () => (getCurrentFileName() ? sanitizeConfigWithName(currentName()) : {}), - currentName, - execOf, - fetchByName: (name) => sanitizeConfigWithName(name), - names, - outDir, - pathOf, - remove, - resetShouldWarn, - sanitizeConfig, - sanitizeConfigWithName, - save, - setCurrent, - setEnvVar, - validateConfig, -}; +export function current(): EVMBaseElectronConfiguration { + return sanitizeConfigWithName(currentName()); +} + +export function maybeCurrent(): EVMBaseElectronConfiguration | null { + const currentFileName = getCurrentFileName(); + return currentFileName ? sanitizeConfigWithName(currentFileName) : null; +} + +export function fetchByName(name: string): EVMBaseElectronConfiguration { + return sanitizeConfigWithName(name); +} diff --git a/src/utils/arm.js b/src/utils/arm.ts similarity index 67% rename from src/utils/arm.js rename to src/utils/arm.ts index 4fe56804..880c32bc 100644 --- a/src/utils/arm.js +++ b/src/utils/arm.ts @@ -1,9 +1,9 @@ -const cp = require('child_process'); +import { execSync } from 'node:child_process'; // See https://developer.apple.com/documentation/apple-silicon/about-the-rosetta-translation-environment. -const getIsArm = () => { +export const getIsArm = () => { try { - const isCurrentlyTranslated = cp.execSync('sysctl sysctl.proc_translated', { stdio: 'pipe' }); + const isCurrentlyTranslated = execSync('sysctl sysctl.proc_translated', { stdio: 'pipe' }); return ( process.arch === 'arm64' || @@ -15,7 +15,3 @@ const getIsArm = () => { return false; } }; - -module.exports = { - getIsArm, -}; diff --git a/src/utils/crbug.js b/src/utils/crbug.ts similarity index 83% rename from src/utils/crbug.js rename to src/utils/crbug.ts index f8002238..5bd8f9b2 100644 --- a/src/utils/crbug.js +++ b/src/utils/crbug.ts @@ -1,13 +1,9 @@ -const fetch = require('node-fetch'); -const chrome = require('@marshallofsound/chrome-cookies-secure'); -const { fatal } = require('./logging'); - const BASE_URL = 'https://issues.chromium.org'; -const getPayload = (html, start, end) => +const getPayload = (html: string, start: string, end: string) => html.substring(html.indexOf(start) + start.length, html.indexOf(end)); -async function getXsrfToken(osid) { +async function getXsrfToken(osid: string) { const html = await fetch(`${BASE_URL}/issues`, { headers: { Cookie: `OSID=${osid}`, @@ -29,8 +25,10 @@ async function getXsrfToken(osid) { return parsed[2]; } -async function getBugInfo(bugNr) { +async function getBugInfo(bugNr: number) { const profile = process.env.CHROME_SECURITY_PROFILE ?? 'Profile 1'; + // @ts-expect-error + const chrome = await import('@marshallofsound/chrome-cookies-secure'); const { OSID } = await chrome.getCookiesPromised(BASE_URL, 'object', profile); const xsrfToken = await getXsrfToken(OSID); @@ -65,18 +63,20 @@ async function getBugInfo(bugNr) { return result; } -function parseCveFromIssue(issue) { +type RawIssue = [[never, any[]]]; + +function parseCveFromIssue(issue: RawIssue): string | null { const CVE_ID = 1223136; const issueData = issue[0][1]; const issueMetaData = issueData[issueData.length - 1]; - const cveData = issueMetaData[2][14].find((d) => d[0] === CVE_ID); + const cveData = issueMetaData[2][14].find((d: number[]) => d[0] === CVE_ID); const cve = cveData[cveData.length - 2]; return /\d{4}-\d{4,7}/.test(cve) ? `CVE-${cve}` : null; } -async function getCveForBugNr(bugNr) { +export async function getCveForBugNr(bugNr: number) { if (Number.isNaN(bugNr)) { throw new Error(`Invalid Chromium bug number ${bugNr}`); } @@ -88,7 +88,3 @@ async function getCveForBugNr(bugNr) { throw new Error(`Failed to fetch CVE for ${bugNr} - ${error}`); } } - -module.exports = { - getCveForBugNr, -}; diff --git a/src/utils/depot-tools.js b/src/utils/depot-tools.ts similarity index 73% rename from src/utils/depot-tools.js rename to src/utils/depot-tools.ts index b6ddc12f..41a76d68 100644 --- a/src/utils/depot-tools.js +++ b/src/utils/depot-tools.ts @@ -1,27 +1,35 @@ -const fs = require('fs'); -const path = require('path'); -const os = require('os'); -const childProcess = require('child_process'); -const pathKey = require('path-key'); - -const { color, fatal } = require('./logging'); - -const defaultDepotPath = path.resolve(__dirname, '..', '..', 'third_party', 'depot_tools'); +import childProcess, { ExecFileSyncOptions } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import os from 'node:os'; + +import { EVMBaseElectronConfiguration } from '../evm-config.schema.js'; +import { color, fatal } from './logging.js'; +import { reclientEnv } from './reclient.js'; +import { sisoEnv } from './siso.js'; + +const defaultDepotPath = path.resolve( + import.meta.dirname, + '..', + '..', + 'third_party', + 'depot_tools', +); const DEPOT_TOOLS_DIR = process.env.DEPOT_TOOLS_DIR || defaultDepotPath; const markerFilePath = path.join(DEPOT_TOOLS_DIR, '.disable_auto_update'); -function updateDepotTools() { +function updateDepotTools(): void { const depot_dir = DEPOT_TOOLS_DIR; console.log(`Updating ${color.path(depot_dir)}`); if (os.platform() === 'win32') { - depotExecFileSync({}, 'cmd.exe', ['/c', path.resolve(depot_dir, 'update_depot_tools.bat')]); + depotExecFileSync(null, 'cmd.exe', ['/c', path.resolve(depot_dir, 'update_depot_tools.bat')]); } else { - depotExecFileSync({}, path.resolve(depot_dir, 'update_depot_tools')); + depotExecFileSync(null, path.resolve(depot_dir, 'update_depot_tools')); } } -function ensureDepotTools() { +export function ensureDepotTools(): void { const depot_dir = DEPOT_TOOLS_DIR; // If it doesn't exist, create it. @@ -79,7 +87,10 @@ function platformOpts() { return opts; } -function depotOpts(config, opts = {}) { +export function depotOpts( + config: EVMBaseElectronConfiguration | null, + opts: ExecFileSyncOptions = {}, +): ExecFileSyncOptions { // some defaults opts = { encoding: 'utf8', @@ -95,25 +106,30 @@ function depotOpts(config, opts = {}) { // Circular reference so we have to delay load ...process.env, ...platformOpts(), - ...config.env, + ...config?.env, ...opts.env, // Circular reference so we have to delay load - ...require('./reclient').env(config), - ...require('./siso').env(config), + ...reclientEnv(config), + ...sisoEnv(config), }; // put depot tools at the front of the path - const key = pathKey(); const paths = [DEPOT_TOOLS_DIR]; // Remove any duplicates on path so that DEPOT_TOOLS_DIR isn't added if it is already there - const currentPath = process.env[key].split(path.delimiter); - opts.env[key] = Array.from(new Set([...paths, ...currentPath])).join(path.delimiter); + const currentPath = process.env.PATH?.split(path.delimiter) || []; + opts.env!.PATH = Array.from(new Set([...paths, ...currentPath])).join(path.delimiter); return opts; } -function depotSpawnSync(config, cmd, args, opts_in, fatalMessage) { +export function depotSpawnSync( + config: EVMBaseElectronConfiguration | null, + cmd: string, + args: string[], + opts_in: ExecFileSyncOptions & { msg?: string }, + fatalMessage?: string, +) { const opts = depotOpts(config, opts_in); if (os.platform() === 'win32' && ['python', 'python3'].includes(cmd)) { cmd = `${cmd}.bat`; @@ -128,13 +144,17 @@ function depotSpawnSync(config, cmd, args, opts_in, fatalMessage) { const result = childProcess.spawnSync(cmd, args, opts); if (fatalMessage !== undefined && result.status !== 0) { fatal(fatalMessage); - return; } return result; } -function depotExecFileSync(config, exec, args, opts_in) { +export function depotExecFileSync( + config: EVMBaseElectronConfiguration | null, + exec: string, + args: string[] = [], + opts_in?: ExecFileSyncOptions, +) { const opts = depotOpts(config, opts_in); if (['python', 'python3'].includes(exec) && !opts.cwd && !path.isAbsolute(args[0])) { args[0] = path.resolve(DEPOT_TOOLS_DIR, args[0]); @@ -146,7 +166,7 @@ function depotExecFileSync(config, exec, args, opts_in) { return childProcess.execFileSync(exec, args, opts); } -function setAutoUpdate(enable) { +export function setDepotToolsAutoUpdate(enable: boolean) { try { if (enable) { if (fs.existsSync(markerFilePath)) { @@ -158,15 +178,8 @@ function setAutoUpdate(enable) { console.info(`${color.info} Automatic depot_tools updates disabled`); } } catch (e) { - fatal(e); + fatal(`${e}`); } } -module.exports = { - opts: depotOpts, - path: DEPOT_TOOLS_DIR, - ensure: ensureDepotTools, - execFileSync: depotExecFileSync, - spawnSync: depotSpawnSync, - setAutoUpdate, -}; +export const depotPath = DEPOT_TOOLS_DIR; diff --git a/src/utils/download.js b/src/utils/download.ts similarity index 57% rename from src/utils/download.js rename to src/utils/download.ts index c8475b1b..74f8ebfe 100644 --- a/src/utils/download.js +++ b/src/utils/download.ts @@ -1,16 +1,20 @@ -const stream = require('stream'); -const ProgressBar = require('progress'); +import * as stream from 'node:stream'; +import ProgressBar from 'progress'; const MB_BYTES = 1024 * 1024; -const progressStream = function (total, tokens) { +export const progressStream = function (total: number, tokens: string) { var pt = new stream.PassThrough(); pt.on('pipe', function (_stream) { const bar = new ProgressBar(tokens, { total: Math.round(total) }); + let start: number = 0; pt.on('data', function (chunk) { - const elapsed = new Date() - bar.start; + if (start === 0) { + start = +new Date(); + } + const elapsed = +new Date() - start; const rate = bar.curr / (elapsed / 1000); bar.tick(chunk.length, { mbRate: (rate / MB_BYTES).toFixed(2), @@ -20,7 +24,3 @@ const progressStream = function (total, tokens) { return pt; }; - -module.exports = { - progressStream, -}; diff --git a/src/utils/gerrit.js b/src/utils/gerrit.ts similarity index 50% rename from src/utils/gerrit.js rename to src/utils/gerrit.ts index fb11e57e..3f4e6ae7 100644 --- a/src/utils/gerrit.js +++ b/src/utils/gerrit.ts @@ -1,7 +1,9 @@ -const d = require('debug')('build-tools:gerrit'); +import debug from 'debug'; -const { getCveForBugNr } = require('./crbug'); -const { fatal, color } = require('./logging'); +import { getCveForBugNr } from './crbug.js'; +import { fatal, color } from './logging.js'; + +const d = debug('build-tools:gerrit'); const GERRIT_SOURCES = [ 'chromium-review.googlesource.com', @@ -11,13 +13,17 @@ const GERRIT_SOURCES = [ 'dawn-review.googlesource.com', ]; -async function getGerritPatchDetailsFromURL(gerritUrl, security) { +export async function getGerritPatchDetailsFromURL(gerritUrl: URL, security: boolean) { const { host, pathname } = gerritUrl; if (!GERRIT_SOURCES.includes(host)) { fatal('Unsupported gerrit host'); } - const [, repo, number] = /^\/c\/(.+?)\/\+\/(\d+)/.exec(pathname); + const result = /^\/c\/(.+?)\/\+\/(\d+)/.exec(pathname); + if (!result) { + fatal(`Invalid gerrit URL: ${gerritUrl}`); + } + const [, repo, number] = result; d(`fetching patch from gerrit`); const changeId = `${repo}~${number}`; @@ -30,15 +36,23 @@ async function getGerritPatchDetailsFromURL(gerritUrl, security) { .then((resp) => resp.text()) .then((text) => Buffer.from(text, 'base64').toString('utf8')); - const [, commitId] = /^From ([0-9a-f]+)/.exec(patch); + const fromResult = /^From ([0-9a-f]+)/.exec(patch); + if (!fromResult) { + fatal(`Invalid patch format from gerrit: ${patch}`); + } + const [, commitId] = fromResult; const bugNumber = /^(?:Bug|Fixed)[:=] ?(.+)$/im.exec(patch)?.[1] || /^Bug= ?chromium:(.+)$/m.exec(patch)?.[1]; - let cve = ''; + if (!bugNumber) { + fatal(`No bug number found in patch: ${patch}`); + } + + let cve: string | null = null; if (security) { try { - cve = await getCveForBugNr(bugNumber.replace('chromium:', '')); + cve = await getCveForBugNr(parseInt(bugNumber.replace('chromium:', ''), 10)); } catch (err) { d(err); console.error( @@ -47,18 +61,14 @@ async function getGerritPatchDetailsFromURL(gerritUrl, security) { } } - const patchDirName = - { - 'chromium-review.googlesource.com:chromium/src': 'chromium', - 'skia-review.googlesource.com:skia': 'skia', - 'webrtc-review.googlesource.com:src': 'webrtc', - }[`${host}:${repo}`] || repo.split('/').reverse()[0]; + const hostMap: Record = { + 'chromium-review.googlesource.com:chromium/src': 'chromium', + 'skia-review.googlesource.com:skia': 'skia', + 'webrtc-review.googlesource.com:src': 'webrtc', + }; + const patchDirName: string = hostMap[`${host}:${repo}`] || repo.split('/').reverse()[0]; const shortCommit = commitId.substr(0, 12); return { patchDirName, shortCommit, patch, bugNumber, cve }; } - -module.exports = { - getGerritPatchDetailsFromURL, -}; diff --git a/src/utils/git.js b/src/utils/git.ts similarity index 65% rename from src/utils/git.js rename to src/utils/git.ts index 51c2cfd8..53d7e397 100644 --- a/src/utils/git.js +++ b/src/utils/git.ts @@ -1,15 +1,15 @@ -const cp = require('child_process'); +import { spawnSync } from 'node:child_process'; -const { maybeAutoFix } = require('./maybe-auto-fix'); -const { color } = require('./logging'); +import { maybeAutoFix } from './maybe-auto-fix.js'; +import { color } from './logging.js'; -const spawnSyncWithLog = (cmd, args) => { +const spawnSyncWithLog = (cmd: string, args: string[]) => { console.log(color.childExec(cmd, args, {})); - return cp.spawnSync(cmd, args); + return spawnSync(cmd, args); }; -function checkGlobalGitConfig() { - const { stdout: fileMode } = cp.spawnSync('git', ['config', '--global', 'core.filemode']); +export function checkGlobalGitConfig() { + const { stdout: fileMode } = spawnSync('git', ['config', '--global', 'core.filemode']); if (fileMode.toString().trim() !== 'false') { maybeAutoFix(() => { @@ -17,14 +17,14 @@ function checkGlobalGitConfig() { }, new Error('git config --global core.filemode must be set to false.')); } - const { stdout: autoCrlf } = cp.spawnSync('git', ['config', '--global', 'core.autocrlf']); + const { stdout: autoCrlf } = spawnSync('git', ['config', '--global', 'core.autocrlf']); if (autoCrlf.toString().trim() !== 'false') { maybeAutoFix(() => { spawnSyncWithLog('git', ['config', '--global', 'core.autocrlf', 'false']); }, new Error('git config --global core.autocrlf must be set to false.')); } - const { stdout: autoSetupRebase } = cp.spawnSync('git', [ + const { stdout: autoSetupRebase } = spawnSync('git', [ 'config', '--global', 'branch.autosetuprebase', @@ -35,28 +35,24 @@ function checkGlobalGitConfig() { }, new Error('git config --global branch.autosetuprebase must be set to always.')); } - const { stdout: fscache } = cp.spawnSync('git', ['config', '--global', 'core.fscache']); + const { stdout: fscache } = spawnSync('git', ['config', '--global', 'core.fscache']); if (fscache.toString().trim() !== 'true') { maybeAutoFix(() => { spawnSyncWithLog('git', ['config', '--global', 'core.fscache', 'true']); }, new Error('git config --global core.fscache should be set to true.')); } - const { stdout: preloadIndex } = cp.spawnSync('git', ['config', '--global', 'core.preloadindex']); + const { stdout: preloadIndex } = spawnSync('git', ['config', '--global', 'core.preloadindex']); if (preloadIndex.toString().trim() !== 'true') { maybeAutoFix(() => { spawnSyncWithLog('git', ['config', '--global', 'core.preloadindex', 'true']); }, new Error('git config --global core.preloadindex should be set to true.')); } - const { stdout: longPaths } = cp.spawnSync('git', ['config', '--global', 'core.longpaths']); + const { stdout: longPaths } = spawnSync('git', ['config', '--global', 'core.longpaths']); if (longPaths.toString().trim() !== 'true') { maybeAutoFix(() => { spawnSyncWithLog('git', ['config', '--global', 'core.longpaths', 'true']); }, new Error('git config --global core.longpaths should be set to true.')); } } - -module.exports = { - checkGlobalGitConfig, -}; diff --git a/src/utils/github-auth.js b/src/utils/github-auth.ts similarity index 83% rename from src/utils/github-auth.js rename to src/utils/github-auth.ts index c6e982ff..3793e706 100644 --- a/src/utils/github-auth.js +++ b/src/utils/github-auth.ts @@ -1,11 +1,11 @@ -const { spawnSync } = require('child_process'); -const { createOAuthDeviceAuth } = require('@octokit/auth-oauth-device'); +import { spawnSync } from 'node:child_process'; +import { createOAuthDeviceAuth } from '@octokit/auth-oauth-device'; -const { color } = require('./logging'); +import { color } from './logging.js'; const ELECTRON_BUILD_TOOLS_GITHUB_CLIENT_ID = '03581ca0d21228704ab3'; -function runGhCliCommand(args) { +function runGhCliCommand(args: string[]): string { const { error, status, stdout } = spawnSync('gh', args, { encoding: 'utf8' }); if (status !== 0) { @@ -19,7 +19,7 @@ function runGhCliCommand(args) { return stdout; } -async function getGitHubAuthToken(scopes = []) { +export async function getGitHubAuthToken(scopes: string[] = []): Promise { if (process.env.ELECTRON_BUILD_TOOLS_GH_AUTH) { return process.env.ELECTRON_BUILD_TOOLS_GH_AUTH; } @@ -55,7 +55,7 @@ async function getGitHubAuthToken(scopes = []) { return await createGitHubAuthToken(scopes); } -async function createGitHubAuthToken(scopes = []) { +export async function createGitHubAuthToken(scopes: string[] = []): Promise { const auth = createOAuthDeviceAuth({ clientType: 'oauth-app', clientId: ELECTRON_BUILD_TOOLS_GITHUB_CLIENT_ID, @@ -71,8 +71,3 @@ async function createGitHubAuthToken(scopes = []) { }); return token; } - -module.exports = { - createGitHubAuthToken, - getGitHubAuthToken, -}; diff --git a/src/utils/headers.js b/src/utils/headers.ts similarity index 62% rename from src/utils/headers.js rename to src/utils/headers.ts index 52049f80..ad2e5328 100644 --- a/src/utils/headers.js +++ b/src/utils/headers.ts @@ -1,17 +1,19 @@ -const childProcess = require('child_process'); -const fs = require('fs'); -const path = require('path'); +import childProcess from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; -const evmConfig = require('../evm-config'); -const { ensureDir } = require('./paths'); +import { ensureDir } from './paths.js'; +import * as evmConfig from '../evm-config.js'; -function ensureNodeHeaders(config, useRemote) { +import { EVMBaseElectronConfiguration } from '../evm-config.schema.js'; + +export function ensureNodeHeaders(config: EVMBaseElectronConfiguration, useRemote: boolean): void { const src_dir = path.resolve(config.root, 'src'); const out_dir = evmConfig.outDir(config); const node_headers_dir = path.resolve(out_dir, 'gen', 'node_headers'); const electron_spec_dir = path.resolve(src_dir, 'electron', 'spec'); - let needs_build; + let needs_build: boolean; try { const filename = path.resolve(electron_spec_dir, 'package.json'); const package_time = fs.lstatSync(filename); @@ -23,10 +25,10 @@ function ensureNodeHeaders(config, useRemote) { if (needs_build) { const exec = process.execPath; - const args = [path.resolve(__dirname, '..', 'e'), 'build', 'electron:node_headers']; + const args = [path.resolve(import.meta.dirname, '..', 'e'), 'build', 'electron:node_headers']; if (!useRemote) args.push('--no-remote'); - const opts = { stdio: 'inherit', encoding: 'utf8' }; + const opts = { stdio: 'inherit', encoding: 'utf8' } as const; childProcess.execFileSync(exec, args, opts); } @@ -38,7 +40,3 @@ function ensureNodeHeaders(config, useRemote) { ); } } - -module.exports = { - ensureNodeHeaders, -}; diff --git a/src/utils/logging.js b/src/utils/logging.ts similarity index 50% rename from src/utils/logging.js rename to src/utils/logging.ts index 1c8f3da6..d6a4dce5 100644 --- a/src/utils/logging.js +++ b/src/utils/logging.ts @@ -1,16 +1,17 @@ -const chalk = require('chalk'); +import chalk from 'chalk'; +import { ExecFileSyncOptions } from 'node:child_process'; -const color = { - cmd: (str) => `"${chalk.cyan(str)}"`, - config: (str) => `${chalk.blueBright(str)}`, - git: (str) => `${chalk.greenBright(str)}`, - path: (str) => `${chalk.magentaBright(str)}`, - childExec: (cmd, args, opts) => { +export const color = { + cmd: (str: string) => `"${chalk.cyan(str)}"`, + config: (str: string) => `${chalk.blueBright(str)}`, + git: (str: string) => `${chalk.greenBright(str)}`, + path: (str: string) => `${chalk.magentaBright(str)}`, + childExec: (cmd: string, args: string[], opts: ExecFileSyncOptions) => { args = args || []; const cmdstr = [cmd, ...args].join(' '); const parts = ['Running', color.cmd(cmdstr)]; if (opts && opts.cwd) { - parts.push('in', color.path(opts.cwd)); + parts.push('in', color.path(String(opts.cwd))); } return parts.join(' '); }, @@ -20,7 +21,7 @@ const color = { warn: chalk.bgYellowBright.black('WARN'), }; -function logError(e) { +export function logError(e: string | Error) { if (typeof e === 'string') { console.error(`${color.err} ${e}`); } else { @@ -28,13 +29,7 @@ function logError(e) { } } -function fatal(e, code = 1) { +export function fatal(e: string | Error, code = 1): never { logError(e); process.exit(code); } - -module.exports = { - color, - fatal, - logError, -}; diff --git a/src/utils/maybe-auto-fix.js b/src/utils/maybe-auto-fix.ts similarity index 71% rename from src/utils/maybe-auto-fix.js rename to src/utils/maybe-auto-fix.ts index 51c88fc8..167ba119 100644 --- a/src/utils/maybe-auto-fix.js +++ b/src/utils/maybe-auto-fix.ts @@ -1,7 +1,8 @@ -const readlineSync = require('readline-sync'); -const { color } = require('./logging'); +import readlineSync from 'readline-sync'; -const maybeAutoFix = (fn, err) => { +import { color } from './logging.js'; + +export const maybeAutoFix = (fn: () => void, err: Error) => { if (process.env.ELECTRON_BUILD_TOOLS_AUTO_FIX) return fn(); // If we're running in CI we can't prompt the user if (process.env.CI) throw err; @@ -12,7 +13,3 @@ const maybeAutoFix = (fn, err) => { console.log(''); fn(); }; - -module.exports = { - maybeAutoFix, -}; diff --git a/src/utils/paths.js b/src/utils/paths.js deleted file mode 100644 index 3a610911..00000000 --- a/src/utils/paths.js +++ /dev/null @@ -1,26 +0,0 @@ -const fs = require('fs'); -const os = require('os'); -const path = require('path'); -const { color } = require('./logging'); - -function resolvePath(p) { - if (path.isAbsolute(p)) return p; - if (p.startsWith('~/')) return path.resolve(os.homedir(), p.substr(2)); - return path.resolve(process.cwd(), p); -} - -function ensureDir(dir) { - dir = resolvePath(dir); - if (!fs.existsSync(dir)) { - console.log(`Creating ${color.path(dir)}`); - fs.mkdirSync(dir, { recursive: true }); - } -} - -const deleteDir = (dir) => fs.rmSync(dir, { force: true, recursive: true }); - -module.exports = { - resolvePath, - ensureDir, - deleteDir, -}; diff --git a/src/utils/paths.ts b/src/utils/paths.ts new file mode 100644 index 00000000..122e23e6 --- /dev/null +++ b/src/utils/paths.ts @@ -0,0 +1,23 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import { color } from './logging.js'; + +export function resolvePath(p: string): string { + if (path.isAbsolute(p)) return p; + if (p.startsWith('~/')) return path.resolve(os.homedir(), p.substr(2)); + return path.resolve(process.cwd(), p); +} + +export function ensureDir(dir: string): void { + dir = resolvePath(dir); + if (!fs.existsSync(dir)) { + console.log(`Creating ${color.path(dir)}`); + fs.mkdirSync(dir, { recursive: true }); + } +} + +export function deleteDir(dir: string): void { + fs.rmSync(dir, { force: true, recursive: true }); +} diff --git a/src/utils/reclient.js b/src/utils/reclient.ts similarity index 66% rename from src/utils/reclient.js rename to src/utils/reclient.ts index 8ef33c69..4184d879 100644 --- a/src/utils/reclient.js +++ b/src/utils/reclient.ts @@ -1,24 +1,23 @@ -const childProcess = require('child_process'); -const fs = require('fs'); -const path = require('path'); -const tar = require('tar'); +import childProcess from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import tar from 'tar'; -const { color, fatal } = require('./logging'); -const { deleteDir } = require('./paths'); +import { color, fatal } from './logging.js'; +import { deleteDir } from './paths.js'; +import { EVMBaseElectronConfiguration } from '../evm-config.schema.js'; -const reclientDir = path.resolve(__dirname, '..', '..', 'third_party', 'reclient'); +const reclientDir = path.resolve(import.meta.dirname, '..', '..', 'third_party', 'reclient'); const reclientTagFile = path.resolve(reclientDir, '.tag'); -const rbeHelperPath = path.resolve( + +const DEFAULT_RBE_HELPER_PATH = path.resolve( reclientDir, `electron-rbe-credential-helper${process.platform === 'win32' ? '.exe' : ''}`, ); const RBE_SERVICE_ADDRESS = 'rbe.notgoma.com:443'; - const CREDENTIAL_HELPER_TAG = 'v0.5.0'; -let usingRemote = true; - -function downloadAndPrepareRBECredentialHelper(config) { +export function downloadAndPrepareRBECredentialHelper(config: EVMBaseElectronConfiguration): void { if (config.remoteBuild === 'none') return; // If a custom reclient credentials helper is specified, expect @@ -30,7 +29,7 @@ function downloadAndPrepareRBECredentialHelper(config) { // Reclient itself comes down with a "gclient sync" // run. We just need to ensure we have the cred helper - let targetPlatform = null; + let targetPlatform: string | null = null; switch (process.platform) { case 'win32': { targetPlatform = `windows-${process.arch === 'arm64' ? 'arm64' : 'amd64'}`; @@ -68,7 +67,7 @@ function downloadAndPrepareRBECredentialHelper(config) { console.log(`Downloading ${color.cmd(downloadURL)} into ${color.path(tmpDownload)}`); const { status } = childProcess.spawnSync( process.execPath, - [path.resolve(__dirname, '..', 'download.js'), downloadURL, tmpDownload], + [path.resolve(import.meta.dirname, '..', 'download.js'), downloadURL, tmpDownload], { stdio: 'inherit', }, @@ -87,7 +86,7 @@ function downloadAndPrepareRBECredentialHelper(config) { }); if (process.platform === 'win32') { - fs.renameSync(rbeHelperPath.replace(/\.exe$/, ''), rbeHelperPath); + fs.renameSync(DEFAULT_RBE_HELPER_PATH.replace(/\.exe$/, ''), DEFAULT_RBE_HELPER_PATH); } deleteDir(tmpDownload); @@ -95,8 +94,8 @@ function downloadAndPrepareRBECredentialHelper(config) { return; } -function reclientEnv(config) { - if (config?.remoteBuild === 'none' || !usingRemote) { +export function reclientEnv(config: EVMBaseElectronConfiguration | null): Record { + if (config?.remoteBuild === 'none') { return {}; } @@ -106,16 +105,15 @@ function reclientEnv(config) { RBE_credentials_helper_args: 'print', RBE_experimental_credentials_helper: getHelperPath(config), RBE_experimental_credentials_helper_args: 'print', - }; - - // When building Chromium, don't fail early on local fallbacks - // as they are expected. - if (config.defaultTarget === 'chrome') { - reclientEnv.RBE_fail_early_min_action_count = 0; - reclientEnv.RBE_fail_early_min_fallback_ratio = 0; - } - - const result = childProcess.spawnSync(rbeHelperPath, ['flags'], { + ...(config?.defaultTarget === 'chrome' + ? { + RBE_fail_early_min_action_count: '0', + RBE_fail_early_min_fallback_ratio: '0', + } + : {}), + } as const; + + const result = childProcess.spawnSync(getHelperPath(config), ['flags'], { stdio: 'pipe', }); @@ -132,8 +130,8 @@ function reclientEnv(config) { return reclientEnv; } -function ensureHelperAuth(config) { - const result = childProcess.spawnSync(rbeHelperPath, ['status'], { +export function ensureHelperAuth(config: EVMBaseElectronConfiguration): void { + const result = childProcess.spawnSync(getHelperPath(config), ['status'], { stdio: 'pipe', }); if (result.status !== 0) { @@ -147,19 +145,10 @@ function ensureHelperAuth(config) { } } -function getHelperPath(config) { - return config.rbeHelperPath || rbeHelperPath; +export function getHelperPath(config: EVMBaseElectronConfiguration | null): string { + return config?.rbeHelperPath || DEFAULT_RBE_HELPER_PATH; } -function getServiceAddress(config) { - return config.rbeServiceAddress || RBE_SERVICE_ADDRESS; +export function getServiceAddress(config: EVMBaseElectronConfiguration | null): string { + return config?.rbeServiceAddress || RBE_SERVICE_ADDRESS; } - -module.exports = { - env: reclientEnv, - downloadAndPrepareRBECredentialHelper, - helperPath: getHelperPath, - serviceAddress: getServiceAddress, - auth: ensureHelperAuth, - usingRemote, -}; diff --git a/src/utils/refresh-path.js b/src/utils/refresh-path.js deleted file mode 100644 index 1f96ecf8..00000000 --- a/src/utils/refresh-path.js +++ /dev/null @@ -1,15 +0,0 @@ -const cp = require('child_process'); -const path = require('path'); - -const refreshPathVariable = () => { - if (process.platform === 'win32') { - const file = path.resolve(__dirname, 'get-path.bat'); - const output = cp.execFileSync(file, { shell: true }); - const pathOut = output.toString(); - process.env.PATH = pathOut; - } -}; - -module.exports = { - refreshPathVariable, -}; diff --git a/src/utils/refresh-path.ts b/src/utils/refresh-path.ts new file mode 100644 index 00000000..f08cb7d8 --- /dev/null +++ b/src/utils/refresh-path.ts @@ -0,0 +1,11 @@ +import { execFileSync } from 'node:child_process'; +import path from 'node:path'; + +export const refreshPathVariable = () => { + if (process.platform === 'win32') { + const file = path.resolve(import.meta.dirname, '..', '..', 'src', 'utils', 'get-path.bat'); + const output = execFileSync(file, { shell: true }); + const pathOut = output.toString(); + process.env.PATH = pathOut; + } +}; diff --git a/src/utils/sdk.js b/src/utils/sdk.ts similarity index 78% rename from src/utils/sdk.js rename to src/utils/sdk.ts index 612592d7..27ee2ab3 100644 --- a/src/utils/sdk.js +++ b/src/utils/sdk.ts @@ -1,20 +1,20 @@ -const cp = require('child_process'); -const chalk = require('chalk'); -const fs = require('fs'); -const path = require('path'); -const semver = require('semver'); -const { ensureDir } = require('./paths'); -const evmConfig = require('../evm-config'); - -const { color, fatal } = require('./logging'); -const { deleteDir } = require('./paths'); - -const SDKDir = path.resolve(__dirname, '..', '..', 'third_party', 'SDKs'); +import { spawnSync } from 'node:child_process'; +import chalk from 'chalk'; +import fs from 'node:fs'; +import path from 'node:path'; +import semver from 'semver'; + +import { deleteDir, ensureDir } from './paths.js'; +import * as evmConfig from '../evm-config.js'; +import { color, fatal } from './logging.js'; +import { EVMBaseElectronConfiguration } from '../evm-config.schema.js'; + +const SDKDir = path.resolve(import.meta.dirname, '..', '..', 'third_party', 'SDKs'); const SDKZip = path.resolve(SDKDir, 'MacOSX.sdk.zip'); const XcodeBaseURL = 'https://dev-cdn-experimental.electronjs.org/xcode/'; -const SDKs = { +const SDKs: Record = { '15.4': { fileName: 'MacOSX-15.4.sdk.zip', sha256: '7bb880365a1adb9f99f011f5bec32eefc2130d3df4d0460e090a65e1387af221', @@ -49,17 +49,17 @@ const SDKs = { }, }; -const fallbackSDK = () => { +function fallbackSDK(): string { const semverFallback = Object.keys(SDKs) - .map((v) => semver.valid(semver.coerce(v))) - .sort(semver.rcompare)[0]; + .map((v) => semver.valid(semver.coerce(v))!) + .sort((a, b) => semver.rcompare(a, b))[0]; return semverFallback.substring(0, semverFallback.length - 2); -}; +} -function getSDKVersion() { +function getSDKVersion(): 'unknown' | string { const { SDKROOT } = evmConfig.current().env; - if (!fs.existsSync(SDKROOT)) { + if (!SDKROOT || !fs.existsSync(SDKROOT)) { return 'unknown'; } @@ -70,7 +70,7 @@ function getSDKVersion() { return json.MinimalDisplayName; } -function removeUnusedSDKs() { +function removeUnusedSDKs(): void { const recent = fs .readdirSync(SDKDir) .map((sdk) => { @@ -78,7 +78,7 @@ function removeUnusedSDKs() { const { atime } = fs.statSync(sdkPath); return { name: sdkPath, atime }; }) - .sort((a, b) => b.atime - a.atime); + .sort((a, b) => +b.atime - +a.atime); const { preserveSDK } = evmConfig.current(); for (const { name } of recent.slice(preserveSDK)) { @@ -87,15 +87,15 @@ function removeUnusedSDKs() { } // Potentially remove unused Xcode versions. -function maybeRemoveOldXcodes() { - const XcodeDir = path.resolve(__dirname, '..', '..', 'third_party', 'Xcode'); +function maybeRemoveOldXcodes(): void { + const XcodeDir = path.resolve(import.meta.dirname, '..', '..', 'third_party', 'Xcode'); if (fs.existsSync(XcodeDir)) { deleteDir(XcodeDir); } } // Extract the SDK version from the toolchain file and normalize it. -function extractSDKVersion(toolchainFile) { +function extractSDKVersion(toolchainFile: string): string | null { const contents = fs.readFileSync(toolchainFile, 'utf8'); const match = /macOS\s+(?:(\d+(?:\.\d+)?)\s+SDK|SDK\s+(\d+(?:\.\d+)?))/.exec(contents); @@ -105,7 +105,7 @@ function extractSDKVersion(toolchainFile) { return null; } -function expectedSDKVersion() { +function expectedSDKVersion(): string { const { root } = evmConfig.current(); // The current Xcode version and associated SDK can be found in build/mac_toolchain.py. @@ -120,7 +120,7 @@ function expectedSDKVersion() { } const version = extractSDKVersion(macToolchainPy); - if (isNaN(Number(version)) || !SDKs[version]) { + if (isNaN(Number(version)) || !version || !SDKs[version]) { console.warn( color.warn, `Automatically detected an unknown macOS SDK ${color.path( @@ -138,14 +138,14 @@ function expectedSDKVersion() { function ensureViableXCode() { const xcodeBuildExec = '/usr/bin/xcodebuild'; if (fs.existsSync(xcodeBuildExec)) { - const result = cp.spawnSync(xcodeBuildExec, ['-version']); + const result = spawnSync(xcodeBuildExec, ['-version']); if (result.status === 0) { const match = result.stdout .toString() .trim() .match(/Xcode (\d+\.\d+)/); if (match) { - if (!semver.satisfies(semver.coerce(match[1]), '>14')) { + if (!semver.satisfies(semver.coerce(match[1])!, '>14')) { fatal(`Xcode version ${match[1]} is not supported, please upgrade to Xcode 15 or newer`); } else { return; @@ -172,7 +172,7 @@ You can validate your install with "${chalk.green( )}" once you are ready or just run this command again`); } -function ensureSDKAndSymlink(config) { +export function ensureSDKAndSymlink(config: EVMBaseElectronConfiguration): string { const localPath = ensureSDK(); const outDir = evmConfig.outDir(config); @@ -189,23 +189,23 @@ function ensureSDKAndSymlink(config) { return `//out/${path.basename(outDir)}/${outRelative}`; } -function ensureSDK(version) { +export function ensureSDK(versionOverride?: string): string { // For testing purposes if (process.env.__VITEST__) { console.log('TEST: ensureSDK called'); - return; + return ''; } ensureViableXCode(); - if (version && !SDKs[version]) { + if (versionOverride && !SDKs[versionOverride]) { const availableVersions = Object.keys(SDKs).join(', '); fatal( - `SDK version ${version} is invalid or unsupported - please use one of the following: ${availableVersions}`, + `SDK version ${versionOverride} is invalid or unsupported - please use one of the following: ${availableVersions}`, ); } - const expected = version || expectedSDKVersion(); + const expected = versionOverride || expectedSDKVersion(); const eventualVersionedPath = path.resolve(SDKDir, `MacOSX${expected}.sdk`); const shouldEnsureSDK = !fs.existsSync(eventualVersionedPath) || getSDKVersion() !== expected; @@ -233,9 +233,9 @@ function ensureSDK(version) { if (shouldDownload) { const sdkURL = `${XcodeBaseURL}${SDKs[expected].fileName}`; console.log(`Downloading ${color.cmd(sdkURL)} into ${color.path(SDKZip)}`); - const { status } = cp.spawnSync( + const { status } = spawnSync( process.execPath, - [path.resolve(__dirname, '..', 'download.js'), sdkURL, SDKZip], + [path.resolve(import.meta.dirname, '..', 'download.js'), sdkURL, SDKZip], { stdio: 'inherit', }, @@ -262,7 +262,7 @@ function ensureSDK(version) { deleteDir(unzipPath); try { - const { status } = cp.spawnSync('unzip', ['-q', '-o', SDKZip, '-d', unzipPath], { + const { status } = spawnSync('unzip', ['-q', '-o', SDKZip, '-d', unzipPath], { stdio: 'inherit', }); if (status !== 0) { @@ -271,7 +271,7 @@ function ensureSDK(version) { } catch (error) { deleteDir(SDKZip); deleteDir(unzipPath); - fatal(error); + fatal(`Failed to unzip SDK zip file: ${error}`); } fs.renameSync(path.resolve(unzipPath, 'MacOSX.sdk'), eventualVersionedPath); @@ -293,12 +293,7 @@ function ensureSDK(version) { } // Hash MacOSX.sdk directory zip with sha256. -function hashFile(file) { +function hashFile(file: string): string { console.log(`Calculating hash for ${color.path(file)}`); - return cp.spawnSync('shasum', ['-a', '256', file]).stdout.toString().split(' ')[0].trim(); + return spawnSync('shasum', ['-a', '256', file]).stdout.toString().split(' ')[0].trim(); } - -module.exports = { - ensureSDK, - ensureSDKAndSymlink, -}; diff --git a/src/utils/setup-reclient-chromium.js b/src/utils/setup-reclient-chromium.ts similarity index 78% rename from src/utils/setup-reclient-chromium.js rename to src/utils/setup-reclient-chromium.ts index 3a5f540a..164e5a8e 100644 --- a/src/utils/setup-reclient-chromium.js +++ b/src/utils/setup-reclient-chromium.ts @@ -1,25 +1,25 @@ -const { execFileSync } = require('child_process'); -const { existsSync } = require('fs'); -const path = require('path'); -const process = require('process'); +import { execFileSync, ExecFileSyncOptions } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; -const { color, fatal } = require('./logging'); -const { spawnSync } = require('../utils/depot-tools'); -const evmConfig = require('../evm-config'); +import { color, fatal } from './logging.js'; +import { depotSpawnSync } from '../utils/depot-tools.js'; +import * as evmConfig from '../evm-config.js'; -const execFileSyncWithLog = (cmd, args, opts) => { +const execFileSyncWithLog = (cmd: string, args: string[], opts: ExecFileSyncOptions): void => { console.log(color.childExec(cmd, args, opts)); - return execFileSync(cmd, args, opts); + execFileSync(cmd, args, opts); }; -const isReclientConfigured = () => { +const isReclientConfigured = (): boolean => { const { root } = evmConfig.current(); const srcDir = path.resolve(root, 'src'); const engflowConfigsDir = path.resolve(srcDir, 'third_party', 'engflow-reclient-configs'); return existsSync(engflowConfigsDir); }; -function configureReclient() { +export function configureReclient(): void { const { root, defaultTarget } = evmConfig.current(); if (isReclientConfigured() || defaultTarget !== 'chrome') { @@ -47,19 +47,19 @@ function configureReclient() { // Pinning to prevent unexpected breakage. const ENGFLOW_CONFIG_SHA = process.env.ENGFLOW_CONFIG_SHA || '7851c9387a770d6381f4634cb293293d2b30c502'; - spawnSync(evmConfig.current(), 'git', ['checkout', ENGFLOW_CONFIG_SHA], { + depotSpawnSync(evmConfig.current(), 'git', ['checkout', ENGFLOW_CONFIG_SHA], { cwd: engflowConfigsDir, stdio: 'ignore', }); const reclientConfigPatchPath = path.resolve( - __dirname, + import.meta.dirname, '..', '..', 'tools', 'engflow_reclient_configs.patch', ); - spawnSync( + depotSpawnSync( evmConfig.current(), 'git', ['apply', reclientConfigPatchPath], @@ -71,7 +71,7 @@ function configureReclient() { ); const configureReclientScript = path.join(engflowConfigsDir, 'configure_reclient.py'); - spawnSync( + depotSpawnSync( evmConfig.current(), 'python3', [configureReclientScript, '--src_dir=src', '--force'], @@ -88,7 +88,7 @@ function configureReclient() { 'reclient_cfgs', 'configure_reclient_cfgs.py', ); - spawnSync( + depotSpawnSync( evmConfig.current(), 'python3', [ @@ -111,7 +111,3 @@ function configureReclient() { console.info(`${color.info} Successfully configured EngFlow reclient configs for Chromium`); } } - -module.exports = { - configureReclient, -}; diff --git a/src/utils/siso.js b/src/utils/siso.ts similarity index 66% rename from src/utils/siso.js rename to src/utils/siso.ts index e15b0fb6..609667d2 100644 --- a/src/utils/siso.js +++ b/src/utils/siso.ts @@ -1,40 +1,41 @@ -const fs = require('fs'); -const path = require('path'); +import fs from 'node:fs'; +import path from 'node:path'; -const reclient = require('./reclient'); +import { EVMBaseElectronConfiguration } from '../evm-config.schema.js'; +import { getServiceAddress, getHelperPath } from './reclient.js'; const SISO_REAPI_INSTANCE = 'projects/electron-rbe/instances/default_instance'; const SISO_PROJECT = SISO_REAPI_INSTANCE.split('/')[1]; -const sisoEnv = (config) => { - if (config.remoteBuild !== 'siso') return {}; +export const sisoEnv = (config: EVMBaseElectronConfiguration | null): Record => { + if (config?.remoteBuild !== 'siso') return {}; return { SISO_PROJECT, SISO_REAPI_INSTANCE, - SISO_REAPI_ADDRESS: reclient.serviceAddress(config), - SISO_CREDENTIAL_HELPER: reclient.helperPath(config), + SISO_REAPI_ADDRESS: getServiceAddress(config), + SISO_CREDENTIAL_HELPER: getHelperPath(config), }; }; -function sisoFlags(config) { +export function sisoFlags(config: EVMBaseElectronConfiguration): string[] { if (config.remoteBuild !== 'siso') return []; return [ '-remote_jobs', - 200, + '200', '-project', SISO_PROJECT, '-reapi_instance', SISO_REAPI_INSTANCE, '-reapi_address', - reclient.serviceAddress(config), + getServiceAddress(config), '-load', path.resolve(config.root, 'src/electron/build/siso/main.star'), ]; } -async function ensureBackendStarlark(config) { +export async function ensureBackendStarlark(config: EVMBaseElectronConfiguration): Promise { if (config.remoteBuild !== 'siso') return; const backendConfig = path.resolve(config.root, 'src/electron/build/siso/backend.star'); @@ -62,9 +63,3 @@ async function ensureBackendStarlark(config) { await fs.promises.copyFile(backendConfig, starlarkPath); } } - -module.exports = { - env: sisoEnv, - flags: sisoFlags, - ensureBackendStarlark, -}; diff --git a/src/utils/which.js b/src/utils/which.js deleted file mode 100644 index c259a729..00000000 --- a/src/utils/which.js +++ /dev/null @@ -1,29 +0,0 @@ -const which = require('which').sync; - -const { maybeAutoFix } = require('./maybe-auto-fix'); -const { refreshPathVariable } = require('./refresh-path'); -const { fatal } = require('./logging'); - -const whichAndFix = (cmd, check, fix) => { - const found = check ? check() : !!which(cmd, { nothrow: true }); - if (!found) { - maybeAutoFix( - fix, - new Error( - `A required dependency "${cmd}" could not be located, it probably has to be installed.`, - ), - ); - - refreshPathVariable(); - - if (!(check ? check() : which(cmd, { nothrow: true }))) { - fatal( - `A required dependency "${cmd}" could not be located and we could not install it - it likely has to be installed manually.`, - ); - } - } -}; - -module.exports = { - whichAndFix, -}; diff --git a/tests/e-init.spec.mjs b/tests/e-init.spec.mjs index 6c8427c1..ab012cde 100644 --- a/tests/e-init.spec.mjs +++ b/tests/e-init.spec.mjs @@ -1,7 +1,7 @@ import fs from 'fs'; import path from 'path'; -import { validateConfig } from '../src/evm-config'; +import { validateConfig } from '../dist/evm-config'; import createSandbox from './sandbox'; diff --git a/tests/e-show.spec.mjs b/tests/e-show.spec.mjs index c49ff1fa..8f5dc864 100644 --- a/tests/e-show.spec.mjs +++ b/tests/e-show.spec.mjs @@ -1,6 +1,5 @@ import os from 'os'; import path from 'path'; -import pathKey from 'path-key'; import createSandbox from './sandbox'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; @@ -146,7 +145,7 @@ describe('e-show', () => { }, {}); const envKeys = Object.keys(env).sort(); expect(envKeys).toEqual( - expect.arrayContaining(['CHROMIUM_BUILDTOOLS_PATH', 'GIT_CACHE_PATH', pathKey()]), + expect.arrayContaining(['CHROMIUM_BUILDTOOLS_PATH', 'GIT_CACHE_PATH', 'PATH']), ); expect(envKeys).toEqual( (isWindows ? expect : expect.not).arrayContaining(['DEPOT_TOOLS_WIN_TOOLCHAIN']), diff --git a/tests/evm-config.spec.mjs b/tests/evm-config.spec.mjs index 6ed80a72..b28a630b 100644 --- a/tests/evm-config.spec.mjs +++ b/tests/evm-config.spec.mjs @@ -3,7 +3,7 @@ import path from 'path'; import YAML from 'yaml'; -const { sanitizeConfig, validateConfig, fetchByName } = require('../src/evm-config'); +const { sanitizeConfig, validateConfig, fetchByName } = require('../dist/evm-config'); import { beforeAll, afterAll, describe, expect, it, vi } from 'vitest'; diff --git a/tests/sandbox.js b/tests/sandbox.js index b0efd609..6cee0f6c 100644 --- a/tests/sandbox.js +++ b/tests/sandbox.js @@ -2,12 +2,7 @@ const childProcess = require('child_process'); const fs = require('fs'); const os = require('os'); const path = require('path'); -const { deleteDir } = require('../src/utils/paths'); - -// Get the PATH environment variable key cross-platform -// It's usually PATH, but on Windows it can be any casing like Path... -// https://github.com/sindresorhus/path-key -const pathKey = require('path-key')(); +const { deleteDir } = require('../dist/utils/paths'); // execFileSync() wrapper that adds exec'ed scripts to code coverage. // Returns { exitCode:number, stderr:string, stdout:string } @@ -50,7 +45,7 @@ function runSync(args, options) { return ret; } -const buildToolsSrcDir = path.resolve(__dirname, '..', 'src'); +const buildToolsSrcDir = path.resolve(__dirname, '..', 'dist'); // An `e init` helper. // Example use: result = eInitRunner().root('~/electron-src') @@ -239,7 +234,7 @@ function createSandbox() { EVM_CONFIG: evm_config_dir, // we want to detect vitest __VITEST__: 1, - [pathKey]: process.env[pathKey], + PATH: process.env.PATH, }, }; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..19d0cefd --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "removeComments": false, + "preserveConstEnums": true, + "sourceMap": true, + "declaration": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noImplicitThis": true, + "noUnusedParameters": true, + "esModuleInterop": true, + "pretty": true, + "outDir": "dist", + "types": ["node"] + }, + "formatCodeOptions": { + "indentSize": 2, + "tabSize": 2 + }, + "exclude": [ + "node_modules", + "dist", + "test" + ] +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 4216e3be..2c880aad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,6 +10,15 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" +"@apidevtools/json-schema-ref-parser@^11.5.5": + version "11.9.3" + resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.9.3.tgz#0e0c9061fc41cf03737d499a4e6a8299fdd2bfa7" + integrity sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ== + dependencies: + "@jsdevtools/ono" "^7.1.3" + "@types/json-schema" "^7.0.15" + js-yaml "^4.1.0" + "@babel/code-frame@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465" @@ -444,6 +453,11 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@jsdevtools/ono@^7.1.3": + version "7.1.3" + resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" + integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== + "@mapbox/node-pre-gyp@^1.0.0": version "1.0.9" resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.9.tgz#09a8781a3a036151cdebbe8719d6f8b25d4058bc" @@ -771,11 +785,21 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== +"@tsconfig/node22@^22.0.2": + version "22.0.2" + resolved "https://registry.yarnpkg.com/@tsconfig/node22/-/node22-22.0.2.tgz#1e04e2c5cc946dac787d69bb502462a851ae51b6" + integrity sha512-Kmwj4u8sDRDrMYRoN9FDEcXD8UpBSaPQQ24Gz+Gamqfm7xxn+GBR7ge/Z7pK8OXNGyUzbSwJj+TH6B+DS/epyA== + "@types/color-name@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== +"@types/command-exists@^1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@types/command-exists/-/command-exists-1.2.3.tgz#b83f6a0b4d5aa2765f39950bca90c8d4203528e0" + integrity sha512-PpbaE2XWLaWYboXD6k70TcXO/OdOyyRFq5TVpmlUELNxdkkmXU9fkImNosmXU1DtsNrqdUgWd/nJQYXgwmtdXQ== + "@types/debug@^4.0.0": version "4.1.8" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.8.tgz#cef723a5d0a90990313faec2d1e22aee5eecb317" @@ -793,11 +817,29 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== +"@types/inquirer@^8.0.0": + version "8.2.11" + resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-8.2.11.tgz#5f741050c696acf1926f136450d8d98ca65d9557" + integrity sha512-15UboTvxb9SOaPG7CcXZ9dkv8lNqfiAwuh/5WxJDLjmElBt9tbx1/FDsEnJddUBKvN4mlPKvr8FyO1rAmBanzg== + dependencies: + "@types/through" "*" + rxjs "^7.2.0" + +"@types/json-schema@^7.0.15": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/lodash@^4.17.7": + version "4.17.18" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.18.tgz#4710e7db5b3857103764bf7b7b666414e6141baf" + integrity sha512-KJ65INaxqxmU6EoCiJmRPZC9H9RVWCRd349tXM2M3O5NA7cY6YL7c0bHAHQ93NOfTObEQ004kd2QVHs/r0+m4g== + "@types/mdast@^3.0.0": version "3.0.11" resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.11.tgz#dc130f7e7d9306124286f6d6cee40cf4d14a3dc0" @@ -817,6 +859,45 @@ dependencies: undici-types "~6.20.0" +"@types/node@^22.7.1": + version "22.15.34" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.15.34.tgz#3995a6461d2cfc51c81907da0065fc328f6a459e" + integrity sha512-8Y6E5WUupYy1Dd0II32BsWAx5MWdcnRd8L84Oys3veg1YrYtNtzgO4CFhiBg6MDSjk7Ay36HYOnU7/tuOzIzcw== + dependencies: + undici-types "~6.21.0" + +"@types/progress@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@types/progress/-/progress-2.0.7.tgz#798b309935ef1cf5bef3b3f7bb8da7b0335bc67e" + integrity sha512-iadjw02vte8qWx7U0YM++EybBha2CQLPGu9iJ97whVgJUT5Zq9MjAPYUnbfRI2Kpehimf1QjFJYxD0t8nqzu5w== + dependencies: + "@types/node" "*" + +"@types/readline-sync@^1.4.8": + version "1.4.8" + resolved "https://registry.yarnpkg.com/@types/readline-sync/-/readline-sync-1.4.8.tgz#dc9767a93fc83825d90331f2549a2e90fc3255f0" + integrity sha512-BL7xOf0yKLA6baAX6MMOnYkoflUyj/c7y3pqMRfU0va7XlwHAOTOIo4x55P/qLfMsuaYdJJKubToLqRVmRtRZA== + +"@types/semver@^7.7.0": + version "7.7.0" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.7.0.tgz#64c441bdae033b378b6eef7d0c3d77c329b9378e" + integrity sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA== + +"@types/tar@^6.1.13": + version "6.1.13" + resolved "https://registry.yarnpkg.com/@types/tar/-/tar-6.1.13.tgz#9b5801c02175344101b4b91086ab2bbc8e93a9b6" + integrity sha512-IznnlmU5f4WcGTh2ltRu/Ijpmk8wiWXfF0VA4s+HPjHZgvFggk1YaIkbo5krX/zUCzWF8N/l4+W/LNxnvAJ8nw== + dependencies: + "@types/node" "*" + minipass "^4.0.0" + +"@types/through@*": + version "0.0.33" + resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.33.tgz#14ebf599320e1c7851e7d598149af183c6b9ea56" + integrity sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ== + dependencies: + "@types/node" "*" + "@types/unist@*", "@types/unist@^2.0.0": version "2.0.6" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" @@ -944,10 +1025,10 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" -ajv-formats@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" - integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== +ajv-formats@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-3.0.1.tgz#3d5dc762bca17679c3c2ea7e90ad6b7532309578" + integrity sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ== dependencies: ajv "^8.0.0" @@ -961,7 +1042,7 @@ ajv@^6.10.0, ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0, ajv@^8.11.0: +ajv@^8.0.0: version "8.11.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f" integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg== @@ -971,6 +1052,16 @@ ajv@^8.0.0, ajv@^8.11.0: require-from-string "^2.0.2" uri-js "^4.2.2" +ajv@^8.17.1: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + ansi-escapes@^4.2.1: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" @@ -1311,7 +1402,7 @@ chai@^5.2.0: loupe "^3.1.0" pathval "^2.0.0" -chalk@^2.4.1, chalk@^2.4.2: +chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -1328,6 +1419,11 @@ chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.4.1.tgz#1b48bf0963ec158dce2aacf69c093ae2dd2092d8" + integrity sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w== + chalk@~5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" @@ -1448,10 +1544,10 @@ command-exists@^1.2.8: resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.8.tgz#715acefdd1223b9c9b37110a149c6392c2852291" integrity sha512-PM54PkseWbiiD/mMsbvW351/u+dafwTJ0ye2qB60G1aGQP9j3xK2gmMDc+R34L3nDtx4qMCitXT75mkbkGJDLw== -commander@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-9.0.0.tgz#86d58f24ee98126568936bd1d3574e0308a99a40" - integrity sha512-JJfP2saEKbQqvW+FI93OYUB4ByV5cizMpFMiiJI8xDbBvQvSkIk0VvQdn1CZ8mqAO8Loq2h0gYTYtDFUZUeERw== +commander@^9.5.0: + version "9.5.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" + integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== commander@~12.1.0: version "12.1.0" @@ -2097,6 +2193,11 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fast-uri@^3.0.1: + version "3.0.6" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.6.tgz#88f130b77cfaea2378d56bf970dea21257a68748" + integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw== + fastq@^1.6.0: version "1.15.0" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" @@ -3024,6 +3125,21 @@ json-parse-better-errors@^1.0.1: resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== +json-schema-to-typescript@^15.0.4: + version "15.0.4" + resolved "https://registry.yarnpkg.com/json-schema-to-typescript/-/json-schema-to-typescript-15.0.4.tgz#a530c7f17312503b262ae12233749732171840f3" + integrity sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ== + dependencies: + "@apidevtools/json-schema-ref-parser" "^11.5.5" + "@types/json-schema" "^7.0.15" + "@types/lodash" "^4.17.7" + is-glob "^4.0.3" + js-yaml "^4.1.0" + lodash "^4.17.21" + minimist "^1.2.8" + prettier "^3.2.5" + tinyglobby "^0.2.9" + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -3687,7 +3803,7 @@ minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3: dependencies: yallist "^4.0.0" -minipass@^4.2.4: +minipass@^4.0.0, minipass@^4.2.4: version "4.2.8" resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.8.tgz#f0010f64393ecfc1d1ccb5f582bcaf45f48e1a3a" integrity sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ== @@ -4265,6 +4381,11 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +prettier@^3.2.5: + version "3.5.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.5.3.tgz#4fc2ce0d657e7a02e602549f053b239cb7dfe1b5" + integrity sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw== + prettier@^3.3.3: version "3.3.3" resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105" @@ -4543,6 +4664,13 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" +rxjs@^7.2.0: + version "7.8.2" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.2.tgz#955bc473ed8af11a002a2be52071bf475638607b" + integrity sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA== + dependencies: + tslib "^2.1.0" + rxjs@^7.5.5: version "7.5.5" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.5.tgz#2ebad89af0f560f460ad5cc4213219e1f7dd4e9f" @@ -4804,16 +4932,7 @@ string-argv@~0.3.2: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -4888,14 +5007,7 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -5023,6 +5135,14 @@ tinyglobby@^0.2.13: fdir "^6.4.4" picomatch "^4.0.2" +tinyglobby@^0.2.9: + version "0.2.14" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.14.tgz#5280b0cf3f972b050e74ae88406c0a6a58f4079d" + integrity sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ== + dependencies: + fdir "^6.4.4" + picomatch "^4.0.2" + tinypool@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.0.2.tgz#706193cc532f4c100f66aa00b01c42173d9051b2" @@ -5144,6 +5264,11 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" +typescript@^5.8.3: + version "5.8.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e" + integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ== + uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" @@ -5164,6 +5289,11 @@ undici-types@~6.20.0: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== +undici-types@~6.21.0: + version "6.21.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" + integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== + unique-filename@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" @@ -5457,7 +5587,7 @@ word-wrap@^1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.4.tgz#cb4b50ec9aca570abd1f52f33cd45b6c61739a9f" integrity sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -5475,15 +5605,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"