diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml index d23925b5..302a580f 100644 --- a/.github/workflows/codspeed.yml +++ b/.github/workflows/codspeed.yml @@ -82,8 +82,11 @@ jobs: - name: Run benchmarks # use version from `main` branch to always test the latest version, in real projects, use a tag, like `@v2` uses: CodSpeedHQ/action@main + env: + CODSPEED_WALLTIME_PROFILER: samply with: mode: walltime + runner-version: v4.17.7-alpha.2 run: | pnpm turbo run bench --filter=@codspeed/tinybench-plugin pnpm turbo run bench --filter=@codspeed/vitest-plugin @@ -173,3 +176,4 @@ jobs: working-directory: examples/with-electron-and-walltime run: xvfb-run -a pnpm bench:electron mode: walltime + runner-version: v4.17.7-alpha.2 diff --git a/packages/core/package.json b/packages/core/package.json index 187dac49..4aadb5fd 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -32,7 +32,6 @@ "homepage": "https://codspeed.io", "license": "Apache-2.0", "devDependencies": { - "@types/find-up": "^4.0.0", "@types/stack-trace": "^0.0.30", "node-addon-api": "^5.1.0", "node-gyp": "^12.2.0", @@ -41,7 +40,6 @@ }, "dependencies": { "axios": "^1.4.0", - "find-up": "^6.3.0", "form-data": "^4.0.4", "node-gyp-build": "^4.6.0", "stack-trace": "1.0.0-pre2" diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index da252118..7588febd 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -1,14 +1,36 @@ -import { Options as FindupOptions, findUpSync } from "find-up"; -import path, { dirname } from "path"; +import { existsSync, statSync } from "fs"; +import path, { dirname, join } from "path"; import { get as getStackTrace } from "stack-trace"; import { fileURLToPath } from "url"; -export function getGitDir(path: string): string | undefined { - const dotGitPath = findUpSync(".git", { - cwd: path, - type: "directory", - } as FindupOptions); - return dotGitPath ? dirname(dotGitPath) : undefined; +export function getGitDir(fromPath: string): string | undefined { + // Walk up from the starting path looking for the first ancestor that contains + // a `.git` entry, accepting it whether it's a directory (regular checkout) or + // a file (a gitlink, as used by git worktrees and submodules). Restricting to + // directories would miss those layouts and either fail or wrongly resolve to a + // parent repository. + let current = directoryOf(fromPath); + for (;;) { + if (existsSync(join(current, ".git"))) { + return current; + } + const parent = dirname(current); + if (parent === current) { + return undefined; + } + current = parent; + } +} + +/** The starting directory for the walk: `p` itself if it's a directory, else its parent. */ +function directoryOf(p: string): string { + try { + return statSync(p).isDirectory() ? p : dirname(p); + } catch { + // Path doesn't exist (e.g. a not-yet-written file): treat it as a file and + // start from its containing directory. + return dirname(p); + } } /** diff --git a/packages/vitest-plugin/package.json b/packages/vitest-plugin/package.json index 54be7e73..4964d4f7 100644 --- a/packages/vitest-plugin/package.json +++ b/packages/vitest-plugin/package.json @@ -41,13 +41,13 @@ "peerDependencies": { "tinybench": ">=2.9.0", "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", - "vitest": "^3.2 || ^4" + "vitest": "^3.2 || ^4 || ^5.0.0-beta" }, "devDependencies": { "@total-typescript/shoehorn": "^0.1.1", "execa": "^8.0.1", "tinybench": "^2.9.0", "vite": "^7.0.0", - "vitest": "^4.0.18" + "vitest": "5.0.0-beta.5" } } diff --git a/packages/vitest-plugin/rollup.config.ts b/packages/vitest-plugin/rollup.config.ts index fe9ff81a..f5b61aa5 100644 --- a/packages/vitest-plugin/rollup.config.ts +++ b/packages/vitest-plugin/rollup.config.ts @@ -32,4 +32,10 @@ export default defineConfig([ plugins: jsPlugins(pkg.version), external: ["@codspeed/core", /^vitest/], }, + { + input: "src/v5/runner.ts", + output: { file: "dist/v5-runner.mjs", format: "es" }, + plugins: jsPlugins(pkg.version), + external: ["@codspeed/core", /^vitest/, "tinybench"], + }, ]); diff --git a/packages/vitest-plugin/src/__tests__/index.test.ts b/packages/vitest-plugin/src/__tests__/index.test.ts index 98bb38e0..f37ac93f 100644 --- a/packages/vitest-plugin/src/__tests__/index.test.ts +++ b/packages/vitest-plugin/src/__tests__/index.test.ts @@ -43,6 +43,19 @@ vi.mock("fs", () => { console.warn = vi.fn(); +const EXPECTED_EXEC_ARGV = [ + "--interpreted-frames-native-stack", + "--allow-natives-syntax", + "--hash-seed=1", + "--random-seed=1", + "--no-opt", + "--predictable", + "--predictable-gc-schedule", + "--expose-gc", + "--no-concurrent-sweeping", + "--max-old-space-size=4096", +]; + describe("codSpeedPlugin", () => { beforeAll(() => { // Set environment variables to trigger instrumented mode @@ -54,6 +67,7 @@ describe("codSpeedPlugin", () => { // Clean up environment variables delete process.env.CODSPEED_ENV; delete process.env.CODSPEED_RUNNER_MODE; + fsMocks.setMockVersion("4.0.18"); }); it("should have a name", async () => { @@ -65,7 +79,9 @@ describe("codSpeedPlugin", () => { }); describe("apply", () => { - it("should not apply the plugin when the mode is not benchmark", async () => { + it("should not apply the plugin when the mode is not benchmark (v3/v4)", async () => { + fsMocks.setMockVersion("4.0.18"); + const applyPlugin = applyPluginFunction( {}, fromPartial({ mode: "test" }), @@ -74,7 +90,8 @@ describe("codSpeedPlugin", () => { expect(applyPlugin).toBe(false); }); - it("should apply the plugin when there is no instrumentation", async () => { + it("should apply the plugin when there is no instrumentation (v3/v4)", async () => { + fsMocks.setMockVersion("4.0.18"); coreMocks.InstrumentHooks.isInstrumented.mockReturnValue(false); const applyPlugin = applyPluginFunction( @@ -88,7 +105,8 @@ describe("codSpeedPlugin", () => { expect(applyPlugin).toBe(true); }); - it("should apply the plugin when there is instrumentation", async () => { + it("should apply the plugin when there is instrumentation (v3/v4)", async () => { + fsMocks.setMockVersion("4.0.18"); coreMocks.InstrumentHooks.isInstrumented.mockReturnValue(true); const applyPlugin = applyPluginFunction( @@ -98,14 +116,32 @@ describe("codSpeedPlugin", () => { expect(applyPlugin).toBe(true); }); + + it("should stay active regardless of mode on v5 (benchmark gating happens in config)", async () => { + fsMocks.setMockVersion("5.0.0-beta.5"); + coreMocks.InstrumentHooks.isInstrumented.mockReturnValue(true); + + const applyPlugin = applyPluginFunction( + {}, + fromPartial({ mode: "test" }), + ); + + expect(applyPlugin).toBe(true); + fsMocks.setMockVersion("4.0.18"); + }); }); it("should apply the codspeed config for v4", () => { + fsMocks.setMockVersion("4.0.18"); const config = resolvedCodSpeedPlugin.config; if (typeof config !== "function") throw new Error("config is not a function"); - const result = config.call({} as never, {}, fromPartial({})); + const result = config.call( + {} as never, + {}, + fromPartial({ mode: "benchmark" }), + ); expect(result).toStrictEqual({ test: { @@ -113,18 +149,7 @@ describe("codSpeedPlugin", () => { expect.stringContaining("packages/vitest-plugin/src/globalSetup.ts"), ], pool: "forks", - execArgv: [ - "--interpreted-frames-native-stack", - "--allow-natives-syntax", - "--hash-seed=1", - "--random-seed=1", - "--no-opt", - "--predictable", - "--predictable-gc-schedule", - "--expose-gc", - "--no-concurrent-sweeping", - "--max-old-space-size=4096", - ], + execArgv: EXPECTED_EXEC_ARGV, runner: expect.stringContaining( "packages/vitest-plugin/src/analysis.ts", ), @@ -133,16 +158,18 @@ describe("codSpeedPlugin", () => { }); it("should apply the codspeed config for v3 with poolOptions", () => { - // Set mock version to v3 fsMocks.setMockVersion("3.2.0"); - // Create a new plugin instance to pick up the mocked version const v3Plugin = codspeedPlugin(); const config = v3Plugin.config; if (typeof config !== "function") throw new Error("config is not a function"); - const result = config.call({} as never, {}, fromPartial({})); + const result = config.call( + {} as never, + {}, + fromPartial({ mode: "benchmark" }), + ); expect(result).toStrictEqual({ test: { @@ -152,18 +179,7 @@ describe("codSpeedPlugin", () => { pool: "forks", poolOptions: { forks: { - execArgv: [ - "--interpreted-frames-native-stack", - "--allow-natives-syntax", - "--hash-seed=1", - "--random-seed=1", - "--no-opt", - "--predictable", - "--predictable-gc-schedule", - "--expose-gc", - "--no-concurrent-sweeping", - "--max-old-space-size=4096", - ], + execArgv: EXPECTED_EXEC_ARGV, }, }, runner: expect.stringContaining( @@ -172,7 +188,51 @@ describe("codSpeedPlugin", () => { }, }); - // Reset mock version back to v4 fsMocks.setMockVersion("4.0.18"); }); + + describe("v5 config", () => { + it("should not inject config when benchmarks are not enabled", () => { + fsMocks.setMockVersion("5.0.0-beta.5"); + const v5Plugin = codspeedPlugin(); + const config = v5Plugin.config; + if (typeof config !== "function") + throw new Error("config is not a function"); + + const result = config.call({} as never, {}, fromPartial({ mode: "test" })); + + expect(result).toBeUndefined(); + fsMocks.setMockVersion("4.0.18"); + }); + + it("should inject the v5 runner when benchmarks are enabled", () => { + fsMocks.setMockVersion("5.0.0-beta.5"); + const v5Plugin = codspeedPlugin(); + const config = v5Plugin.config; + if (typeof config !== "function") + throw new Error("config is not a function"); + + const result = config.call( + {} as never, + // `benchmark.enabled` is a Vitest 5 config field the v3/4 typings (which + // this file may be compiled against) don't expose. + { test: { benchmark: { enabled: true } } } as never, + fromPartial({ mode: "test" }), + ); + + expect(result).toStrictEqual({ + test: { + globalSetup: [ + expect.stringContaining( + "packages/vitest-plugin/src/globalSetup.ts", + ), + ], + pool: "forks", + execArgv: EXPECTED_EXEC_ARGV, + runner: expect.stringContaining("packages/vitest-plugin/src/v5"), + }, + }); + fsMocks.setMockVersion("4.0.18"); + }); + }); }); diff --git a/packages/vitest-plugin/src/__tests__/instrumented.test.ts b/packages/vitest-plugin/src/__tests__/instrumented.test.ts index ee880797..f727be1b 100644 --- a/packages/vitest-plugin/src/__tests__/instrumented.test.ts +++ b/packages/vitest-plugin/src/__tests__/instrumented.test.ts @@ -1,8 +1,16 @@ import { fromPartial } from "@total-typescript/shoehorn"; import { describe, expect, it, vi, type RunnerTestSuite } from "vitest"; +// `vitest/suite` only exists on Vitest 3/4; this file is excluded from the test +// run under v5+ (see vitest.config.ts). +// eslint-disable-next-line import/no-unresolved import { getBenchFn } from "vitest/suite"; import { AnalysisRunner as CodSpeedRunner } from "../analysis"; +// The legacy AnalysisRunner targets the Vitest 3/4 benchmark backend +// (`NodeBenchmarkRunner`, `vitest/suite`), which Vitest 5 removed. This whole +// file is excluded from the test run under v5+ (see vitest.config.ts); the v5 +// path is covered separately. + const coreMocks = vi.hoisted(() => { return { InstrumentHooks: { diff --git a/packages/vitest-plugin/src/analysis.ts b/packages/vitest-plugin/src/analysis.ts index 424e1a70..e2ed4855 100644 --- a/packages/vitest-plugin/src/analysis.ts +++ b/packages/vitest-plugin/src/analysis.ts @@ -8,7 +8,11 @@ import { wrapWithRootFrame, } from "@codspeed/core"; import { Benchmark, type RunnerTestSuite } from "vitest"; +// `vitest/runners` and `vitest/suite` only exist on Vitest 3/4; this runner is +// loaded only there. +// eslint-disable-next-line import/no-unresolved import { NodeBenchmarkRunner } from "vitest/runners"; +// eslint-disable-next-line import/no-unresolved import { getBenchFn } from "vitest/suite"; import { callSuiteHook, @@ -36,11 +40,14 @@ async function runAnalysisBench( currentSuiteName: string, ) { const uri = `${currentSuiteName}::${benchmark.name}`; - const fn = getBenchFn(benchmark); + // tinybench's bench fn carries a `this: Bench` requirement on Vitest 3/4 that + // we don't need (the work under test is self-contained); call it as a plain + // parameterless function. The cast also smooths over the typing differences + // across supported Vitest versions. + const fn = getBenchFn(benchmark) as () => unknown; await optimizeFunction(async () => { await callSuiteHook(suite, benchmark, "beforeEach"); - // @ts-expect-error we do not need to bind the function to an instance of tinybench's Bench await fn(); await callSuiteHook(suite, benchmark, "afterEach"); }); @@ -50,7 +57,6 @@ async function runAnalysisBench( global.gc?.(); await wrapWithRootFrame(async () => { InstrumentHooks.startBenchmark(); - // @ts-expect-error we do not need to bind the function to an instance of tinybench's Bench await fn(); InstrumentHooks.stopBenchmark(); InstrumentHooks.setExecutedBenchmark(process.pid, uri); diff --git a/packages/vitest-plugin/src/common.ts b/packages/vitest-plugin/src/common.ts index 9ef422a7..e281276a 100644 --- a/packages/vitest-plugin/src/common.ts +++ b/packages/vitest-plugin/src/common.ts @@ -1,6 +1,8 @@ import { getGitDir } from "@codspeed/core"; import path from "path"; import { Benchmark, type RunnerTask, type RunnerTestSuite } from "vitest"; +// `vitest/suite` only exists on Vitest 3/4; this module is used only there. +// eslint-disable-next-line import/no-unresolved import { getHooks } from "vitest/suite"; type SuiteHooks = ReturnType; @@ -19,8 +21,11 @@ export async function callSuiteHook( const hooks = getSuiteHooks(suite, name); - // @ts-expect-error TODO: add support for hooks parameters - await Promise.all(hooks.map((fn) => fn())); + // TODO: add support for hook parameters. The hook signature differs across + // supported Vitest versions, so we call them through a parameterless cast. + await Promise.all( + (hooks as Array<() => unknown>).map((fn) => fn()), + ); if (name === "afterEach" && suite?.suite) { await callSuiteHook(suite.suite, currentTask, name); diff --git a/packages/vitest-plugin/src/index.ts b/packages/vitest-plugin/src/index.ts index 2bdf59ed..b23ecb5f 100644 --- a/packages/vitest-plugin/src/index.ts +++ b/packages/vitest-plugin/src/index.ts @@ -7,11 +7,10 @@ import { SetupInstrumentsRequestBody, SetupInstrumentsResponse, } from "@codspeed/core"; -import { readFileSync } from "fs"; -import { createRequire } from "module"; import { join } from "path"; import { Plugin } from "vite"; import { type ViteUserConfig } from "vitest/config"; +import { getVitestMajorVersion, isVitestV5OrHigher } from "./version"; // get this file's directory path from import.meta.url const __dirname = new URL(".", import.meta.url).pathname; @@ -23,33 +22,54 @@ function getCodSpeedFileFromName(name: string) { return join(__dirname, `${name}.${fileExtension}`); } -function getVitestMajorVersion(): number | null { - try { - // Resolve vitest from the project's perspective (cwd), not from the plugin's location - // This ensures we detect the vitest version the user has installed - const require = createRequire(join(process.cwd(), "package.json")); - const vitestPkgPath = require.resolve("vitest/package.json"); - const vitestPkg = JSON.parse(readFileSync(vitestPkgPath, "utf-8")); - return parseInt(vitestPkg.version.split(".")[0], 10); - } catch { - return null; - } -} - +/** + * Resolve the runner file Vitest should load. + * + * Vitest 5 dropped the dedicated benchmark runner and runs benches inside the + * unified `TestRunner`; a single runner file covers both walltime and analysis + * there, selecting behavior at runtime from the instrument mode. Vitest 3/4 keep + * one runner per mode (`analysis` / `walltime`). + */ function getRunnerFile(): string | undefined { const instrumentMode = getInstrumentMode(); if (instrumentMode === "disabled") { return undefined; } + if (isVitestV5OrHigher()) { + return isFileInTs + ? join(__dirname, "v5", "runner.ts") + : join(__dirname, "v5-runner.mjs"); + } + return getCodSpeedFileFromName(instrumentMode); } +/** + * Whether the current invocation is running benchmarks. + * + * Vitest 3/4 expose this as the Vite `mode` (`"benchmark"`). Vitest 5 no longer + * uses a dedicated mode — `vitest bench` runs under the `"test"` mode but flips + * `test.benchmark.enabled`, so we read that off the incoming config instead. + */ +function isBenchmarkRun(config: ViteUserConfig, mode: string): boolean { + if (isVitestV5OrHigher()) { + // `benchmark.enabled` only exists on the Vitest 5 config; the v3/4 typings + // we may be compiled against don't know about it. + const benchmark = config.test?.benchmark as { enabled?: boolean } | undefined; + return benchmark?.enabled === true; + } + return mode === "benchmark"; +} + export default function codspeedPlugin(): Plugin { return { name: "codspeed:vitest", apply(_, { mode }) { - if (mode !== "benchmark") { + // On Vitest 5 the benchmark signal isn't available on the Vite `mode`, so + // we keep the plugin active and gate the actual config injection in + // `config()` where `test.benchmark.enabled` is visible. + if (!isVitestV5OrHigher() && mode !== "benchmark") { return false; } if ( @@ -61,13 +81,17 @@ export default function codspeedPlugin(): Plugin { return true; }, enforce: "post", - config(): ViteUserConfig { + config(incomingConfig, { mode }): ViteUserConfig | undefined { + if (!isBenchmarkRun(incomingConfig, mode)) { + return undefined; + } + const runnerFile = getRunnerFile(); const runnerMode = getCodspeedRunnerMode(); const v8Flags = getV8Flags(); const vitestMajorVersion = getVitestMajorVersion(); // by default, assume Vitest v4 or higher - const isVitestV4OrHigher = (vitestMajorVersion ?? 4) >= 4; + const isVitestV4OrHigher = (vitestMajorVersion ?? 5) >= 4; const config: ViteUserConfig = { test: { @@ -89,9 +113,7 @@ export default function codspeedPlugin(): Plugin { runner: runnerFile, }), ...(runnerMode === "walltime" && { - benchmark: { - includeSamples: true, - }, + benchmark: getWalltimeBenchmarkConfig(), }), }, }; @@ -101,6 +123,18 @@ export default function codspeedPlugin(): Plugin { }; } +/** + * Ask Vitest to keep tinybench's per-iteration samples so the runner can compute + * walltime quantiles. The option was renamed when Vitest 5 moved to tinybench v6 + * (`includeSamples` → `retainSamples`), so we set whichever the running version + * understands. + */ +function getWalltimeBenchmarkConfig(): Record { + return isVitestV5OrHigher() + ? { retainSamples: true } + : { includeSamples: true }; +} + /** * Dynamically setup the CodSpeed instruments. */ diff --git a/packages/vitest-plugin/src/instrument.ts b/packages/vitest-plugin/src/instrument.ts new file mode 100644 index 00000000..c3f6a790 --- /dev/null +++ b/packages/vitest-plugin/src/instrument.ts @@ -0,0 +1,232 @@ +import { + calculateQuantiles, + InstrumentHooks, + MARKER_TYPE_BENCHMARK_END, + MARKER_TYPE_BENCHMARK_START, + msToNs, + msToS, + wrapWithRootFrame, + type Benchmark, + type BenchmarkStats, +} from "@codspeed/core"; +import type * as tinybench from "tinybench"; + +export type Tinybench = typeof tinybench; + +/** A tinybench task, exposing the `fn` the runner wraps with the root frame. */ +export interface TinybenchTask { + name: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + fn: (...args: any[]) => any; + result?: TinybenchTaskResult; +} + +/** tinybench's per-task setup/teardown hook signature. */ +export type TinybenchHook = ( + task: TinybenchTask, + mode: "run" | "warmup", +) => Promise | void; + +/** The mutable subset of a tinybench Bench the runner reaches into. */ +export interface TinybenchBench { + setup: TinybenchHook; + teardown: TinybenchHook; +} + +/** The minimal task shape `patchTaskRunWithRootFrame` mutates. */ +interface RunnableTask { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + fn: (...args: any[]) => any; +} + +/** The tinybench Task prototype whose `run` we wrap. */ +interface TinybenchTaskClass { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + prototype: { run: (this: any) => Promise }; +} + +/** + * The tinybench statistics shape (latency/throughput) shared across the v2 and + * v6 lines. Only the fields the conversion needs are modeled. + */ +interface TinybenchStatistics { + min: number; + max: number; + mean: number; + sd: number; + samples: number[] | undefined; +} + +interface TinybenchTaskResult { + state?: string; + totalTime: number; + latency: TinybenchStatistics; +} + +/** The subset of tinybench bench options that maps onto a CodSpeed benchmark config. */ +export interface TinybenchOptions { + time?: number; + warmupTime?: number; + warmupIterations?: number; + iterations?: number; +} + +/** + * Carries the instrumentation window across a single task's setup/teardown pair. + * Tasks run strictly sequentially, so a single value is enough. + */ +export interface InstrumentWindow { + runStart: bigint | null; +} + +/** + * Wrap each task's function with the root frame so collected stacks can be + * attributed to a benchmark. The window itself is driven by the bench's + * setup/teardown hooks (see installInstrumentHooks). Patching the shared + * `Task.prototype.run` in place applies to every Bench instance and only needs + * to happen once. + * + * The Task class is taken from a live instance rather than imported so the + * prototype we mutate is the exact one the host (Vitest) constructed tasks + * against, even when multiple copies of tinybench are installed. + */ +export function patchTaskRunWithRootFrame(TaskClass: TinybenchTaskClass): void { + const originalRun = TaskClass.prototype.run; + + TaskClass.prototype.run = async function (this: RunnableTask) { + const originalFn = this.fn; + this.fn = wrapWithRootFrame(() => originalFn.call(this)); + + try { + return await originalRun.call(this); + } finally { + this.fn = originalFn; + } + }; +} + +/** + * Drive the instrumentation window from each bench's run-mode setup/teardown + * hooks so it brackets only tinybench's measured loop, excluding the warmup + * that runs beforehand and the statistics computation tinybench performs after + * the loop. Wrapping the whole `Task.run()` would otherwise fold all of that + * framework overhead into the recorded sample. + * + * User-provided hooks are preserved and keep their order relative to the work + * under test. + */ +export function installInstrumentHooks( + bench: TinybenchBench, + window: InstrumentWindow, + getUri: (taskName: string) => string, +): void { + const userSetup = bench.setup; + const userTeardown = bench.teardown; + + bench.setup = async (task, mode) => { + await userSetup(task, mode); + if (mode === "run") { + InstrumentHooks.startBenchmark(); + window.runStart = InstrumentHooks.currentTimestamp(); + } + }; + + bench.teardown = async (task, mode) => { + if (mode === "run") { + closeInstrumentWindow(getUri(task.name), window); + } + await userTeardown(task, mode); + }; +} + +function closeInstrumentWindow(uri: string, window: InstrumentWindow): void { + const runEnd = InstrumentHooks.currentTimestamp(); + const pid = process.pid; + + // Benchmark markers must land inside the sample window opened by + // startBenchmark(), so they have to be emitted before stopBenchmark() + // closes it. The runner consumes the FIFO stream in order, so a marker + // sent after StopBenchmark falls outside the sample and breaks the + // expected SampleStart > BenchmarkStart > BenchmarkEnd > SampleEnd nesting. + InstrumentHooks.addMarker(pid, MARKER_TYPE_BENCHMARK_START, window.runStart!); + InstrumentHooks.addMarker(pid, MARKER_TYPE_BENCHMARK_END, runEnd); + + InstrumentHooks.stopBenchmark(); + InstrumentHooks.setExecutedBenchmark(pid, uri); + window.runStart = null; +} + +/** + * Convert a completed tinybench task into a CodSpeed walltime benchmark. Returns + * null when the task produced no samples (e.g. fully optimized out), in which + * case there is nothing to record. + */ +export function tinybenchTaskToBenchmark( + task: TinybenchTask, + uri: string, + options: TinybenchOptions, +): Benchmark | null { + const stats = tinybenchResultToStats(task.result, options); + if (stats === null) { + return null; + } + + return { + name: task.name, + uri, + config: { + max_rounds: options.iterations ?? null, + max_time_ns: options.time ? msToNs(options.time) : null, + min_round_time_ns: null, // tinybench does not have an option for this + warmup_time_ns: + options.warmupIterations !== 0 && options.warmupTime + ? msToNs(options.warmupTime) + : null, + }, + stats, + }; +} + +function tinybenchResultToStats( + result: TinybenchTaskResult | undefined, + options: TinybenchOptions, +): BenchmarkStats | null { + if (!result) { + throw new Error("No benchmark data available in result"); + } + + const { totalTime, latency } = result; + const { min, max, mean, sd, samples } = latency; + + const sortedTimesNs = (samples ?? []) + .map(msToNs) + .sort((a, b) => a - b); + const meanNs = msToNs(mean); + const stdevNs = msToNs(sd); + + if (sortedTimesNs.length == 0) { + // Sometimes the benchmarks can be completely optimized out and not even + // run, but their beforeEach and afterEach hooks are still executed, and the + // task is still considered a success. + return null; + } + + const { q1_ns, q3_ns, median_ns, iqr_outlier_rounds, stdev_outlier_rounds } = + calculateQuantiles({ meanNs, stdevNs, sortedTimesNs }); + + return { + min_ns: msToNs(min), + max_ns: msToNs(max), + mean_ns: meanNs, + stdev_ns: stdevNs, + q1_ns, + median_ns, + q3_ns, + total_time: msToS(totalTime), + iter_per_round: 1, // tinybench runs one iteration per round + rounds: sortedTimesNs.length, + iqr_outlier_rounds, + stdev_outlier_rounds, + warmup_iters: options.warmupIterations ?? 0, + }; +} diff --git a/packages/vitest-plugin/src/v5/runner.ts b/packages/vitest-plugin/src/v5/runner.ts new file mode 100644 index 00000000..3f4ec614 --- /dev/null +++ b/packages/vitest-plugin/src/v5/runner.ts @@ -0,0 +1,140 @@ +import { + getGitDir, + getInstrumentMode, + setupCore, + writeWalltimeResults, + type Benchmark, +} from "@codspeed/core"; +import path from "path"; +// `TestRunner` is the unified runner Vitest 5 introduced; it replaces the +// `NodeBenchmarkRunner` the legacy seam subclasses, and is re-exported from the +// package root. It is read off the namespace (rather than a named import) so the +// plugin still type-checks against Vitest 3/4, which don't export it; this +// module only ever runs under v5. +import * as vitest from "vitest"; +import { + installInstrumentHooks, + patchTaskRunWithRootFrame, + tinybenchTaskToBenchmark, + type InstrumentWindow, + type TinybenchBench, + type TinybenchOptions, + type TinybenchTask, +} from "../instrument"; + +const TestRunner = (vitest as unknown as { TestRunner: unknown }).TestRunner; + +/** + * Vitest 5 runs benchmarks inside `test()` through `TestRunner`, calling the + * static `TestRunner.runBenchmarks(tinybench)` with a fully built tinybench + * instance. That static is referenced directly (not through `this`), so a + * runner subclass cannot intercept it — we patch the static in place instead. + * Importing this module installs the patch as a side effect, which is how the + * runner file Vitest loads via `test.runner` wires us in. + */ + +interface TinybenchWithTasks extends TinybenchBench { + name: string; + tasks: TinybenchTask[]; + run(): Promise; + // tinybench exposes the resolved options on the instance + opts?: TinybenchOptions; +} + +/** Minimal shape of the current Vitest test task we read for URI construction. */ +interface CurrentTest { + fullTestName?: string; + file?: { filepath: string }; +} + +let isTaskPatched = false; +const instrumentWindow: InstrumentWindow = { runStart: null }; + +function getCurrentTest(): CurrentTest | undefined { + // `getCurrentTest` is a static on the runner; typings don't surface it. + const getter = (TestRunner as unknown as { getCurrentTest?: () => unknown }) + .getCurrentTest; + return getter ? (getter() as CurrentTest | undefined) : undefined; +} + +/** + * Build the benchmark URI from the running test and the tinybench task name. + * Matches the legacy convention: git-relative file, then the suite/test path, + * then the bench name, all `::`-separated. + */ +function buildUri(taskName: string): string { + const test = getCurrentTest(); + const filepath = test?.file?.filepath; + if (!filepath) { + throw new Error("[CodSpeed] could not resolve the running benchmark file"); + } + const gitDir = getGitDir(filepath); + if (gitDir === undefined) { + throw new Error("Could not find a git repository"); + } + const relativeFile = path.relative(gitDir, filepath); + // `fullTestName` uses " > " between suite levels; normalize to "::". + const testPath = (test?.fullTestName ?? "").split(" > ").join("::"); + return [relativeFile, testPath, taskName].filter(Boolean).join("::"); +} + +function collectWalltimeResults(tinybench: TinybenchWithTasks): void { + const options: TinybenchOptions = tinybench.opts ?? {}; + const benchmarks: Benchmark[] = []; + + for (const task of tinybench.tasks) { + if (task.result?.state && task.result.state !== "completed") continue; + const benchmark = tinybenchTaskToBenchmark(task, buildUri(task.name), options); + if (benchmark) { + benchmarks.push(benchmark); + } + } + + if (benchmarks.length > 0) { + writeWalltimeResults(benchmarks); + console.log( + `[CodSpeed] Done collecting walltime data for ${benchmarks.length} benches.`, + ); + } +} + +function patchRunBenchmarks(): void { + const Runner = TestRunner as unknown as { + runBenchmarks: (tinybench: TinybenchWithTasks) => Promise; + }; + const originalRunBenchmarks = Runner.runBenchmarks.bind(TestRunner); + const isWalltime = getInstrumentMode() === "walltime"; + + Runner.runBenchmarks = async (tinybench: TinybenchWithTasks) => { + setupCore(); + + if (!isTaskPatched) { + // tinybench's `Task` class isn't exported from the bench instance, so we + // reach it through a constructed task (Vitest adds them before running). + const TaskClass = tinybench.tasks[0]?.constructor as + | { prototype: { run: (this: unknown) => Promise } } + | undefined; + if (TaskClass) { + isTaskPatched = true; + patchTaskRunWithRootFrame(TaskClass); + } + } + + installInstrumentHooks(tinybench, instrumentWindow, buildUri); + + const result = await originalRunBenchmarks(tinybench); + + if (isWalltime) { + collectWalltimeResults(tinybench); + } + + return result; + }; +} + +patchRunBenchmarks(); + +// The runner file must default-export a class for Vitest to accept it. We do not +// override any behavior through the class (the interception happens via the +// static patch above), so we hand back the stock runner unchanged. +export default TestRunner; diff --git a/packages/vitest-plugin/src/version.ts b/packages/vitest-plugin/src/version.ts new file mode 100644 index 00000000..e3fbfb7f --- /dev/null +++ b/packages/vitest-plugin/src/version.ts @@ -0,0 +1,31 @@ +import { readFileSync } from "fs"; +import { createRequire } from "module"; +import { join } from "path"; + +/** + * Resolve the major version of the Vitest the *user's project* depends on, not + * the one bundled alongside this plugin. Returns null when it cannot be found. + */ +export function getVitestMajorVersion(): number | null { + try { + const require = createRequire(join(process.cwd(), "package.json")); + const vitestPkgPath = require.resolve("vitest/package.json"); + const vitestPkg = JSON.parse(readFileSync(vitestPkgPath, "utf-8")); + return parseInt(vitestPkg.version.split(".")[0], 10); + } catch { + return null; + } +} + +/** + * Vitest 5 reworked the benchmark backend: the dedicated `NodeBenchmarkRunner` + * and the `vitest/runners` / `vitest/suite` entrypoints are gone, benchmarks run + * inside `test()` through the unified `TestRunner`, and tinybench moved to v6. + * The two integration seams (legacy runner subclass vs. `TestRunner` patch) hinge + * on this, so detection lives in one place. + * + * When the version cannot be detected we assume the latest supported major. + */ +export function isVitestV5OrHigher(): boolean { + return (getVitestMajorVersion() ?? 5) >= 5; +} diff --git a/packages/vitest-plugin/src/vitest-legacy.d.ts b/packages/vitest-plugin/src/vitest-legacy.d.ts new file mode 100644 index 00000000..be776ad8 --- /dev/null +++ b/packages/vitest-plugin/src/vitest-legacy.d.ts @@ -0,0 +1,44 @@ +// Vitest 3/4 exposed benchmark internals through `vitest/runners` and +// `vitest/suite`, and surfaced the `Benchmark` task type from `vitest`. Vitest 5 +// removed all of these (benchmarks now run through the unified `TestRunner`). +// +// The legacy runner (`analysis.ts`, `walltime/`) still imports them and is only +// loaded when the user runs Vitest 3/4, but the plugin is type-checked against +// whichever Vitest is installed — including 5, where these are gone. These +// ambient declarations keep the legacy code compiling there without affecting +// runtime: the modules are never imported under v5. + +import type * as tinybench from "tinybench"; + +declare module "vitest" { + import type { RunnerTestCase } from "vitest"; + + // In v3/4 a benchmark is a test case carrying tinybench output on its result. + interface Benchmark extends RunnerTestCase { + meta: RunnerTestCase["meta"] & { benchmark?: boolean }; + } +} + +declare module "vitest/runners" { + import type { RunnerTestSuite } from "vitest"; + + export class NodeBenchmarkRunner { + constructor(config?: unknown); + config: unknown; + runSuite(suite: RunnerTestSuite): Promise; + importTinybench(): Promise; + } +} + +declare module "vitest/suite" { + import type { Benchmark } from "vitest"; + + export function getBenchFn(benchmark: Benchmark): () => unknown; + export function getBenchOptions(benchmark: Benchmark): { + time?: number; + warmupTime?: number; + warmupIterations?: number; + iterations?: number; + }; + export function getHooks(suite: unknown): Record unknown>>; +} diff --git a/packages/vitest-plugin/src/walltime/index.ts b/packages/vitest-plugin/src/walltime/index.ts index 18c71215..0bf28354 100644 --- a/packages/vitest-plugin/src/walltime/index.ts +++ b/packages/vitest-plugin/src/walltime/index.ts @@ -1,42 +1,24 @@ -import { - InstrumentHooks, - MARKER_TYPE_BENCHMARK_END, - MARKER_TYPE_BENCHMARK_START, - setupCore, - wrapWithRootFrame, - writeWalltimeResults, -} from "@codspeed/core"; +import { setupCore, writeWalltimeResults } from "@codspeed/core"; import type * as tinybench from "tinybench"; import { RunnerTaskEventPack, RunnerTaskResultPack, type RunnerTestSuite, } from "vitest"; +// `vitest/runners` only exists on Vitest 3/4; this runner is loaded only there. +// eslint-disable-next-line import/no-unresolved import { NodeBenchmarkRunner } from "vitest/runners"; import { patchRootSuiteWithFullFilePath } from "../common"; +import { + installInstrumentHooks, + patchTaskRunWithRootFrame, + type InstrumentWindow, + type TinybenchBench, +} from "../instrument"; import { extractBenchmarkResults } from "./utils"; type Tinybench = typeof tinybench; -/** A tinybench task, exposing the `fn` the runner wraps with the root frame. */ -interface TinybenchTask { - name: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - fn: (...args: any[]) => any; -} - -/** tinybench's per-task setup/teardown hook signature. */ -type TinybenchHook = ( - task: TinybenchTask, - mode: "run" | "warmup", -) => Promise | void; - -/** The mutable subset of a tinybench Bench the runner reaches into. */ -interface TinybenchBench { - setup: TinybenchHook; - teardown: TinybenchHook; -} - /** * WalltimeRunner uses Vitest's default benchmark execution * and extracts results from the suite after completion @@ -46,9 +28,7 @@ export class WalltimeRunner extends NodeBenchmarkRunner { private suiteUris = new Map(); /// Suite ID of the currently running suite, to allow constructing the URI in the context of tinybench tasks private currentSuiteId: string | null = null; - // Carries the window start timestamp from the setup hook to the teardown - // hook. Tasks run strictly sequentially, so a single field is enough. - private runStart: bigint | null = null; + private window: InstrumentWindow = { runStart: null }; async runSuite(suite: RunnerTestSuite): Promise { patchRootSuiteWithFullFilePath(suite); @@ -101,7 +81,7 @@ export class WalltimeRunner extends NodeBenchmarkRunner { // through a fresh module-shaped object that Vitest destructures from. if (!this.isTinybenchHookedWithCodspeed) { this.isTinybenchHookedWithCodspeed = true; - this.patchTaskWithRootFrame(tinybench); + patchTaskRunWithRootFrame(tinybench.Task); } return { @@ -110,38 +90,10 @@ export class WalltimeRunner extends NodeBenchmarkRunner { }; } - /** - * Wrap each task's function with the root frame so collected stacks can be - * attributed to a benchmark. The window itself is driven by the bench's - * setup/teardown hooks (see createInstrumentedBench). - */ - private patchTaskWithRootFrame(tinybench: Tinybench): void { - const originalRun = tinybench.Task.prototype.run; - - tinybench.Task.prototype.run = async function () { - const task = this as unknown as TinybenchTask; - const originalFn = task.fn; - task.fn = wrapWithRootFrame(() => originalFn.call(task)); - - try { - await originalRun.call(this); - } finally { - task.fn = originalFn; - } - - return this; - }; - } - /** * Drive the instrumentation window from each bench's run-mode setup/teardown - * hooks so it brackets only tinybench's measured loop, excluding the warmup - * that Vitest runs beforehand and the statistics computation tinybench - * performs after the loop. Wrapping the whole `Task.run()` would otherwise - * fold all of that framework overhead into the recorded sample. - * - * User-provided hooks are preserved and keep their order relative to the work - * under test. + * hooks so it brackets only tinybench's measured loop. See + * installInstrumentHooks for the rationale. */ private createInstrumentedBench( tinybench: Tinybench, @@ -154,50 +106,17 @@ export class WalltimeRunner extends NodeBenchmarkRunner { // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor(...benchArgs: any[]) { super(...benchArgs); - runner.installInstrumentHooks(this as unknown as TinybenchBench); + installInstrumentHooks( + this as unknown as TinybenchBench, + runner.window, + (taskName) => runner.getBenchmarkUri(taskName), + ); } } return InstrumentedBench; } - private installInstrumentHooks(bench: TinybenchBench): void { - const userSetup = bench.setup; - const userTeardown = bench.teardown; - - bench.setup = async (task, mode) => { - await userSetup(task, mode); - if (mode === "run") { - InstrumentHooks.startBenchmark(); - this.runStart = InstrumentHooks.currentTimestamp(); - } - }; - - bench.teardown = async (task, mode) => { - if (mode === "run") { - this.closeInstrumentWindow(this.getBenchmarkUri(task.name)); - } - await userTeardown(task, mode); - }; - } - - private closeInstrumentWindow(uri: string): void { - const runEnd = InstrumentHooks.currentTimestamp(); - const pid = process.pid; - - // Benchmark markers must land inside the sample window opened by - // startBenchmark(), so they have to be emitted before stopBenchmark() - // closes it. The runner consumes the FIFO stream in order, so a marker - // sent after StopBenchmark falls outside the sample and breaks the - // expected SampleStart > BenchmarkStart > BenchmarkEnd > SampleEnd nesting. - InstrumentHooks.addMarker(pid, MARKER_TYPE_BENCHMARK_START, this.runStart!); - InstrumentHooks.addMarker(pid, MARKER_TYPE_BENCHMARK_END, runEnd); - - InstrumentHooks.stopBenchmark(); - InstrumentHooks.setExecutedBenchmark(pid, uri); - this.runStart = null; - } - // Allow tinybench to retrieve the path to the currently running suite async onTaskUpdate( _: RunnerTaskResultPack[], diff --git a/packages/vitest-plugin/src/walltime/utils.ts b/packages/vitest-plugin/src/walltime/utils.ts index 2b3c1f33..445db682 100644 --- a/packages/vitest-plugin/src/walltime/utils.ts +++ b/packages/vitest-plugin/src/walltime/utils.ts @@ -1,17 +1,18 @@ -import { - calculateQuantiles, - msToNs, - msToS, - type Benchmark, - type BenchmarkStats, -} from "@codspeed/core"; +import { type Benchmark } from "@codspeed/core"; import { type RunnerTaskResult, type RunnerTestSuite, type Benchmark as VitestBenchmark, } from "vitest"; +// `vitest/suite` only exists on Vitest 3/4; this module is used only there. +// eslint-disable-next-line import/no-unresolved import { getBenchOptions } from "vitest/suite"; import { isVitestTaskBenchmark } from "../common"; +import { + tinybenchTaskToBenchmark, + type TinybenchOptions, + type TinybenchTask, +} from "../instrument"; export async function extractBenchmarkResults( suite: RunnerTestSuite, @@ -22,7 +23,7 @@ export async function extractBenchmarkResults( for (const task of suite.tasks) { if (isVitestTaskBenchmark(task) && task.result?.state === "pass") { - const benchmark = await processBenchmarkTask(task, currentPath); + const benchmark = processBenchmarkTask(task, currentPath); if (benchmark) { benchmarks.push(benchmark); } @@ -35,10 +36,10 @@ export async function extractBenchmarkResults( return benchmarks; } -async function processBenchmarkTask( +function processBenchmarkTask( task: VitestBenchmark, suitePath: string, -): Promise { +): Benchmark | null { const uri = `${suitePath}::${task.name}`; const result = task.result; @@ -48,83 +49,64 @@ async function processBenchmarkTask( } try { - // Get tinybench configuration options from vitest const benchOptions = getBenchOptions(task); + const benchmark = tinybenchTaskToBenchmark( + adaptLegacyResult(task.name, result), + uri, + benchOptions as TinybenchOptions, + ); - const stats = convertVitestResultToBenchmarkStats(result, benchOptions); - - if (stats === null) { + if (benchmark === null) { console.log(` ✔ No walltime data to collect for ${uri}`); return null; } - const coreBenchmark: Benchmark = { - name: task.name, - uri, - config: { - max_rounds: benchOptions.iterations ?? null, - max_time_ns: benchOptions.time ? msToNs(benchOptions.time) : null, - min_round_time_ns: null, // tinybench does not have an option for this - warmup_time_ns: - benchOptions.warmupIterations !== 0 && benchOptions.warmupTime - ? msToNs(benchOptions.warmupTime) - : null, - }, - stats, - }; - console.log(` ✔ Collected walltime data for ${uri}`); - return coreBenchmark; + return benchmark; } catch (error) { console.warn(` ⚠ Failed to process benchmark result for ${uri}:`, error); return null; } } -function convertVitestResultToBenchmarkStats( - result: RunnerTaskResult, - benchOptions: { - time?: number; - warmupTime?: number; - warmupIterations?: number; - iterations?: number; - }, -): BenchmarkStats | null { - const benchmark = result.benchmark; +/** + * Vitest 3/4 attaches the raw tinybench v2 result under `result.benchmark`, + * whose statistics are a flat object ({ totalTime, min, max, mean, sd, samples }). + * Reshape it into the `{ result: { totalTime, latency } }` form the shared + * converter expects (tinybench v6 nests statistics under `latency`). + */ +interface LegacyBenchmarkStats { + totalTime: number; + min: number; + max: number; + mean: number; + sd: number; + samples: number[]; +} +function adaptLegacyResult( + name: string, + result: RunnerTaskResult, +): TinybenchTask { + // `result.benchmark` only exists on the Vitest 3/4 task result; the v5 typings + // (compiled against here) dropped it. + const benchmark = (result as { benchmark?: LegacyBenchmarkStats }).benchmark; if (!benchmark) { throw new Error("No benchmark data available in result"); } - const { totalTime, min, max, mean, sd, samples } = benchmark; - - // Get individual sample times in nanoseconds and sort them - const sortedTimesNs = samples.map(msToNs).sort((a, b) => a - b); - const meanNs = msToNs(mean); - const stdevNs = msToNs(sd); - - if (sortedTimesNs.length == 0) { - // Sometimes the benchmarks can be completely optimized out and not even run, but its beforeEach and afterEach hooks are still executed, and the task is still considered a success. - // This is the case for the hooks.bench.ts example in this package - return null; - } - - const { q1_ns, q3_ns, median_ns, iqr_outlier_rounds, stdev_outlier_rounds } = - calculateQuantiles({ meanNs, stdevNs, sortedTimesNs }); - return { - min_ns: msToNs(min), - max_ns: msToNs(max), - mean_ns: meanNs, - stdev_ns: stdevNs, - q1_ns, - median_ns, - q3_ns, - total_time: msToS(totalTime), - iter_per_round: 1, // as there is only one round in tinybench, we define that there were n rounds of 1 iteration - rounds: sortedTimesNs.length, - iqr_outlier_rounds, - stdev_outlier_rounds, - warmup_iters: benchOptions.warmupIterations ?? 0, + name, + fn: () => undefined, + result: { + totalTime: benchmark.totalTime, + latency: { + min: benchmark.min, + max: benchmark.max, + mean: benchmark.mean, + sd: benchmark.sd, + samples: benchmark.samples, + }, + }, }; } diff --git a/packages/vitest-plugin/vitest.config.ts b/packages/vitest-plugin/vitest.config.ts index 672e3a27..444869bd 100644 --- a/packages/vitest-plugin/vitest.config.ts +++ b/packages/vitest-plugin/vitest.config.ts @@ -1,6 +1,18 @@ +import { createRequire } from "module"; import { defineConfig } from "vitest/config"; import codspeedPlugin from "./dist/index.mjs"; +// The legacy runner tests exercise the Vitest 3/4 benchmark backend +// (`vitest/suite`, `NodeBenchmarkRunner`), which Vitest 5 removed. Exclude them +// when running under v5+ so the file's static imports don't fail to resolve. +const require = createRequire(import.meta.url); +const vitestMajor = parseInt( + (require("vitest/package.json").version as string).split(".")[0], + 10, +); +const legacyOnlyTests = + vitestMajor >= 5 ? ["**/__tests__/instrumented.test.ts"] : []; + export default defineConfig({ // @ts-expect-error - TODO: investigate why importing from '.' wants to import only "main" field and thus fail plugins: [codspeedPlugin()], @@ -8,7 +20,7 @@ export default defineConfig({ __VERSION__: JSON.stringify("1.0.0"), }, test: { - exclude: ["**/tests/**/*", "**/.rollup.cache/**/*"], + exclude: ["**/tests/**/*", "**/.rollup.cache/**/*", ...legacyOnlyTests], mockReset: true, }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d59ca9e2..54b16077 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -282,9 +282,6 @@ importers: axios: specifier: ^1.4.0 version: 1.4.0 - find-up: - specifier: ^6.3.0 - version: 6.3.0 form-data: specifier: ^4.0.4 version: 4.0.4 @@ -295,9 +292,6 @@ importers: specifier: 1.0.0-pre2 version: 1.0.0-pre2 devDependencies: - '@types/find-up': - specifier: ^4.0.0 - version: 4.0.0 '@types/stack-trace': specifier: ^0.0.30 version: 0.0.30 @@ -365,8 +359,8 @@ importers: specifier: ^7.0.0 version: 7.1.3(@types/node@20.19.11)(yaml@2.9.0) vitest: - specifier: ^4.0.18 - version: 4.0.18(@types/node@20.19.11)(yaml@2.9.0) + specifier: 5.0.0-beta.5 + version: 5.0.0-beta.5(@types/node@20.19.11)(vite@7.1.3(@types/node@20.19.11)(yaml@2.9.0)) packages: @@ -1641,14 +1635,6 @@ packages: resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} engines: {node: '>=6.0.0'} - '@jridgewell/resolve-uri@3.1.0': - resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} - engines: {node: '>=6.0.0'} - - '@jridgewell/resolve-uri@3.1.1': - resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} - engines: {node: '>=6.0.0'} - '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -1657,9 +1643,6 @@ packages: resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} engines: {node: '>=6.0.0'} - '@jridgewell/sourcemap-codec@1.4.14': - resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} - '@jridgewell/sourcemap-codec@1.4.15': resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} @@ -1669,11 +1652,8 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@jridgewell/trace-mapping@0.3.18': - resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==} - - '@jridgewell/trace-mapping@0.3.29': - resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} @@ -2214,10 +2194,6 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/find-up@4.0.0': - resolution: {integrity: sha512-QlRNKeOPFWKisbNtKVOOGXw3AeLbkw8UmT/EyEGM6brfqpYffKBcch7f1y40NYN9O90aK2+K0xBMDJfOAsg2qg==} - deprecated: This is a stub types definition. find-up provides its own type definitions, so you do not need this installed. - '@types/graceful-fs@4.1.6': resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==} @@ -2493,12 +2469,26 @@ packages: vite: optional: true + '@vitest/mocker@5.0.0-beta.5': + resolution: {integrity: sha512-NZCB4PeGl+YqWBxk4lAsH1oS8EvBG3b/hDaOCgEmsoZSda5q9jUxU8P46HyczFp34tcnxp92KVlNDiKcDAE59w==} + 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@3.2.4': resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} '@vitest/pretty-format@4.0.18': resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} + '@vitest/pretty-format@5.0.0-beta.5': + resolution: {integrity: sha512-eFj80bS7sN1aOOV7Ibi/sDYhhzs1fj2S8+/Y2mjOw2POpSW/6Esjj7FIdj0cD2/cdsKFumEokh+ijVijisd9+w==} + '@vitest/runner@3.2.4': resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} @@ -2517,12 +2507,18 @@ packages: '@vitest/spy@4.0.18': resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} + '@vitest/spy@5.0.0-beta.5': + resolution: {integrity: sha512-AF4gJhHwopexCGrdUnt8Y3+3eVvgMie4tHmlIJi2i3DH6TQlPN2V2/wJfrQAC5ZAgqSM278qmkJCYGC4aOfwSg==} + '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} '@vitest/utils@4.0.18': resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + '@vitest/utils@5.0.0-beta.5': + resolution: {integrity: sha512-M+DZy1Q7v6//AbTYjVy86ChuF4tgSdGySVpH49kSkES16IRL5QRSmtAPdZVQ19S6Eke+NzAqWn1PUZNKljftfg==} + '@yarnpkg/lockfile@1.1.0': resolution: {integrity: sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==} @@ -3344,6 +3340,9 @@ 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.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -3517,6 +3516,10 @@ packages: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + expect@29.5.0: resolution: {integrity: sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -3595,10 +3598,6 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} - find-up@6.3.0: - resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - flat-cache@3.0.4: resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -4670,10 +4669,6 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - locate-path@7.2.0: - resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} @@ -5180,10 +5175,6 @@ packages: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} - p-limit@4.0.0: - resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - p-locate@2.0.0: resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} engines: {node: '>=4'} @@ -5196,10 +5187,6 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} - p-locate@6.0.0: - resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - p-map-series@2.1.0: resolution: {integrity: sha512-RpYIIK1zXSNEOdwxcfe7FdvGcs7+y5n8rifMhMNWvaxRNMPINJHF5GDeuVxWqnfrcHPSCnp7Oo5yNXHId9Av2Q==} engines: {node: '>=8'} @@ -5278,10 +5265,6 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} - path-exists@5.0.0: - resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -5862,6 +5845,9 @@ packages: std-env@3.9.0: resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -6025,8 +6011,8 @@ packages: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} - tinyrainbow@3.0.3: - resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} tinyspy@4.0.3: @@ -6389,6 +6375,47 @@ packages: jsdom: optional: true + vitest@5.0.0-beta.5: + resolution: {integrity: sha512-Gi7moR+KBro+LGC1jLGalpv4Ujb83dBLOH9o1f9VnWrvl0tRp4p1njh3Gikcexic0xe4foueQvgImq/hKET+bw==} + engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 5.0.0-beta.5 + '@vitest/browser-preview': 5.0.0-beta.5 + '@vitest/browser-webdriverio': 5.0.0-beta.5 + '@vitest/coverage-istanbul': 5.0.0-beta.5 + '@vitest/coverage-v8': 5.0.0-beta.5 + '@vitest/ui': 5.0.0-beta.5 + happy-dom: '*' + jsdom: '*' + vite: ^6.4.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 + walk-up-path@3.0.1: resolution: {integrity: sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==} @@ -6531,21 +6558,17 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - yocto-queue@1.0.0: - resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} - engines: {node: '>=12.20'} - snapshots: '@ampproject/remapping@2.2.1': dependencies: '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.18 + '@jridgewell/trace-mapping': 0.3.31 '@ampproject/remapping@2.3.0': dependencies: '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/trace-mapping': 0.3.31 '@apidevtools/json-schema-ref-parser@9.0.9': dependencies: @@ -6608,14 +6631,14 @@ snapshots: dependencies: '@babel/types': 7.22.5 '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.18 + '@jridgewell/trace-mapping': 0.3.31 jsesc: 2.5.2 '@babel/generator@7.22.5': dependencies: '@babel/types': 7.22.5 '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.18 + '@jridgewell/trace-mapping': 0.3.31 jsesc: 2.5.2 '@babel/generator@7.28.0': @@ -6623,7 +6646,7 @@ snapshots: '@babel/parser': 7.28.0 '@babel/types': 7.28.1 '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 '@babel/helper-annotate-as-pure@7.22.5': @@ -8065,7 +8088,7 @@ snapshots: '@jest/test-result': 29.5.0 '@jest/transform': 29.5.0 '@jest/types': 29.5.0 - '@jridgewell/trace-mapping': 0.3.18 + '@jridgewell/trace-mapping': 0.3.31 '@types/node': 20.19.11 chalk: 4.1.2 collect-v8-coverage: 1.0.1 @@ -8094,7 +8117,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/trace-mapping': 0.3.31 '@types/node': 20.19.11 chalk: 4.1.2 collect-v8-coverage: 1.0.2 @@ -8126,13 +8149,13 @@ snapshots: '@jest/source-map@29.4.3': dependencies: - '@jridgewell/trace-mapping': 0.3.18 + '@jridgewell/trace-mapping': 0.3.31 callsites: 3.1.0 graceful-fs: 4.2.11 '@jest/source-map@29.6.3': dependencies: - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/trace-mapping': 0.3.31 callsites: 3.1.0 graceful-fs: 4.2.11 @@ -8168,7 +8191,7 @@ snapshots: dependencies: '@babel/core': 7.21.4 '@jest/types': 29.5.0 - '@jridgewell/trace-mapping': 0.3.18 + '@jridgewell/trace-mapping': 0.3.31 babel-plugin-istanbul: 6.1.1 chalk: 4.1.2 convert-source-map: 2.0.0 @@ -8188,7 +8211,7 @@ snapshots: dependencies: '@babel/core': 7.28.0 '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/trace-mapping': 0.3.31 babel-plugin-istanbul: 6.1.1 chalk: 4.1.2 convert-source-map: 2.0.0 @@ -8225,44 +8248,33 @@ snapshots: '@jridgewell/gen-mapping@0.3.12': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/gen-mapping@0.3.3': dependencies: '@jridgewell/set-array': 1.1.2 - '@jridgewell/sourcemap-codec': 1.5.4 - '@jridgewell/trace-mapping': 0.3.18 - - '@jridgewell/resolve-uri@3.1.0': {} - - '@jridgewell/resolve-uri@3.1.1': {} + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/set-array@1.1.2': {} - '@jridgewell/sourcemap-codec@1.4.14': {} - '@jridgewell/sourcemap-codec@1.4.15': {} '@jridgewell/sourcemap-codec@1.5.4': {} '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/trace-mapping@0.3.18': - dependencies: - '@jridgewell/resolve-uri': 3.1.0 - '@jridgewell/sourcemap-codec': 1.4.14 - - '@jridgewell/trace-mapping@0.3.29': + '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping@0.3.9': dependencies: - '@jridgewell/resolve-uri': 3.1.1 - '@jridgewell/sourcemap-codec': 1.5.4 + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 '@jsdevtools/ono@7.1.3': {} @@ -8902,10 +8914,6 @@ snapshots: '@types/estree@1.0.8': {} - '@types/find-up@4.0.0': - dependencies: - find-up: 6.3.0 - '@types/graceful-fs@4.1.6': dependencies: '@types/node': 20.19.11 @@ -9155,13 +9163,13 @@ snapshots: '@vitest/spy': 4.0.18 '@vitest/utils': 4.0.18 chai: 6.2.2 - tinyrainbow: 3.0.3 + tinyrainbow: 3.1.0 '@vitest/mocker@3.2.4(vite@7.1.3(@types/node@20.19.11)(yaml@2.9.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 - magic-string: 0.30.17 + magic-string: 0.30.21 optionalDependencies: vite: 7.1.3(@types/node@20.19.11)(yaml@2.9.0) @@ -9173,13 +9181,26 @@ snapshots: optionalDependencies: vite: 7.1.3(@types/node@20.19.11)(yaml@2.9.0) + '@vitest/mocker@5.0.0-beta.5(vite@7.1.3(@types/node@20.19.11)(yaml@2.9.0))': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.0-beta.5 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.1.3(@types/node@20.19.11)(yaml@2.9.0) + '@vitest/pretty-format@3.2.4': dependencies: tinyrainbow: 2.0.0 '@vitest/pretty-format@4.0.18': dependencies: - tinyrainbow: 3.0.3 + tinyrainbow: 3.1.0 + + '@vitest/pretty-format@5.0.0-beta.5': + dependencies: + tinyrainbow: 3.1.0 '@vitest/runner@3.2.4': dependencies: @@ -9195,7 +9216,7 @@ snapshots: '@vitest/snapshot@3.2.4': dependencies: '@vitest/pretty-format': 3.2.4 - magic-string: 0.30.17 + magic-string: 0.30.21 pathe: 2.0.3 '@vitest/snapshot@4.0.18': @@ -9210,6 +9231,8 @@ snapshots: '@vitest/spy@4.0.18': {} + '@vitest/spy@5.0.0-beta.5': {} + '@vitest/utils@3.2.4': dependencies: '@vitest/pretty-format': 3.2.4 @@ -9219,7 +9242,13 @@ snapshots: '@vitest/utils@4.0.18': dependencies: '@vitest/pretty-format': 4.0.18 - tinyrainbow: 3.0.3 + tinyrainbow: 3.1.0 + + '@vitest/utils@5.0.0-beta.5': + dependencies: + '@vitest/pretty-format': 5.0.0-beta.5 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 '@yarnpkg/lockfile@1.1.0': {} @@ -10184,6 +10213,8 @@ snapshots: es-module-lexer@1.7.0: {} + es-module-lexer@2.1.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -10470,6 +10501,8 @@ snapshots: expect-type@1.2.2: {} + expect-type@1.3.0: {} + expect@29.5.0: dependencies: '@jest/expect-utils': 29.5.0 @@ -10550,11 +10583,6 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 - find-up@6.3.0: - dependencies: - locate-path: 7.2.0 - path-exists: 5.0.0 - flat-cache@3.0.4: dependencies: flatted: 3.2.7 @@ -12087,10 +12115,6 @@ snapshots: dependencies: p-locate: 5.0.0 - locate-path@7.2.0: - dependencies: - p-locate: 6.0.0 - lodash.camelcase@4.3.0: {} lodash.debounce@4.0.8: {} @@ -12697,10 +12721,6 @@ snapshots: dependencies: yocto-queue: 0.1.0 - p-limit@4.0.0: - dependencies: - yocto-queue: 1.0.0 - p-locate@2.0.0: dependencies: p-limit: 1.3.0 @@ -12713,10 +12733,6 @@ snapshots: dependencies: p-limit: 3.1.0 - p-locate@6.0.0: - dependencies: - p-limit: 4.0.0 - p-map-series@2.1.0: {} p-map@4.0.0: @@ -12805,8 +12821,6 @@ snapshots: path-exists@4.0.0: {} - path-exists@5.0.0: {} - path-is-absolute@1.0.1: {} path-key@3.1.1: {} @@ -13385,6 +13399,8 @@ snapshots: std-env@3.9.0: {} + std-env@4.1.0: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -13560,7 +13576,7 @@ snapshots: tinyrainbow@2.0.0: {} - tinyrainbow@3.0.3: {} + tinyrainbow@3.1.0: {} tinyspy@4.0.3: {} @@ -13809,13 +13825,13 @@ snapshots: v8-to-istanbul@9.1.0: dependencies: - '@jridgewell/trace-mapping': 0.3.18 + '@jridgewell/trace-mapping': 0.3.31 '@types/istanbul-lib-coverage': 2.0.4 convert-source-map: 1.9.0 v8-to-istanbul@9.3.0: dependencies: - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/trace-mapping': 0.3.31 '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 @@ -13911,7 +13927,7 @@ snapshots: '@vitest/spy': 4.0.18 '@vitest/utils': 4.0.18 es-module-lexer: 1.7.0 - expect-type: 1.2.2 + expect-type: 1.3.0 magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 @@ -13920,7 +13936,7 @@ snapshots: tinybench: 2.9.0 tinyexec: 1.0.2 tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 + tinyrainbow: 3.1.0 vite: 7.1.3(@types/node@20.19.11)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: @@ -13938,6 +13954,32 @@ snapshots: - tsx - yaml + vitest@5.0.0-beta.5(@types/node@20.19.11)(vite@7.1.3(@types/node@20.19.11)(yaml@2.9.0)): + dependencies: + '@types/chai': 5.2.2 + '@vitest/mocker': 5.0.0-beta.5(vite@7.1.3(@types/node@20.19.11)(yaml@2.9.0)) + '@vitest/pretty-format': 5.0.0-beta.5 + '@vitest/spy': 5.0.0-beta.5 + '@vitest/utils': 5.0.0-beta.5 + chai: 6.2.2 + 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.3 + std-env: 4.1.0 + tinybench: 6.0.2 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 7.1.3(@types/node@20.19.11)(yaml@2.9.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.19.11 + transitivePeerDependencies: + - msw + walk-up-path@3.0.1: {} walker@1.0.8: @@ -14115,5 +14157,3 @@ snapshots: yn@3.1.1: {} yocto-queue@0.1.0: {} - - yocto-queue@1.0.0: {}