Skip to content
Closed
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
6 changes: 6 additions & 0 deletions packages/cli/src/__tests__/oxlint-plugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,12 @@ new RuleTester({
filename: 'vite.config.ts',
output: `import { defineConfig } from 'vite-plus'`,
},
{
code: `import { defineConfig } from 'vite'`,
errors: 1,
filename: 'nuxt.config.ts',
output: `import { defineConfig } from 'vite-plus'`,
},
{
code: `export { defineConfig } from "vite"`,
errors: 1,
Expand Down
19 changes: 18 additions & 1 deletion packages/cli/src/__tests__/resolve-vite-config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import path from 'node:path';

import { afterEach, beforeEach, describe, expect, it } from 'vitest';

import { findViteConfigUp } from '../resolve-vite-config.js';
import { findViteConfig, findViteConfigUp } from '../resolve-vite-config.js';

describe('findViteConfigUp', () => {
let tempDir: string;
Expand Down Expand Up @@ -117,4 +117,21 @@ describe('findViteConfigUp', () => {
const result = findViteConfigUp(subDir, tempDir);
expect(result).toBe(path.join(tempDir, 'vite.config.mjs'));
});

it('should find nuxt config files', () => {
const subDir = path.join(tempDir, 'packages', 'my-lib');
fs.mkdirSync(subDir, { recursive: true });
fs.writeFileSync(path.join(tempDir, 'nuxt.config.ts'), '');

const result = findViteConfigUp(subDir, tempDir);
expect(result).toBe(path.join(tempDir, 'nuxt.config.ts'));
});

it('should prefer vite config files over nuxt config files', () => {
fs.writeFileSync(path.join(tempDir, 'vite.config.ts'), '');
fs.writeFileSync(path.join(tempDir, 'nuxt.config.ts'), '');

const result = findViteConfig(tempDir);
expect(result).toBe(path.join(tempDir, 'vite.config.ts'));
});
});
2 changes: 1 addition & 1 deletion packages/cli/src/create/org-resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ export async function resolveOrgManifestForCreate(args: {

/**
* Read the `create` config (`defaultTemplate` + validated `templates`) from
* a workspace's `vite.config.ts` in a single config evaluation.
* a workspace's supported config file in a single config evaluation.
*
* By default, walks up from `startDir` via `findWorkspaceRoot` (monorepo
* markers only — `pnpm-workspace.yaml`, `workspaces` in `package.json`,
Expand Down
21 changes: 4 additions & 17 deletions packages/cli/src/init-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { hasConfigKey, mergeJsonConfig } from '../binding/index.js';
import { createDefaultVitePlusLintConfig } from './oxlint-plugin-config.ts';
import { fmt as resolveFmt } from './resolve-fmt.ts';
import { runCommandSilently } from './utils/command.ts';
import { findSupportedConfigFile } from './utils/config-files.ts';
import { BASEURL_TSCONFIG_WARNING, VITE_PLUS_NAME } from './utils/constants.ts';
import { warnMsg } from './utils/terminal.ts';
import { fixBaseUrlInTsconfig, hasBaseUrlInTsconfig } from './utils/tsconfig.ts';
Expand Down Expand Up @@ -32,15 +33,6 @@ function normalizeInitCommand(command: string | undefined): string | undefined {
return command === 'format' ? 'fmt' : command;
}

const VITE_CONFIG_FILES = [
'vite.config.ts',
'vite.config.mts',
'vite.config.cts',
'vite.config.js',
'vite.config.mjs',
'vite.config.cjs',
] as const;

export interface InitCommandInspection {
handled: boolean;
configKey?: 'lint' | 'fmt';
Expand Down Expand Up @@ -113,13 +105,7 @@ function resolveGeneratedConfigPath(
}

function findViteConfigPath(projectPath: string): string | null {
for (const filename of VITE_CONFIG_FILES) {
const fullPath = path.join(projectPath, filename);
if (fs.existsSync(fullPath)) {
return fullPath;
}
}
return null;
return findSupportedConfigFile(projectPath) ?? null;
}

function ensureViteConfigPath(projectPath: string): string {
Expand Down Expand Up @@ -197,7 +183,8 @@ export function inspectInitCommand(

/**
* Merge generated tool config from `vp lint/fmt --init` (and fmt --migrate)
* into the project's vite config, then remove the generated standalone file.
* into the project's supported config file, then remove the generated
* standalone file.
*
* Returns true when the command was an init/migrate command (handled), false otherwise.
*/
Expand Down
33 changes: 33 additions & 0 deletions packages/cli/src/migration/__tests__/detector.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import fs from 'node:fs';
import { mkdtempSync } from 'node:fs';
import os from 'node:os';
import path from 'node:path';

import { afterEach, beforeEach, describe, expect, it } from 'vitest';

import { detectConfigs } from '../detector.js';

describe('detectConfigs', () => {
let tempDir: string;

beforeEach(() => {
tempDir = fs.realpathSync(mkdtempSync(path.join(os.tmpdir(), 'vite-config-detect-')));
});

afterEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true });
});

it('detects nuxt config files as supported config entries', () => {
fs.writeFileSync(path.join(tempDir, 'nuxt.config.ts'), '');

expect(detectConfigs(tempDir).viteConfig).toBe('nuxt.config.ts');
});

it('prefers vite config files over nuxt config files', () => {
fs.writeFileSync(path.join(tempDir, 'vite.config.ts'), '');
fs.writeFileSync(path.join(tempDir, 'nuxt.config.ts'), '');

expect(detectConfigs(tempDir).viteConfig).toBe('vite.config.ts');
});
});
20 changes: 5 additions & 15 deletions packages/cli/src/migration/detector.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import fs from 'node:fs';
import path from 'node:path';

