diff --git a/packages/cli/src/__tests__/oxlint-plugin.spec.ts b/packages/cli/src/__tests__/oxlint-plugin.spec.ts index 9297027348..8d579662cc 100644 --- a/packages/cli/src/__tests__/oxlint-plugin.spec.ts +++ b/packages/cli/src/__tests__/oxlint-plugin.spec.ts @@ -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, diff --git a/packages/cli/src/__tests__/resolve-vite-config.spec.ts b/packages/cli/src/__tests__/resolve-vite-config.spec.ts index e73a5187ce..e5139b7374 100644 --- a/packages/cli/src/__tests__/resolve-vite-config.spec.ts +++ b/packages/cli/src/__tests__/resolve-vite-config.spec.ts @@ -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; @@ -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')); + }); }); diff --git a/packages/cli/src/create/org-resolve.ts b/packages/cli/src/create/org-resolve.ts index 3fd50d1d2b..5c6c0086bc 100644 --- a/packages/cli/src/create/org-resolve.ts +++ b/packages/cli/src/create/org-resolve.ts @@ -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`, diff --git a/packages/cli/src/init-config.ts b/packages/cli/src/init-config.ts index 310ef73b71..95288f29ee 100644 --- a/packages/cli/src/init-config.ts +++ b/packages/cli/src/init-config.ts @@ -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'; @@ -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'; @@ -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 { @@ -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. */ diff --git a/packages/cli/src/migration/__tests__/detector.spec.ts b/packages/cli/src/migration/__tests__/detector.spec.ts new file mode 100644 index 0000000000..71ae7ed14e --- /dev/null +++ b/packages/cli/src/migration/__tests__/detector.spec.ts @@ -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'); + }); +}); diff --git a/packages/cli/src/migration/detector.ts b/packages/cli/src/migration/detector.ts index 4614d03bbd..a4fbfc8a5e 100644 --- a/packages/cli/src/migration/detector.ts +++ b/packages/cli/src/migration/detector.ts @@ -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; @@ -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/ diff --git a/packages/cli/src/oxlint-plugin.ts b/packages/cli/src/oxlint-plugin.ts index 135f449209..d58e14123c 100644 --- a/packages/cli/src/oxlint-plugin.ts +++ b/packages/cli/src/oxlint-plugin.ts @@ -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 @@ -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 { diff --git a/packages/cli/src/resolve-vite-config.ts b/packages/cli/src/resolve-vite-config.ts index 52d24619ce..27a08c4e25 100644 --- a/packages/cli/src/resolve-vite-config.ts +++ b/packages/cli/src/resolve-vite-config.ts @@ -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); } /** @@ -93,7 +66,7 @@ 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'); @@ -101,12 +74,17 @@ export async function resolveViteConfig(cwd: string, options?: ResolveViteConfig // 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'); } } } diff --git a/packages/cli/src/utils/config-files.ts b/packages/cli/src/utils/config-files.ts new file mode 100644 index 0000000000..27afe7369a --- /dev/null +++ b/packages/cli/src/utils/config-files.ts @@ -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; +} diff --git a/packages/cli/src/vite-config-entry-basenames.json b/packages/cli/src/vite-config-entry-basenames.json index 0dbfd0a00b..dd237e0fa4 100644 --- a/packages/cli/src/vite-config-entry-basenames.json +++ b/packages/cli/src/vite-config-entry-basenames.json @@ -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",