mirror of
https://github.com/millionco/react-doctor.git
synced 2026-09-14 20:00:24 +08:00
167 lines
6.2 KiB
TypeScript
167 lines
6.2 KiB
TypeScript
import * as path from "node:path";
|
|
import { Command, Option } from "commander";
|
|
import {
|
|
DEFAULT_BENCHMARK_MODES,
|
|
DEFAULT_CACHE_COHORTS,
|
|
DEFAULT_OUTPUT_DIRECTORY,
|
|
DEFAULT_SAMPLE_COUNT,
|
|
DEFAULT_WARMUP_COUNT,
|
|
DEFAULT_WORKER_COUNTS,
|
|
} from "./constants.ts";
|
|
import type {
|
|
BenchmarkCacheCohort,
|
|
BenchmarkCliOptions,
|
|
BenchmarkMode,
|
|
PerformanceCommandOptions,
|
|
} from "./types.ts";
|
|
|
|
export const parsePositiveInteger = (name: string, value: string, allowZero: boolean): number => {
|
|
const parsedValue = Number(value);
|
|
const minimumValue = allowZero ? 0 : 1;
|
|
if (!/^\d+$/.test(value) || !Number.isSafeInteger(parsedValue) || parsedValue < minimumValue) {
|
|
throw new Error(`${name} must be an integer greater than or equal to ${minimumValue}`);
|
|
}
|
|
return parsedValue;
|
|
};
|
|
|
|
const parseModes = (value: string): BenchmarkMode[] => {
|
|
if (value === "all" || value === "both") return ["lint", "full"];
|
|
const modes = value.split(",").map((mode) => {
|
|
if (mode !== "lint" && mode !== "full") throw new Error(`Unknown benchmark mode: ${mode}`);
|
|
return mode;
|
|
});
|
|
return [...new Set(modes)];
|
|
};
|
|
|
|
const parseCacheCohorts = (value: string): BenchmarkCacheCohort[] => {
|
|
if (value === "all") return ["no-cache", "cold", "hot"];
|
|
const cacheCohorts = value.split(",").map((cacheCohort) => {
|
|
if (cacheCohort !== "no-cache" && cacheCohort !== "cold" && cacheCohort !== "hot") {
|
|
throw new Error(`Unknown cache cohort: ${cacheCohort}`);
|
|
}
|
|
return cacheCohort;
|
|
});
|
|
return [...new Set(cacheCohorts)];
|
|
};
|
|
|
|
const parseWorkerCounts = (value: string): Array<number | "auto"> => {
|
|
const workerCounts: Array<number | "auto"> = [];
|
|
for (const workerValue of value.split(",")) {
|
|
if (workerValue === "auto") {
|
|
workerCounts.push("auto");
|
|
continue;
|
|
}
|
|
workerCounts.push(parsePositiveInteger("--workers", workerValue, false));
|
|
}
|
|
return [...new Set(workerCounts)];
|
|
};
|
|
|
|
const parseCorpusNames = (value: string): string[] => {
|
|
const names = value
|
|
.split(",")
|
|
.map((name) => name.trim())
|
|
.filter((name) => name.length > 0);
|
|
if (names.length === 0) throw new Error("--corpus requires at least one target name");
|
|
return [...new Set(names)];
|
|
};
|
|
|
|
export interface SharedBenchmarkCommandInput {
|
|
readonly name: string;
|
|
readonly description: string;
|
|
readonly outputDirectoryDefault: string;
|
|
readonly cacheCohortsDefault: readonly string[];
|
|
}
|
|
|
|
export const buildSharedBenchmarkCommand = (input: SharedBenchmarkCommandInput): Command =>
|
|
new Command()
|
|
.name(input.name)
|
|
.description(input.description)
|
|
.addOption(
|
|
new Option("--samples <count>", "measured samples per series")
|
|
.default(DEFAULT_SAMPLE_COUNT)
|
|
.argParser((value) => parsePositiveInteger("--samples", value, false)),
|
|
)
|
|
.addOption(
|
|
new Option("--warmups <count>", "excluded warmup samples per series")
|
|
.default(DEFAULT_WARMUP_COUNT)
|
|
.argParser((value) => parsePositiveInteger("--warmups", value, true)),
|
|
)
|
|
.option(
|
|
"--workers <counts>",
|
|
"comma-separated worker counts or auto",
|
|
DEFAULT_WORKER_COUNTS.join(","),
|
|
)
|
|
.option(
|
|
"--mode <modes>",
|
|
"lint, full, both, or a comma-separated list",
|
|
DEFAULT_BENCHMARK_MODES.join(","),
|
|
)
|
|
.option(
|
|
"--cache <cohorts>",
|
|
"no-cache, cold, hot, all, or a comma-separated list",
|
|
input.cacheCohortsDefault.join(","),
|
|
)
|
|
.option("--out <directory>", "artifact directory", input.outputDirectoryDefault)
|
|
.option(
|
|
"--cli <path>",
|
|
"built React Doctor CLI to benchmark",
|
|
"packages/react-doctor/dist/cli.js",
|
|
)
|
|
.option("--compare <results.json>", "compare against a previous result")
|
|
.option("--profile", "capture V8 CPU profiles in a dedicated sample", false)
|
|
.option("--heap-profile", "capture V8 heap profiles in a dedicated sample", false)
|
|
.option("--rule-timings", "capture Oxlint per-rule timings in a dedicated sample", false)
|
|
.option("--strict", "exit non-zero when diagnostics differ from the --compare baseline", false)
|
|
.showHelpAfterError()
|
|
.allowExcessArguments(false)
|
|
.exitOverride();
|
|
|
|
export const parseUserArguments = (command: Command, argumentsList: string[]): Command =>
|
|
command.parse(argumentsList[0] === "--" ? argumentsList.slice(1) : argumentsList, {
|
|
from: "user",
|
|
});
|
|
|
|
export const toBenchmarkCliOptions = (
|
|
commandOptions: PerformanceCommandOptions,
|
|
directories: string[],
|
|
): BenchmarkCliOptions => ({
|
|
directories: [...new Set(directories.map((directory) => path.resolve(directory)))],
|
|
samples: commandOptions.samples,
|
|
warmups: commandOptions.warmups,
|
|
workerCounts: parseWorkerCounts(commandOptions.workers),
|
|
modes: parseModes(commandOptions.mode),
|
|
cacheCohorts: parseCacheCohorts(commandOptions.cache),
|
|
outputDirectory: path.resolve(commandOptions.out),
|
|
comparePath: commandOptions.compare ? path.resolve(commandOptions.compare) : null,
|
|
cliPath: path.resolve(commandOptions.cli),
|
|
profile: commandOptions.profile,
|
|
heapProfile: commandOptions.heapProfile,
|
|
ruleTimings: commandOptions.ruleTimings,
|
|
corpus: commandOptions.corpus === undefined ? [] : parseCorpusNames(commandOptions.corpus),
|
|
strict: Boolean(commandOptions.strict),
|
|
});
|
|
|
|
export const parsePerformanceArguments = (argumentsList: string[]): BenchmarkCliOptions => {
|
|
const command = buildSharedBenchmarkCommand({
|
|
name: "react-doctor-performance",
|
|
description: "Benchmark the built React Doctor CLI against arbitrary directories",
|
|
outputDirectoryDefault: DEFAULT_OUTPUT_DIRECTORY,
|
|
cacheCohortsDefault: DEFAULT_CACHE_COHORTS,
|
|
})
|
|
.argument("[directories...]", "directories to benchmark")
|
|
.option(
|
|
"--corpus <names>",
|
|
"comma-separated corpus.json target names fetched by performance:corpus",
|
|
);
|
|
parseUserArguments(command, argumentsList);
|
|
const directoriesArgument: unknown = command.processedArgs[0];
|
|
const directories = Array.isArray(directoriesArgument)
|
|
? directoriesArgument.filter((entry): entry is string => typeof entry === "string")
|
|
: [];
|
|
const options = toBenchmarkCliOptions(command.opts<PerformanceCommandOptions>(), directories);
|
|
if (options.directories.length === 0 && (options.corpus?.length ?? 0) === 0) {
|
|
throw new Error("Pass at least one directory or --corpus <names>");
|
|
}
|
|
return options;
|
|
};
|