import { findSupportedConfigFile } from '../utils/config-files.ts';

export interface ConfigFiles {
viteConfig?: string;
vitestConfig?: string;
Expand Down Expand Up @@ -44,22 +46,10 @@ export const PRETTIER_CONFIG_FILES = [
export function detectConfigs(projectPath: string): ConfigFiles {
const configs: ConfigFiles = {};

// Check for vite.config.*
// Check for supported Vite/Nuxt config entry files.
// https://vite.dev/config/
const viteConfigs = [
'vite.config.ts',
'vite.config.mts',
'vite.config.cts',
'vite.config.js',
'vite.config.mjs',
'vite.config.cjs',
];
for (const config of viteConfigs) {
if (fs.existsSync(path.join(projectPath, config))) {
configs.viteConfig = config;
break;
}
}
const supportedConfigFile = findSupportedConfigFile(projectPath);
configs.viteConfig = supportedConfigFile ? path.basename(supportedConfigFile) : undefined;

// Check for vitest.config.*
// https://vitest.dev/config/
Expand Down
6 changes: 2 additions & 4 deletions packages/cli/src/oxlint-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
PREFER_VITE_PLUS_IMPORTS_RULE_NAME,
VITE_PLUS_OXLINT_PLUGIN_NAME,
} from './oxlint-plugin-config.ts';
import viteConfigEntryBasenames from './vite-config-entry-basenames.json' with { type: 'json' };
import { VITE_CONFIG_ENTRY_BASENAMES } from './utils/config-files.ts';

// `declare module 'vitest…'` and `declare module '@vitest/browser…'` are
// intentionally preserved by `vp migrate` (see migration's import_rewriter and
Expand All @@ -34,14 +34,12 @@ function isVitestFamilyDeclareModuleSpecifier(specifier: string): boolean {
// `vite-config-entry-basenames.json` at compile time (import_rewriter.rs). The
// lint rule sees one file at a time, so it recognizes the standard basenames only
// (no migrate-resolved custom path). vitest/tsdown/@vitest are unaffected.
const VITE_CONFIG_FILE_BASENAMES = new Set(viteConfigEntryBasenames);

function isViteSpecifier(specifier: string): boolean {
return specifier === 'vite' || specifier.startsWith('vite/');
}

function isViteConfigFile(filename: string): boolean {
return VITE_CONFIG_FILE_BASENAMES.has(path.basename(filename));
return VITE_CONFIG_ENTRY_BASENAMES.has(path.basename(filename));
}

function rewriteVitePlusImportSpecifier(specifier: string): string | null {
Expand Down
64 changes: 21 additions & 43 deletions packages/cli/src/resolve-vite-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,57 +2,30 @@ import fs from 'node:fs';
import path from 'node:path';

import { withConfigMetadataResolution } from './define-config.ts';

// Mirrors Vite's own DEFAULT_CONFIG_FILES order so finders here pick the same
// file Vite loads when a directory contains more than one config (e.g. a
// `vite.config.js` next to a stray `vite.config.ts`). Readers evaluate via
// Vite's loader, so a different order would make read and write target
// different files.
const VITE_CONFIG_FILES = [
'vite.config.js',
'vite.config.mjs',
'vite.config.ts',
'vite.config.cjs',
'vite.config.mts',
'vite.config.cts',
];
import {
findSupportedConfigFile,
findSupportedConfigFileUp,
hasSupportedConfigFile,
} from './utils/config-files.ts';

/**
* Find a vite config file by walking up from `startDir` to `stopDir`.
* Find a supported 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;
return findSupportedConfigFileUp(startDir, stopDir);
}

/**
* Find a vite config file directly in `dir` (no walking up). Returns the
* absolute path of the first config file found, or undefined. Covers every
* supported extension (`.ts/.js/.mjs/.mts/.cjs/.cts`).
* Find a supported config file directly in `dir` (no walking up). Returns the
* absolute path of the first config file found, or undefined.
*/
export function findViteConfig(dir: string): string | undefined {
const filename = VITE_CONFIG_FILES.find((f) => fs.existsSync(path.join(dir, f)));
return filename ? path.join(dir, filename) : undefined;
return findSupportedConfigFile(dir);
}

export function hasViteConfig(dir: string): boolean {
return findViteConfig(dir) !== undefined;
return hasSupportedConfigFile(dir);
}

/**
Expand Down Expand Up @@ -93,20 +66,25 @@ export interface ResolveViteConfigOptions {
}

/**
* Resolve vite.config.ts and return the config object.
* Resolve the project's supported config file and return the config object.
*/
export async function resolveViteConfig(cwd: string, options?: ResolveViteConfigOptions) {
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?.traverseUp && !hasViteConfig(cwd)) {
const configFile = findViteConfig(cwd);
if (configFile) {
return resolveConfig({ root: cwd, configFile }, 'build');
}

if (options?.traverseUp) {
const workspaceRoot = findWorkspaceRoot(cwd);
if (workspaceRoot) {
const configFile = findViteConfigUp(path.dirname(cwd), workspaceRoot);
if (configFile) {
return resolveConfig({ root: cwd, configFile }, 'build');
const upConfigFile = findViteConfigUp(path.dirname(cwd), workspaceRoot);
if (upConfigFile) {
return resolveConfig({ root: cwd, configFile: upConfigFile }, 'build');
}
}
}
Expand Down
50 changes: 50 additions & 0 deletions packages/cli/src/utils/config-files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import fs from 'node:fs';
import path from 'node:path';

import configEntryFiles from '../vite-config-entry-basenames.json' with { type: 'json' };

export const VITE_CONFIG_ENTRY_FILES = configEntryFiles;
export const VITE_CONFIG_ENTRY_BASENAMES = new Set(VITE_CONFIG_ENTRY_FILES.map((file) => path.basename(file)));

function isWithinStopDir(dir: string, stopDir: string): boolean {
const relative = path.relative(stopDir, dir);
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
}

export function findSupportedConfigFile(dir: string): string | undefined {
for (const filename of VITE_CONFIG_ENTRY_FILES) {
const fullPath = path.join(dir, filename);
if (fs.existsSync(fullPath)) {
return fullPath;
}
}
return undefined;
}

export function findSupportedConfigFileUp(startDir: string, stopDir: string): string | undefined {
let dir = path.resolve(startDir);
const stop = path.resolve(stopDir);

while (true) {
const configFile = findSupportedConfigFile(dir);
if (configFile) {
return configFile;
}

if (dir === stop) {
break;
}

const parent = path.dirname(dir);
if (parent === dir || !isWithinStopDir(parent, stop)) {
break;
}
dir = parent;
}

return undefined;
}

export function hasSupportedConfigFile(dir: string): boolean {
return findSupportedConfigFile(dir) !== undefined;
}
6 changes: 6 additions & 0 deletions packages/cli/src/vite-config-entry-basenames.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
"vite.config.js",
"vite.config.mjs",
"vite.config.cjs",
"nuxt.config.ts",
"nuxt.config.mts",
"nuxt.config.cts",
"nuxt.config.js",
"nuxt.config.mjs",
"nuxt.config.cjs",
"vitest.config.ts",
"vitest.config.mts",
"vitest.config.cts",
Expand Down