From 24fb2f7ded73a27c27cadeed2b81488254e1c8d3 Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Sat, 21 Mar 2026 00:42:24 +0900 Subject: [PATCH 1/4] fix(cli): traverse parent directories to find vite.config.ts in vp pack --- .../src/__tests__/resolve-vite-config.spec.ts | 92 +++++++++++++++++++ packages/cli/src/pack-bin.ts | 2 +- packages/cli/src/resolve-vite-config.ts | 90 +++++++++++++++++- 3 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 packages/cli/src/__tests__/resolve-vite-config.spec.ts diff --git a/packages/cli/src/__tests__/resolve-vite-config.spec.ts b/packages/cli/src/__tests__/resolve-vite-config.spec.ts new file mode 100644 index 0000000000..a0d3236db4 --- /dev/null +++ b/packages/cli/src/__tests__/resolve-vite-config.spec.ts @@ -0,0 +1,92 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { mkdtempSync } from 'node:fs'; +import { tmpdir } from 'node:os'; + +import { findViteConfigUp } from '../resolve-vite-config'; + +describe('findViteConfigUp', () => { + let tempDir: string; + + beforeEach(() => { + tempDir = mkdtempSync(path.join(tmpdir(), 'vite-config-test-')); + }); + + afterEach(() => { + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + + it('should find config in the start directory', () => { + fs.writeFileSync(path.join(tempDir, 'vite.config.ts'), ''); + const result = findViteConfigUp(tempDir, tempDir); + expect(result).toBe(path.join(tempDir, 'vite.config.ts')); + }); + + it('should find config in a parent directory', () => { + const subDir = path.join(tempDir, 'packages', 'my-lib'); + fs.mkdirSync(subDir, { recursive: true }); + fs.writeFileSync(path.join(tempDir, 'vite.config.ts'), ''); + + const result = findViteConfigUp(subDir, tempDir); + expect(result).toBe(path.join(tempDir, 'vite.config.ts')); + }); + + it('should find config in an intermediate directory', () => { + const subDir = path.join(tempDir, 'packages', 'my-lib', 'src'); + fs.mkdirSync(subDir, { recursive: true }); + fs.writeFileSync(path.join(tempDir, 'packages', 'vite.config.ts'), ''); + + const result = findViteConfigUp(subDir, tempDir); + expect(result).toBe(path.join(tempDir, 'packages', 'vite.config.ts')); + }); + + it('should return undefined when no config exists', () => { + const subDir = path.join(tempDir, 'packages', 'my-lib'); + fs.mkdirSync(subDir, { recursive: true }); + + const result = findViteConfigUp(subDir, tempDir); + expect(result).toBeUndefined(); + }); + + it('should not traverse beyond stopDir', () => { + const parentConfig = path.join(tempDir, 'vite.config.ts'); + fs.writeFileSync(parentConfig, ''); + const stopDir = path.join(tempDir, 'packages'); + const subDir = path.join(stopDir, 'my-lib'); + fs.mkdirSync(subDir, { recursive: true }); + + const result = findViteConfigUp(subDir, stopDir); + // Should not find the config in tempDir because stopDir is packages/ + expect(result).toBeUndefined(); + }); + + it('should prefer the closest config file', () => { + const subDir = path.join(tempDir, 'packages', 'my-lib'); + fs.mkdirSync(subDir, { recursive: true }); + fs.writeFileSync(path.join(tempDir, 'vite.config.ts'), ''); + fs.writeFileSync(path.join(tempDir, 'packages', 'vite.config.ts'), ''); + + const result = findViteConfigUp(subDir, tempDir); + expect(result).toBe(path.join(tempDir, 'packages', 'vite.config.ts')); + }); + + it('should find .js config files', () => { + const subDir = path.join(tempDir, 'packages', 'my-lib'); + fs.mkdirSync(subDir, { recursive: true }); + fs.writeFileSync(path.join(tempDir, 'vite.config.js'), ''); + + const result = findViteConfigUp(subDir, tempDir); + expect(result).toBe(path.join(tempDir, 'vite.config.js')); + }); + + it('should find .mts config files', () => { + const subDir = path.join(tempDir, 'packages', 'my-lib'); + fs.mkdirSync(subDir, { recursive: true }); + fs.writeFileSync(path.join(tempDir, 'vite.config.mts'), ''); + + const result = findViteConfigUp(subDir, tempDir); + expect(result).toBe(path.join(tempDir, 'vite.config.mts')); + }); +}); diff --git a/packages/cli/src/pack-bin.ts b/packages/cli/src/pack-bin.ts index 5acb4898e5..3d7f600946 100644 --- a/packages/cli/src/pack-bin.ts +++ b/packages/cli/src/pack-bin.ts @@ -128,7 +128,7 @@ cli } async function runBuild() { - const viteConfig = await resolveViteConfig(process.cwd()); + const viteConfig = await resolveViteConfig(process.cwd(), { traverseUp: true }); const configFiles: string[] = []; if (viteConfig.configFile) { diff --git a/packages/cli/src/resolve-vite-config.ts b/packages/cli/src/resolve-vite-config.ts index 8cbb1acb67..6a378602e7 100644 --- a/packages/cli/src/resolve-vite-config.ts +++ b/packages/cli/src/resolve-vite-config.ts @@ -1,8 +1,96 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +const VITE_CONFIG_FILES = [ + 'vite.config.js', + 'vite.config.mjs', + 'vite.config.ts', + 'vite.config.cjs', + 'vite.config.mts', + 'vite.config.cts', +]; + +/** + * Find a vite config file by walking up from `startDir` to `stopDir`. + * Returns the absolute path of the first config file found, or undefined. + */ +export function findViteConfigUp(startDir: string, stopDir: string): string | undefined { + let dir = path.resolve(startDir); + const stop = path.resolve(stopDir); + + while (true) { + for (const filename of VITE_CONFIG_FILES) { + const filePath = path.join(dir, filename); + if (fs.existsSync(filePath)) { + return filePath; + } + } + const parent = path.dirname(dir); + if (parent === dir || !dir.startsWith(stop)) { + break; + } + dir = parent; + } + return undefined; +} + +function hasViteConfig(dir: string): boolean { + return VITE_CONFIG_FILES.some((f) => fs.existsSync(path.join(dir, f))); +} + +/** + * Find the workspace root by walking up from `startDir` looking for + * monorepo indicators (pnpm-workspace.yaml, workspaces in package.json, lerna.json). + */ +function findWorkspaceRoot(startDir: string): string | undefined { + let dir = path.resolve(startDir); + while (true) { + if (fs.existsSync(path.join(dir, 'pnpm-workspace.yaml'))) { + return dir; + } + const pkgPath = path.join(dir, 'package.json'); + if (fs.existsSync(pkgPath)) { + try { + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); + if (pkg.workspaces) { + return dir; + } + } catch { + // ignore + } + } + if (fs.existsSync(path.join(dir, 'lerna.json'))) { + return dir; + } + const parent = path.dirname(dir); + if (parent === dir) { + break; + } + dir = parent; + } + return undefined; +} + +export interface ResolveViteConfigOptions { + traverseUp?: boolean; +} + /** * Resolve vite.config.ts and return the config object. */ -export async function resolveViteConfig(cwd: string) { +export async function resolveViteConfig(cwd: string, options?: ResolveViteConfigOptions) { const { resolveConfig } = await import('./index.js'); + + if (options?.traverseUp && !hasViteConfig(cwd)) { + const workspaceRoot = findWorkspaceRoot(cwd); + if (workspaceRoot) { + const configFile = findViteConfigUp(path.dirname(cwd), workspaceRoot); + if (configFile) { + return resolveConfig({ root: cwd, configFile }, 'build'); + } + } + } + return resolveConfig({ root: cwd }, 'build'); } From 24c3c14b930c880f88d88b5f315863d9a528d84c Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Sat, 21 Mar 2026 14:49:31 +0900 Subject: [PATCH 2/4] fix(cli): add CJS/MJS config file tests and improve catch comment --- .../src/__tests__/resolve-vite-config.spec.ts | 34 +++++++++++++++++-- packages/cli/src/resolve-vite-config.ts | 4 +-- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/__tests__/resolve-vite-config.spec.ts b/packages/cli/src/__tests__/resolve-vite-config.spec.ts index a0d3236db4..a1205f1d05 100644 --- a/packages/cli/src/__tests__/resolve-vite-config.spec.ts +++ b/packages/cli/src/__tests__/resolve-vite-config.spec.ts @@ -1,9 +1,9 @@ import fs from 'node:fs'; +import { mkdtempSync } from 'node:fs'; +import { tmpdir } from 'node:os'; import path from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { mkdtempSync } from 'node:fs'; -import { tmpdir } from 'node:os'; import { findViteConfigUp } from '../resolve-vite-config'; @@ -11,7 +11,8 @@ describe('findViteConfigUp', () => { let tempDir: string; beforeEach(() => { - tempDir = mkdtempSync(path.join(tmpdir(), 'vite-config-test-')); + // Resolve symlinks (macOS /var -> /private/var) to match path.resolve behavior + tempDir = fs.realpathSync(mkdtempSync(path.join(tmpdir(), 'vite-config-test-'))); }); afterEach(() => { @@ -89,4 +90,31 @@ describe('findViteConfigUp', () => { const result = findViteConfigUp(subDir, tempDir); expect(result).toBe(path.join(tempDir, 'vite.config.mts')); }); + + it('should find .cjs config files', () => { + const subDir = path.join(tempDir, 'packages', 'my-lib'); + fs.mkdirSync(subDir, { recursive: true }); + fs.writeFileSync(path.join(tempDir, 'vite.config.cjs'), ''); + + const result = findViteConfigUp(subDir, tempDir); + expect(result).toBe(path.join(tempDir, 'vite.config.cjs')); + }); + + it('should find .cts config files', () => { + const subDir = path.join(tempDir, 'packages', 'my-lib'); + fs.mkdirSync(subDir, { recursive: true }); + fs.writeFileSync(path.join(tempDir, 'vite.config.cts'), ''); + + const result = findViteConfigUp(subDir, tempDir); + expect(result).toBe(path.join(tempDir, 'vite.config.cts')); + }); + + it('should find .mjs config files', () => { + const subDir = path.join(tempDir, 'packages', 'my-lib'); + fs.mkdirSync(subDir, { recursive: true }); + fs.writeFileSync(path.join(tempDir, 'vite.config.mjs'), ''); + + const result = findViteConfigUp(subDir, tempDir); + expect(result).toBe(path.join(tempDir, 'vite.config.mjs')); + }); }); diff --git a/packages/cli/src/resolve-vite-config.ts b/packages/cli/src/resolve-vite-config.ts index 6a378602e7..969f560aad 100644 --- a/packages/cli/src/resolve-vite-config.ts +++ b/packages/cli/src/resolve-vite-config.ts @@ -26,7 +26,7 @@ export function findViteConfigUp(startDir: string, stopDir: string): string | un } } const parent = path.dirname(dir); - if (parent === dir || !dir.startsWith(stop)) { + if (parent === dir || !parent.startsWith(stop)) { break; } dir = parent; @@ -56,7 +56,7 @@ function findWorkspaceRoot(startDir: string): string | undefined { return dir; } } catch { - // ignore + // Skip malformed package.json and continue searching parent directories } } if (fs.existsSync(path.join(dir, 'lerna.json'))) { From 2160c528852d6864f2b895ae3c7c2ebcc59f20c3 Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Sat, 21 Mar 2026 20:18:13 +0900 Subject: [PATCH 3/4] fix(cli): skip traverseUp when --no-config is specified in vp pack --- packages/cli/src/pack-bin.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/pack-bin.ts b/packages/cli/src/pack-bin.ts index 3d7f600946..80c9dc6ae3 100644 --- a/packages/cli/src/pack-bin.ts +++ b/packages/cli/src/pack-bin.ts @@ -128,7 +128,9 @@ cli } async function runBuild() { - const viteConfig = await resolveViteConfig(process.cwd(), { traverseUp: true }); + const viteConfig = await resolveViteConfig(process.cwd(), { + traverseUp: flags.config !== false, + }); const configFiles: string[] = []; if (viteConfig.configFile) { From f16724836a952d146522e286f65af0a101889ee0 Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Sun, 22 Mar 2026 01:23:09 +0900 Subject: [PATCH 4/4] perf(cli): prioritize vite.config.ts in config file search order --- packages/cli/src/resolve-vite-config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/resolve-vite-config.ts b/packages/cli/src/resolve-vite-config.ts index 969f560aad..085a77529e 100644 --- a/packages/cli/src/resolve-vite-config.ts +++ b/packages/cli/src/resolve-vite-config.ts @@ -2,11 +2,11 @@ import fs from 'node:fs'; import path from 'node:path'; const VITE_CONFIG_FILES = [ + 'vite.config.ts', 'vite.config.js', 'vite.config.mjs', - 'vite.config.ts', - 'vite.config.cjs', 'vite.config.mts', + 'vite.config.cjs', 'vite.config.cts', ];