diff --git a/package.json b/package.json index 23d7e9cba6..1c082839f2 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,6 @@ "long": "^5.3.2", "memoize-immutable": "^3.0.0", "memoize-one": "^6.0.0", - "minimist": "^1.2.8", "mixedtuplemap": "^1.0.0", "namedtuplemap": "^1.0.0", "photon-colors": "^3.3.2", @@ -126,7 +125,6 @@ "@types/clamp": "^1.0.3", "@types/common-tags": "^1.8.4", "@types/jest": "^30.0.0", - "@types/minimist": "^1.2.5", "@types/node": "^22.19.19", "@types/query-string": "^6.3.0", "@types/react": "^18.3.29", diff --git a/src/node-tools/profiler-edit.ts b/src/node-tools/profiler-edit.ts index 07c56c6133..49c11a55f8 100644 --- a/src/node-tools/profiler-edit.ts +++ b/src/node-tools/profiler-edit.ts @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import fs from 'fs'; -import minimist from 'minimist'; +import { Command, CommanderError, Option } from 'commander'; import { unserializeProfileOfArbitraryFormat } from 'firefox-profiler/profile-logic/process-profile'; import { computeCompactedProfile } from 'firefox-profiler/profile-logic/profile-compacting'; @@ -197,28 +197,73 @@ export async function run(options: CliOptions) { console.log('Finished.'); } +function collectWasm( + value: string, + previous: WasmSymbolicationCliSpec[] +): WasmSymbolicationCliSpec[] { + // Accept "=" if the LHS looks like a URL, otherwise treat the + // whole string as a path and infer the URL from the profile. Split on + // the last `=` so URLs containing `=` (e.g. in query strings) survive + // intact; this assumes file paths don't contain `=`. + const eqIndex = value.lastIndexOf('='); + if (eqIndex !== -1 && /^[a-z]+:\/\//i.test(value.slice(0, eqIndex))) { + return [ + ...previous, + { + strippedWasmUrl: value.slice(0, eqIndex), + unstrippedWasmPath: value.slice(eqIndex + 1), + }, + ]; + } + return [...previous, { unstrippedWasmPath: value }]; +} + export function makeOptionsFromArgv(processArgv: string[]): CliOptions { - const argv = minimist(processArgv.slice(2), { - alias: { i: 'input', o: 'output' }, - }); + const program = new Command(); + program + .name('profiler-edit') + .description('Edit and transform Firefox performance profiles') + .exitOverride() + .option( + '-i, --input ', + 'Input profile (file path or http(s) URL)' + ) + .option('-o, --output ', 'Output path (.json or .json.gz)') + .option('--from-file ', 'Load input from a file') + .option('--from-url ', 'Load input from a URL') + .option('--from-hash ', 'Load input from a profile hash') + .option( + '--symbolicate-with-server ', + 'Symbolicate frames using this symbol server URL' + ) + .addOption( + new Option( + '--symbolicate-wasm ', + 'Apply wasm symbol info, as = or just ' + ) + .argParser(collectWasm) + .default([] as WasmSymbolicationCliSpec[]) + ); - const sources: ProfileSource[] = []; + program.parse(processArgv); + const opts = program.opts(); - if (typeof argv.input === 'string' && argv.input !== '') { - if (/^https?:\/\//i.test(argv.input)) { - sources.push({ type: 'URL', url: argv.input }); + const sources: ProfileSource[] = []; + if (typeof opts.input === 'string' && opts.input !== '') { + if (/^https?:\/\//i.test(opts.input)) { + sources.push({ type: 'URL', url: opts.input }); } else { - sources.push({ type: 'FILE', path: argv.input }); + sources.push({ type: 'FILE', path: opts.input }); } } - if (typeof argv['from-file'] === 'string' && argv['from-file'] !== '') { - sources.push({ type: 'FILE', path: argv['from-file'] }); + if (typeof opts.fromFile === 'string' && opts.fromFile !== '') { + sources.push({ type: 'FILE', path: opts.fromFile }); } - if (typeof argv['from-url'] === 'string' && argv['from-url'] !== '') { - sources.push({ type: 'URL', url: argv['from-url'] }); + if (typeof opts.fromUrl === 'string' && opts.fromUrl !== '') { + sources.push({ type: 'URL', url: opts.fromUrl }); } - if (typeof argv['from-hash'] === 'string' && argv['from-hash'] !== '') { - sources.push({ type: 'HASH', hash: argv['from-hash'] }); + if (typeof opts.fromHash === 'string' && opts.fromHash !== '') { + sources.push({ type: 'HASH', hash: opts.fromHash }); } if (sources.length === 0) { @@ -232,55 +277,36 @@ export function makeOptionsFromArgv(processArgv: string[]): CliOptions { ); } - if (!(typeof argv.output === 'string' && argv.output !== '')) { + if (!(typeof opts.output === 'string' && opts.output !== '')) { throw new Error('An output path must be supplied with --output / -o'); } - const symbolicateWasm: WasmSymbolicationCliSpec[] = []; - const rawWasmArg = argv['symbolicate-wasm']; - let wasmArgs: unknown[]; - if (rawWasmArg === undefined) { - wasmArgs = []; - } else if (Array.isArray(rawWasmArg)) { - wasmArgs = rawWasmArg; - } else { - wasmArgs = [rawWasmArg]; - } - for (const arg of wasmArgs) { - if (typeof arg !== 'string' || arg === '') { - throw new Error('--symbolicate-wasm requires a value'); - } - // Accept "=" if the LHS looks like a URL, otherwise treat the - // whole string as a path and infer the URL from the profile. Split on - // the last `=` so URLs containing `=` (e.g. in query strings) survive - // intact; this assumes file paths don't contain `=`. - const eqIndex = arg.lastIndexOf('='); - if (eqIndex !== -1 && /^[a-z]+:\/\//i.test(arg.slice(0, eqIndex))) { - symbolicateWasm.push({ - strippedWasmUrl: arg.slice(0, eqIndex), - unstrippedWasmPath: arg.slice(eqIndex + 1), - }); - } else { - symbolicateWasm.push({ unstrippedWasmPath: arg }); - } - } - return { input: sources[0], - output: argv.output, + output: opts.output, symbolicateWithServer: - typeof argv['symbolicate-with-server'] === 'string' && - argv['symbolicate-with-server'] !== '' - ? argv['symbolicate-with-server'] + typeof opts.symbolicateWithServer === 'string' && + opts.symbolicateWithServer !== '' + ? opts.symbolicateWithServer : undefined, - symbolicateWasm, + symbolicateWasm: opts.symbolicateWasm, }; } if (require.main === module) { - const options = makeOptionsFromArgv(process.argv); - run(options).catch((err) => { - console.error(err); + try { + const options = makeOptionsFromArgv(process.argv); + run(options).catch((err) => { + console.error(err); + process.exit(1); + }); + } catch (err) { + if (err instanceof CommanderError) { + // Commander already wrote its own output and chose the + // appropriate exit code. + process.exit(err.exitCode); + } + console.error(err instanceof Error ? err.message : String(err)); process.exit(1); - }); + } } diff --git a/yarn.lock b/yarn.lock index 9aadcec184..d55c597ecf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2485,7 +2485,7 @@ dependencies: "@types/unist" "*" -"@types/minimist@^1.2.0", "@types/minimist@^1.2.2", "@types/minimist@^1.2.5": +"@types/minimist@^1.2.0", "@types/minimist@^1.2.2": version "1.2.5" resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.5.tgz#ec10755e871497bcd83efe927e43ec46e8c0747e" integrity sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag== @@ -8314,7 +8314,7 @@ minimist-options@4.1.0: is-plain-obj "^1.1.0" kind-of "^6.0.3" -minimist@^1.2.0, minimist@^1.2.6, minimist@^1.2.8: +minimist@^1.2.0, minimist@^1.2.6: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==