diff --git a/apps/oxlint/src-js/js_config.ts b/apps/oxlint/src-js/js_config.ts index 527e2f53b94d3..d6d49e12838b3 100644 --- a/apps/oxlint/src-js/js_config.ts +++ b/apps/oxlint/src-js/js_config.ts @@ -105,14 +105,19 @@ export async function loadJsConfigs(paths: string[]): Promise { throw new Error(`Configuration file must have a default export that is an object.`); } - if (!isDefineConfig(config)) { - throw new Error( - `Configuration file must wrap its default export with defineConfig() from "oxlint".`, - ); + // Vite config files (e.g. `vite.config.ts`) are not Oxlint configs, + // so skip `defineConfig()` and `extends` validation. + // The `.lint` field extraction is handled on the Rust side. + if (!path.endsWith("/vite.config.ts")) { + if (!isDefineConfig(config)) { + throw new Error( + `Configuration file must wrap its default export with defineConfig() from "oxlint".`, + ); + } + + validateConfigExtends(config as object); } - validateConfigExtends(config as object); - return { path, config }; }), ); diff --git a/apps/oxlint/src/config_loader.rs b/apps/oxlint/src/config_loader.rs index b23e86e9e92be..98adef789bfee 100644 --- a/apps/oxlint/src/config_loader.rs +++ b/apps/oxlint/src/config_loader.rs @@ -12,7 +12,9 @@ use oxc_linter::{ }; use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet}; -use crate::{DEFAULT_JSONC_OXLINTRC_NAME, DEFAULT_OXLINTRC_NAME, DEFAULT_TS_OXLINTRC_NAME}; +use crate::{ + DEFAULT_JSONC_OXLINTRC_NAME, DEFAULT_OXLINTRC_NAME, DEFAULT_TS_OXLINTRC_NAME, VITE_CONFIG_NAME, +}; #[cfg(feature = "napi")] use crate::js_config; @@ -487,6 +489,12 @@ impl<'a> ConfigLoader<'a> { return Oxlintrc::from_file(&jsonc_path).map(Some); } + // Fallback: check for vite.config.ts with .lint field (lowest priority) + let vite_path = dir.join(VITE_CONFIG_NAME); + if vite_path.is_file() { + return self.load_root_js_config(&vite_path).map(Some); + } + Ok(None) } diff --git a/apps/oxlint/src/js_config.rs b/apps/oxlint/src/js_config.rs index af1f45c373c48..3b5bb9430ca35 100644 --- a/apps/oxlint/src/js_config.rs +++ b/apps/oxlint/src/js_config.rs @@ -5,6 +5,7 @@ use oxc_diagnostics::OxcDiagnostic; use oxc_linter::Oxlintrc; use crate::run::JsLoadJsConfigsCb; +use crate::{VITE_CONFIG_NAME, VITE_OXLINT_CONFIG_FIELD}; /// Callback type for loading JavaScript/TypeScript config files. pub type JsConfigLoaderCb = @@ -118,7 +119,27 @@ fn parse_js_config_response(json: &str) -> Result, Vec config, Err(err) => { errors.push( diff --git a/apps/oxlint/src/lib.rs b/apps/oxlint/src/lib.rs index 464dfd3d3c6ac..8f634a2690516 100644 --- a/apps/oxlint/src/lib.rs +++ b/apps/oxlint/src/lib.rs @@ -59,6 +59,9 @@ static GLOBAL: mimalloc_safe::MiMalloc = mimalloc_safe::MiMalloc; const DEFAULT_OXLINTRC_NAME: &str = ".oxlintrc.json"; const DEFAULT_JSONC_OXLINTRC_NAME: &str = ".oxlintrc.jsonc"; const DEFAULT_TS_OXLINTRC_NAME: &str = "oxlint.config.ts"; +/// Vite config file that may contain oxlint config under a `.lint` field. +const VITE_CONFIG_NAME: &str = "vite.config.ts"; +const VITE_OXLINT_CONFIG_FIELD: &str = "lint"; /// Return a JSON blob containing metadata for all available oxlint rules. /// diff --git a/apps/oxlint/src/lsp/server_linter.rs b/apps/oxlint/src/lsp/server_linter.rs index 0265f459a8ad8..0298190eda5a3 100644 --- a/apps/oxlint/src/lsp/server_linter.rs +++ b/apps/oxlint/src/lsp/server_linter.rs @@ -494,6 +494,7 @@ impl Tool for ServerLinter { "**/.oxlintrc.json".to_string(), "**/.oxlintrc.jsonc".to_string(), "**/oxlint.config.ts".to_string(), + "**/vite.config.ts".to_string(), ] } Some(v) => vec![v.to_string()], @@ -1090,10 +1091,11 @@ mod test_watchers { let patterns = Tester::new("fixtures/lsp/watchers/default", json!({})).get_watcher_patterns(); - assert_eq!(patterns.len(), 3); + assert_eq!(patterns.len(), 4); assert_eq!(patterns[0], "**/.oxlintrc.json".to_string()); assert_eq!(patterns[1], "**/.oxlintrc.jsonc".to_string()); assert_eq!(patterns[2], "**/oxlint.config.ts".to_string()); + assert_eq!(patterns[3], "**/vite.config.ts".to_string()); } #[test] @@ -1106,10 +1108,11 @@ mod test_watchers { ) .get_watcher_patterns(); - assert_eq!(patterns.len(), 3); + assert_eq!(patterns.len(), 4); assert_eq!(patterns[0], "**/.oxlintrc.json".to_string()); assert_eq!(patterns[1], "**/.oxlintrc.jsonc".to_string()); assert_eq!(patterns[2], "**/oxlint.config.ts".to_string()); + assert_eq!(patterns[3], "**/vite.config.ts".to_string()); } #[test] @@ -1131,12 +1134,13 @@ mod test_watchers { let patterns = Tester::new("fixtures/lsp/watchers/linter_extends", json!({})) .get_watcher_patterns(); - // The `.oxlintrc.json` extends `./lint.json` -> 4 watchers (json, jsonc, ts, lint.json) - assert_eq!(patterns.len(), 4); + // The `.oxlintrc.json` extends `./lint.json` -> 5 watchers (json, jsonc, ts, vite, lint.json) + assert_eq!(patterns.len(), 5); assert_eq!(patterns[0], "**/.oxlintrc.json".to_string()); assert_eq!(patterns[1], "**/.oxlintrc.jsonc".to_string()); assert_eq!(patterns[2], "**/oxlint.config.ts".to_string()); - assert_eq!(patterns[3], "lint.json".to_string()); + assert_eq!(patterns[3], "**/vite.config.ts".to_string()); + assert_eq!(patterns[4], "lint.json".to_string()); } #[test] @@ -1164,11 +1168,12 @@ mod test_watchers { ) .get_watcher_patterns(); - assert_eq!(patterns.len(), 4); + assert_eq!(patterns.len(), 5); assert_eq!(patterns[0], "**/.oxlintrc.json".to_string()); assert_eq!(patterns[1], "**/.oxlintrc.jsonc".to_string()); assert_eq!(patterns[2], "**/oxlint.config.ts".to_string()); - assert_eq!(patterns[3], "**/tsconfig*.json".to_string()); + assert_eq!(patterns[3], "**/vite.config.ts".to_string()); + assert_eq!(patterns[4], "**/tsconfig*.json".to_string()); } } @@ -1219,11 +1224,12 @@ mod test_watchers { "typeAware": true })); assert!(watch_patterns.is_some()); - assert_eq!(watch_patterns.as_ref().unwrap().len(), 4); + assert_eq!(watch_patterns.as_ref().unwrap().len(), 5); assert_eq!(watch_patterns.as_ref().unwrap()[0], "**/.oxlintrc.json".to_string()); assert_eq!(watch_patterns.as_ref().unwrap()[1], "**/.oxlintrc.jsonc".to_string()); assert_eq!(watch_patterns.as_ref().unwrap()[2], "**/oxlint.config.ts".to_string()); - assert_eq!(watch_patterns.as_ref().unwrap()[3], "**/tsconfig*.json".to_string()); + assert_eq!(watch_patterns.as_ref().unwrap()[3], "**/vite.config.ts".to_string()); + assert_eq!(watch_patterns.as_ref().unwrap()[4], "**/tsconfig*.json".to_string()); } } } diff --git a/apps/oxlint/test/fixtures/vite_config_basic/files/test.js b/apps/oxlint/test/fixtures/vite_config_basic/files/test.js new file mode 100644 index 0000000000000..d4e2f029dd793 --- /dev/null +++ b/apps/oxlint/test/fixtures/vite_config_basic/files/test.js @@ -0,0 +1,3 @@ +debugger; +if (x == 1) { +} diff --git a/apps/oxlint/test/fixtures/vite_config_basic/options.json b/apps/oxlint/test/fixtures/vite_config_basic/options.json new file mode 100644 index 0000000000000..c6d966f1b525b --- /dev/null +++ b/apps/oxlint/test/fixtures/vite_config_basic/options.json @@ -0,0 +1,3 @@ +{ + "singleThread": true +} diff --git a/apps/oxlint/test/fixtures/vite_config_basic/output.snap.md b/apps/oxlint/test/fixtures/vite_config_basic/output.snap.md new file mode 100644 index 0000000000000..46c5d64fcc56d --- /dev/null +++ b/apps/oxlint/test/fixtures/vite_config_basic/output.snap.md @@ -0,0 +1,29 @@ +# Exit code +1 + +# stdout +``` + x eslint(no-debugger): `debugger` statement is not allowed + ,-[files/test.js:1:1] + 1 | debugger; + : ^^^^^^^^^ + 2 | if (x == 1) { + `---- + help: Remove the debugger statement + + ! eslint(eqeqeq): Expected === and instead saw == + ,-[files/test.js:2:7] + 1 | debugger; + 2 | if (x == 1) { + : ^^ + 3 | } + `---- + help: Prefer === operator + +Found 1 warning and 1 error. +Finished in Xms on 1 file with 94 rules using X threads. +``` + +# stderr +``` +``` diff --git a/apps/oxlint/test/fixtures/vite_config_basic/vite.config.ts b/apps/oxlint/test/fixtures/vite_config_basic/vite.config.ts new file mode 100644 index 0000000000000..bbcc165cfb347 --- /dev/null +++ b/apps/oxlint/test/fixtures/vite_config_basic/vite.config.ts @@ -0,0 +1,8 @@ +export default { + lint: { + rules: { + "no-debugger": "error", + eqeqeq: "warn", + }, + }, +}; diff --git a/apps/oxlint/test/fixtures/vite_config_no_lint_field/files/test.js b/apps/oxlint/test/fixtures/vite_config_no_lint_field/files/test.js new file mode 100644 index 0000000000000..eab74692130a6 --- /dev/null +++ b/apps/oxlint/test/fixtures/vite_config_no_lint_field/files/test.js @@ -0,0 +1 @@ +debugger; diff --git a/apps/oxlint/test/fixtures/vite_config_no_lint_field/options.json b/apps/oxlint/test/fixtures/vite_config_no_lint_field/options.json new file mode 100644 index 0000000000000..c6d966f1b525b --- /dev/null +++ b/apps/oxlint/test/fixtures/vite_config_no_lint_field/options.json @@ -0,0 +1,3 @@ +{ + "singleThread": true +} diff --git a/apps/oxlint/test/fixtures/vite_config_no_lint_field/output.snap.md b/apps/oxlint/test/fixtures/vite_config_no_lint_field/output.snap.md new file mode 100644 index 0000000000000..80e33f5608bfe --- /dev/null +++ b/apps/oxlint/test/fixtures/vite_config_no_lint_field/output.snap.md @@ -0,0 +1,13 @@ +# Exit code +1 + +# stdout +``` +Failed to parse oxlint configuration file. + + x Expected a `lint` field in the default export of /vite.config.ts +``` + +# stderr +``` +``` diff --git a/apps/oxlint/test/fixtures/vite_config_no_lint_field/vite.config.ts b/apps/oxlint/test/fixtures/vite_config_no_lint_field/vite.config.ts new file mode 100644 index 0000000000000..4ed8f62dd39b0 --- /dev/null +++ b/apps/oxlint/test/fixtures/vite_config_no_lint_field/vite.config.ts @@ -0,0 +1,3 @@ +export default { + plugins: [], +}; diff --git a/apps/oxlint/test/fixtures/vite_config_priority/files/test.js b/apps/oxlint/test/fixtures/vite_config_priority/files/test.js new file mode 100644 index 0000000000000..d4e2f029dd793 --- /dev/null +++ b/apps/oxlint/test/fixtures/vite_config_priority/files/test.js @@ -0,0 +1,3 @@ +debugger; +if (x == 1) { +} diff --git a/apps/oxlint/test/fixtures/vite_config_priority/options.json b/apps/oxlint/test/fixtures/vite_config_priority/options.json new file mode 100644 index 0000000000000..c6d966f1b525b --- /dev/null +++ b/apps/oxlint/test/fixtures/vite_config_priority/options.json @@ -0,0 +1,3 @@ +{ + "singleThread": true +} diff --git a/apps/oxlint/test/fixtures/vite_config_priority/output.snap.md b/apps/oxlint/test/fixtures/vite_config_priority/output.snap.md new file mode 100644 index 0000000000000..04520bd567524 --- /dev/null +++ b/apps/oxlint/test/fixtures/vite_config_priority/output.snap.md @@ -0,0 +1,20 @@ +# Exit code +1 + +# stdout +``` + x eslint(no-debugger): `debugger` statement is not allowed + ,-[files/test.js:1:1] + 1 | debugger; + : ^^^^^^^^^ + 2 | if (x == 1) { + `---- + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in Xms on 1 file with 93 rules using X threads. +``` + +# stderr +``` +``` diff --git a/apps/oxlint/test/fixtures/vite_config_priority/oxlint.config.ts b/apps/oxlint/test/fixtures/vite_config_priority/oxlint.config.ts new file mode 100644 index 0000000000000..d6cfc712973b5 --- /dev/null +++ b/apps/oxlint/test/fixtures/vite_config_priority/oxlint.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "#oxlint"; + +export default defineConfig({ + rules: { + "no-debugger": "error", + }, +}); diff --git a/apps/oxlint/test/fixtures/vite_config_priority/vite.config.ts b/apps/oxlint/test/fixtures/vite_config_priority/vite.config.ts new file mode 100644 index 0000000000000..465d63c1233e9 --- /dev/null +++ b/apps/oxlint/test/fixtures/vite_config_priority/vite.config.ts @@ -0,0 +1,7 @@ +export default { + lint: { + rules: { + eqeqeq: "warn", + }, + }, +}; diff --git a/apps/oxlint/test/lsp/init/init.test.ts b/apps/oxlint/test/lsp/init/init.test.ts index e4379c5dfab00..33372461f95ac 100644 --- a/apps/oxlint/test/lsp/init/init.test.ts +++ b/apps/oxlint/test/lsp/init/init.test.ts @@ -37,8 +37,14 @@ describe("LSP initialization", () => { }); it.each([ - [undefined, ["**/.oxlintrc.json", "**/.oxlintrc.jsonc", "**/oxlint.config.ts"]], - [{ configPath: "" }, ["**/.oxlintrc.json", "**/.oxlintrc.jsonc", "**/oxlint.config.ts"]], + [ + undefined, + ["**/.oxlintrc.json", "**/.oxlintrc.jsonc", "**/oxlint.config.ts", "**/vite.config.ts"], + ], + [ + { configPath: "" }, + ["**/.oxlintrc.json", "**/.oxlintrc.jsonc", "**/oxlint.config.ts", "**/vite.config.ts"], + ], [{ configPath: "./custom-config.json" }, ["./custom-config.json"]], ])( "should send correct dynamic watch pattern registration for config: %s",