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..a1205f1d05 --- /dev/null +++ b/packages/cli/src/__tests__/resolve-vite-config.spec.ts @@ -0,0 +1,120 @@ +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 { findViteConfigUp } from '../resolve-vite-config'; + +describe('findViteConfigUp', () => { + let tempDir: string; + + beforeEach(() => { + // Resolve symlinks (macOS /var -> /private/var) to match path.resolve behavior + tempDir = fs.realpathSync(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')); + }); + + 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/pack-bin.ts b/packages/cli/src/pack-bin.ts index 5acb4898e5..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()); + const viteConfig = await resolveViteConfig(process.cwd(), { + traverseUp: flags.config !== false, + }); 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..085a77529e 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.ts', + 'vite.config.js', + 'vite.config.mjs', + 'vite.config.mts', + 'vite.config.cjs', + '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 || !parent.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 { + // Skip malformed package.json and continue searching parent directories + } + } + 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'); }