diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index aa04051c9c..fa3f476f8b 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -42,6 +42,7 @@ const guideSidebar = [ items: [ { text: 'Lint', link: '/guide/lint' }, { text: 'Format', link: '/guide/fmt' }, + { text: 'Nested Configuration', link: '/guide/nested-config' }, ], }, { text: 'Test', link: '/guide/test' }, diff --git a/docs/guide/fmt.md b/docs/guide/fmt.md index bb9a59d02a..eb112e1584 100644 --- a/docs/guide/fmt.md +++ b/docs/guide/fmt.md @@ -30,6 +30,8 @@ For editors, point the formatter config path at `./vite.config.ts` so format-on- For the upstream formatter behavior and configuration reference, see the [Oxfmt docs](https://oxc.rs/docs/guide/usage/formatter.html). +In monorepos, `vp fmt` walks up from the current working directory and uses the first `vite.config.ts` it finds — unlike `vp lint`, which is cwd-only. See [Nested Configuration](/guide/nested-config) for the full resolution rules. + ```ts import { defineConfig } from 'vite-plus'; diff --git a/docs/guide/lint.md b/docs/guide/lint.md index ffd29ecd36..73518217f4 100644 --- a/docs/guide/lint.md +++ b/docs/guide/lint.md @@ -22,6 +22,8 @@ Put lint configuration directly in the `lint` block in `vite.config.ts` so all y For the upstream rule set, options, and compatibility details, see the [Oxlint docs](https://oxc.rs/docs/guide/usage/linter.html). +In monorepos, `vp lint` uses `/vite.config.ts` if it exists, and falls back to Oxlint's built-in defaults otherwise — it does not walk up. See [Nested Configuration](/guide/nested-config) for details. + ```ts import { defineConfig } from 'vite-plus'; diff --git a/docs/guide/nested-config.md b/docs/guide/nested-config.md new file mode 100644 index 0000000000..8b42ae499a --- /dev/null +++ b/docs/guide/nested-config.md @@ -0,0 +1,112 @@ +# Nested Configuration + +Vite+ supports multiple `vite.config.ts` files in a repository, so packages in a monorepo can have their own lint and format settings while sharing a baseline. + +## How `vp lint` and `vp fmt` pick a config + +Config resolution is driven by the current working directory (cwd): + +- **`vp lint`** uses `/vite.config.ts`. If that file is missing, built-in defaults apply — `vp lint` does **not** walk up the directory tree looking for a parent `vite.config.ts`. +- **`vp fmt`** walks up from cwd and uses the first `vite.config.ts` it finds. If none is found, built-in defaults apply. + +In both cases, the selected config applies to every path in the run — there is no per-file resolution, and configs are never merged. + +If your monorepo needs different settings per package, run `vp lint` / `vp fmt` from each package directory (for example, via `vp run -r lint`), or pin a specific config with `-c`. + +If you only want to exclude files or folders from an otherwise-shared config, use [`lint.ignorePatterns`](/config/lint) or [`fmt.ignorePatterns`](/config/fmt) instead. + +::: tip Breaking change since the April 2026 release + +Earlier versions of Vite+ pinned every `vp lint` / `vp fmt` invocation to the workspace-root `vite.config.ts`, regardless of cwd. Vite+ now lets cwd-based resolution select the config, so running from a sub-package picks up that sub-package's own `vite.config.ts`. See [#1378](https://github.com/voidzero-dev/vite-plus/pull/1378) for the migration notes. + +::: + +## Example + +Given this layout: + +``` +my-project/ +├── vite.config.ts +├── src/ +│ └── index.ts +├── package1/ +│ ├── vite.config.ts +│ └── src/index.ts +└── package2/ + └── src/index.ts +``` + +`vp lint`: + +| cwd | config used | +| ---------------------------- | ----------------------------------- | +| `my-project/` | `my-project/vite.config.ts` | +| `my-project/package1/` | `my-project/package1/vite.config.ts`| +| `my-project/package1/src/` | built-in defaults (no walk-up) | +| `my-project/package2/` | built-in defaults (no walk-up) | + +`vp fmt`: + +| cwd | config used | +| ---------------------------- | ------------------------------------ | +| `my-project/` | `my-project/vite.config.ts` | +| `my-project/package1/` | `my-project/package1/vite.config.ts` | +| `my-project/package1/src/` | `my-project/package1/vite.config.ts` | +| `my-project/package2/` | `my-project/vite.config.ts` | + +## Pinning a config with `-c` + +`-c` / `--config` bypasses cwd-based resolution. The specified file is used for every path in the run: + +```bash +vp lint -c vite.config.ts +vp fmt --check -c vite.config.ts +``` + +This also works when you need a one-off config, for example a permissive CI variant. + +## `--disable-nested-config` (lint only) + +`vp lint` accepts `--disable-nested-config` to stop any auto-loading of nested lint configuration files that may exist in the tree: + +```bash +vp lint --disable-nested-config +vp check --disable-nested-config +``` + +This flag has no effect on `vite.config.ts` resolution, which is already cwd-only for `vp lint`. `vp fmt` has no equivalent flag; use `-c` to pin a single format config. + +## Monorepo pattern: share a base config + +Configs are never merged automatically — the selected config fully replaces any other. To share a baseline, import the parent config and spread it: + +```ts [my-project/vite.config.ts] +import { defineConfig } from 'vite-plus'; + +export default defineConfig({ + lint: { + rules: { + 'no-debugger': 'error', + }, + }, +}); +``` + +```ts [my-project/package1/vite.config.ts] +import { defineConfig } from 'vite-plus'; +import baseConfig from '../vite.config.ts'; + +export default defineConfig({ + ...baseConfig, + lint: { + ...baseConfig.lint, + rules: { + ...baseConfig.lint?.rules, + 'no-console': 'off', + }, + }, +}); +``` + +This keeps the shared baseline in one place and makes package configs small and focused. diff --git a/package.json b/package.json index 98018d0aad..7b990aec4c 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "bootstrap-cli:ci": "pnpm install-global-cli", "install-global-cli": "tool install-global-cli", "tsgo": "tsgo -b tsconfig.json", - "lint": "vp lint --type-aware --type-check --threads 4", + "check": "vp check --disable-nested-config", "test": "vp test run && pnpm -r snap-test", "fmt": "vp fmt", "test:unit": "vp test run", @@ -37,7 +37,7 @@ "zod": "catalog:" }, "lint-staged": { - "*.@(js|ts|tsx|md|yaml|yml)": "vp check --fix", + "*.@(js|ts|tsx|md|yaml|yml)": "vp check --fix --disable-nested-config", "*.rs": "cargo fmt --" }, "engines": { diff --git a/packages/cli/binding/src/check/mod.rs b/packages/cli/binding/src/check/mod.rs index 62efc8348d..071d2bccc1 100644 --- a/packages/cli/binding/src/check/mod.rs +++ b/packages/cli/binding/src/check/mod.rs @@ -40,7 +40,6 @@ pub(crate) async fn execute_check( let has_paths = !paths.is_empty(); let mut fmt_fix_started: Option = None; let mut deferred_lint_pass: Option<(String, String)> = None; - let resolved_vite_config = resolver.resolve_universal_vite_config().await?; if !no_fmt { let mut args = if fix { vec![] } else { vec!["--check".to_string()] }; @@ -55,7 +54,6 @@ pub(crate) async fn execute_check( let captured = resolve_and_capture_output( resolver, SynthesizableSubcommand::Fmt { args }, - Some(&resolved_vite_config), envs, cwd, cwd_arc, @@ -116,6 +114,7 @@ pub(crate) async fn execute_check( } if !no_lint { + let resolved_vite_config = resolver.resolve_universal_vite_config().await?; let lint_message_kind = LintMessageKind::from_lint_config(resolved_vite_config.lint.as_ref()); let mut args = Vec::new(); @@ -137,7 +136,6 @@ pub(crate) async fn execute_check( let captured = resolve_and_capture_output( resolver, SynthesizableSubcommand::Lint { args }, - Some(&resolved_vite_config), envs, cwd, cwd_arc, @@ -204,7 +202,6 @@ pub(crate) async fn execute_check( let captured = resolve_and_capture_output( resolver, SynthesizableSubcommand::Fmt { args }, - Some(&resolved_vite_config), envs, cwd, cwd_arc, diff --git a/packages/cli/binding/src/cli/execution.rs b/packages/cli/binding/src/cli/execution.rs index 887db64efc..055a635a82 100644 --- a/packages/cli/binding/src/cli/execution.rs +++ b/packages/cli/binding/src/cli/execution.rs @@ -7,22 +7,19 @@ use vite_task::ExitStatus; use super::{ resolver::SubcommandResolver, - types::{CapturedCommandOutput, ResolvedUniversalViteConfig, SynthesizableSubcommand}, + types::{CapturedCommandOutput, SynthesizableSubcommand}, }; /// Resolve a subcommand into a prepared `tokio::process::Command`. async fn resolve_and_build_command( resolver: &SubcommandResolver, subcommand: SynthesizableSubcommand, - resolved_vite_config: Option<&ResolvedUniversalViteConfig>, envs: &Arc, Arc>>, cwd: &AbsolutePathBuf, cwd_arc: &Arc, ) -> Result { - let resolved = resolver - .resolve(subcommand, resolved_vite_config, envs, cwd_arc) - .await - .map_err(|e| Error::Anyhow(e))?; + let resolved = + resolver.resolve(subcommand, envs, cwd_arc).await.map_err(|e| Error::Anyhow(e))?; // Resolve the program path using `which` to handle Windows .cmd/.bat files (PATHEXT) let program_path = { @@ -52,14 +49,11 @@ async fn resolve_and_build_command( pub(super) async fn resolve_and_execute( resolver: &SubcommandResolver, subcommand: SynthesizableSubcommand, - resolved_vite_config: Option<&ResolvedUniversalViteConfig>, envs: &Arc, Arc>>, cwd: &AbsolutePathBuf, cwd_arc: &Arc, ) -> Result { - let mut cmd = - resolve_and_build_command(resolver, subcommand, resolved_vite_config, envs, cwd, cwd_arc) - .await?; + let mut cmd = resolve_and_build_command(resolver, subcommand, envs, cwd, cwd_arc).await?; let mut child = cmd.spawn().map_err(|e| Error::Anyhow(e.into()))?; let status = child.wait().await.map_err(|e| Error::Anyhow(e.into()))?; Ok(ExitStatus(status.code().unwrap_or(1) as u8)) @@ -75,16 +69,13 @@ pub(super) enum FilterStream { pub(super) async fn resolve_and_execute_with_filter( resolver: &SubcommandResolver, subcommand: SynthesizableSubcommand, - resolved_vite_config: Option<&ResolvedUniversalViteConfig>, envs: &Arc, Arc>>, cwd: &AbsolutePathBuf, cwd_arc: &Arc, stream: FilterStream, filter: impl Fn(&str) -> Cow<'_, str>, ) -> Result { - let mut cmd = - resolve_and_build_command(resolver, subcommand, resolved_vite_config, envs, cwd, cwd_arc) - .await?; + let mut cmd = resolve_and_build_command(resolver, subcommand, envs, cwd, cwd_arc).await?; match stream { FilterStream::Stdout => cmd.stdout(Stdio::piped()), FilterStream::Stderr => cmd.stderr(Stdio::piped()), @@ -111,15 +102,12 @@ pub(super) async fn resolve_and_execute_with_filter( pub(crate) async fn resolve_and_capture_output( resolver: &SubcommandResolver, subcommand: SynthesizableSubcommand, - resolved_vite_config: Option<&ResolvedUniversalViteConfig>, envs: &Arc, Arc>>, cwd: &AbsolutePathBuf, cwd_arc: &Arc, force_color_if_terminal: bool, ) -> Result { - let mut cmd = - resolve_and_build_command(resolver, subcommand, resolved_vite_config, envs, cwd, cwd_arc) - .await?; + let mut cmd = resolve_and_build_command(resolver, subcommand, envs, cwd, cwd_arc).await?; cmd.stdout(Stdio::piped()); cmd.stderr(Stdio::piped()); if force_color_if_terminal && std::io::stdout().is_terminal() { diff --git a/packages/cli/binding/src/cli/handler.rs b/packages/cli/binding/src/cli/handler.rs index 1b7f17ba80..cb953d3b3c 100644 --- a/packages/cli/binding/src/cli/handler.rs +++ b/packages/cli/binding/src/cli/handler.rs @@ -71,8 +71,7 @@ impl CommandHandler for VitePlusCommandHandler { ))) } CLIArgs::Synthesizable(subcmd) => { - let resolved = - self.resolver.resolve(subcmd, None, &command.envs, &command.cwd).await?; + let resolved = self.resolver.resolve(subcmd, &command.envs, &command.cwd).await?; Ok(HandledCommand::Synthesized(resolved.into_synthetic_plan_request())) } CLIArgs::ViteTask(cmd) => Ok(HandledCommand::ViteTaskCommand(cmd)), diff --git a/packages/cli/binding/src/cli/mod.rs b/packages/cli/binding/src/cli/mod.rs index c6f91fa404..9f18a6368f 100644 --- a/packages/cli/binding/src/cli/mod.rs +++ b/packages/cli/binding/src/cli/mod.rs @@ -82,7 +82,6 @@ async fn execute_direct_subcommand( resolve_and_execute_with_filter( &resolver, other, - None, &envs, cwd, &cwd_arc, @@ -94,7 +93,6 @@ async fn execute_direct_subcommand( resolve_and_execute_with_filter( &resolver, other, - None, &envs, cwd, &cwd_arc, @@ -103,7 +101,7 @@ async fn execute_direct_subcommand( ) .await? } else { - resolve_and_execute(&resolver, other, None, &envs, cwd, &cwd_arc).await? + resolve_and_execute(&resolver, other, &envs, cwd, &cwd_arc).await? } } }; diff --git a/packages/cli/binding/src/cli/resolver.rs b/packages/cli/binding/src/cli/resolver.rs index 043f5c8e82..3acd88c50a 100644 --- a/packages/cli/binding/src/cli/resolver.rs +++ b/packages/cli/binding/src/cli/resolver.rs @@ -65,32 +65,17 @@ impl SubcommandResolver { pub(super) async fn resolve( &self, subcommand: SynthesizableSubcommand, - resolved_vite_config: Option<&ResolvedUniversalViteConfig>, envs: &Arc, Arc>>, cwd: &Arc, ) -> anyhow::Result { match subcommand { - SynthesizableSubcommand::Lint { mut args } => { + SynthesizableSubcommand::Lint { args } => { let cli_options = self.cli_options()?; let resolved = (cli_options.lint)().await?; let js_path = resolved.bin_path; let js_path_str = js_path .to_str() .ok_or_else(|| anyhow::anyhow!("lint JS path is not valid UTF-8"))?; - let owned_resolved_vite_config; - let resolved_vite_config = if let Some(config) = resolved_vite_config { - config - } else { - owned_resolved_vite_config = self.resolve_universal_vite_config().await?; - &owned_resolved_vite_config - }; - - if let (Some(_), Some(config_file)) = - (&resolved_vite_config.lint, &resolved_vite_config.config_file) - { - args.insert(0, "-c".to_string()); - args.insert(1, config_file.clone()); - } Ok(ResolvedSubcommand { program: Arc::from(OsStr::new("node")), @@ -106,27 +91,13 @@ impl SubcommandResolver { envs: merge_resolved_envs_with_version(envs, resolved.envs), }) } - SynthesizableSubcommand::Fmt { mut args } => { + SynthesizableSubcommand::Fmt { args } => { let cli_options = self.cli_options()?; let resolved = (cli_options.fmt)().await?; let js_path = resolved.bin_path; let js_path_str = js_path .to_str() .ok_or_else(|| anyhow::anyhow!("fmt JS path is not valid UTF-8"))?; - let owned_resolved_vite_config; - let resolved_vite_config = if let Some(config) = resolved_vite_config { - config - } else { - owned_resolved_vite_config = self.resolve_universal_vite_config().await?; - &owned_resolved_vite_config - }; - - if let (Some(_), Some(config_file)) = - (&resolved_vite_config.fmt, &resolved_vite_config.config_file) - { - args.insert(0, "-c".to_string()); - args.insert(1, config_file.clone()); - } Ok(ResolvedSubcommand { program: Arc::from(OsStr::new("node")), diff --git a/packages/cli/snap-tests/docs-nested-config/package.json b/packages/cli/snap-tests/docs-nested-config/package.json new file mode 100644 index 0000000000..a534beb58c --- /dev/null +++ b/packages/cli/snap-tests/docs-nested-config/package.json @@ -0,0 +1,7 @@ +{ + "name": "docs-nested-config-test", + "version": "0.0.0", + "private": true, + "type": "module", + "packageManager": "pnpm@10.16.1" +} diff --git a/packages/cli/snap-tests/docs-nested-config/packages/pkg-a/package.json b/packages/cli/snap-tests/docs-nested-config/packages/pkg-a/package.json new file mode 100644 index 0000000000..14d6f2e880 --- /dev/null +++ b/packages/cli/snap-tests/docs-nested-config/packages/pkg-a/package.json @@ -0,0 +1,5 @@ +{ + "name": "pkg-a", + "version": "0.0.0", + "private": true +} diff --git a/packages/cli/snap-tests/docs-nested-config/packages/pkg-a/src/index.js b/packages/cli/snap-tests/docs-nested-config/packages/pkg-a/src/index.js new file mode 100644 index 0000000000..8b63cec403 --- /dev/null +++ b/packages/cli/snap-tests/docs-nested-config/packages/pkg-a/src/index.js @@ -0,0 +1,6 @@ +function pkgA() { + debugger; + return "hello from pkg-a"; +} + +export { pkgA }; diff --git a/packages/cli/snap-tests/docs-nested-config/packages/pkg-a/vite.config.ts b/packages/cli/snap-tests/docs-nested-config/packages/pkg-a/vite.config.ts new file mode 100644 index 0000000000..059c840d83 --- /dev/null +++ b/packages/cli/snap-tests/docs-nested-config/packages/pkg-a/vite.config.ts @@ -0,0 +1,12 @@ +// Nested config in pkg-a: relaxes the root's strict rules. +// cwd walk-up from pkg-a/ should pick this file, not the root. +export default { + lint: { + rules: { + 'no-debugger': 'off', + }, + }, + fmt: { + singleQuote: false, + }, +}; diff --git a/packages/cli/snap-tests/docs-nested-config/packages/pkg-b/package.json b/packages/cli/snap-tests/docs-nested-config/packages/pkg-b/package.json new file mode 100644 index 0000000000..b225a0d50c --- /dev/null +++ b/packages/cli/snap-tests/docs-nested-config/packages/pkg-b/package.json @@ -0,0 +1,5 @@ +{ + "name": "pkg-b", + "version": "0.0.0", + "private": true +} diff --git a/packages/cli/snap-tests/docs-nested-config/packages/pkg-b/src/index.js b/packages/cli/snap-tests/docs-nested-config/packages/pkg-b/src/index.js new file mode 100644 index 0000000000..55c4412ff8 --- /dev/null +++ b/packages/cli/snap-tests/docs-nested-config/packages/pkg-b/src/index.js @@ -0,0 +1,6 @@ +function pkgB() { + debugger; + return "hello from pkg-b"; +} + +export { pkgB }; diff --git a/packages/cli/snap-tests/docs-nested-config/pnpm-workspace.yaml b/packages/cli/snap-tests/docs-nested-config/pnpm-workspace.yaml new file mode 100644 index 0000000000..18ec407efc --- /dev/null +++ b/packages/cli/snap-tests/docs-nested-config/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - 'packages/*' diff --git a/packages/cli/snap-tests/docs-nested-config/snap.txt b/packages/cli/snap-tests/docs-nested-config/snap.txt new file mode 100644 index 0000000000..5f985344c5 --- /dev/null +++ b/packages/cli/snap-tests/docs-nested-config/snap.txt @@ -0,0 +1,113 @@ +> # Pins each claim in docs/guide/nested-config.md. Fixture layout: +> # root vite.config.ts -> no-debugger:error, singleQuote:true +> # root vite.config.permissive.ts -> no-debugger:off, singleQuote:false +> # packages/pkg-a/vite.config.ts -> no-debugger:off, singleQuote:false +> # packages/pkg-b/ -> no local vite.config.ts +> # Both pkg-a/src/index.js and pkg-b/src/index.js contain `debugger;` and double-quoted strings. +> # Claim 1: from a cwd that contains a vite.config.ts, both lint and fmt use it. +> # pkg-a's own config wins: no-debugger:off and singleQuote:false -> lint and fmt pass. +> cd packages/pkg-a && vp lint +Found 0 warnings and 0 errors. +Finished in ms on 2 files with rules using threads. + +> cd packages/pkg-a && vp fmt --check src +Checking formatting... +All matched files use the correct format. +Finished in ms on 1 files using threads. + +> # Claim 2a: `vp lint` does NOT walk up. pkg-b has no local vite.config.ts, +> # so Oxlint's built-in defaults apply -> no-debugger fires as a warning (exit 0), +> # not as the root's `error`. Root's vite.config.ts is NOT consulted. +> cd packages/pkg-b && vp lint + + ⚠ eslint(no-debugger): `debugger` statement is not allowed + ╭─[src/index.js:2:3] + 1 │ function pkgB() { + 2 │ debugger; + · ───────── + 3 │ return "hello from pkg-b"; + ╰──── + help: Remove the debugger statement + +Found 1 warning and 0 errors. +Finished in ms on 1 file with rules using threads. + +> # Claim 2b: `vp fmt` DOES walk up. From pkg-b (no local config) it reaches the root, +> # which sets singleQuote:true -> double-quoted strings fail (exit 1). +[1]> cd packages/pkg-b && vp fmt --check src +Checking formatting... +src/index.js (ms) + +Format issues found in above 1 files. Run without `--check` to fix. +Finished in ms on 1 files using threads. + +> # Claim 2c: `vp lint` does not walk up even when an ancestor config exists. +> # From pkg-a/src (pkg-a has a vite.config.ts one level up) oxlint defaults still apply, +> # proving lint resolution does not traverse parent directories. +> cd packages/pkg-a/src && vp lint + + ⚠ eslint(no-debugger): `debugger` statement is not allowed + ╭─[index.js:2:3] + 1 │ function pkgA() { + 2 │ debugger; + · ───────── + 3 │ return "hello from pkg-a"; + ╰──── + help: Remove the debugger statement + +Found 1 warning and 0 errors. +Finished in ms on 1 file with rules using threads. + +> # Claim 2d: `vp fmt` walks up several levels when needed. +> # From pkg-a/src (no config) fmt finds pkg-a/vite.config.ts -> singleQuote:false -> passes. +> cd packages/pkg-a/src && vp fmt --check . +Checking formatting... +All matched files use the correct format. +Finished in ms on 1 files using threads. + +> # Claim 3: running `vp lint` / `vp fmt --check` directly at the workspace root +> # applies the root config to every file, including files in pkg-a that have +> # their own vite.config.ts (configs are not merged and not resolved per-file). +> # `--threads=1` is only used to keep oxlint's multi-error output order stable +> # across snapshot runs; it does not affect resolution. +[1]> vp lint --threads=1 + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[packages/pkg-b/src/index.js:2:3] + 1 │ function pkgB() { + 2 │ debugger; + · ───────── + 3 │ return "hello from pkg-b"; + ╰──── + help: Remove the debugger statement + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[packages/pkg-a/src/index.js:2:3] + 1 │ function pkgA() { + 2 │ debugger; + · ───────── + 3 │ return "hello from pkg-a"; + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 2 errors. +Finished in ms on 5 files with rules using threads. + +[1]> vp fmt --check +Checking formatting... +packages/pkg-a/src/index.js (ms) +packages/pkg-b/src/index.js (ms) + +Format issues found in above 2 files. Run without `--check` to fix. +Finished in ms on 10 files using threads. + +> # Claim 4: `-c ` bypasses cwd-based resolution. The permissive config applies +> # to every targeted file, so both debugger statements and double-quoted strings pass. +> vp lint -c vite.config.permissive.ts +Found 0 warnings and 0 errors. +Finished in ms on 5 files with rules using threads. + +> vp fmt -c vite.config.permissive.ts --check packages/pkg-a/src packages/pkg-b/src +Checking formatting... +All matched files use the correct format. +Finished in ms on 2 files using threads. diff --git a/packages/cli/snap-tests/docs-nested-config/steps.json b/packages/cli/snap-tests/docs-nested-config/steps.json new file mode 100644 index 0000000000..87b5d0aeb2 --- /dev/null +++ b/packages/cli/snap-tests/docs-nested-config/steps.json @@ -0,0 +1,40 @@ +{ + "ignoredPlatforms": ["win32"], + "commands": [ + "# Pins each claim in docs/guide/nested-config.md. Fixture layout:", + "# root vite.config.ts -> no-debugger:error, singleQuote:true", + "# root vite.config.permissive.ts -> no-debugger:off, singleQuote:false", + "# packages/pkg-a/vite.config.ts -> no-debugger:off, singleQuote:false", + "# packages/pkg-b/ -> no local vite.config.ts", + "# Both pkg-a/src/index.js and pkg-b/src/index.js contain `debugger;` and double-quoted strings.", + "# Claim 1: from a cwd that contains a vite.config.ts, both lint and fmt use it.", + "# pkg-a's own config wins: no-debugger:off and singleQuote:false -> lint and fmt pass.", + "cd packages/pkg-a && vp lint", + "cd packages/pkg-a && vp fmt --check src", + "# Claim 2a: `vp lint` does NOT walk up. pkg-b has no local vite.config.ts,", + "# so Oxlint's built-in defaults apply -> no-debugger fires as a warning (exit 0),", + "# not as the root's `error`. Root's vite.config.ts is NOT consulted.", + "cd packages/pkg-b && vp lint", + "# Claim 2b: `vp fmt` DOES walk up. From pkg-b (no local config) it reaches the root,", + "# which sets singleQuote:true -> double-quoted strings fail (exit 1).", + "cd packages/pkg-b && vp fmt --check src", + "# Claim 2c: `vp lint` does not walk up even when an ancestor config exists.", + "# From pkg-a/src (pkg-a has a vite.config.ts one level up) oxlint defaults still apply,", + "# proving lint resolution does not traverse parent directories.", + "cd packages/pkg-a/src && vp lint", + "# Claim 2d: `vp fmt` walks up several levels when needed.", + "# From pkg-a/src (no config) fmt finds pkg-a/vite.config.ts -> singleQuote:false -> passes.", + "cd packages/pkg-a/src && vp fmt --check .", + "# Claim 3: running `vp lint` / `vp fmt --check` directly at the workspace root", + "# applies the root config to every file, including files in pkg-a that have", + "# their own vite.config.ts (configs are not merged and not resolved per-file).", + "# `--threads=1` is only used to keep oxlint's multi-error output order stable", + "# across snapshot runs; it does not affect resolution.", + "vp lint --threads=1", + "vp fmt --check", + "# Claim 4: `-c ` bypasses cwd-based resolution. The permissive config applies", + "# to every targeted file, so both debugger statements and double-quoted strings pass.", + "vp lint -c vite.config.permissive.ts", + "vp fmt -c vite.config.permissive.ts --check packages/pkg-a/src packages/pkg-b/src" + ] +} diff --git a/packages/cli/snap-tests/docs-nested-config/vite.config.permissive.ts b/packages/cli/snap-tests/docs-nested-config/vite.config.permissive.ts new file mode 100644 index 0000000000..b0518c13c2 --- /dev/null +++ b/packages/cli/snap-tests/docs-nested-config/vite.config.permissive.ts @@ -0,0 +1,12 @@ +// Pinned-via-`-c` config: relaxes the root's strict rules. +// Used to prove that `-c ` disables the cwd walk-up. +export default { + lint: { + rules: { + 'no-debugger': 'off', + }, + }, + fmt: { + singleQuote: false, + }, +}; diff --git a/packages/cli/snap-tests/docs-nested-config/vite.config.ts b/packages/cli/snap-tests/docs-nested-config/vite.config.ts new file mode 100644 index 0000000000..07a67246ed --- /dev/null +++ b/packages/cli/snap-tests/docs-nested-config/vite.config.ts @@ -0,0 +1,11 @@ +// Root config: strict rules and single quotes. +export default { + lint: { + rules: { + 'no-debugger': 'error', + }, + }, + fmt: { + singleQuote: true, + }, +}; diff --git a/packages/cli/snap-tests/nested-oxc-config-proj-1/package.json b/packages/cli/snap-tests/nested-oxc-config-proj-1/package.json new file mode 100644 index 0000000000..d3aa196bfd --- /dev/null +++ b/packages/cli/snap-tests/nested-oxc-config-proj-1/package.json @@ -0,0 +1,7 @@ +{ + "name": "nested-oxc-config-proj-1-test", + "version": "0.0.0", + "private": true, + "type": "module", + "packageManager": "pnpm@10.16.1" +} diff --git a/packages/cli/snap-tests/nested-oxc-config-proj-1/packages/proj-1/package.json b/packages/cli/snap-tests/nested-oxc-config-proj-1/packages/proj-1/package.json new file mode 100644 index 0000000000..cf0fabf744 --- /dev/null +++ b/packages/cli/snap-tests/nested-oxc-config-proj-1/packages/proj-1/package.json @@ -0,0 +1,5 @@ +{ + "name": "proj-1", + "version": "0.0.0", + "private": true +} diff --git a/packages/cli/snap-tests/nested-oxc-config-proj-1/packages/proj-1/src/index.js b/packages/cli/snap-tests/nested-oxc-config-proj-1/packages/proj-1/src/index.js new file mode 100644 index 0000000000..6754512952 --- /dev/null +++ b/packages/cli/snap-tests/nested-oxc-config-proj-1/packages/proj-1/src/index.js @@ -0,0 +1,6 @@ +function hello() { + debugger; + return "hello from proj-1"; +} + +export { hello }; diff --git a/packages/cli/snap-tests/nested-oxc-config-proj-1/packages/proj-1/vite.config.ts b/packages/cli/snap-tests/nested-oxc-config-proj-1/packages/proj-1/vite.config.ts new file mode 100644 index 0000000000..f1bf2c29de --- /dev/null +++ b/packages/cli/snap-tests/nested-oxc-config-proj-1/packages/proj-1/vite.config.ts @@ -0,0 +1,13 @@ +// Nested proj-1 config: relaxes the root's strict rules. +// Running `vp lint` / `vp fmt` from packages/proj-1 should pick this up +// (matching oxc-project/oxc#20416), but currently the root config is used instead. +export default { + lint: { + rules: { + 'no-debugger': 'off', + }, + }, + fmt: { + singleQuote: false, + }, +}; diff --git a/packages/cli/snap-tests/nested-oxc-config-proj-1/pnpm-workspace.yaml b/packages/cli/snap-tests/nested-oxc-config-proj-1/pnpm-workspace.yaml new file mode 100644 index 0000000000..18ec407efc --- /dev/null +++ b/packages/cli/snap-tests/nested-oxc-config-proj-1/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - 'packages/*' diff --git a/packages/cli/snap-tests/nested-oxc-config-proj-1/snap.txt b/packages/cli/snap-tests/nested-oxc-config-proj-1/snap.txt new file mode 100644 index 0000000000..28cbd1e06e --- /dev/null +++ b/packages/cli/snap-tests/nested-oxc-config-proj-1/snap.txt @@ -0,0 +1,38 @@ +> # Regression test for oxc-project/oxc#20416: nested vite.config.ts in +> # packages/proj-1 must override the workspace root config when `vp lint` +> # and `vp fmt` run from that cwd. Root has `no-debugger: error` and +> # `singleQuote: true`, proj-1 overrides both to `off` / `false`. If the +> # root config leaks through, lint reports a debugger violation and fmt +> # reports a quote-style diff. +> cd packages/proj-1 && vp lint +Found 0 warnings and 0 errors. +Finished in ms on 2 files with rules using threads. + +> cd packages/proj-1 && vp fmt --check src +Checking formatting... +All matched files use the correct format. +Finished in ms on 1 files using threads. + +> # Running from the workspace root must use the root config, so both +> # commands should fail: lint flags the `debugger;` and fmt flags the +> # double-quoted string in packages/proj-1/src/index.js. +[1]> vp lint + + × eslint(no-debugger): `debugger` statement is not allowed + ╭─[packages/proj-1/src/index.js:2:3] + 1 │ function hello() { + 2 │ debugger; + · ───────── + 3 │ return "hello from proj-1"; + ╰──── + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in ms on 3 files with rules using threads. + +[1]> vp fmt --check packages/proj-1/src +Checking formatting... +packages/proj-1/src/index.js (ms) + +Format issues found in above 1 files. Run without `--check` to fix. +Finished in ms on 1 files using threads. diff --git a/packages/cli/snap-tests/nested-oxc-config-proj-1/steps.json b/packages/cli/snap-tests/nested-oxc-config-proj-1/steps.json new file mode 100644 index 0000000000..aa3afeb4ba --- /dev/null +++ b/packages/cli/snap-tests/nested-oxc-config-proj-1/steps.json @@ -0,0 +1,18 @@ +{ + "ignoredPlatforms": ["win32"], + "commands": [ + "# Regression test for oxc-project/oxc#20416: nested vite.config.ts in", + "# packages/proj-1 must override the workspace root config when `vp lint`", + "# and `vp fmt` run from that cwd. Root has `no-debugger: error` and", + "# `singleQuote: true`, proj-1 overrides both to `off` / `false`. If the", + "# root config leaks through, lint reports a debugger violation and fmt", + "# reports a quote-style diff.", + "cd packages/proj-1 && vp lint", + "cd packages/proj-1 && vp fmt --check src", + "# Running from the workspace root must use the root config, so both", + "# commands should fail: lint flags the `debugger;` and fmt flags the", + "# double-quoted string in packages/proj-1/src/index.js.", + "vp lint", + "vp fmt --check packages/proj-1/src" + ] +} diff --git a/packages/cli/snap-tests/nested-oxc-config-proj-1/vite.config.ts b/packages/cli/snap-tests/nested-oxc-config-proj-1/vite.config.ts new file mode 100644 index 0000000000..197e12a53d --- /dev/null +++ b/packages/cli/snap-tests/nested-oxc-config-proj-1/vite.config.ts @@ -0,0 +1,12 @@ +// Root config: strict rules that should NOT apply when running from packages/proj-1. +// If proj-1's nested vite.config.ts is loaded correctly, these rules are overridden. +export default { + lint: { + rules: { + 'no-debugger': 'error', + }, + }, + fmt: { + singleQuote: true, + }, +}; diff --git a/packages/cli/snap-tests/workspace-lint-ignore-patterns/package.json b/packages/cli/snap-tests/workspace-lint-ignore-patterns/package.json new file mode 100644 index 0000000000..bc2d22d2cd --- /dev/null +++ b/packages/cli/snap-tests/workspace-lint-ignore-patterns/package.json @@ -0,0 +1,9 @@ +{ + "name": "workspace-lint-ignore-patterns-test", + "version": "0.0.0", + "workspaces": [ + "packages/*" + ], + "type": "module", + "packageManager": "pnpm@10.16.1" +} diff --git a/packages/cli/snap-tests/workspace-lint-ignore-patterns/packages/ignored/package.json b/packages/cli/snap-tests/workspace-lint-ignore-patterns/packages/ignored/package.json new file mode 100644 index 0000000000..57597bc3ca --- /dev/null +++ b/packages/cli/snap-tests/workspace-lint-ignore-patterns/packages/ignored/package.json @@ -0,0 +1,4 @@ +{ + "name": "ignored", + "version": "0.0.0" +} diff --git a/packages/cli/snap-tests/workspace-lint-ignore-patterns/packages/ignored/src/index.js b/packages/cli/snap-tests/workspace-lint-ignore-patterns/packages/ignored/src/index.js new file mode 100644 index 0000000000..234a2b27cd --- /dev/null +++ b/packages/cli/snap-tests/workspace-lint-ignore-patterns/packages/ignored/src/index.js @@ -0,0 +1,6 @@ +function ignored() { + console.log('this file matches root ignorePatterns'); + return 'ignored'; +} + +export { ignored }; diff --git a/packages/cli/snap-tests/workspace-lint-ignore-patterns/packages/included/package.json b/packages/cli/snap-tests/workspace-lint-ignore-patterns/packages/included/package.json new file mode 100644 index 0000000000..78dce56cb9 --- /dev/null +++ b/packages/cli/snap-tests/workspace-lint-ignore-patterns/packages/included/package.json @@ -0,0 +1,4 @@ +{ + "name": "included", + "version": "0.0.0" +} diff --git a/packages/cli/snap-tests/workspace-lint-ignore-patterns/packages/included/src/index.js b/packages/cli/snap-tests/workspace-lint-ignore-patterns/packages/included/src/index.js new file mode 100644 index 0000000000..cef2fde5d8 --- /dev/null +++ b/packages/cli/snap-tests/workspace-lint-ignore-patterns/packages/included/src/index.js @@ -0,0 +1,6 @@ +function included() { + console.log('this file is not in ignorePatterns'); + return 'included'; +} + +export { included }; diff --git a/packages/cli/snap-tests/workspace-lint-ignore-patterns/packages/included/vite.config.ts b/packages/cli/snap-tests/workspace-lint-ignore-patterns/packages/included/vite.config.ts new file mode 100644 index 0000000000..c1a657f0b3 --- /dev/null +++ b/packages/cli/snap-tests/workspace-lint-ignore-patterns/packages/included/vite.config.ts @@ -0,0 +1,7 @@ +export default { + lint: { + rules: { + 'no-console': 'off', + }, + }, +}; diff --git a/packages/cli/snap-tests/workspace-lint-ignore-patterns/pnpm-workspace.yaml b/packages/cli/snap-tests/workspace-lint-ignore-patterns/pnpm-workspace.yaml new file mode 100644 index 0000000000..4de91a383a --- /dev/null +++ b/packages/cli/snap-tests/workspace-lint-ignore-patterns/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - '.' diff --git a/packages/cli/snap-tests/workspace-lint-ignore-patterns/snap.txt b/packages/cli/snap-tests/workspace-lint-ignore-patterns/snap.txt new file mode 100644 index 0000000000..d8ac056eb4 --- /dev/null +++ b/packages/cli/snap-tests/workspace-lint-ignore-patterns/snap.txt @@ -0,0 +1,15 @@ +> # Tests whether oxlint honors a `**/`-prefixed glob in root lint.ignorePatterns. +> # Mirrors how the vp repo declares ignorePatterns: ['**/snap-tests/**']. +[1]> vp lint + + × eslint(no-console): Unexpected console statement. + ╭─[packages/included/src/index.js:2:3] + 1 │ function included() { + 2 │ console.log('this file is not in ignorePatterns'); + · ─────────── + 3 │ return 'included'; + ╰──── + help: Delete this console statement. + +Found 0 warnings and 1 error. +Finished in ms on 3 files with rules using threads. diff --git a/packages/cli/snap-tests/workspace-lint-ignore-patterns/steps.json b/packages/cli/snap-tests/workspace-lint-ignore-patterns/steps.json new file mode 100644 index 0000000000..83db74abda --- /dev/null +++ b/packages/cli/snap-tests/workspace-lint-ignore-patterns/steps.json @@ -0,0 +1,8 @@ +{ + "ignoredPlatforms": ["win32"], + "commands": [ + "# Tests whether oxlint honors a `**/`-prefixed glob in root lint.ignorePatterns.", + "# Mirrors how the vp repo declares ignorePatterns: ['**/snap-tests/**'].", + "vp lint" + ] +} diff --git a/packages/cli/snap-tests/workspace-lint-ignore-patterns/vite.config.ts b/packages/cli/snap-tests/workspace-lint-ignore-patterns/vite.config.ts new file mode 100644 index 0000000000..3801117ebb --- /dev/null +++ b/packages/cli/snap-tests/workspace-lint-ignore-patterns/vite.config.ts @@ -0,0 +1,18 @@ +export default { + lint: { + options: { + typeAware: true, + typeCheck: true, + }, + plugins: ['unicorn', 'typescript', 'oxc'], + categories: { + correctness: 'error', + perf: 'error', + suspicious: 'error', + }, + rules: { + 'no-console': 'error', + }, + ignorePatterns: ['**/ignored/**'], + }, +}; diff --git a/packages/cli/snap-tests/workspace-lint-subpackage/snap.txt b/packages/cli/snap-tests/workspace-lint-subpackage/snap.txt index 1f7571585f..7ef2a5f3fb 100644 --- a/packages/cli/snap-tests/workspace-lint-subpackage/snap.txt +++ b/packages/cli/snap-tests/workspace-lint-subpackage/snap.txt @@ -1,7 +1,11 @@ > cd packages/app-a && vp lint # sub-workspace has no-console:off but root has no-console:warn +Found 0 warnings and 0 errors. +Finished in ms on 2 files with rules using threads. + +> vp lint # at root, no-console:warn should flag app-a's console.log ⚠ eslint(no-console): Unexpected console statement. - ╭─[src/index.js:2:3] + ╭─[packages/app-a/src/index.js:2:3] 1 │ function hello() { 2 │ console.log('hello from app-a'); · ─────────── @@ -10,4 +14,4 @@ help: Delete this console statement. Found 1 warning and 0 errors. -Finished in ms on 2 files with rules using threads. +Finished in ms on 3 files with rules using threads. diff --git a/packages/cli/snap-tests/workspace-lint-subpackage/steps.json b/packages/cli/snap-tests/workspace-lint-subpackage/steps.json index 992fa4aac4..0cc7757e03 100644 --- a/packages/cli/snap-tests/workspace-lint-subpackage/steps.json +++ b/packages/cli/snap-tests/workspace-lint-subpackage/steps.json @@ -1,6 +1,7 @@ { "ignoredPlatforms": ["win32"], "commands": [ - "cd packages/app-a && vp lint # sub-workspace has no-console:off but root has no-console:warn" + "cd packages/app-a && vp lint # sub-workspace has no-console:off but root has no-console:warn", + "vp lint # at root, no-console:warn should flag app-a's console.log" ] } diff --git a/vite.config.ts b/vite.config.ts index 093cde7a97..4bd73d01f4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -69,7 +69,9 @@ export default defineConfig({ '**/tmp/**', 'packages/cli/snap-tests/check-*/**', 'packages/cli/snap-tests/fmt-ignore-patterns/src/ignored', + 'packages/cli/snap-tests/nested-oxc-config-proj-1/**', 'packages/cli/snap-tests-global/migration-lint-staged-ts-config', + 'packages/cli/snap-tests/docs-nested-config', 'docs/**', 'ecosystem-ci/*/**', 'packages/test/**.cjs',