From 7406d93b6cc6a676a02ddc11713fe8355a2935b6 Mon Sep 17 00:00:00 2001 From: Aiden Bai Date: Wed, 12 Aug 2026 03:15:17 +0000 Subject: [PATCH 1/2] refactor: reduce dependency surface --- .changeset/trim-cli-dependencies.md | 5 + package.json | 1 - packages/core/package.json | 1 - packages/deslop-js/package.json | 1 - packages/evals/package.json | 3 +- .../evals/src/cleanup-evaluation-sandboxes.ts | 5 +- packages/evals/src/run-corpus-evaluation.ts | 4 +- packages/evals/src/run-evaluation-attempts.ts | 5 +- .../evals/src/run-matrix-corpus-evaluation.ts | 6 +- .../src/run-matrix-evaluation-attempts.ts | 5 +- .../src/utils/create-concurrency-limit.ts | 35 +++ .../src/utils/create-paired-ndjson-writer.ts | 5 +- .../tests/create-concurrency-limit.test.ts | 40 +++ packages/react-doctor/package.json | 4 - .../src/cli/ink/components/action-menu.tsx | 4 +- .../src/cli/ink/components/project-select.tsx | 6 +- .../cli/ink/components/scanning-spinner.tsx | 16 + .../src/cli/ink/components/scanning.tsx | 4 +- .../src/cli/utils/build-code-frame.ts | 29 +- .../react-doctor/src/cli/utils/constants.ts | 2 + .../src/cli/utils/highlight-code-line.ts | 14 + .../src/cli/utils/render-code-frame.ts | 67 +++++ .../src/cli/utils/terminal-symbols.ts | 20 ++ .../tests/build-code-frame.test.ts | 33 +++ .../react-doctor/tests/ink/scan-app.test.tsx | 42 +-- .../tests/performance-harness.test.ts | 6 + packages/react-doctor/vite.config.ts | 1 - pnpm-lock.yaml | 273 ------------------ scripts/performance/summarize-distribution.ts | 35 ++- 29 files changed, 321 insertions(+), 351 deletions(-) create mode 100644 .changeset/trim-cli-dependencies.md create mode 100644 packages/evals/src/utils/create-concurrency-limit.ts create mode 100644 packages/evals/tests/create-concurrency-limit.test.ts create mode 100644 packages/react-doctor/src/cli/ink/components/scanning-spinner.tsx create mode 100644 packages/react-doctor/src/cli/utils/highlight-code-line.ts create mode 100644 packages/react-doctor/src/cli/utils/render-code-frame.ts create mode 100644 packages/react-doctor/src/cli/utils/terminal-symbols.ts diff --git a/.changeset/trim-cli-dependencies.md b/.changeset/trim-cli-dependencies.md new file mode 100644 index 0000000000..b173a09f24 --- /dev/null +++ b/.changeset/trim-cli-dependencies.md @@ -0,0 +1,5 @@ +--- +"react-doctor": patch +--- + +Reduce the CLI dependency graph by replacing narrow code-frame, terminal-symbol, and spinner helpers with local implementations. diff --git a/package.json b/package.json index 42c9835249..763e973bdb 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ "@voidzero-dev/vite-plus-core": "^0.1.15", "commander": "^14.0.3", "cross-env": "^10.1.0", - "simple-statistics": "^7.9.3", "tsx": "^4.22.4", "turbo": "^2.9.7", "typescript": "^6.0.3", diff --git a/packages/core/package.json b/packages/core/package.json index b7fde12338..195934998a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -39,7 +39,6 @@ "typescript": ">=5.0.4 <7" }, "devDependencies": { - "@effect/vitest": "4.0.0-beta.102", "@types/node": "^25.6.0", "@types/picomatch": "^4.0.3", "@types/semver": "^7.7.1" diff --git a/packages/deslop-js/package.json b/packages/deslop-js/package.json index aed13be8a1..be9d4379a1 100644 --- a/packages/deslop-js/package.json +++ b/packages/deslop-js/package.json @@ -77,7 +77,6 @@ "typescript": ">=5.0.4 <6" }, "devDependencies": { - "@types/minimatch": "^5.1.2", "@types/node": "^25.6.0", "tsx": "^4.21.0" } diff --git a/packages/evals/package.json b/packages/evals/package.json index 0f81250758..945a92c60b 100644 --- a/packages/evals/package.json +++ b/packages/evals/package.json @@ -14,8 +14,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@daytona/sdk": "^0.196.0", - "p-limit": "^3.1.0" + "@daytona/sdk": "^0.196.0" }, "devDependencies": { "@types/node": "^25.6.0", diff --git a/packages/evals/src/cleanup-evaluation-sandboxes.ts b/packages/evals/src/cleanup-evaluation-sandboxes.ts index 8ac13fafea..1b59562d07 100644 --- a/packages/evals/src/cleanup-evaluation-sandboxes.ts +++ b/packages/evals/src/cleanup-evaluation-sandboxes.ts @@ -1,10 +1,9 @@ import { DaytonaNotFoundError, SandboxState } from "@daytona/sdk"; import type { Daytona, Sandbox } from "@daytona/sdk"; -import pLimit from "p-limit"; - import { SANDBOX_CLEANUP_CONCURRENCY, SANDBOX_DELETE_TIMEOUT_SECONDS } from "./constants.js"; import { toErrorMessage } from "./utils/to-error-message.js"; import { runBeforeDeadline } from "./utils/run-before-deadline.js"; +import { createConcurrencyLimit } from "./utils/create-concurrency-limit.js"; export interface CleanupEvaluationSandboxesInput { daytona: Daytona; @@ -17,7 +16,7 @@ export const cleanupEvaluationSandboxes = async ({ evaluationId, deadlineMilliseconds, }: CleanupEvaluationSandboxesInput): Promise => { - const cleanupLimit = pLimit(SANDBOX_CLEANUP_CONCURRENCY); + const cleanupLimit = createConcurrencyLimit(SANDBOX_CLEANUP_CONCURRENCY); const remainingSandboxes = await runBeforeDeadline({ operation: async () => { const sandboxes: Sandbox[] = []; diff --git a/packages/evals/src/run-corpus-evaluation.ts b/packages/evals/src/run-corpus-evaluation.ts index 6dfdb2ca18..b5ac7843b4 100644 --- a/packages/evals/src/run-corpus-evaluation.ts +++ b/packages/evals/src/run-corpus-evaluation.ts @@ -2,7 +2,6 @@ import { randomUUID } from "node:crypto"; import { open } from "node:fs/promises"; import { Daytona, DaytonaNotFoundError, Image } from "@daytona/sdk"; -import pLimit from "p-limit"; import { cleanupEvaluationSandboxes } from "./cleanup-evaluation-sandboxes.js"; import { deleteDaytonaSnapshotBeforeDeadline } from "./utils/delete-daytona-snapshot-before-deadline.js"; @@ -44,6 +43,7 @@ import type { EvaluationOptions } from "./parse-evaluation-arguments.js"; import { runEvaluationAttempts } from "./run-evaluation-attempts.js"; import { runMatrixCorpusEvaluation } from "./run-matrix-corpus-evaluation.js"; import { createPairedNdjsonWriter } from "./utils/create-paired-ndjson-writer.js"; +import { createConcurrencyLimit } from "./utils/create-concurrency-limit.js"; import { getEvaluationAttemptDeadlineMilliseconds } from "./utils/get-evaluation-attempt-deadline-milliseconds.js"; import { getEvaluatorSourceHash } from "./utils/get-evaluator-source-hash.js"; import { getEvaluationTimeoutSeconds } from "./utils/get-evaluation-timeout-seconds.js"; @@ -196,7 +196,7 @@ export const runCorpusEvaluation = async (options: EvaluationOptions): Promise diff --git a/packages/evals/src/run-evaluation-attempts.ts b/packages/evals/src/run-evaluation-attempts.ts index 980c8e5061..1796607b4c 100644 --- a/packages/evals/src/run-evaluation-attempts.ts +++ b/packages/evals/src/run-evaluation-attempts.ts @@ -1,9 +1,8 @@ -import pLimit from "p-limit"; - import { EVALUATION_RETRY_REPOSITORIES_PER_SANDBOX } from "./constants.js"; import type { CorpusEvaluationRecord, CorpusRepositoryGroup } from "./corpus.js"; import { groupCorpusRepositories } from "./group-corpus-repositories.js"; import { partitionRepositoryGroups } from "./utils/partition-repository-groups.js"; +import { createConcurrencyLimit } from "./utils/create-concurrency-limit.js"; export interface EvaluationRetry { attemptNumber: number; @@ -38,7 +37,7 @@ export const runEvaluationAttempts = async ({ }: RunEvaluationAttemptsInput): Promise => { let pendingRepositoryGroups = repositoryGroups; for (const [attemptIndex, concurrency] of attemptConcurrencies.entries()) { - const limit = pLimit(concurrency); + const limit = createConcurrencyLimit(concurrency); const repositoryBatchSize = attemptIndex === 0 ? repositoriesPerSandbox : EVALUATION_RETRY_REPOSITORIES_PER_SANDBOX; const repositoryBatches = partitionRepositoryGroups( diff --git a/packages/evals/src/run-matrix-corpus-evaluation.ts b/packages/evals/src/run-matrix-corpus-evaluation.ts index 1e6f5f08f9..bb4090e862 100644 --- a/packages/evals/src/run-matrix-corpus-evaluation.ts +++ b/packages/evals/src/run-matrix-corpus-evaluation.ts @@ -5,7 +5,6 @@ import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; import { Daytona, DaytonaNotFoundError, Image } from "@daytona/sdk"; -import pLimit from "p-limit"; import { buildMatrixEvaluationPlan } from "./build-matrix-evaluation-plan.js"; import type { MatrixEvaluationLane } from "./build-matrix-evaluation-plan.js"; @@ -37,6 +36,7 @@ import { runMatrixEvaluationAttempts } from "./run-matrix-evaluation-attempts.js import { abortWriters } from "./utils/abort-writers.js"; import { assertMatrixBaseRecord } from "./utils/assert-matrix-base-record.js"; import { createMatrixBaseArtifactBinding } from "./utils/matrix-base-artifact-binding.js"; +import { createConcurrencyLimit } from "./utils/create-concurrency-limit.js"; import type { MatrixBaseArtifactBinding } from "./utils/matrix-base-artifact-binding.js"; import { deleteDaytonaSnapshotBeforeDeadline } from "./utils/delete-daytona-snapshot-before-deadline.js"; import { getEvaluationAttemptDeadlineMilliseconds } from "./utils/get-evaluation-attempt-deadline-milliseconds.js"; @@ -266,7 +266,9 @@ export const runMatrixCorpusEvaluation = async (options: EvaluationOptions): Pro Math.min(options.concurrency, concurrency), ), ]; - const limitSandboxCreation = pLimit(Math.min(options.concurrency, SANDBOX_CREATE_CONCURRENCY)); + const limitSandboxCreation = createConcurrencyLimit( + Math.min(options.concurrency, SANDBOX_CREATE_CONCURRENCY), + ); const createSandbox = (sandboxName: string, deadlineMilliseconds: number) => limitSandboxCreation(() => daytona.create( diff --git a/packages/evals/src/run-matrix-evaluation-attempts.ts b/packages/evals/src/run-matrix-evaluation-attempts.ts index 43b2e53dca..0a120265d8 100644 --- a/packages/evals/src/run-matrix-evaluation-attempts.ts +++ b/packages/evals/src/run-matrix-evaluation-attempts.ts @@ -1,10 +1,9 @@ -import pLimit from "p-limit"; - import type { CorpusRepository, CorpusRepositoryGroup } from "./corpus.js"; import type { MatrixEvaluationLane } from "./build-matrix-evaluation-plan.js"; import type { MatrixEvaluationFailure } from "./evaluate-matrix-repository-batch.js"; import { groupCorpusRepositories } from "./group-corpus-repositories.js"; import { partitionRepositoryGroups } from "./utils/partition-repository-groups.js"; +import { createConcurrencyLimit } from "./utils/create-concurrency-limit.js"; import { toErrorMessage } from "./utils/to-error-message.js"; export interface RunMatrixEvaluationAttemptsInput { @@ -136,7 +135,7 @@ export const runMatrixEvaluationAttempts = async ({ ).map((repositoryBatch) => ({ repositoryGroups: repositoryBatch, lanes })); for (const [attemptIndex, concurrency] of attemptConcurrencies.entries()) { - const limit = pLimit(concurrency); + const limit = createConcurrencyLimit(concurrency); const workResults = await Promise.allSettled( pendingWork.map((work) => limit(() => evaluateRepositoryBatch(work.repositoryGroups, work.lanes, attemptIndex)), diff --git a/packages/evals/src/utils/create-concurrency-limit.ts b/packages/evals/src/utils/create-concurrency-limit.ts new file mode 100644 index 0000000000..a2888e37df --- /dev/null +++ b/packages/evals/src/utils/create-concurrency-limit.ts @@ -0,0 +1,35 @@ +interface ConcurrencyLimit { + (operation: () => Result | PromiseLike): Promise; +} + +export const createConcurrencyLimit = (concurrency: number): ConcurrencyLimit => { + if (!Number.isInteger(concurrency) || concurrency < 1) { + throw new TypeError("Concurrency must be a positive integer"); + } + + const pendingOperations: Array<() => void> = []; + let activeOperationCount = 0; + + const startNextOperations = (): void => { + while (activeOperationCount < concurrency) { + const startOperation = pendingOperations.shift(); + if (!startOperation) return; + activeOperationCount += 1; + startOperation(); + } + }; + + return (operation: () => Result | PromiseLike): Promise => + new Promise((resolve, reject) => { + pendingOperations.push(() => { + Promise.resolve() + .then(operation) + .then(resolve, reject) + .finally(() => { + activeOperationCount -= 1; + startNextOperations(); + }); + }); + startNextOperations(); + }); +}; diff --git a/packages/evals/src/utils/create-paired-ndjson-writer.ts b/packages/evals/src/utils/create-paired-ndjson-writer.ts index 0cf207b5bf..7c8921cf6c 100644 --- a/packages/evals/src/utils/create-paired-ndjson-writer.ts +++ b/packages/evals/src/utils/create-paired-ndjson-writer.ts @@ -1,7 +1,6 @@ import type { Writable } from "node:stream"; -import pLimit from "p-limit"; - +import { createConcurrencyLimit } from "./create-concurrency-limit.js"; import { serializeNdjsonRecord } from "./serialize-ndjson-record.js"; import { writeWritableContents } from "./write-writable-contents.js"; @@ -51,7 +50,7 @@ export const createPairedNdjsonWriter = ({ baselineFileHandle, treatmentOutput, }: CreatePairedNdjsonWriterInput): PairedNdjsonWriter => { - const limitWrite = pLimit(1); + const limitWrite = createConcurrencyLimit(1); let baselineOffset = 0; let hasWriteFailed = false; let writeFailure: unknown; diff --git a/packages/evals/tests/create-concurrency-limit.test.ts b/packages/evals/tests/create-concurrency-limit.test.ts new file mode 100644 index 0000000000..77f8198d0d --- /dev/null +++ b/packages/evals/tests/create-concurrency-limit.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { createConcurrencyLimit } from "../src/utils/create-concurrency-limit.js"; + +describe("createConcurrencyLimit", () => { + it("runs queued operations in FIFO order without exceeding the limit", async () => { + const limit = createConcurrencyLimit(2); + const startedOperations: number[] = []; + let activeOperationCount = 0; + let maximumActiveOperationCount = 0; + + const results = [1, 2, 3, 4].map((operationId) => + limit(async () => { + startedOperations.push(operationId); + activeOperationCount += 1; + maximumActiveOperationCount = Math.max(maximumActiveOperationCount, activeOperationCount); + await Promise.resolve(); + activeOperationCount -= 1; + return operationId; + }), + ); + + await expect(Promise.all(results)).resolves.toEqual([1, 2, 3, 4]); + expect(startedOperations).toEqual([1, 2, 3, 4]); + expect(maximumActiveOperationCount).toBe(2); + }); + + it("releases a slot when an operation rejects", async () => { + const limit = createConcurrencyLimit(1); + const rejectedOperation = limit(() => Promise.reject(new Error("failed"))); + const nextOperation = limit(() => "completed"); + + await expect(rejectedOperation).rejects.toThrow("failed"); + await expect(nextOperation).resolves.toBe("completed"); + }); + + it("rejects invalid concurrency", () => { + expect(() => createConcurrencyLimit(0)).toThrow("positive integer"); + }); +}); diff --git a/packages/react-doctor/package.json b/packages/react-doctor/package.json index 50cb86a5b7..1cd614f471 100644 --- a/packages/react-doctor/package.json +++ b/packages/react-doctor/package.json @@ -57,14 +57,12 @@ }, "dependencies": { "@astrojs/compiler": "^4.0.0", - "@babel/code-frame": "^7.29.0", "@sentry/node": "^10.54.0", "agent-install": "0.0.5", "conf": "^15.1.0", "confbox": "^0.2.4", "deslop-js": "workspace:*", "eslint-plugin-react-hooks": "^7.1.1", - "figures": "^6.1.0", "jiti": "^2.7.0", "magicast": "^0.5.3", "oxc-resolver": "^11.24.2", @@ -82,12 +80,10 @@ "@react-doctor/api": "workspace:*", "@react-doctor/core": "workspace:*", "@react-doctor/language-server": "workspace:*", - "@types/babel__code-frame": "^7.27.0", "@types/prompts": "^2.4.9", "@types/react": "^19.2.14", "commander": "^14.0.3", "ink": "^7.1.0", - "ink-spinner": "^5.0.0", "ink-testing-library": "^4.0.0", "ora": "^9.4.0", "react": "19.2.5", diff --git a/packages/react-doctor/src/cli/ink/components/action-menu.tsx b/packages/react-doctor/src/cli/ink/components/action-menu.tsx index 106dff14b2..b132683379 100644 --- a/packages/react-doctor/src/cli/ink/components/action-menu.tsx +++ b/packages/react-doctor/src/cli/ink/components/action-menu.tsx @@ -1,10 +1,10 @@ -import figures from "figures"; import { Box, Text, useInput } from "ink"; import type { ReactNode } from "react"; import { TUI_REPORT_ACTION_MENU_ITEM_GAP_ROWS, TUI_REPORT_ACTION_MENU_MARGIN_ROWS, } from "../../utils/constants.js"; +import { terminalSymbols } from "../../utils/terminal-symbols.js"; import { useState } from "../react-runtime.js"; export interface ActionMenuAction { @@ -59,7 +59,7 @@ export const ActionMenu = ({ } > - {isSelected ? figures.pointer : figures.pointerSmall} {action.label} + {isSelected ? terminalSymbols.pointer : terminalSymbols.pointerSmall} {action.label} {isSelected ? action.description : null} diff --git a/packages/react-doctor/src/cli/ink/components/project-select.tsx b/packages/react-doctor/src/cli/ink/components/project-select.tsx index a2f6520e13..4673f834f8 100644 --- a/packages/react-doctor/src/cli/ink/components/project-select.tsx +++ b/packages/react-doctor/src/cli/ink/components/project-select.tsx @@ -1,5 +1,4 @@ import path from "node:path"; -import figures from "figures"; import { Box, Text, useInput } from "ink"; import type { ReactNode } from "react"; import type { WorkspacePackage } from "@react-doctor/core"; @@ -15,6 +14,7 @@ import { clampNumber } from "../../utils/clamp-number.js"; import { isPrintableInput } from "../../utils/is-printable-input.js"; import { recordCount } from "../../utils/record-metric.js"; import { resolveVisibleStart } from "../../utils/resolve-visible-start.js"; +import { terminalSymbols } from "../../utils/terminal-symbols.js"; import { useExitOnCtrlC } from "../hooks/use-exit-on-ctrl-c.js"; import { useStdoutDimensions } from "../hooks/use-stdout-dimensions.js"; import { fuzzyMatch } from "../lib/fuzzy-match.js"; @@ -269,10 +269,10 @@ export const ProjectSelect = ({ packages, rootDirectory, onSubmit }: ProjectSele return ( - {isSelected ? `${figures.pointer} ` : " "} + {isSelected ? `${terminalSymbols.pointer} ` : " "} - {isChecked ? `${figures.radioOn} ` : `${figures.radioOff} `} + {isChecked ? `${terminalSymbols.radioOn} ` : `${terminalSymbols.radioOff} `} { + const [frameIndex, setFrameIndex] = useState(0); + + useEffect(() => { + const intervalId = setInterval(() => { + setFrameIndex((currentFrameIndex) => (currentFrameIndex + 1) % TUI_SPINNER_FRAMES.length); + }, TUI_SPINNER_FRAME_INTERVAL_MS); + return () => clearInterval(intervalId); + }, []); + + return {TUI_SPINNER_FRAMES[frameIndex]}; +}; diff --git a/packages/react-doctor/src/cli/ink/components/scanning.tsx b/packages/react-doctor/src/cli/ink/components/scanning.tsx index d96b4af069..0ff20c3382 100644 --- a/packages/react-doctor/src/cli/ink/components/scanning.tsx +++ b/packages/react-doctor/src/cli/ink/components/scanning.tsx @@ -1,8 +1,8 @@ import { Box, Text } from "ink"; -import Spinner from "ink-spinner"; import type { Diagnostic as LiveDiagnostic } from "@react-doctor/core/schemas"; import { formatDiagnosticSite } from "../../utils/format-diagnostic-site.js"; import { severityVariant } from "../lib/severity-variants.js"; +import { ScanningSpinner } from "./scanning-spinner.js"; export interface ScanningProps { readonly progressText: string | null; @@ -14,7 +14,7 @@ export const Scanning = ({ progressText, recent }: ScanningProps) => { - + {progressText ?? "Scanning…"} diff --git a/packages/react-doctor/src/cli/utils/build-code-frame.ts b/packages/react-doctor/src/cli/utils/build-code-frame.ts index 9f7304e760..16d73cfe84 100644 --- a/packages/react-doctor/src/cli/utils/build-code-frame.ts +++ b/packages/react-doctor/src/cli/utils/build-code-frame.ts @@ -1,10 +1,10 @@ import * as fs from "node:fs"; -import { codeFrameColumns } from "@babel/code-frame"; import { CODE_FRAME_LINES_ABOVE, CODE_FRAME_LINES_BELOW, CODE_FRAME_MAX_LINE_LENGTH_CHARS, } from "@react-doctor/core"; +import { renderCodeFrame } from "./render-code-frame.js"; import { resolveAbsolutePath } from "./resolve-absolute-path.js"; interface CodeFrameInput { @@ -16,8 +16,7 @@ interface CodeFrameInput { // `line`..`endLine` range — used to batch several same-file sites of one // rule into a single spanning frame instead of near-duplicate boxes. readonly endLine?: number; - // Short label rendered inline at the caret (e.g. the rule title). Keep - // it brief — babel prints it right after the `^`. + // Short label rendered inline at the caret (e.g. the rule title). readonly message?: string; } @@ -40,23 +39,15 @@ export const buildCodeFrame = (input: CodeFrameInput): string | null => { return null; } - // A single huge line (minified output, a giant inline data literal) - // only renders an unreadable wall of text, so skip the frame and let - // the caller fall back to the bare `file:line` reference. - const offendingLine = source.split("\n", input.line)[input.line - 1] ?? ""; - if (offendingLine.length > CODE_FRAME_MAX_LINE_LENGTH_CHARS) return null; - - // A spanning frame marks every line in the range and has no single - // caret column; a single-site frame points the caret at the column. - const isRange = input.endLine != null && input.endLine > input.line; - const location = isRange - ? { start: { line: input.line }, end: { line: input.endLine! } } - : { start: { line: input.line, column: input.column > 0 ? input.column : undefined } }; - - return codeFrameColumns(source, location, { - highlightCode: true, + const endLine = input.endLine != null && input.endLine > input.line ? input.endLine : undefined; + return renderCodeFrame({ + source, + line: input.line, + column: endLine === undefined && input.column > 0 ? input.column : undefined, + endLine, + message: input.message, linesAbove: CODE_FRAME_LINES_ABOVE, linesBelow: CODE_FRAME_LINES_BELOW, - ...(input.message ? { message: input.message } : {}), + maximumLineLength: CODE_FRAME_MAX_LINE_LENGTH_CHARS, }); }; diff --git a/packages/react-doctor/src/cli/utils/constants.ts b/packages/react-doctor/src/cli/utils/constants.ts index f2149e5e00..710b911ec0 100644 --- a/packages/react-doctor/src/cli/utils/constants.ts +++ b/packages/react-doctor/src/cli/utils/constants.ts @@ -112,6 +112,8 @@ export const TUI_MIN_NODE_MAJOR_VERSION = 22; export const TUI_LIVE_FEED_MAX_ENTRIES = 25; export const TUI_PROGRESS_UPDATE_INTERVAL_MS = 250; +export const TUI_SPINNER_FRAME_INTERVAL_MS = 80; +export const TUI_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; export const TUI_RECENT_LIVE_DIAGNOSTIC_COUNT = 5; export const TUI_DETAIL_INDENT_COLUMNS = 2; export const TUI_PROJECT_SELECT_CHROME_ROWS = 3; diff --git a/packages/react-doctor/src/cli/utils/highlight-code-line.ts b/packages/react-doctor/src/cli/utils/highlight-code-line.ts new file mode 100644 index 0000000000..bc61acd38a --- /dev/null +++ b/packages/react-doctor/src/cli/utils/highlight-code-line.ts @@ -0,0 +1,14 @@ +import { highlighter } from "@react-doctor/core"; + +const CODE_TOKEN_PATTERN = + /\/\/.*|\/\*.*?\*\/|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|\b(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|export|extends|false|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|true|try|type|typeof|undefined|var|void|while|with|yield)\b|\b(?:0[xob][\da-f]+|\d+(?:\.\d+)?)\b/giu; + +export const highlightCodeLine = (codeLine: string): string => + codeLine.replace(CODE_TOKEN_PATTERN, (token) => { + if (token.startsWith("/")) return highlighter.gray(token); + if (token.startsWith('"') || token.startsWith("'") || token.startsWith("`")) { + return highlighter.success(token); + } + if (/^\d|^0[xob]/i.test(token)) return highlighter.warn(token); + return highlighter.info(token); + }); diff --git a/packages/react-doctor/src/cli/utils/render-code-frame.ts b/packages/react-doctor/src/cli/utils/render-code-frame.ts new file mode 100644 index 0000000000..9626058099 --- /dev/null +++ b/packages/react-doctor/src/cli/utils/render-code-frame.ts @@ -0,0 +1,67 @@ +import { highlighter } from "@react-doctor/core"; +import { highlightCodeLine } from "./highlight-code-line.js"; + +interface RenderCodeFrameInput { + readonly source: string; + readonly line: number; + readonly column?: number; + readonly endLine?: number; + readonly message?: string; + readonly linesAbove: number; + readonly linesBelow: number; + readonly maximumLineLength: number; +} + +const SOURCE_LINE_BREAK_PATTERN = /\r\n|[\n\r\u2028\u2029]/; + +export const renderCodeFrame = ({ + source, + line, + column, + endLine, + message, + linesAbove, + linesBelow, + maximumLineLength, +}: RenderCodeFrameInput): string | null => { + const sourceLines = source.split(SOURCE_LINE_BREAK_PATTERN); + if (line < 1 || line > sourceLines.length) return null; + const offendingLine = sourceLines[line - 1]; + if (offendingLine === undefined || offendingLine.length > maximumLineLength) return null; + + const lastMarkedLine = Math.min(Math.max(endLine ?? line, line), sourceLines.length); + const firstDisplayedLine = Math.max(line - linesAbove, 1); + const lastDisplayedLine = Math.min(lastMarkedLine + linesBelow, sourceLines.length); + const lineNumberWidth = String(lastDisplayedLine).length; + const hasCaret = lastMarkedLine === line && column !== undefined; + const outputLines: string[] = []; + + if (message && !hasCaret) { + outputLines.push(`${" ".repeat(lineNumberWidth + 2)}${highlighter.error(message)}`); + } + + for ( + let displayedLineNumber = firstDisplayedLine; + displayedLineNumber <= lastDisplayedLine; + displayedLineNumber += 1 + ) { + const sourceLine = sourceLines[displayedLineNumber - 1] ?? ""; + const isMarked = displayedLineNumber >= line && displayedLineNumber <= lastMarkedLine; + const gutter = ` ${String(displayedLineNumber).padStart(lineNumberWidth)} |`; + const marker = isMarked ? highlighter.error(">") : " "; + outputLines.push( + `${marker}${highlighter.dim(gutter)}${sourceLine.length > 0 ? ` ${highlightCodeLine(sourceLine)}` : ""}`, + ); + + if (isMarked && hasCaret) { + const markerSpacing = sourceLine.slice(0, Math.max(column - 1, 0)).replace(/[^\t]/g, " "); + const emptyGutter = gutter.replace(/\d/g, " "); + const markerMessage = message ? ` ${highlighter.error(message)}` : ""; + outputLines.push( + ` ${highlighter.dim(emptyGutter)} ${markerSpacing}${highlighter.error("^")}${markerMessage}`, + ); + } + } + + return outputLines.join("\n"); +}; diff --git a/packages/react-doctor/src/cli/utils/terminal-symbols.ts b/packages/react-doctor/src/cli/utils/terminal-symbols.ts new file mode 100644 index 0000000000..45cbed0e94 --- /dev/null +++ b/packages/react-doctor/src/cli/utils/terminal-symbols.ts @@ -0,0 +1,20 @@ +const supportsUnicodeSymbols = + process.platform !== "win32" + ? process.env.TERM !== "linux" + : Boolean(process.env.WT_SESSION) || + Boolean(process.env.TERMINUS_SUBLIME) || + process.env.ConEmuTask === "{cmd::Cmder}" || + process.env.TERM_PROGRAM === "Terminus-Sublime" || + process.env.TERM_PROGRAM === "vscode" || + process.env.TERM === "xterm-256color" || + process.env.TERM === "alacritty" || + process.env.TERM === "rxvt-unicode" || + process.env.TERM === "rxvt-unicode-256color" || + process.env.TERMINAL_EMULATOR === "JetBrains-JediTerm"; + +export const terminalSymbols = { + pointer: supportsUnicodeSymbols ? "❯" : ">", + pointerSmall: "›", + radioOn: supportsUnicodeSymbols ? "◉" : "(*)", + radioOff: supportsUnicodeSymbols ? "◯" : "( )", +}; diff --git a/packages/react-doctor/tests/build-code-frame.test.ts b/packages/react-doctor/tests/build-code-frame.test.ts index c289abd889..12acaee4e9 100644 --- a/packages/react-doctor/tests/build-code-frame.test.ts +++ b/packages/react-doctor/tests/build-code-frame.test.ts @@ -32,6 +32,39 @@ describe("buildCodeFrame", () => { }); expect(frame).not.toBeNull(); expect(frame).toContain("eval(value)"); + expect(frame).toContain(" | ^"); + }); + + it("renders a labeled multi-line range with bounded context", () => { + const filePath = writeFile( + "range.tsx", + ["const before = 1;", "const first = 2;", "const second = 3;", "const after = 4;"].join("\n"), + ); + expect( + buildCodeFrame({ + filePath, + line: 2, + column: 1, + endLine: 3, + message: "Repeated issue", + rootDirectory: temporaryDirectory, + }), + ).toBe( + [ + " Repeated issue", + " 1 | const before = 1;", + "> 2 | const first = 2;", + "> 3 | const second = 3;", + " 4 | const after = 4;", + ].join("\n"), + ); + }); + + it("returns null when the location is past the end of the file", () => { + const filePath = writeFile("short.tsx", "const value = 1;\n"); + expect( + buildCodeFrame({ filePath, line: 5, column: 1, rootDirectory: temporaryDirectory }), + ).toBeNull(); }); it("returns null when the offending line is too long to render usefully", () => { diff --git a/packages/react-doctor/tests/ink/scan-app.test.tsx b/packages/react-doctor/tests/ink/scan-app.test.tsx index 8285d02762..55f8d030f4 100644 --- a/packages/react-doctor/tests/ink/scan-app.test.tsx +++ b/packages/react-doctor/tests/ink/scan-app.test.tsx @@ -2,7 +2,6 @@ import { render } from "ink-testing-library"; import { afterEach, describe, expect, it, vi } from "vite-plus/test"; import { GITHUB_ACTIONS_SETUP_URL } from "@react-doctor/core"; import type { Diagnostic, ScoreResult } from "@react-doctor/core"; -import figures from "figures"; import { TUI_DEFAULT_TERMINAL_COLUMNS, TUI_REPORT_COMPACT_MAX_ROWS, @@ -10,6 +9,7 @@ import { TUI_REPORT_STATUS_ROWS, TUI_REPORT_VIEWPORT_MARGIN_ROWS, } from "../../src/cli/utils/constants.js"; +import { terminalSymbols } from "../../src/cli/utils/terminal-symbols.js"; import * as exitGracefullyModule from "../../src/cli/utils/exit-gracefully.js"; import * as launchAgent from "../../src/cli/utils/launch-agent.js"; import * as openUrlModule from "../../src/cli/utils/open-url.js"; @@ -137,7 +137,7 @@ describe("ScanApp", () => { expect(frame).toContain("demo-app"); expect(frame).toContain("React Doctor"); expect(frame).toContain("┌─────┐"); - expect(frame).toContain(`${figures.pointer} Review 2 issues`); + expect(frame).toContain(`${terminalSymbols.pointer} Review 2 issues`); expect(frame).not.toContain("Top 1 error to review first"); expect(frame).not.toContain("Why Your users briefly see stale state on every prop"); const frameLines = frame.split("\n"); @@ -474,7 +474,7 @@ describe("ScanApp", () => { await flush(); resizeTerminal(stdout, { rows: 30 }); await flush(); - expect(lastFrame()).toContain(`${figures.pointer} Review 2 issues`); + expect(lastFrame()).toContain(`${terminalSymbols.pointer} Review 2 issues`); expect(lastFrame()).toContain("Add to GitHub Actions (Recommended)"); expect(lastFrame()).toContain("58"); stdin.write("\r"); @@ -515,7 +515,7 @@ describe("ScanApp", () => { resizeTerminal(stdout, { rows: 30 }); await flush(); - expect(lastFrame()).toContain(`${figures.pointer} Review 2 issues`); + expect(lastFrame()).toContain(`${terminalSymbols.pointer} Review 2 issues`); stdin.write("\r"); await flush(); @@ -530,7 +530,7 @@ describe("ScanApp", () => { await flush(); const returnedLandingFrame = lastFrame() ?? ""; expect(returnedLandingFrame).toContain( - `${figures.pointer} Add to GitHub Actions (Recommended)`, + `${terminalSymbols.pointer} Add to GitHub Actions (Recommended)`, ); expect(returnedLandingFrame).toContain("› Review 2 issues"); expect(returnedLandingFrame).not.toContain("Top 1 error to review first"); @@ -574,7 +574,7 @@ describe("ScanApp", () => { stdin.write("\u001B"); await flush(); - expect(lastFrame()).toContain(`${figures.pointer} Hand off to an agent`); + expect(lastFrame()).toContain(`${terminalSymbols.pointer} Hand off to an agent`); expect(lastFrame()).toContain("› Review 1 issue"); unmount(); }); @@ -865,7 +865,7 @@ describe("ScanApp", () => { await flush(); expect(lastFrame()).toContain("┌─────┐"); - expect(lastFrame()).toContain(`${figures.pointer} Review 30 issues`); + expect(lastFrame()).toContain(`${terminalSymbols.pointer} Review 30 issues`); stdin.write("\r"); await flush(); @@ -1013,14 +1013,14 @@ describe("ScanApp", () => { ); await flush(); - expect(lastFrame()).toContain(`${figures.pointer} Review 1 issue`); + expect(lastFrame()).toContain(`${terminalSymbols.pointer} Review 1 issue`); expect(lastFrame()).toContain("Add to GitHub Actions (Recommended)"); expect(lastFrame()).toContain( - `${figures.pointer} Review 1 issue\n\n› Add to GitHub Actions (Recommended)`, + `${terminalSymbols.pointer} Review 1 issue\n\n› Add to GitHub Actions (Recommended)`, ); stdin.write("j"); await flush(); - expect(lastFrame()).toContain(`${figures.pointer} Add to GitHub Actions (Recommended)`); + expect(lastFrame()).toContain(`${terminalSymbols.pointer} Add to GitHub Actions (Recommended)`); expect(lastFrame()).toContain("Used by teams at PayPal, Rippling, and Alibaba."); stdin.write("\r"); await flush(); @@ -1037,13 +1037,13 @@ describe("ScanApp", () => { ); expect(ciSetupLines[trustLineIndex + 1]).toContain(GITHUB_ACTIONS_SETUP_URL); expect(lastFrame()).not.toContain("`doctor` package script"); - expect(lastFrame()).toContain(`${figures.pointer} Yes, add the workflow`); + expect(lastFrame()).toContain(`${terminalSymbols.pointer} Yes, add the workflow`); expect(lastFrame()).toContain("Open the GitHub Actions guide"); stdin.write("\u001B"); await flush(); expect(onAddToCi).not.toHaveBeenCalled(); - expect(lastFrame()).toContain(`${figures.pointer} Add to GitHub Actions (Recommended)`); + expect(lastFrame()).toContain(`${terminalSymbols.pointer} Add to GitHub Actions (Recommended)`); stdin.write("\r"); await flush(); @@ -1149,7 +1149,7 @@ describe("ScanApp", () => { await flush(); stdin.write("j"); await flush(); - expect(lastFrame()).toContain(`${figures.pointer} Hand off to an agent`); + expect(lastFrame()).toContain(`${terminalSymbols.pointer} Hand off to an agent`); stdin.write("\r"); await flush(); @@ -1158,14 +1158,16 @@ describe("ScanApp", () => { "Scan every pull request to prevent new React issues while you fix the backlog.", ); expect(lastFrame()).toContain("Used by teams at PayPal, Rippling, and Alibaba."); - expect(lastFrame()).toContain(`${figures.pointer} Add to GitHub Actions first (Recommended)`); + expect(lastFrame()).toContain( + `${terminalSymbols.pointer} Add to GitHub Actions first (Recommended)`, + ); expect(lastFrame()).toContain("Continue without GitHub Actions"); stdin.write("\u001B"); await flush(); expect(onAddToCi).not.toHaveBeenCalled(); expect(lastFrame()).toContain(" Choose how to continue"); - expect(lastFrame()).toContain(`${figures.pointer} Codex`); + expect(lastFrame()).toContain(`${terminalSymbols.pointer} Codex`); expect(lastFrame()).toContain("Cursor"); expect(lastFrame()).toContain("Copy prompt"); @@ -1214,16 +1216,16 @@ describe("ScanApp", () => { stdin.write("\r"); await flush(); expect(onAddToCi).toHaveBeenCalledOnce(); - expect(lastFrame()).toContain(`${figures.pointer} Codex`); + expect(lastFrame()).toContain(`${terminalSymbols.pointer} Codex`); stdin.write("\u001B"); await flush(); expect(lastFrame()).not.toContain("Add to GitHub Actions (Recommended)"); - expect(lastFrame()).toContain(`${figures.pointer} Hand off to an agent`); + expect(lastFrame()).toContain(`${terminalSymbols.pointer} Hand off to an agent`); stdin.write("\r"); await flush(); - expect(lastFrame()).toContain(`${figures.pointer} Codex`); + expect(lastFrame()).toContain(`${terminalSymbols.pointer} Codex`); expect(lastFrame()).not.toContain("Add React Doctor to GitHub Actions first"); stdin.write("\r"); @@ -1262,13 +1264,13 @@ describe("ScanApp", () => { stdin.write("j"); await flush(); - expect(lastFrame()).toContain(`${figures.pointer} Hand off to an agent`); + expect(lastFrame()).toContain(`${terminalSymbols.pointer} Hand off to an agent`); stdin.write("\r"); await flush(); expect(writeDiagnosticsDirectory).not.toHaveBeenCalled(); expect(lastFrame()).toContain("Choose how to continue"); - expect(lastFrame()).toContain(`${figures.pointer} Copy prompt`); + expect(lastFrame()).toContain(`${terminalSymbols.pointer} Copy prompt`); expect(lastFrame()).toContain("Paste into any agent or edit it first"); stdin.write("\r"); await flush(); diff --git a/packages/react-doctor/tests/performance-harness.test.ts b/packages/react-doctor/tests/performance-harness.test.ts index e497c847a4..1368966e1f 100644 --- a/packages/react-doctor/tests/performance-harness.test.ts +++ b/packages/react-doctor/tests/performance-harness.test.ts @@ -477,6 +477,12 @@ describe("performance harness", () => { maximum: 100, medianAbsoluteDeviation: 1, }); + expect(summarizeDistribution([10, 2, 8, 4])).toEqual({ + minimum: 2, + median: 6, + maximum: 10, + medianAbsoluteDeviation: 3, + }); }); it("validates reports and hashes diagnostics", () => { diff --git a/packages/react-doctor/vite.config.ts b/packages/react-doctor/vite.config.ts index 6ee26f0426..2645778837 100644 --- a/packages/react-doctor/vite.config.ts +++ b/packages/react-doctor/vite.config.ts @@ -73,7 +73,6 @@ export default defineConfig({ alwaysBundle: [ "commander", "ink", - "ink-spinner", "ora", "react", "react-devtools-core", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 044e59b086..05f748b9ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,9 +40,6 @@ importers: cross-env: specifier: ^10.1.0 version: 10.1.0 - simple-statistics: - specifier: ^7.9.3 - version: 7.9.3 tsx: specifier: ^4.22.4 version: 4.22.4 @@ -117,9 +114,6 @@ importers: specifier: '>=5.0.4 <7' version: 6.0.3 devDependencies: - '@effect/vitest': - specifier: 4.0.0-beta.102 - version: 4.0.0-beta.102(effect@4.0.0-beta.102)(vitest@4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(vite@7.3.1(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.46.0)(tsx@4.22.4)(yaml@2.9.0))) '@types/node': specifier: ^25.6.0 version: 25.6.0 @@ -164,9 +158,6 @@ importers: specifier: '>=5.0.4 <6' version: 5.9.3 devDependencies: - '@types/minimatch': - specifier: ^5.1.2 - version: 5.1.2 '@types/node': specifier: ^25.6.0 version: 25.6.0 @@ -189,9 +180,6 @@ importers: '@daytona/sdk': specifier: ^0.196.0 version: 0.196.0(ws@8.21.1) - p-limit: - specifier: ^3.1.0 - version: 3.1.0 devDependencies: '@types/node': specifier: ^25.6.0 @@ -265,9 +253,6 @@ importers: '@astrojs/compiler': specifier: ^4.0.0 version: 4.0.0 - '@babel/code-frame': - specifier: ^7.29.0 - version: 7.29.0 '@sentry/node': specifier: ^10.54.0 version: 10.54.0(@opentelemetry/exporter-trace-otlp-http@0.219.0(@opentelemetry/api@1.9.1)) @@ -286,9 +271,6 @@ importers: eslint-plugin-react-hooks: specifier: ^7.1.1 version: 7.1.1(eslint@9.39.2(jiti@2.7.0)) - figures: - specifier: ^6.1.0 - version: 6.1.0 jiti: specifier: ^2.7.0 version: 2.7.0 @@ -335,9 +317,6 @@ importers: '@react-doctor/language-server': specifier: workspace:* version: link:../language-server - '@types/babel__code-frame': - specifier: ^7.27.0 - version: 7.27.0 '@types/prompts': specifier: ^2.4.9 version: 2.4.9 @@ -350,9 +329,6 @@ importers: ink: specifier: ^7.1.0 version: 7.1.0(@types/react@19.2.14)(react-devtools-core@7.0.1)(react@19.2.5) - ink-spinner: - specifier: ^5.0.0 - version: 5.0.0(ink@7.1.0(@types/react@19.2.14)(react-devtools-core@7.0.1)(react@19.2.5))(react@19.2.5) ink-testing-library: specifier: ^4.0.0 version: 4.0.0(@types/react@19.2.14) @@ -642,12 +618,6 @@ packages: peerDependencies: effect: ^4.0.0-beta.102 - '@effect/vitest@4.0.0-beta.102': - resolution: {integrity: sha512-4dipFAYG6imOzrY3zy3BgzCJkbb9xESyzUef0WSx8bsK0/SpITqbJpdwKeOyDWLZ+rimjua8IbTFMAde865pIQ==} - peerDependencies: - effect: ^4.0.0-beta.102 - vitest: ^3.0.0 || ^4.0.0 - '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -2490,9 +2460,6 @@ packages: '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} - '@types/babel__code-frame@7.27.0': - resolution: {integrity: sha512-Dwlo+LrxDx/0SpfmJ/BKveHf7QXWvLBLc+x03l5sbzykj3oB9nHygCpSECF1a+s+QIxbghe+KHqC90vGtxLRAA==} - '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -2508,9 +2475,6 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/minimatch@5.1.2': - resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} - '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} @@ -2539,35 +2503,6 @@ packages: resolution: {integrity: sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@vitest/expect@4.1.7': - resolution: {integrity: sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==} - - '@vitest/mocker@4.1.7': - resolution: {integrity: sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==} - peerDependencies: - msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - - '@vitest/pretty-format@4.1.7': - resolution: {integrity: sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==} - - '@vitest/runner@4.1.7': - resolution: {integrity: sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==} - - '@vitest/snapshot@4.1.7': - resolution: {integrity: sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==} - - '@vitest/spy@4.1.7': - resolution: {integrity: sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==} - - '@vitest/utils@4.1.7': - resolution: {integrity: sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==} - '@voidzero-dev/vite-plus-core@0.1.20': resolution: {integrity: sha512-4KmzRfzwTeG3JuvDijrdqWusSgRvLMKDPrVsDdtbDVVjEMq0VnM8lSH+Nvepd6Pg+SuSVUP212OIfH/3Yn1bfA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2866,10 +2801,6 @@ packages: caniuse-lite@1.0.30001769: resolution: {integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==} - chai@6.2.2: - resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} - engines: {node: '>=18'} - chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -2900,10 +2831,6 @@ packages: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} - cli-spinners@2.9.2: - resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} - engines: {node: '>=6'} - cli-spinners@3.4.0: resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==} engines: {node: '>=18.20'} @@ -3050,9 +2977,6 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - es-module-lexer@2.1.0: - resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} - es-object-atoms@1.1.2: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} engines: {node: '>= 0.4'} @@ -3148,9 +3072,6 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -3163,10 +3084,6 @@ packages: resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==} engines: {node: '>=0.10.0'} - expect-type@1.3.0: - resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} - engines: {node: '>=12.0.0'} - extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} @@ -3202,10 +3119,6 @@ packages: picomatch: optional: true - figures@6.1.0: - resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} - engines: {node: '>=18'} - file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -3379,13 +3292,6 @@ packages: resolution: {integrity: sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} - ink-spinner@5.0.0: - resolution: {integrity: sha512-EYEasbEjkqLGyPOUc8hBJZNuC5GvXGMLu0w5gdTNskPc7Izc5vO3tdQEYnzvshucyGCBXc86ig0ujXPMWaQCdA==} - engines: {node: '>=14.16'} - peerDependencies: - ink: '>=4.0.0' - react: '>=18.0.0' - ink-testing-library@4.0.0: resolution: {integrity: sha512-yF92kj3pmBvk7oKbSq5vEALO//o7Z9Ck/OaLNlkzXNeYdwfpxMQkSowGTFUCS5MSu9bWfSZMewGpp7bFc66D7Q==} engines: {node: '>=18'} @@ -3692,9 +3598,6 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - magicast@0.5.3: resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==} @@ -4069,9 +3972,6 @@ packages: resolution: {integrity: sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==} engines: {node: '>= 0.4'} - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -4079,9 +3979,6 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - simple-statistics@7.9.3: - resolution: {integrity: sha512-WXpxUfo7BJCRpyl4besiuMV7wNj9xiPIq7IKmUQO4upIaF8pK2AXwhjttHN5L8KXZrLkGMCHGHq4p+pJXiIahQ==} - sirv@3.0.2: resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} engines: {node: '>=18'} @@ -4118,9 +4015,6 @@ packages: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - std-env@4.0.0: resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} @@ -4216,10 +4110,6 @@ packages: resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} engines: {node: ^20.0.0 || >=22.0.0} - tinyrainbow@3.1.0: - resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} - engines: {node: '>=14.0.0'} - to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -4341,47 +4231,6 @@ packages: yaml: optional: true - vitest@4.1.7: - resolution: {integrity: sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==} - engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@opentelemetry/api': ^1.9.0 - '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.7 - '@vitest/browser-preview': 4.1.7 - '@vitest/browser-webdriverio': 4.1.7 - '@vitest/coverage-istanbul': 4.1.7 - '@vitest/coverage-v8': 4.1.7 - '@vitest/ui': 4.1.7 - happy-dom: '*' - jsdom: '*' - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@opentelemetry/api': - optional: true - '@types/node': - optional: true - '@vitest/browser-playwright': - optional: true - '@vitest/browser-preview': - optional: true - '@vitest/browser-webdriverio': - optional: true - '@vitest/coverage-istanbul': - optional: true - '@vitest/coverage-v8': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - vscode-jsonrpc@8.2.0: resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} engines: {node: '>=14.0.0'} @@ -4420,11 +4269,6 @@ packages: engines: {node: '>= 8'} hasBin: true - why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} - hasBin: true - widest-line@6.0.0: resolution: {integrity: sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA==} engines: {node: '>=20'} @@ -5037,11 +4881,6 @@ snapshots: - bufferutil - utf-8-validate - '@effect/vitest@4.0.0-beta.102(effect@4.0.0-beta.102)(vitest@4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(vite@7.3.1(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.46.0)(tsx@4.22.4)(yaml@2.9.0)))': - dependencies: - effect: 4.0.0-beta.102 - vitest: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(vite@7.3.1(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.46.0)(tsx@4.22.4)(yaml@2.9.0)) - '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -6350,8 +6189,6 @@ snapshots: tslib: 2.8.1 optional: true - '@types/babel__code-frame@7.27.0': {} - '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -6365,8 +6202,6 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/minimatch@5.1.2': {} - '@types/node@12.20.55': {} '@types/node@25.6.0': @@ -6394,47 +6229,6 @@ snapshots: '@typescript-eslint/types@8.59.3': {} - '@vitest/expect@4.1.7': - dependencies: - '@standard-schema/spec': 1.1.0 - '@types/chai': 5.2.3 - '@vitest/spy': 4.1.7 - '@vitest/utils': 4.1.7 - chai: 6.2.2 - tinyrainbow: 3.1.0 - - '@vitest/mocker@4.1.7(vite@7.3.1(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.46.0)(tsx@4.22.4)(yaml@2.9.0))': - dependencies: - '@vitest/spy': 4.1.7 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 7.3.1(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.46.0)(tsx@4.22.4)(yaml@2.9.0) - - '@vitest/pretty-format@4.1.7': - dependencies: - tinyrainbow: 3.1.0 - - '@vitest/runner@4.1.7': - dependencies: - '@vitest/utils': 4.1.7 - pathe: 2.0.3 - - '@vitest/snapshot@4.1.7': - dependencies: - '@vitest/pretty-format': 4.1.7 - '@vitest/utils': 4.1.7 - magic-string: 0.30.21 - pathe: 2.0.3 - - '@vitest/spy@4.1.7': {} - - '@vitest/utils@4.1.7': - dependencies: - '@vitest/pretty-format': 4.1.7 - convert-source-map: 2.0.0 - tinyrainbow: 3.1.0 - '@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.46.0)(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0)': dependencies: '@oxc-project/runtime': 0.127.0 @@ -6731,8 +6525,6 @@ snapshots: caniuse-lite@1.0.30001769: {} - chai@6.2.2: {} - chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -6756,8 +6548,6 @@ snapshots: dependencies: restore-cursor: 5.1.0 - cli-spinners@2.9.2: {} - cli-spinners@3.4.0: {} cli-truncate@6.0.0: @@ -6891,8 +6681,6 @@ snapshots: es-module-lexer@1.7.0: {} - es-module-lexer@2.1.0: {} - es-object-atoms@1.1.2: dependencies: es-errors: 1.3.0 @@ -7087,10 +6875,6 @@ snapshots: estraverse@5.3.0: {} - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.8 - esutils@2.0.3: {} events@3.3.0: {} @@ -7099,8 +6883,6 @@ snapshots: dependencies: homedir-polyfill: 1.0.3 - expect-type@1.3.0: {} - extendable-error@0.1.7: {} fast-check@4.9.0: @@ -7131,10 +6913,6 @@ snapshots: optionalDependencies: picomatch: 4.0.4 - figures@6.1.0: - dependencies: - is-unicode-supported: 2.1.0 - file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -7299,12 +7077,6 @@ snapshots: ini@7.0.0: {} - ink-spinner@5.0.0(ink@7.1.0(@types/react@19.2.14)(react-devtools-core@7.0.1)(react@19.2.5))(react@19.2.5): - dependencies: - cli-spinners: 2.9.2 - ink: 7.1.0(@types/react@19.2.14)(react-devtools-core@7.0.1)(react@19.2.5) - react: 19.2.5 - ink-testing-library@4.0.0(@types/react@19.2.14): optionalDependencies: '@types/react': 19.2.14 @@ -7545,10 +7317,6 @@ snapshots: dependencies: yallist: 3.1.1 - magic-string@0.30.21: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - magicast@0.5.3: dependencies: '@babel/parser': 7.29.7 @@ -8030,14 +7798,10 @@ snapshots: shell-quote@1.10.0: {} - siginfo@2.0.0: {} - signal-exit@3.0.7: {} signal-exit@4.1.0: {} - simple-statistics@7.9.3: {} - sirv@3.0.2: dependencies: '@polka/url': 1.0.0-next.29 @@ -8075,8 +7839,6 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 - stackback@0.0.2: {} - std-env@4.0.0: {} stdin-discarder@0.3.2: {} @@ -8168,8 +7930,6 @@ snapshots: tinypool@2.1.0: {} - tinyrainbow@3.1.0: {} - to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -8343,34 +8103,6 @@ snapshots: tsx: 4.22.4 yaml: 2.9.0 - vitest@4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(vite@7.3.1(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.46.0)(tsx@4.22.4)(yaml@2.9.0)): - dependencies: - '@vitest/expect': 4.1.7 - '@vitest/mocker': 4.1.7(vite@7.3.1(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.46.0)(tsx@4.22.4)(yaml@2.9.0)) - '@vitest/pretty-format': 4.1.7 - '@vitest/runner': 4.1.7 - '@vitest/snapshot': 4.1.7 - '@vitest/spy': 4.1.7 - '@vitest/utils': 4.1.7 - es-module-lexer: 2.1.0 - expect-type: 1.3.0 - magic-string: 0.30.21 - obug: 2.1.1 - pathe: 2.0.3 - picomatch: 4.0.4 - std-env: 4.0.0 - tinybench: 2.9.0 - tinyexec: 1.1.1 - tinyglobby: 0.2.16 - tinyrainbow: 3.1.0 - vite: 7.3.1(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.46.0)(tsx@4.22.4)(yaml@2.9.0) - why-is-node-running: 2.3.0 - optionalDependencies: - '@opentelemetry/api': 1.9.1 - '@types/node': 25.6.0 - transitivePeerDependencies: - - msw - vscode-jsonrpc@8.2.0: {} vscode-languageclient@9.0.1: @@ -8407,11 +8139,6 @@ snapshots: dependencies: isexe: 2.0.0 - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 - widest-line@6.0.0: dependencies: string-width: 8.2.1 diff --git a/scripts/performance/summarize-distribution.ts b/scripts/performance/summarize-distribution.ts index 8a6d2cf7ca..46bb58f27b 100644 --- a/scripts/performance/summarize-distribution.ts +++ b/scripts/performance/summarize-distribution.ts @@ -1,15 +1,38 @@ -import { max, median, medianAbsoluteDeviation, min } from "simple-statistics"; import type { DistributionSummary } from "./types.ts"; -export const summarizeDistribution = (values: number[]): DistributionSummary => { +const calculateMedian = (sortedValues: ReadonlyArray): number => { + const upperMiddleIndex = Math.floor(sortedValues.length / 2); + const upperMiddleValue = sortedValues[upperMiddleIndex]; + if (upperMiddleValue === undefined) { + throw new Error("Cannot calculate the median of an empty distribution"); + } + if (sortedValues.length % 2 === 1) return upperMiddleValue; + + const lowerMiddleValue = sortedValues[upperMiddleIndex - 1]; + if (lowerMiddleValue === undefined) { + throw new Error("Cannot calculate the median of an empty distribution"); + } + return (lowerMiddleValue + upperMiddleValue) / 2; +}; + +export const summarizeDistribution = (values: ReadonlyArray): DistributionSummary => { if (values.length === 0) { throw new Error("Cannot summarize an empty distribution"); } - const medianValue = median(values); + const sortedValues = [...values].sort((firstValue, secondValue) => firstValue - secondValue); + const medianValue = calculateMedian(sortedValues); + const minimum = sortedValues[0]; + const maximum = sortedValues[sortedValues.length - 1]; + if (minimum === undefined || maximum === undefined) { + throw new Error("Cannot summarize an empty distribution"); + } + const absoluteDeviations = sortedValues + .map((value) => Math.abs(value - medianValue)) + .sort((firstValue, secondValue) => firstValue - secondValue); return { - minimum: min(values), + minimum, median: medianValue, - maximum: max(values), - medianAbsoluteDeviation: medianAbsoluteDeviation(values), + maximum, + medianAbsoluteDeviation: calculateMedian(absoluteDeviations), }; }; From 4d3298a4117746098a5624995037d418757049b1 Mon Sep 17 00:00:00 2001 From: Aiden Bai Date: Wed, 12 Aug 2026 07:27:03 +0000 Subject: [PATCH 2/2] refactor: limit dependency cleanup to runtime deps --- .changeset/trim-cli-dependencies.md | 2 +- package.json | 1 + packages/core/package.json | 1 + packages/deslop-js/package.json | 1 + packages/react-doctor/package.json | 2 + .../cli/ink/components/scanning-spinner.tsx | 16 -- .../src/cli/ink/components/scanning.tsx | 4 +- .../react-doctor/src/cli/utils/constants.ts | 2 - .../tests/build-code-frame.test.ts | 26 +- .../tests/performance-harness.test.ts | 6 - packages/react-doctor/vite.config.ts | 1 + pnpm-lock.yaml | 256 ++++++++++++++++++ scripts/performance/summarize-distribution.ts | 35 +-- 13 files changed, 285 insertions(+), 68 deletions(-) delete mode 100644 packages/react-doctor/src/cli/ink/components/scanning-spinner.tsx diff --git a/.changeset/trim-cli-dependencies.md b/.changeset/trim-cli-dependencies.md index b173a09f24..4a81418e13 100644 --- a/.changeset/trim-cli-dependencies.md +++ b/.changeset/trim-cli-dependencies.md @@ -2,4 +2,4 @@ "react-doctor": patch --- -Reduce the CLI dependency graph by replacing narrow code-frame, terminal-symbol, and spinner helpers with local implementations. +Reduce the CLI dependency graph by replacing narrow code-frame and terminal-symbol helpers with local implementations. diff --git a/package.json b/package.json index 763e973bdb..42c9835249 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "@voidzero-dev/vite-plus-core": "^0.1.15", "commander": "^14.0.3", "cross-env": "^10.1.0", + "simple-statistics": "^7.9.3", "tsx": "^4.22.4", "turbo": "^2.9.7", "typescript": "^6.0.3", diff --git a/packages/core/package.json b/packages/core/package.json index 195934998a..b7fde12338 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -39,6 +39,7 @@ "typescript": ">=5.0.4 <7" }, "devDependencies": { + "@effect/vitest": "4.0.0-beta.102", "@types/node": "^25.6.0", "@types/picomatch": "^4.0.3", "@types/semver": "^7.7.1" diff --git a/packages/deslop-js/package.json b/packages/deslop-js/package.json index be9d4379a1..aed13be8a1 100644 --- a/packages/deslop-js/package.json +++ b/packages/deslop-js/package.json @@ -77,6 +77,7 @@ "typescript": ">=5.0.4 <6" }, "devDependencies": { + "@types/minimatch": "^5.1.2", "@types/node": "^25.6.0", "tsx": "^4.21.0" } diff --git a/packages/react-doctor/package.json b/packages/react-doctor/package.json index 1cd614f471..0ed3c056ab 100644 --- a/packages/react-doctor/package.json +++ b/packages/react-doctor/package.json @@ -80,10 +80,12 @@ "@react-doctor/api": "workspace:*", "@react-doctor/core": "workspace:*", "@react-doctor/language-server": "workspace:*", + "@types/babel__code-frame": "^7.27.0", "@types/prompts": "^2.4.9", "@types/react": "^19.2.14", "commander": "^14.0.3", "ink": "^7.1.0", + "ink-spinner": "^5.0.0", "ink-testing-library": "^4.0.0", "ora": "^9.4.0", "react": "19.2.5", diff --git a/packages/react-doctor/src/cli/ink/components/scanning-spinner.tsx b/packages/react-doctor/src/cli/ink/components/scanning-spinner.tsx deleted file mode 100644 index d8e068ae14..0000000000 --- a/packages/react-doctor/src/cli/ink/components/scanning-spinner.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Text } from "ink"; -import { TUI_SPINNER_FRAME_INTERVAL_MS, TUI_SPINNER_FRAMES } from "../../utils/constants.js"; -import { useEffect, useState } from "../react-runtime.js"; - -export const ScanningSpinner = () => { - const [frameIndex, setFrameIndex] = useState(0); - - useEffect(() => { - const intervalId = setInterval(() => { - setFrameIndex((currentFrameIndex) => (currentFrameIndex + 1) % TUI_SPINNER_FRAMES.length); - }, TUI_SPINNER_FRAME_INTERVAL_MS); - return () => clearInterval(intervalId); - }, []); - - return {TUI_SPINNER_FRAMES[frameIndex]}; -}; diff --git a/packages/react-doctor/src/cli/ink/components/scanning.tsx b/packages/react-doctor/src/cli/ink/components/scanning.tsx index 0ff20c3382..d96b4af069 100644 --- a/packages/react-doctor/src/cli/ink/components/scanning.tsx +++ b/packages/react-doctor/src/cli/ink/components/scanning.tsx @@ -1,8 +1,8 @@ import { Box, Text } from "ink"; +import Spinner from "ink-spinner"; import type { Diagnostic as LiveDiagnostic } from "@react-doctor/core/schemas"; import { formatDiagnosticSite } from "../../utils/format-diagnostic-site.js"; import { severityVariant } from "../lib/severity-variants.js"; -import { ScanningSpinner } from "./scanning-spinner.js"; export interface ScanningProps { readonly progressText: string | null; @@ -14,7 +14,7 @@ export const Scanning = ({ progressText, recent }: ScanningProps) => { - + {progressText ?? "Scanning…"} diff --git a/packages/react-doctor/src/cli/utils/constants.ts b/packages/react-doctor/src/cli/utils/constants.ts index 710b911ec0..f2149e5e00 100644 --- a/packages/react-doctor/src/cli/utils/constants.ts +++ b/packages/react-doctor/src/cli/utils/constants.ts @@ -112,8 +112,6 @@ export const TUI_MIN_NODE_MAJOR_VERSION = 22; export const TUI_LIVE_FEED_MAX_ENTRIES = 25; export const TUI_PROGRESS_UPDATE_INTERVAL_MS = 250; -export const TUI_SPINNER_FRAME_INTERVAL_MS = 80; -export const TUI_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; export const TUI_RECENT_LIVE_DIAGNOSTIC_COUNT = 5; export const TUI_DETAIL_INDENT_COLUMNS = 2; export const TUI_PROJECT_SELECT_CHROME_ROWS = 3; diff --git a/packages/react-doctor/tests/build-code-frame.test.ts b/packages/react-doctor/tests/build-code-frame.test.ts index 12acaee4e9..1968c54018 100644 --- a/packages/react-doctor/tests/build-code-frame.test.ts +++ b/packages/react-doctor/tests/build-code-frame.test.ts @@ -1,6 +1,7 @@ import * as fs from "node:fs"; import os from "node:os"; import * as path from "node:path"; +import { stripVTControlCharacters } from "node:util"; import { afterEach, beforeEach, describe, expect, it } from "vite-plus/test"; import { CODE_FRAME_MAX_LINE_LENGTH_CHARS } from "@react-doctor/core"; import { buildCodeFrame } from "../src/cli/utils/build-code-frame.js"; @@ -31,8 +32,9 @@ describe("buildCodeFrame", () => { rootDirectory: temporaryDirectory, }); expect(frame).not.toBeNull(); - expect(frame).toContain("eval(value)"); - expect(frame).toContain(" | ^"); + const plainFrame = stripVTControlCharacters(frame ?? ""); + expect(plainFrame).toContain("eval(value)"); + expect(plainFrame).toContain(" | ^"); }); it("renders a labeled multi-line range with bounded context", () => { @@ -40,16 +42,16 @@ describe("buildCodeFrame", () => { "range.tsx", ["const before = 1;", "const first = 2;", "const second = 3;", "const after = 4;"].join("\n"), ); - expect( - buildCodeFrame({ - filePath, - line: 2, - column: 1, - endLine: 3, - message: "Repeated issue", - rootDirectory: temporaryDirectory, - }), - ).toBe( + const frame = buildCodeFrame({ + filePath, + line: 2, + column: 1, + endLine: 3, + message: "Repeated issue", + rootDirectory: temporaryDirectory, + }); + expect(frame).not.toBeNull(); + expect(stripVTControlCharacters(frame ?? "")).toBe( [ " Repeated issue", " 1 | const before = 1;", diff --git a/packages/react-doctor/tests/performance-harness.test.ts b/packages/react-doctor/tests/performance-harness.test.ts index 1368966e1f..e497c847a4 100644 --- a/packages/react-doctor/tests/performance-harness.test.ts +++ b/packages/react-doctor/tests/performance-harness.test.ts @@ -477,12 +477,6 @@ describe("performance harness", () => { maximum: 100, medianAbsoluteDeviation: 1, }); - expect(summarizeDistribution([10, 2, 8, 4])).toEqual({ - minimum: 2, - median: 6, - maximum: 10, - medianAbsoluteDeviation: 3, - }); }); it("validates reports and hashes diagnostics", () => { diff --git a/packages/react-doctor/vite.config.ts b/packages/react-doctor/vite.config.ts index 2645778837..6ee26f0426 100644 --- a/packages/react-doctor/vite.config.ts +++ b/packages/react-doctor/vite.config.ts @@ -73,6 +73,7 @@ export default defineConfig({ alwaysBundle: [ "commander", "ink", + "ink-spinner", "ora", "react", "react-devtools-core", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 05f748b9ec..6cb2a6f0a6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,6 +40,9 @@ importers: cross-env: specifier: ^10.1.0 version: 10.1.0 + simple-statistics: + specifier: ^7.9.3 + version: 7.9.3 tsx: specifier: ^4.22.4 version: 4.22.4 @@ -114,6 +117,9 @@ importers: specifier: '>=5.0.4 <7' version: 6.0.3 devDependencies: + '@effect/vitest': + specifier: 4.0.0-beta.102 + version: 4.0.0-beta.102(effect@4.0.0-beta.102)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(vite@7.3.1(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.46.0)(tsx@4.22.4)(yaml@2.9.0))) '@types/node': specifier: ^25.6.0 version: 25.6.0 @@ -158,6 +164,9 @@ importers: specifier: '>=5.0.4 <6' version: 5.9.3 devDependencies: + '@types/minimatch': + specifier: ^5.1.2 + version: 5.1.2 '@types/node': specifier: ^25.6.0 version: 25.6.0 @@ -317,6 +326,9 @@ importers: '@react-doctor/language-server': specifier: workspace:* version: link:../language-server + '@types/babel__code-frame': + specifier: ^7.27.0 + version: 7.27.0 '@types/prompts': specifier: ^2.4.9 version: 2.4.9 @@ -329,6 +341,9 @@ importers: ink: specifier: ^7.1.0 version: 7.1.0(@types/react@19.2.14)(react-devtools-core@7.0.1)(react@19.2.5) + ink-spinner: + specifier: ^5.0.0 + version: 5.0.0(ink@7.1.0(@types/react@19.2.14)(react-devtools-core@7.0.1)(react@19.2.5))(react@19.2.5) ink-testing-library: specifier: ^4.0.0 version: 4.0.0(@types/react@19.2.14) @@ -618,6 +633,12 @@ packages: peerDependencies: effect: ^4.0.0-beta.102 + '@effect/vitest@4.0.0-beta.102': + resolution: {integrity: sha512-4dipFAYG6imOzrY3zy3BgzCJkbb9xESyzUef0WSx8bsK0/SpITqbJpdwKeOyDWLZ+rimjua8IbTFMAde865pIQ==} + peerDependencies: + effect: ^4.0.0-beta.102 + vitest: ^3.0.0 || ^4.0.0 + '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -2460,6 +2481,9 @@ packages: '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@types/babel__code-frame@7.27.0': + resolution: {integrity: sha512-Dwlo+LrxDx/0SpfmJ/BKveHf7QXWvLBLc+x03l5sbzykj3oB9nHygCpSECF1a+s+QIxbghe+KHqC90vGtxLRAA==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -2475,6 +2499,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/minimatch@5.1.2': + resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} + '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} @@ -2503,6 +2530,35 @@ packages: resolution: {integrity: sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} + + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} + + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} + + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} + + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} + '@voidzero-dev/vite-plus-core@0.1.20': resolution: {integrity: sha512-4KmzRfzwTeG3JuvDijrdqWusSgRvLMKDPrVsDdtbDVVjEMq0VnM8lSH+Nvepd6Pg+SuSVUP212OIfH/3Yn1bfA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2801,6 +2857,10 @@ packages: caniuse-lite@1.0.30001769: resolution: {integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -2831,6 +2891,10 @@ packages: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + cli-spinners@3.4.0: resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==} engines: {node: '>=18.20'} @@ -2977,6 +3041,9 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.3.1: + resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} + es-object-atoms@1.1.2: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} engines: {node: '>= 0.4'} @@ -3072,6 +3139,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -3084,6 +3154,10 @@ packages: resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==} engines: {node: '>=0.10.0'} + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} @@ -3292,6 +3366,13 @@ packages: resolution: {integrity: sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + ink-spinner@5.0.0: + resolution: {integrity: sha512-EYEasbEjkqLGyPOUc8hBJZNuC5GvXGMLu0w5gdTNskPc7Izc5vO3tdQEYnzvshucyGCBXc86ig0ujXPMWaQCdA==} + engines: {node: '>=14.16'} + peerDependencies: + ink: '>=4.0.0' + react: '>=18.0.0' + ink-testing-library@4.0.0: resolution: {integrity: sha512-yF92kj3pmBvk7oKbSq5vEALO//o7Z9Ck/OaLNlkzXNeYdwfpxMQkSowGTFUCS5MSu9bWfSZMewGpp7bFc66D7Q==} engines: {node: '>=18'} @@ -3598,6 +3679,9 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magicast@0.5.3: resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==} @@ -3972,6 +4056,9 @@ packages: resolution: {integrity: sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -3979,6 +4066,9 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-statistics@7.9.3: + resolution: {integrity: sha512-WXpxUfo7BJCRpyl4besiuMV7wNj9xiPIq7IKmUQO4upIaF8pK2AXwhjttHN5L8KXZrLkGMCHGHq4p+pJXiIahQ==} + sirv@3.0.2: resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} engines: {node: '>=18'} @@ -4015,6 +4105,9 @@ packages: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + std-env@4.0.0: resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} @@ -4110,6 +4203,10 @@ packages: resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} engines: {node: ^20.0.0 || >=22.0.0} + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + engines: {node: '>=14.0.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -4231,6 +4328,47 @@ packages: yaml: optional: true + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vscode-jsonrpc@8.2.0: resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} engines: {node: '>=14.0.0'} @@ -4269,6 +4407,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + widest-line@6.0.0: resolution: {integrity: sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA==} engines: {node: '>=20'} @@ -4881,6 +5024,11 @@ snapshots: - bufferutil - utf-8-validate + '@effect/vitest@4.0.0-beta.102(effect@4.0.0-beta.102)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(vite@7.3.1(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.46.0)(tsx@4.22.4)(yaml@2.9.0)))': + dependencies: + effect: 4.0.0-beta.102 + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(vite@7.3.1(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.46.0)(tsx@4.22.4)(yaml@2.9.0)) + '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -6189,6 +6337,8 @@ snapshots: tslib: 2.8.1 optional: true + '@types/babel__code-frame@7.27.0': {} + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -6202,6 +6352,8 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/minimatch@5.1.2': {} + '@types/node@12.20.55': {} '@types/node@25.6.0': @@ -6229,6 +6381,47 @@ snapshots: '@typescript-eslint/types@8.59.3': {} + '@vitest/expect@4.1.10': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + chai: 6.2.2 + tinyrainbow: 3.1.1 + + '@vitest/mocker@4.1.10(vite@7.3.1(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.46.0)(tsx@4.22.4)(yaml@2.9.0))': + dependencies: + '@vitest/spy': 4.1.10 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.1(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.46.0)(tsx@4.22.4)(yaml@2.9.0) + + '@vitest/pretty-format@4.1.10': + dependencies: + tinyrainbow: 3.1.1 + + '@vitest/runner@4.1.10': + dependencies: + '@vitest/utils': 4.1.10 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.10': {} + + '@vitest/utils@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.1 + '@voidzero-dev/vite-plus-core@0.1.20(@types/node@25.6.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.46.0)(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0)': dependencies: '@oxc-project/runtime': 0.127.0 @@ -6525,6 +6718,8 @@ snapshots: caniuse-lite@1.0.30001769: {} + chai@6.2.2: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -6548,6 +6743,8 @@ snapshots: dependencies: restore-cursor: 5.1.0 + cli-spinners@2.9.2: {} + cli-spinners@3.4.0: {} cli-truncate@6.0.0: @@ -6681,6 +6878,8 @@ snapshots: es-module-lexer@1.7.0: {} + es-module-lexer@2.3.1: {} + es-object-atoms@1.1.2: dependencies: es-errors: 1.3.0 @@ -6875,6 +7074,10 @@ snapshots: estraverse@5.3.0: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + esutils@2.0.3: {} events@3.3.0: {} @@ -6883,6 +7086,8 @@ snapshots: dependencies: homedir-polyfill: 1.0.3 + expect-type@1.4.0: {} + extendable-error@0.1.7: {} fast-check@4.9.0: @@ -7077,6 +7282,12 @@ snapshots: ini@7.0.0: {} + ink-spinner@5.0.0(ink@7.1.0(@types/react@19.2.14)(react-devtools-core@7.0.1)(react@19.2.5))(react@19.2.5): + dependencies: + cli-spinners: 2.9.2 + ink: 7.1.0(@types/react@19.2.14)(react-devtools-core@7.0.1)(react@19.2.5) + react: 19.2.5 + ink-testing-library@4.0.0(@types/react@19.2.14): optionalDependencies: '@types/react': 19.2.14 @@ -7317,6 +7528,10 @@ snapshots: dependencies: yallist: 3.1.1 + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + magicast@0.5.3: dependencies: '@babel/parser': 7.29.7 @@ -7798,10 +8013,14 @@ snapshots: shell-quote@1.10.0: {} + siginfo@2.0.0: {} + signal-exit@3.0.7: {} signal-exit@4.1.0: {} + simple-statistics@7.9.3: {} + sirv@3.0.2: dependencies: '@polka/url': 1.0.0-next.29 @@ -7839,6 +8058,8 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 + stackback@0.0.2: {} + std-env@4.0.0: {} stdin-discarder@0.3.2: {} @@ -7930,6 +8151,8 @@ snapshots: tinypool@2.1.0: {} + tinyrainbow@3.1.1: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -8103,6 +8326,34 @@ snapshots: tsx: 4.22.4 yaml: 2.9.0 + vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(vite@7.3.1(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.46.0)(tsx@4.22.4)(yaml@2.9.0)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@7.3.1(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.46.0)(tsx@4.22.4)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.1 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.1.1 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.1 + vite: 7.3.1(@types/node@25.6.0)(jiti@2.7.0)(lightningcss@1.33.0)(terser@5.46.0)(tsx@4.22.4)(yaml@2.9.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@types/node': 25.6.0 + transitivePeerDependencies: + - msw + vscode-jsonrpc@8.2.0: {} vscode-languageclient@9.0.1: @@ -8139,6 +8390,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + widest-line@6.0.0: dependencies: string-width: 8.2.1 diff --git a/scripts/performance/summarize-distribution.ts b/scripts/performance/summarize-distribution.ts index 46bb58f27b..8a6d2cf7ca 100644 --- a/scripts/performance/summarize-distribution.ts +++ b/scripts/performance/summarize-distribution.ts @@ -1,38 +1,15 @@ +import { max, median, medianAbsoluteDeviation, min } from "simple-statistics"; import type { DistributionSummary } from "./types.ts"; -const calculateMedian = (sortedValues: ReadonlyArray): number => { - const upperMiddleIndex = Math.floor(sortedValues.length / 2); - const upperMiddleValue = sortedValues[upperMiddleIndex]; - if (upperMiddleValue === undefined) { - throw new Error("Cannot calculate the median of an empty distribution"); - } - if (sortedValues.length % 2 === 1) return upperMiddleValue; - - const lowerMiddleValue = sortedValues[upperMiddleIndex - 1]; - if (lowerMiddleValue === undefined) { - throw new Error("Cannot calculate the median of an empty distribution"); - } - return (lowerMiddleValue + upperMiddleValue) / 2; -}; - -export const summarizeDistribution = (values: ReadonlyArray): DistributionSummary => { +export const summarizeDistribution = (values: number[]): DistributionSummary => { if (values.length === 0) { throw new Error("Cannot summarize an empty distribution"); } - const sortedValues = [...values].sort((firstValue, secondValue) => firstValue - secondValue); - const medianValue = calculateMedian(sortedValues); - const minimum = sortedValues[0]; - const maximum = sortedValues[sortedValues.length - 1]; - if (minimum === undefined || maximum === undefined) { - throw new Error("Cannot summarize an empty distribution"); - } - const absoluteDeviations = sortedValues - .map((value) => Math.abs(value - medianValue)) - .sort((firstValue, secondValue) => firstValue - secondValue); + const medianValue = median(values); return { - minimum, + minimum: min(values), median: medianValue, - maximum, - medianAbsoluteDeviation: calculateMedian(absoluteDeviations), + maximum: max(values), + medianAbsoluteDeviation: medianAbsoluteDeviation(values), }; };