From e09c1af2cac3a99573dd7390b5fce29ad95d83e3 Mon Sep 17 00:00:00 2001 From: MK Date: Wed, 7 Oct 2026 16:55:51 +0800 Subject: [PATCH 1/2] perf(cli): skip config runtime initialization when no config exists `vp check` initializes Vite and Vitest to read metadata even when the selected workspace root has no `vite.config.*` file. `resolveUniversalViteConfig` now returns empty metadata when no supported config file exists. This avoids unnecessary startup work. `resolveViteConfig` loads `define-config` only when runtime resolution is necessary. Each call checks the filesystem, so later calls detect new config files. Existing configs retain runtime evaluation, plugin hooks, and error handling. Related to #2698. --- .../resolve-universal-vite-config.spec.ts | 102 ++++++++++++++++++ packages/cli/src/resolve-vite-config.ts | 8 +- 2 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 packages/cli/src/__tests__/resolve-universal-vite-config.spec.ts diff --git a/packages/cli/src/__tests__/resolve-universal-vite-config.spec.ts b/packages/cli/src/__tests__/resolve-universal-vite-config.spec.ts new file mode 100644 index 0000000000..7bab49fbb0 --- /dev/null +++ b/packages/cli/src/__tests__/resolve-universal-vite-config.spec.ts @@ -0,0 +1,102 @@ +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { VITE_CONFIG_FILES } from '../utils/constants.ts'; + +describe('resolveUniversalViteConfig', () => { + let root: string; + const resolveConfig = vi.fn(); + const metadataImport = vi.fn(); + const publicImport = vi.fn(); + + beforeEach(() => { + root = mkdtempSync(path.join(tmpdir(), 'vp-config-metadata-')); + vi.resetModules(); + resolveConfig.mockReset(); + metadataImport.mockClear(); + publicImport.mockClear(); + vi.doMock('../define-config.ts', () => { + metadataImport(); + return { withConfigMetadataResolution: (fn: () => Promise) => fn() }; + }); + vi.doMock('../index.js', () => { + publicImport(); + return { resolveConfig }; + }); + }); + + afterEach(() => { + vi.doUnmock('../define-config.ts'); + vi.doUnmock('../index.js'); + vi.resetModules(); + rmSync(root, { recursive: true, force: true }); + }); + + it('returns empty metadata without importing the Vite or Vitest entry points', async () => { + const { resolveUniversalViteConfig } = await import('../resolve-vite-config.ts'); + expect(await resolveUniversalViteConfig(null, root)).toBe('{}'); + expect(metadataImport).not.toHaveBeenCalled(); + expect(publicImport).not.toHaveBeenCalled(); + expect(resolveConfig).not.toHaveBeenCalled(); + }); + + it('does not select a config above the supplied workspace root', async () => { + writeFileSync(path.join(root, 'vite.config.ts'), 'export default {};'); + const child = path.join(root, 'workspace'); + mkdirSync(child); + const { resolveUniversalViteConfig } = await import('../resolve-vite-config.ts'); + expect(await resolveUniversalViteConfig(null, child)).toBe('{}'); + expect(publicImport).not.toHaveBeenCalled(); + }); + + it.each(VITE_CONFIG_FILES)('retains runtime resolution for %s', async (filename) => { + const configFile = path.join(root, filename); + writeFileSync(configFile, 'export default {};'); + resolveConfig.mockResolvedValue({ configFile, check: { lint: false } }); + const { resolveUniversalViteConfig } = await import('../resolve-vite-config.ts'); + expect(JSON.parse(await resolveUniversalViteConfig(null, root))).toEqual({ + configFile, + check: { lint: false }, + }); + expect(resolveConfig).toHaveBeenCalledWith({ root }, 'build'); + }); + + it('observes config creation, changes and removal between calls', async () => { + const { resolveUniversalViteConfig } = await import('../resolve-vite-config.ts'); + const configFile = path.join(root, 'vite.config.ts'); + expect(await resolveUniversalViteConfig(null, root)).toBe('{}'); + writeFileSync(configFile, 'export default { check: { fmt: false } };'); + resolveConfig.mockResolvedValueOnce({ configFile, check: { fmt: false } }); + expect(JSON.parse(await resolveUniversalViteConfig(null, root)).check).toEqual({ fmt: false }); + writeFileSync(configFile, 'export default { check: { lint: false } };'); + resolveConfig.mockResolvedValueOnce({ configFile, check: { lint: false } }); + expect(JSON.parse(await resolveUniversalViteConfig(null, root)).check).toEqual({ lint: false }); + rmSync(configFile); + expect(await resolveUniversalViteConfig(null, root)).toBe('{}'); + expect(resolveConfig).toHaveBeenCalledTimes(2); + }); + + it('preserves the callback error even without a config', async () => { + const { resolveUniversalViteConfig } = await import('../resolve-vite-config.ts'); + const error = new Error('callback failed'); + await expect(resolveUniversalViteConfig(error, root)).rejects.toBe(error); + expect(publicImport).not.toHaveBeenCalled(); + }); + + it('retains errors from runtime config evaluation', async () => { + writeFileSync(path.join(root, 'vite.config.ts'), 'throw new Error("invalid config");'); + const error = new Error('invalid config'); + resolveConfig.mockRejectedValue(error); + const log = vi.spyOn(console, 'error').mockImplementation(() => {}); + try { + const { resolveUniversalViteConfig } = await import('../resolve-vite-config.ts'); + await expect(resolveUniversalViteConfig(null, root)).rejects.toBe(error); + expect(log).toHaveBeenCalledWith('[Vite+] resolve universal vite config error:', error); + } finally { + log.mockRestore(); + } + }); +}); diff --git a/packages/cli/src/resolve-vite-config.ts b/packages/cli/src/resolve-vite-config.ts index 0632f109b1..a0fdb48235 100644 --- a/packages/cli/src/resolve-vite-config.ts +++ b/packages/cli/src/resolve-vite-config.ts @@ -1,7 +1,6 @@ import fs from 'node:fs'; import path from 'node:path'; -import { withConfigMetadataResolution } from './define-config.ts'; import { VITE_CONFIG_FILES } from './utils/constants.ts'; /** @@ -83,6 +82,7 @@ export interface ResolveViteConfigOptions { * Resolve vite.config.ts and return the config object. */ export async function resolveViteConfig(cwd: string, options?: ResolveViteConfigOptions) { + const { withConfigMetadataResolution } = await import('./define-config.ts'); const { resolveConfig } = await import('./index.js'); // This loads the config purely to read a non-plugin block (lint/fmt/pack/run/ @@ -106,6 +106,12 @@ export async function resolveUniversalViteConfig(err: null | Error, viteConfigCw if (err) { throw err; } + // Rust asks for metadata at the selected workspace root. With no config + // there, none of these fields exist; avoid initializing Vite and Vitest. + // Check on every call so a newly created config is still observed. + if (!hasViteConfig(viteConfigCwd)) { + return '{}'; + } try { const config = await resolveViteConfig(viteConfigCwd); From 48d1c9247304ad62b3ce2b2f9ce34055774b0915 Mon Sep 17 00:00:00 2001 From: MK Date: Wed, 7 Oct 2026 17:23:12 +0800 Subject: [PATCH 2/2] perf(cli): reuse the discovered Vite config path --- .../__tests__/resolve-universal-vite-config.spec.ts | 13 ++++++++++++- packages/cli/src/resolve-vite-config.ts | 10 ++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/__tests__/resolve-universal-vite-config.spec.ts b/packages/cli/src/__tests__/resolve-universal-vite-config.spec.ts index 7bab49fbb0..64f7a20b92 100644 --- a/packages/cli/src/__tests__/resolve-universal-vite-config.spec.ts +++ b/packages/cli/src/__tests__/resolve-universal-vite-config.spec.ts @@ -61,7 +61,18 @@ describe('resolveUniversalViteConfig', () => { configFile, check: { lint: false }, }); - expect(resolveConfig).toHaveBeenCalledWith({ root }, 'build'); + expect(resolveConfig).toHaveBeenCalledWith({ root, configFile }, 'build'); + }); + + it('uses the first supported config when multiple config files exist', async () => { + for (const filename of VITE_CONFIG_FILES) { + writeFileSync(path.join(root, filename), 'export default {};'); + } + const configFile = path.join(root, VITE_CONFIG_FILES[0]); + resolveConfig.mockResolvedValue({ configFile }); + const { resolveUniversalViteConfig } = await import('../resolve-vite-config.ts'); + expect(JSON.parse(await resolveUniversalViteConfig(null, root))).toEqual({ configFile }); + expect(resolveConfig).toHaveBeenCalledWith({ root, configFile }, 'build'); }); it('observes config creation, changes and removal between calls', async () => { diff --git a/packages/cli/src/resolve-vite-config.ts b/packages/cli/src/resolve-vite-config.ts index a0fdb48235..1bdb1f7f5b 100644 --- a/packages/cli/src/resolve-vite-config.ts +++ b/packages/cli/src/resolve-vite-config.ts @@ -75,6 +75,8 @@ export function findWorkspaceRoot(startDir: string): string | undefined { } export interface ResolveViteConfigOptions { + /** Use a config file already found by the caller. */ + configFile?: string; traverseUp?: boolean; } @@ -88,6 +90,9 @@ export async function resolveViteConfig(cwd: string, options?: ResolveViteConfig // This loads the config purely to read a non-plugin block (lint/fmt/pack/run/ // staged/create…), so skip the user's plugin factory while it evaluates. return withConfigMetadataResolution(async () => { + if (options?.configFile) { + return resolveConfig({ root: cwd, configFile: options.configFile }, 'build'); + } if (options?.traverseUp && !hasViteConfig(cwd)) { const workspaceRoot = findWorkspaceRoot(cwd); if (workspaceRoot) { @@ -109,11 +114,12 @@ export async function resolveUniversalViteConfig(err: null | Error, viteConfigCw // Rust asks for metadata at the selected workspace root. With no config // there, none of these fields exist; avoid initializing Vite and Vitest. // Check on every call so a newly created config is still observed. - if (!hasViteConfig(viteConfigCwd)) { + const configFile = findViteConfig(viteConfigCwd); + if (!configFile) { return '{}'; } try { - const config = await resolveViteConfig(viteConfigCwd); + const config = await resolveViteConfig(viteConfigCwd, { configFile }); return JSON.stringify({ configFile: config.configFile,