Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions packages/cli/src/__tests__/resolve-vite-config.spec.ts
Original file line number Diff line number Diff line change
@@ -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'));
});
});
4 changes: 3 additions & 1 deletion packages/cli/src/pack-bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
90 changes: 89 additions & 1 deletion packages/cli/src/resolve-vite-config.ts
Original file line number Diff line number Diff line change
@@ -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');
}

Expand Down
Loading