From f07ebde3f64e4da0c88f03760f32b33cfe8ad8c9 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 23 Oct 2024 16:14:56 +0200 Subject: [PATCH 1/9] fix(vitest): don't fail if the working directory starts with a small drive letter Fixes #5772 Fixes #5798 Fixes #5251 --- packages/vite-node/src/utils.ts | 16 ---------------- packages/vitest/src/node/cli/cac.ts | 8 ++++++++ 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/packages/vite-node/src/utils.ts b/packages/vite-node/src/utils.ts index a4e5b5bc6586..48a5132253ff 100644 --- a/packages/vite-node/src/utils.ts +++ b/packages/vite-node/src/utils.ts @@ -6,17 +6,6 @@ import { dirname, join, resolve } from 'pathe' export const isWindows = process.platform === 'win32' -const drive = isWindows ? process.cwd()[0] : null -const driveOpposite = drive - ? drive === drive.toUpperCase() - ? drive.toLowerCase() - : drive.toUpperCase() - : null -const driveRegexp = drive ? new RegExp(`(?:^|/@fs/)${drive}(\:[\\/])`) : null -const driveOppositeRegext = driveOpposite - ? new RegExp(`(?:^|/@fs/)${driveOpposite}(\:[\\/])`) - : null - export function slash(str: string) { return str.replace(/\\/g, '/') } @@ -28,11 +17,6 @@ export function normalizeRequestId(id: string, base?: string): string { id = `/${id.slice(base.length)}` } - // keep drive the same as in process cwd - if (driveRegexp && !driveRegexp?.test(id) && driveOppositeRegext?.test(id)) { - id = id.replace(driveOppositeRegext, `${drive}$1`) - } - return id .replace(/^\/@id\/__x00__/, '\0') // virtual modules start with `\0` .replace(/^\/@id\//, '') diff --git a/packages/vitest/src/node/cli/cac.ts b/packages/vitest/src/node/cli/cac.ts index d1c8e78b103d..6992f1b98ad4 100644 --- a/packages/vitest/src/node/cli/cac.ts +++ b/packages/vitest/src/node/cli/cac.ts @@ -259,6 +259,14 @@ function normalizeCliOptions(argv: CliOptions): CliOptions { async function start(mode: VitestRunMode, cliFilters: string[], options: CliOptions): Promise { try { process.title = 'node (vitest)' + // pathe always normalizes the drive letter to be uppercase + // if user runs Vitest from c:/, the built-in ESM loader in tests + // will resolve a different Vitest instance than the one in vite-node + // we can either + // 1. normalize the CWD so it works in the CLI, but might break when API is used directly + // 2. ask "pathe" to not normalize the drive letter + // 2. don't use "pathe" when resolving paths + process.chdir(normalize(process.cwd())) } catch {} From 89c2d8c4f2558c9ffb2e12be84cf817e2a3b3a1c Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 23 Oct 2024 16:40:38 +0200 Subject: [PATCH 2/9] test: add a simple test --- .../fixtures/windows-drive-case/basic.test.ts | 5 +++++ test/cli/test/windows-drive-case.test.ts | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 test/cli/fixtures/windows-drive-case/basic.test.ts create mode 100644 test/cli/test/windows-drive-case.test.ts diff --git a/test/cli/fixtures/windows-drive-case/basic.test.ts b/test/cli/fixtures/windows-drive-case/basic.test.ts new file mode 100644 index 000000000000..4fa6fcaa6e95 --- /dev/null +++ b/test/cli/fixtures/windows-drive-case/basic.test.ts @@ -0,0 +1,5 @@ +import { expect, test } from 'vitest' + +test('basic test', () => { + expect(1).toBe(1) +}) diff --git a/test/cli/test/windows-drive-case.test.ts b/test/cli/test/windows-drive-case.test.ts new file mode 100644 index 000000000000..4b44695faa8f --- /dev/null +++ b/test/cli/test/windows-drive-case.test.ts @@ -0,0 +1,19 @@ +import { join } from 'pathe' +import { expect, test } from 'vitest' +import { runVitestCli } from '../../test-utils' + +const _DRIVE_LETTER_START_RE = /^[A-Z]:\//i +const root = join(import.meta.dirname, '../fixtures/windows-drive-case') +const cwd = root.replace(_DRIVE_LETTER_START_RE, r => r.toLowerCase()) + +test.runIf(process.platform === 'win32')(`works on windows with a lowercase drive: ${cwd}`, async () => { + const { stderr, stdout } = await runVitestCli({ + nodeOptions: { + cwd, + }, + }, '--no-watch') + + expect(cwd[0]).toEqual(cwd[0].toLowerCase()) + expect(stderr).toBe('') + expect(stdout).toContain('1 passed') +}) From 6f08e153d933f01c4502afc5d82bf603b50e5955 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 24 Oct 2024 15:32:09 +0200 Subject: [PATCH 3/9] fix: use node:path directly instead of pathe --- packages/vite-node/src/client.ts | 4 +--- packages/vitest/src/node/cli/cac.ts | 8 -------- packages/vitest/src/node/pools/forks.ts | 2 +- packages/vitest/src/node/pools/threads.ts | 2 +- packages/vitest/src/node/pools/vmForks.ts | 2 +- packages/vitest/src/node/pools/vmThreads.ts | 2 +- packages/vitest/src/node/workspace.ts | 11 +++++++---- packages/vitest/src/paths.ts | 2 +- packages/vitest/src/runtime/runners/index.ts | 2 +- 9 files changed, 14 insertions(+), 21 deletions(-) diff --git a/packages/vite-node/src/client.ts b/packages/vite-node/src/client.ts index 84aed6696067..db67097184d7 100644 --- a/packages/vite-node/src/client.ts +++ b/packages/vite-node/src/client.ts @@ -1,12 +1,10 @@ import type { HotContext, ModuleCache, ViteNodeRunnerOptions } from './types' import { createRequire } from 'node:module' -// we need native dirname, because windows __dirname has \\ -import { dirname } from 'node:path' +import { dirname, resolve } from 'node:path' import { fileURLToPath, pathToFileURL } from 'node:url' import vm from 'node:vm' import createDebug from 'debug' -import { resolve } from 'pathe' import { extractSourceMap } from './source-map' import { cleanUrl, diff --git a/packages/vitest/src/node/cli/cac.ts b/packages/vitest/src/node/cli/cac.ts index 6992f1b98ad4..d1c8e78b103d 100644 --- a/packages/vitest/src/node/cli/cac.ts +++ b/packages/vitest/src/node/cli/cac.ts @@ -259,14 +259,6 @@ function normalizeCliOptions(argv: CliOptions): CliOptions { async function start(mode: VitestRunMode, cliFilters: string[], options: CliOptions): Promise { try { process.title = 'node (vitest)' - // pathe always normalizes the drive letter to be uppercase - // if user runs Vitest from c:/, the built-in ESM loader in tests - // will resolve a different Vitest instance than the one in vite-node - // we can either - // 1. normalize the CWD so it works in the CLI, but might break when API is used directly - // 2. ask "pathe" to not normalize the drive letter - // 2. don't use "pathe" when resolving paths - process.chdir(normalize(process.cwd())) } catch {} diff --git a/packages/vitest/src/node/pools/forks.ts b/packages/vitest/src/node/pools/forks.ts index 0eaaf1851480..cf925eb421fb 100644 --- a/packages/vitest/src/node/pools/forks.ts +++ b/packages/vitest/src/node/pools/forks.ts @@ -7,9 +7,9 @@ import type { SerializedConfig } from '../types/config' import type { WorkspaceProject } from '../workspace' import EventEmitter from 'node:events' import * as nodeos from 'node:os' +import { resolve } from 'node:path' import v8 from 'node:v8' import { createBirpc } from 'birpc' -import { resolve } from 'pathe' import { Tinypool } from 'tinypool' import { groupBy } from '../../utils/base' import { wrapSerializableConfig } from '../../utils/config-helpers' diff --git a/packages/vitest/src/node/pools/threads.ts b/packages/vitest/src/node/pools/threads.ts index 6199231a84a6..4d443975057a 100644 --- a/packages/vitest/src/node/pools/threads.ts +++ b/packages/vitest/src/node/pools/threads.ts @@ -7,9 +7,9 @@ import type { SerializedConfig } from '../types/config' import type { WorkerContext } from '../types/worker' import type { WorkspaceProject } from '../workspace' import * as nodeos from 'node:os' +import { resolve } from 'node:path' import { MessageChannel } from 'node:worker_threads' import { createBirpc } from 'birpc' -import { resolve } from 'pathe' import Tinypool from 'tinypool' import { groupBy } from '../../utils/base' import { envsOrder, groupFilesByEnv } from '../../utils/test-helpers' diff --git a/packages/vitest/src/node/pools/vmForks.ts b/packages/vitest/src/node/pools/vmForks.ts index 78306a98763a..14cd5881a35f 100644 --- a/packages/vitest/src/node/pools/vmForks.ts +++ b/packages/vitest/src/node/pools/vmForks.ts @@ -7,9 +7,9 @@ import type { ResolvedConfig, SerializedConfig } from '../types/config' import type { WorkspaceProject } from '../workspace' import EventEmitter from 'node:events' import * as nodeos from 'node:os' +import { resolve } from 'node:path' import v8 from 'node:v8' import { createBirpc } from 'birpc' -import { resolve } from 'pathe' import Tinypool from 'tinypool' import { rootDir } from '../../paths' import { wrapSerializableConfig } from '../../utils/config-helpers' diff --git a/packages/vitest/src/node/pools/vmThreads.ts b/packages/vitest/src/node/pools/vmThreads.ts index 040537e1a868..01420cfc87be 100644 --- a/packages/vitest/src/node/pools/vmThreads.ts +++ b/packages/vitest/src/node/pools/vmThreads.ts @@ -7,9 +7,9 @@ import type { ResolvedConfig, SerializedConfig } from '../types/config' import type { WorkerContext } from '../types/worker' import type { WorkspaceProject } from '../workspace' import * as nodeos from 'node:os' +import { resolve } from 'node:path' import { MessageChannel } from 'node:worker_threads' import { createBirpc } from 'birpc' -import { resolve } from 'pathe' import Tinypool from 'tinypool' import { rootDir } from '../../paths' import { getWorkerMemoryLimit, stringToBytes } from '../../utils/memory-limit' diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts index 17887cdc40b8..1057a0b609df 100644 --- a/packages/vitest/src/node/workspace.ts +++ b/packages/vitest/src/node/workspace.ts @@ -18,7 +18,8 @@ import type { import { promises as fs } from 'node:fs' import { rm } from 'node:fs/promises' import { tmpdir } from 'node:os' -import { deepMerge, nanoid } from '@vitest/utils' +import path from 'node:path' +import { deepMerge, nanoid, slash } from '@vitest/utils' import fg from 'fast-glob' import mm from 'micromatch' import { @@ -27,7 +28,6 @@ import { join, relative, resolve, - toNamespacedPath, } from 'pathe' import { ViteNodeRunner } from 'vite-node/client' import { ViteNodeServer } from 'vite-node/server' @@ -302,7 +302,10 @@ export class WorkspaceProject { } const files = await fg(include, globOptions) - return files.map(file => resolve(cwd, file)) + // keep the slashes consistent with Vite + // we are not using the pathe here because it normalizes the drive letter on Windows + // and we want to keep it the same as working dir + return files.map(file => slash(path.resolve(cwd, file))) } async isTargetFile(id: string, source?: string): Promise { @@ -329,7 +332,7 @@ export class WorkspaceProject { filterFiles(testFiles: string[], filters: string[], dir: string) { if (filters.length && process.platform === 'win32') { - filters = filters.map(f => toNamespacedPath(f)) + filters = filters.map(f => path.toNamespacedPath(f)) } if (filters.length) { diff --git a/packages/vitest/src/paths.ts b/packages/vitest/src/paths.ts index 55197e204d9a..11ceb81bf9c6 100644 --- a/packages/vitest/src/paths.ts +++ b/packages/vitest/src/paths.ts @@ -1,5 +1,5 @@ +import { resolve } from 'node:path' import url from 'node:url' -import { resolve } from 'pathe' export const rootDir = resolve(url.fileURLToPath(import.meta.url), '../../') export const distDir = resolve( diff --git a/packages/vitest/src/runtime/runners/index.ts b/packages/vitest/src/runtime/runners/index.ts index 49a7e1a87e80..2ac31beba3c7 100644 --- a/packages/vitest/src/runtime/runners/index.ts +++ b/packages/vitest/src/runtime/runners/index.ts @@ -1,7 +1,7 @@ import type { VitestRunner, VitestRunnerConstructor } from '@vitest/runner' import type { SerializedConfig } from '../config' import type { VitestExecutor } from '../execute' -import { resolve } from 'pathe' +import { resolve } from 'node:path' import { takeCoverageInsideWorker } from '../../integrations/coverage' import { distDir } from '../../paths' import { rpc } from '../rpc' From 53f4905b83617261463d2ef740bd99b7f47f6f05 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 24 Oct 2024 15:44:37 +0200 Subject: [PATCH 4/9] fix: use node:path for `spy` resolution --- packages/vitest/src/runtime/mocker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vitest/src/runtime/mocker.ts b/packages/vitest/src/runtime/mocker.ts index c924d010b4d0..fe193f2eaf53 100644 --- a/packages/vitest/src/runtime/mocker.ts +++ b/packages/vitest/src/runtime/mocker.ts @@ -1,11 +1,11 @@ import type { ManualMockedModule, MockedModuleType } from '@vitest/mocker' import type { MockFactory, MockOptions, PendingSuiteMock } from '../types/mocker' import type { VitestExecutor } from './execute' +import { isAbsolute, resolve } from 'node:path' import vm from 'node:vm' import { AutomockedModule, MockerRegistry, mockObject, RedirectedModule } from '@vitest/mocker' import { findMockRedirect } from '@vitest/mocker/redirect' import { highlight } from '@vitest/utils' -import { isAbsolute, resolve } from 'pathe' import { distDir } from '../paths' const spyModulePath = resolve(distDir, 'spy.js') From ab63f197b70b04e1912e0fb796b2814dda790235 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 24 Oct 2024 15:48:40 +0200 Subject: [PATCH 5/9] fix: return the vite-node patch --- packages/vite-node/src/utils.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/vite-node/src/utils.ts b/packages/vite-node/src/utils.ts index 48a5132253ff..edd2896c3b61 100644 --- a/packages/vite-node/src/utils.ts +++ b/packages/vite-node/src/utils.ts @@ -6,6 +6,17 @@ import { dirname, join, resolve } from 'pathe' export const isWindows = process.platform === 'win32' +const drive = isWindows ? process.cwd()[0] : null +const driveOpposite = drive + ? drive === drive.toUpperCase() + ? drive.toLowerCase() + : drive.toUpperCase() + : null +const driveRegexp = drive ? new RegExp(`(?:^|/@fs/)${drive}(\:[\\/])`) : null +const driveOppositeRegext = driveOpposite + ? new RegExp(`(?:^|/@fs/)${driveOpposite}(\:[\\/])`) + : null + export function slash(str: string) { return str.replace(/\\/g, '/') } @@ -17,6 +28,13 @@ export function normalizeRequestId(id: string, base?: string): string { id = `/${id.slice(base.length)}` } + // keep drive the same as in process cwd. ideally, this should be resolved on Vite side + // Vite always resolves drive letters to the upper case because of the use of `realpathSync` + // https://github.com/vitejs/vite/blob/0ab20a3ee26eacf302415b3087732497d0a2f358/packages/vite/src/node/utils.ts#L635 + if (driveRegexp && !driveRegexp?.test(id) && driveOppositeRegext?.test(id)) { + id = id.replace(driveOppositeRegext, `${drive}$1`) + } + return id .replace(/^\/@id\/__x00__/, '\0') // virtual modules start with `\0` .replace(/^\/@id\//, '') From e5bff76afb4eadf1f481e0eea69dcf63488dbb6b Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 24 Oct 2024 17:10:03 +0200 Subject: [PATCH 6/9] chore: cleanup --- packages/vitest/src/node/workspace.ts | 2 +- test/benchmark/test/reporter.test.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts index 1057a0b609df..487fa5dc3709 100644 --- a/packages/vitest/src/node/workspace.ts +++ b/packages/vitest/src/node/workspace.ts @@ -332,7 +332,7 @@ export class WorkspaceProject { filterFiles(testFiles: string[], filters: string[], dir: string) { if (filters.length && process.platform === 'win32') { - filters = filters.map(f => path.toNamespacedPath(f)) + filters = filters.map(f => slash(f)) } if (filters.length) { diff --git a/test/benchmark/test/reporter.test.ts b/test/benchmark/test/reporter.test.ts index 8bb9dd2a2254..d118732ff71d 100644 --- a/test/benchmark/test/reporter.test.ts +++ b/test/benchmark/test/reporter.test.ts @@ -5,6 +5,7 @@ import { runVitest } from '../../test-utils' it('summary', async () => { const root = pathe.join(import.meta.dirname, '../fixtures/reporter') const result = await runVitest({ root }, ['summary.bench.ts'], 'benchmark') + expect(result.stderr).toBe('') expect(result.stdout).not.toContain('NaNx') expect(result.stdout.split('BENCH Summary')[1].replaceAll(/[0-9.]+x/g, '(?)')).toMatchSnapshot() }) From 2f07f1f48af24f77967a0c1b9e360e5416b24b13 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Fri, 25 Oct 2024 13:34:52 +0200 Subject: [PATCH 7/9] chore: cleanup --- packages/vitest/src/runtime/execute.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/vitest/src/runtime/execute.ts b/packages/vitest/src/runtime/execute.ts index 7b07b66bcaa7..a5a6802cb5d5 100644 --- a/packages/vitest/src/runtime/execute.ts +++ b/packages/vitest/src/runtime/execute.ts @@ -17,6 +17,8 @@ import { import { distDir } from '../paths' import { VitestMocker } from './mocker' +const normalizedDistDir = normalize(distDir) + const { readFileSync } = fs export interface ExecuteOptions extends ViteNodeRunnerOptions { @@ -112,7 +114,7 @@ export async function startVitestExecutor(options: ContextExecutorOptions) { } // always externalize Vitest because we import from there before running tests // so we already have it cached by Node.js - if (id.includes(distDir)) { + if (id.includes(distDir) || id.includes(normalizedDistDir)) { const { path } = toFilePath(id, state().config.root) const externalize = pathToFileURL(path).toString() externalizeMap.set(id, externalize) From 6146f0d2a0182fb33eefaa52faedba269b8b4337 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Fri, 25 Oct 2024 15:03:36 +0200 Subject: [PATCH 8/9] fix: check normalized id instead --- packages/browser/src/client/public/error-catcher.js | 4 +++- packages/browser/src/node/serverTester.ts | 1 + packages/ui/package.json | 1 + packages/vitest/src/node/plugins/mocks.ts | 4 +++- test/cli/test/public-api.test.ts | 13 +++++++------ 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/browser/src/client/public/error-catcher.js b/packages/browser/src/client/public/error-catcher.js index a1326694e987..1e8a042cc115 100644 --- a/packages/browser/src/client/public/error-catcher.js +++ b/packages/browser/src/client/public/error-catcher.js @@ -56,7 +56,9 @@ async function reportUnexpectedError( error, ) { const processedError = serializeError(error) - await client.rpc.onUnhandledError(processedError, type) + await client.waitForConnection().then(() => { + return client.rpc.onUnhandledError(processedError, type) + }).catch(console.error) const state = __vitest_browser_runner__ if (state.type === 'orchestrator') { diff --git a/packages/browser/src/node/serverTester.ts b/packages/browser/src/node/serverTester.ts index 0773787cd67e..18ea0c0d99ee 100644 --- a/packages/browser/src/node/serverTester.ts +++ b/packages/browser/src/node/serverTester.ts @@ -60,6 +60,7 @@ export async function resolveTester( try { const indexhtml = await server.vite.transformIndexHtml(url.pathname, testerHtml) return replacer(indexhtml, { + __VITEST_FAVICON__: server.faviconUrl, __VITEST_INJECTOR__: injector, __VITEST_APPEND__: ` __vitest_browser_runner__.runningFiles = ${tests} diff --git a/packages/ui/package.json b/packages/ui/package.json index 8b8552d70b8c..4dd3069e4cf2 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -76,6 +76,7 @@ "codemirror-theme-vars": "^0.1.2", "d3-graph-controller": "^3.0.11", "floating-vue": "^5.2.2", + "rollup": "^4.24.0", "splitpanes": "^3.1.5", "unocss": "^0.63.6", "unplugin-auto-import": "^0.18.3", diff --git a/packages/vitest/src/node/plugins/mocks.ts b/packages/vitest/src/node/plugins/mocks.ts index 865496d762f9..e9def6730850 100644 --- a/packages/vitest/src/node/plugins/mocks.ts +++ b/packages/vitest/src/node/plugins/mocks.ts @@ -1,13 +1,15 @@ import type { Plugin } from 'vite' import { automockPlugin, hoistMocksPlugin } from '@vitest/mocker/node' +import { normalize } from 'pathe' import { distDir } from '../../paths' import { generateCodeFrame } from '../error' export function MocksPlugins(): Plugin[] { + const normalizedDistDir = normalize(distDir) return [ hoistMocksPlugin({ filter(id) { - if (id.includes(distDir)) { + if (id.includes(normalizedDistDir)) { return false } return true diff --git a/test/cli/test/public-api.test.ts b/test/cli/test/public-api.test.ts index e2a551d94bdc..93de5e5e1eb8 100644 --- a/test/cli/test/public-api.test.ts +++ b/test/cli/test/public-api.test.ts @@ -1,4 +1,5 @@ -import type { File, TaskResultPack, UserConfig } from 'vitest' +import type { RunnerTaskResultPack, RunnerTestFile } from 'vitest' +import type { UserConfig } from 'vitest/node' import { resolve } from 'pathe' import { expect, it } from 'vitest' import { runVitest } from '../../test-utils' @@ -15,15 +16,15 @@ it.each([ headless: true, }, }, -] as UserConfig[])('passes down metadata when $name', { timeout: 60_000, retry: 3 }, async (config) => { - const taskUpdate: TaskResultPack[] = [] - const finishedFiles: File[] = [] - const collectedFiles: File[] = [] +] as UserConfig[])('passes down metadata when $name', { timeout: 60_000, retry: 1 }, async (config) => { + const taskUpdate: RunnerTaskResultPack[] = [] + const finishedFiles: RunnerTestFile[] = [] + const collectedFiles: RunnerTestFile[] = [] const { ctx, stdout, stderr } = await runVitest({ root: resolve(__dirname, '..', 'fixtures', 'public-api'), include: ['**/*.spec.ts'], reporters: [ - 'verbose', + ['verbose', { isTTY: false }], { onTaskUpdate(packs) { taskUpdate.push(...packs.filter(i => i[1]?.state === 'pass')) From b703f551942bf87deb48682a66421b3d8c8de9f5 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Fri, 25 Oct 2024 15:04:46 +0200 Subject: [PATCH 9/9] chore: lockfile --- pnpm-lock.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 77cca4916dce..b5c7bede3af1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -752,6 +752,9 @@ importers: floating-vue: specifier: ^5.2.2 version: 5.2.2(vue@3.5.12(typescript@5.6.3)) + rollup: + specifier: ^4.24.0 + version: 4.24.0 splitpanes: specifier: ^3.1.5 version: 3.1.5