Skip to content
Draft
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
113 changes: 113 additions & 0 deletions packages/cli/src/__tests__/resolve-universal-vite-config.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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<unknown>) => 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, 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 () => {
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();
}
});
});
16 changes: 14 additions & 2 deletions packages/cli/src/resolve-vite-config.ts
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand Down Expand Up @@ -76,18 +75,24 @@ export function findWorkspaceRoot(startDir: string): string | undefined {
}

export interface ResolveViteConfigOptions {
/** Use a config file already found by the caller. */
configFile?: string;
traverseUp?: boolean;
}

/**
* 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/
// 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) {
Expand All @@ -106,8 +111,15 @@ 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.
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,
Expand Down
Loading