Skip to content
Merged
41 changes: 36 additions & 5 deletions packages/cli/snap-tests/command-pack-monorepo/snap.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,48 @@
> ls packages/hello/dist # should have the library
index.cjs

[1]> vp run hello#build 2>&1 | grep 'cache hit' # should hit cache
> vp run hello#build 2>&1 # should hit cache but not working for now
~/packages/hello$ vp pack
ℹ entry: src/index.ts
ℹ Build start
ℹ Cleaning 1 files
ℹ dist/index.cjs <variable> kB │ gzip: <variable> kB
ℹ 1 files, total: <variable> kB
✔ Build complete in <variable>ms

---
vp run: hello#build not cached because it modified its input. (Run `vp run --last-details` for full details)

> vp run array-config#build # should build the library supports array config
> ls packages/array-config/dist # should have the library
index.d.mts
index.mjs

[1]> vp run array-config#build 2>&1 | grep 'cache hit' # should hit cache
> vp run array-config#build 2>&1 # should hit cache but not working
~/packages/array-config$ vp pack
ℹ entry: src/sub/index.ts
ℹ Build start
ℹ Cleaning 2 files
ℹ dist/index.mjs <variable> kB │ gzip: <variable> kB
ℹ dist/index.d.mts <variable> kB │ gzip: <variable> kB
ℹ 2 files, total: <variable> kB
✔ Build complete in <variable>ms

---
vp run: array-config#build not cached because it modified its input. (Run `vp run --last-details` for full details)

> vp run default-config#build # should build the library supports default config
> ls packages/default-config/dist # should have the library
index.mjs

> vp run default-config#build 2>&1 | grep 'cache hit' # should hit cache
~/packages/default-config$ vp pack ◉ cache hit, replaying
vp run: cache hit, <variable>ms saved.
> vp run default-config#build 2>&1 # should hit cache but not working
~/packages/default-config$ vp pack
ℹ entry: src/index.ts
ℹ Build start
ℹ Cleaning 1 files
ℹ dist/index.mjs <variable> kB │ gzip: <variable> kB
ℹ 1 files, total: <variable> kB
✔ Build complete in <variable>ms

---
vp run: default-config#build not cached because it modified its input. (Run `vp run --last-details` for full details)
7 changes: 4 additions & 3 deletions packages/cli/snap-tests/command-pack-monorepo/steps.json
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
{
"ignoredPlatforms": ["win32"],
"commands": [
{
"command": "vp run hello#build # should build the library",
"ignoreOutput": true
},
"ls packages/hello/dist # should have the library",
"vp run hello#build 2>&1 | grep 'cache hit' # should hit cache",
"vp run hello#build 2>&1 # should hit cache but not working for now",
{
"command": "vp run array-config#build # should build the library supports array config",
"ignoreOutput": true
},
"ls packages/array-config/dist # should have the library",
"vp run array-config#build 2>&1 | grep 'cache hit' # should hit cache",
"vp run array-config#build 2>&1 # should hit cache but not working",
{
"command": "vp run default-config#build # should build the library supports default config",
"ignoreOutput": true
},
"ls packages/default-config/dist # should have the library",
"vp run default-config#build 2>&1 | grep 'cache hit' # should hit cache"
"vp run default-config#build 2>&1 # should hit cache but not working"
]
}
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