From 5d22f121d45b0eee8eee8628e73196d9aeb75179 Mon Sep 17 00:00:00 2001 From: MK Date: Thu, 26 Feb 2026 23:44:04 +0800 Subject: [PATCH 01/12] feat(cli): add `vp check` command for format, lint, and type checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `vp check` as a built-in composite command that runs `vp fmt --check` then `vp lint --type-aware --type-check` sequentially with fail-fast semantics. Supports `--no-fmt`, `--no-lint`, `--no-type-aware`, and `--no-type-check` flags to selectively disable checks. This is the first composite command in vite-plus โ€” all existing built-in commands delegate to a single underlying tool. --- crates/vite_global_cli/src/cli.rs | 11 + packages/cli/AGENTS.md | 1 + packages/cli/binding/src/cli.rs | 157 +++++++++++-- .../cli-helper-message/snap.txt | 1 + .../snap-tests/check-all-skipped/package.json | 5 + .../cli/snap-tests/check-all-skipped/snap.txt | 1 + .../snap-tests/check-all-skipped/steps.json | 6 + .../snap-tests/check-fail-fast/package.json | 5 + .../cli/snap-tests/check-fail-fast/snap.txt | 7 + .../snap-tests/check-fail-fast/src/index.js | 6 + .../cli/snap-tests/check-fail-fast/steps.json | 6 + .../snap-tests/check-fmt-fail/package.json | 5 + .../cli/snap-tests/check-fmt-fail/snap.txt | 7 + .../snap-tests/check-fmt-fail/src/index.js | 5 + .../cli/snap-tests/check-fmt-fail/steps.json | 6 + .../snap-tests/check-lint-fail/.oxlintrc.json | 5 + .../snap-tests/check-lint-fail/package.json | 5 + .../cli/snap-tests/check-lint-fail/snap.txt | 18 ++ .../snap-tests/check-lint-fail/src/index.js | 6 + .../cli/snap-tests/check-lint-fail/steps.json | 6 + .../cli/snap-tests/check-no-fmt/package.json | 5 + packages/cli/snap-tests/check-no-fmt/snap.txt | 4 + .../cli/snap-tests/check-no-fmt/src/index.js | 5 + .../cli/snap-tests/check-no-fmt/steps.json | 6 + .../cli/snap-tests/check-no-lint/package.json | 5 + .../cli/snap-tests/check-no-lint/snap.txt | 5 + .../cli/snap-tests/check-no-lint/src/index.js | 6 + .../cli/snap-tests/check-no-lint/steps.json | 6 + .../check-no-type-aware/package.json | 5 + .../snap-tests/check-no-type-aware/snap.txt | 8 + .../check-no-type-aware/src/index.js | 5 + .../snap-tests/check-no-type-aware/steps.json | 6 + .../check-no-type-check/package.json | 5 + .../snap-tests/check-no-type-check/snap.txt | 8 + .../check-no-type-check/src/index.js | 5 + .../snap-tests/check-no-type-check/steps.json | 6 + .../cli/snap-tests/check-pass/package.json | 5 + packages/cli/snap-tests/check-pass/snap.txt | 8 + .../cli/snap-tests/check-pass/src/index.js | 5 + packages/cli/snap-tests/check-pass/steps.json | 6 + .../cli/snap-tests/command-helper/snap.txt | 1 + .../cli/snap-tests/command-vp-alias/snap.txt | 1 + rfcs/check-command.md | 209 ++++++++++++++++++ vite.config.ts | 1 + 44 files changed, 572 insertions(+), 23 deletions(-) create mode 100644 packages/cli/snap-tests/check-all-skipped/package.json create mode 100644 packages/cli/snap-tests/check-all-skipped/snap.txt create mode 100644 packages/cli/snap-tests/check-all-skipped/steps.json create mode 100644 packages/cli/snap-tests/check-fail-fast/package.json create mode 100644 packages/cli/snap-tests/check-fail-fast/snap.txt create mode 100644 packages/cli/snap-tests/check-fail-fast/src/index.js create mode 100644 packages/cli/snap-tests/check-fail-fast/steps.json create mode 100644 packages/cli/snap-tests/check-fmt-fail/package.json create mode 100644 packages/cli/snap-tests/check-fmt-fail/snap.txt create mode 100644 packages/cli/snap-tests/check-fmt-fail/src/index.js create mode 100644 packages/cli/snap-tests/check-fmt-fail/steps.json create mode 100644 packages/cli/snap-tests/check-lint-fail/.oxlintrc.json create mode 100644 packages/cli/snap-tests/check-lint-fail/package.json create mode 100644 packages/cli/snap-tests/check-lint-fail/snap.txt create mode 100644 packages/cli/snap-tests/check-lint-fail/src/index.js create mode 100644 packages/cli/snap-tests/check-lint-fail/steps.json create mode 100644 packages/cli/snap-tests/check-no-fmt/package.json create mode 100644 packages/cli/snap-tests/check-no-fmt/snap.txt create mode 100644 packages/cli/snap-tests/check-no-fmt/src/index.js create mode 100644 packages/cli/snap-tests/check-no-fmt/steps.json create mode 100644 packages/cli/snap-tests/check-no-lint/package.json create mode 100644 packages/cli/snap-tests/check-no-lint/snap.txt create mode 100644 packages/cli/snap-tests/check-no-lint/src/index.js create mode 100644 packages/cli/snap-tests/check-no-lint/steps.json create mode 100644 packages/cli/snap-tests/check-no-type-aware/package.json create mode 100644 packages/cli/snap-tests/check-no-type-aware/snap.txt create mode 100644 packages/cli/snap-tests/check-no-type-aware/src/index.js create mode 100644 packages/cli/snap-tests/check-no-type-aware/steps.json create mode 100644 packages/cli/snap-tests/check-no-type-check/package.json create mode 100644 packages/cli/snap-tests/check-no-type-check/snap.txt create mode 100644 packages/cli/snap-tests/check-no-type-check/src/index.js create mode 100644 packages/cli/snap-tests/check-no-type-check/steps.json create mode 100644 packages/cli/snap-tests/check-pass/package.json create mode 100644 packages/cli/snap-tests/check-pass/snap.txt create mode 100644 packages/cli/snap-tests/check-pass/src/index.js create mode 100644 packages/cli/snap-tests/check-pass/steps.json create mode 100644 rfcs/check-command.md diff --git a/crates/vite_global_cli/src/cli.rs b/crates/vite_global_cli/src/cli.rs index 7ed2b1eaae..e78133d82b 100644 --- a/crates/vite_global_cli/src/cli.rs +++ b/crates/vite_global_cli/src/cli.rs @@ -569,6 +569,14 @@ pub enum Commands { args: Vec, }, + /// Run format, lint, and type checks + #[command(disable_help_flag = true)] + Check { + /// Additional arguments + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, + }, + /// Build library #[command(disable_help_flag = true)] Pack { @@ -1795,6 +1803,8 @@ pub async fn run_command(cwd: AbsolutePathBuf, args: Args) -> Result commands::delegate::execute(cwd, "fmt", &args).await, + Commands::Check { args } => commands::delegate::execute(cwd, "check", &args).await, + Commands::Pack { args } => commands::delegate::execute(cwd, "pack", &args).await, Commands::Run { args } => commands::run_or_delegate::execute(cwd, &args).await, @@ -1857,6 +1867,7 @@ fn apply_custom_help(cmd: clap::Command) -> clap::Command { {bold}test{reset} Run tests {bold}lint{reset} Lint code {bold}fmt{reset} Format code + {bold}check{reset} Run format, lint, and type checks {bold}pack{reset} Build library {bold}run{reset} Run tasks {bold}exec{reset} Execute a command from local node_modules/.bin diff --git a/packages/cli/AGENTS.md b/packages/cli/AGENTS.md index fb52bc10c6..e1ec0a9e16 100644 --- a/packages/cli/AGENTS.md +++ b/packages/cli/AGENTS.md @@ -15,6 +15,7 @@ This project is using Vite+, a modern toolchain built on top of Vite, Rolldown, - lint - Lint code - test - Run tests - fmt - Format code +- check - Run format, lint, and type checks - lib - Build library - migrate - Migrate an existing project to Vite+ - create - Create a new monorepo package (in-project) or a new project (global) diff --git a/packages/cli/binding/src/cli.rs b/packages/cli/binding/src/cli.rs index d0646cff46..74a4ff715c 100644 --- a/packages/cli/binding/src/cli.rs +++ b/packages/cli/binding/src/cli.rs @@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize}; use tokio::fs::write; use vite_error::Error; use vite_path::{AbsolutePath, AbsolutePathBuf}; -use vite_shared::{PrependOptions, prepend_to_path_env}; +use vite_shared::{PrependOptions, output, prepend_to_path_env}; use vite_str::Str; use vite_task::{ Command, CommandHandler, ExitStatus, HandledCommand, ScriptCommand, Session, SessionCallbacks, @@ -97,6 +97,21 @@ pub enum SynthesizableSubcommand { #[clap(allow_hyphen_values = true, trailing_var_arg = true)] args: Vec, }, + /// Run format, lint, and type checks + Check { + /// Skip format check + #[arg(long = "no-fmt")] + no_fmt: bool, + /// Skip lint check + #[arg(long = "no-lint")] + no_lint: bool, + /// Disable type-aware linting + #[arg(long = "no-type-aware")] + no_type_aware: bool, + /// Disable TypeScript type checking + #[arg(long = "no-type-check")] + no_type_check: bool, + }, } /// Top-level CLI argument parser for vite-plus. @@ -494,6 +509,11 @@ impl SubcommandResolver { envs: merge_resolved_envs(envs, resolved.envs), }) } + SynthesizableSubcommand::Check { .. } => { + anyhow::bail!( + "Check is a composite command and cannot be resolved to a single subcommand" + ); + } SynthesizableSubcommand::Install { args } => { let package_manager = vite_install::PackageManager::builder(cwd).build_with_default().await?; @@ -589,6 +609,10 @@ impl CommandHandler for VitePlusCommandHandler { let cli_args = CLIArgs::try_parse_from(iter::once("vp").chain(command.args.iter().map(Str::as_str)))?; match cli_args { + CLIArgs::Synthesizable(SynthesizableSubcommand::Check { .. }) => { + // Check is a composite command โ€” run as a subprocess in task scripts + Ok(HandledCommand::Verbatim) + } CLIArgs::Synthesizable(subcmd) => { let resolved = self.resolver.resolve(subcmd, &command.envs, &command.cwd).await?; Ok(HandledCommand::Synthesized(resolved.into_synthetic_plan_request())) @@ -644,31 +668,42 @@ impl UserConfigLoader for VitePlusConfigLoader { } } -/// Execute a synthesizable subcommand directly (not through vite-task Session). -/// No caching, no task graph, no dependency resolution. -async fn execute_direct_subcommand( - subcommand: SynthesizableSubcommand, +/// Create auto-install synthetic plan request +async fn create_install_synthetic_request( cwd: &AbsolutePathBuf, - options: Option, -) -> Result { - let (workspace_root, _) = vite_workspace::find_workspace_root(cwd)?; - let workspace_path: Arc = workspace_root.path.into(); +) -> Result { + let package_manager = vite_install::PackageManager::builder(cwd).build_with_default().await?; + let resolve_command = package_manager.resolve_install_command(&vec![]); - let mut resolver = if let Some(options) = options { - SubcommandResolver::new(Arc::clone(&workspace_path)).with_cli_options(options) - } else { - SubcommandResolver::new(Arc::clone(&workspace_path)) - }; + let mut envs: FxHashMap, Arc> = std::env::vars_os() + .map(|(k, v)| (Arc::from(k.as_os_str()), Arc::from(v.as_os_str()))) + .collect(); - let envs: Arc, Arc>> = Arc::new( - std::env::vars_os() - .map(|(k, v)| (Arc::from(k.as_os_str()), Arc::from(v.as_os_str()))) - .collect(), - ); - let cwd_arc: Arc = cwd.clone().into(); + for (k, v) in resolve_command.envs { + envs.insert(Arc::from(OsStr::new(&k)), Arc::from(OsStr::new(&v))); + } + + Ok(SyntheticPlanRequest { + program: Arc::::from(OsStr::new(&resolve_command.bin_path).to_os_string()), + args: resolve_command.args.into_iter().map(Str::from).collect(), + cache_config: UserCacheConfig::with_config(EnabledCacheConfig { + envs: None, + pass_through_envs: None, + }), + envs: Arc::new(envs), + }) +} +/// Resolve a single subcommand and execute it, returning its exit status. +async fn resolve_and_execute( + resolver: &mut SubcommandResolver, + subcommand: SynthesizableSubcommand, + envs: &Arc, Arc>>, + cwd: &AbsolutePathBuf, + cwd_arc: &Arc, +) -> Result { let resolved = - resolver.resolve(subcommand, &envs, &cwd_arc).await.map_err(|e| Error::Anyhow(e))?; + 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 = { @@ -695,11 +730,86 @@ async fn execute_direct_subcommand( let mut child = cmd.spawn().map_err(|e| Error::Anyhow(e.into()))?; let status = child.wait().await; + let status = status.map_err(|e| Error::Anyhow(e.into()))?; + Ok(ExitStatus(status.code().unwrap_or(1) as u8)) +} + +/// Execute a synthesizable subcommand directly (not through vite-task Session). +/// No caching, no task graph, no dependency resolution. +async fn execute_direct_subcommand( + subcommand: SynthesizableSubcommand, + cwd: &AbsolutePathBuf, + options: Option, +) -> Result { + let (workspace_root, _) = vite_workspace::find_workspace_root(cwd)?; + let workspace_path: Arc = workspace_root.path.into(); + + let mut resolver = if let Some(options) = options { + SubcommandResolver::new(Arc::clone(&workspace_path)).with_cli_options(options) + } else { + SubcommandResolver::new(Arc::clone(&workspace_path)) + }; + + let envs: Arc, Arc>> = Arc::new( + std::env::vars_os() + .map(|(k, v)| (Arc::from(k.as_os_str()), Arc::from(v.as_os_str()))) + .collect(), + ); + let cwd_arc: Arc = cwd.clone().into(); + + let status = match subcommand { + SynthesizableSubcommand::Check { no_fmt, no_lint, no_type_aware, no_type_check } => { + let mut status = ExitStatus::SUCCESS; + + if !no_fmt { + output::info("vp fmt --check"); + status = resolve_and_execute( + &mut resolver, + SynthesizableSubcommand::Fmt { args: vec!["--check".to_string()] }, + &envs, + cwd, + &cwd_arc, + ) + .await?; + if status != ExitStatus::SUCCESS { + resolver.cleanup_temp_files().await; + return Ok(status); + } + } + + if !no_lint { + let mut args = Vec::new(); + if !no_type_aware { + args.push("--type-aware".to_string()); + // --type-check requires --type-aware as prerequisite + if !no_type_check { + args.push("--type-check".to_string()); + } + } + if args.is_empty() { + output::info("vp lint"); + } else { + let cmd = vite_str::format!("vp lint {}", args.join(" ")); + output::info(&cmd); + } + status = resolve_and_execute( + &mut resolver, + SynthesizableSubcommand::Lint { args }, + &envs, + cwd, + &cwd_arc, + ) + .await?; + } + + status + } + other => resolve_and_execute(&mut resolver, other, &envs, cwd, &cwd_arc).await?, + }; resolver.cleanup_temp_files().await; - let status = status.map_err(|e| Error::Anyhow(e.into()))?; - Ok(ExitStatus(status.code().unwrap_or(1) as u8)) + Ok(status) } /// Execute a vite-task command (run, cache) through Session. @@ -817,6 +927,7 @@ fn print_help() { {bold}test{reset} Run tests {bold}lint{reset} Lint code {bold}fmt{reset} Format code + {bold}check{reset} Run format, lint, and type checks {bold}pack{reset} Build library {bold}run{reset} Run tasks {bold}exec{reset} Execute a command from local node_modules/.bin diff --git a/packages/cli/snap-tests-global/cli-helper-message/snap.txt b/packages/cli/snap-tests-global/cli-helper-message/snap.txt index b4380fde07..cc3e73ea1a 100644 --- a/packages/cli/snap-tests-global/cli-helper-message/snap.txt +++ b/packages/cli/snap-tests-global/cli-helper-message/snap.txt @@ -10,6 +10,7 @@ Core Commands: test Run tests lint Lint code fmt Format code + check Run format, lint, and type checks pack Build library run Run tasks exec Execute a command from local node_modules/.bin diff --git a/packages/cli/snap-tests/check-all-skipped/package.json b/packages/cli/snap-tests/check-all-skipped/package.json new file mode 100644 index 0000000000..e93b0b1bbe --- /dev/null +++ b/packages/cli/snap-tests/check-all-skipped/package.json @@ -0,0 +1,5 @@ +{ + "name": "check-all-skipped", + "version": "0.0.0", + "private": true +} diff --git a/packages/cli/snap-tests/check-all-skipped/snap.txt b/packages/cli/snap-tests/check-all-skipped/snap.txt new file mode 100644 index 0000000000..807cee7d67 --- /dev/null +++ b/packages/cli/snap-tests/check-all-skipped/snap.txt @@ -0,0 +1 @@ +> vp check --no-fmt --no-lint \ No newline at end of file diff --git a/packages/cli/snap-tests/check-all-skipped/steps.json b/packages/cli/snap-tests/check-all-skipped/steps.json new file mode 100644 index 0000000000..2052f4dce3 --- /dev/null +++ b/packages/cli/snap-tests/check-all-skipped/steps.json @@ -0,0 +1,6 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": ["vp check --no-fmt --no-lint"] +} diff --git a/packages/cli/snap-tests/check-fail-fast/package.json b/packages/cli/snap-tests/check-fail-fast/package.json new file mode 100644 index 0000000000..a0c96cacf7 --- /dev/null +++ b/packages/cli/snap-tests/check-fail-fast/package.json @@ -0,0 +1,5 @@ +{ + "name": "check-fail-fast", + "version": "0.0.0", + "private": true +} diff --git a/packages/cli/snap-tests/check-fail-fast/snap.txt b/packages/cli/snap-tests/check-fail-fast/snap.txt new file mode 100644 index 0000000000..4e63aeb68e --- /dev/null +++ b/packages/cli/snap-tests/check-fail-fast/snap.txt @@ -0,0 +1,7 @@ +[1]> vp check +info: vp fmt --check +Checking formatting... +src/index.js (ms) + +Format issues found in above 1 files. Run without `--check` to fix. +Finished in ms on 3 files using threads. diff --git a/packages/cli/snap-tests/check-fail-fast/src/index.js b/packages/cli/snap-tests/check-fail-fast/src/index.js new file mode 100644 index 0000000000..ed48a4ad0b --- /dev/null +++ b/packages/cli/snap-tests/check-fail-fast/src/index.js @@ -0,0 +1,6 @@ +function hello( ) { + eval( "code" ) + return "hello" +} + +export { hello } diff --git a/packages/cli/snap-tests/check-fail-fast/steps.json b/packages/cli/snap-tests/check-fail-fast/steps.json new file mode 100644 index 0000000000..d9c26d5a29 --- /dev/null +++ b/packages/cli/snap-tests/check-fail-fast/steps.json @@ -0,0 +1,6 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": ["vp check"] +} diff --git a/packages/cli/snap-tests/check-fmt-fail/package.json b/packages/cli/snap-tests/check-fmt-fail/package.json new file mode 100644 index 0000000000..d54833130f --- /dev/null +++ b/packages/cli/snap-tests/check-fmt-fail/package.json @@ -0,0 +1,5 @@ +{ + "name": "check-fmt-fail", + "version": "0.0.0", + "private": true +} diff --git a/packages/cli/snap-tests/check-fmt-fail/snap.txt b/packages/cli/snap-tests/check-fmt-fail/snap.txt new file mode 100644 index 0000000000..4e63aeb68e --- /dev/null +++ b/packages/cli/snap-tests/check-fmt-fail/snap.txt @@ -0,0 +1,7 @@ +[1]> vp check +info: vp fmt --check +Checking formatting... +src/index.js (ms) + +Format issues found in above 1 files. Run without `--check` to fix. +Finished in ms on 3 files using threads. diff --git a/packages/cli/snap-tests/check-fmt-fail/src/index.js b/packages/cli/snap-tests/check-fmt-fail/src/index.js new file mode 100644 index 0000000000..eed174aaac --- /dev/null +++ b/packages/cli/snap-tests/check-fmt-fail/src/index.js @@ -0,0 +1,5 @@ +function hello( ) { + return "hello" +} + +export { hello } diff --git a/packages/cli/snap-tests/check-fmt-fail/steps.json b/packages/cli/snap-tests/check-fmt-fail/steps.json new file mode 100644 index 0000000000..d9c26d5a29 --- /dev/null +++ b/packages/cli/snap-tests/check-fmt-fail/steps.json @@ -0,0 +1,6 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": ["vp check"] +} diff --git a/packages/cli/snap-tests/check-lint-fail/.oxlintrc.json b/packages/cli/snap-tests/check-lint-fail/.oxlintrc.json new file mode 100644 index 0000000000..d71deeeee8 --- /dev/null +++ b/packages/cli/snap-tests/check-lint-fail/.oxlintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "no-eval": "error" + } +} diff --git a/packages/cli/snap-tests/check-lint-fail/package.json b/packages/cli/snap-tests/check-lint-fail/package.json new file mode 100644 index 0000000000..7d19bd5058 --- /dev/null +++ b/packages/cli/snap-tests/check-lint-fail/package.json @@ -0,0 +1,5 @@ +{ + "name": "check-lint-fail", + "version": "0.0.0", + "private": true +} diff --git a/packages/cli/snap-tests/check-lint-fail/snap.txt b/packages/cli/snap-tests/check-lint-fail/snap.txt new file mode 100644 index 0000000000..0d1afc7fb2 --- /dev/null +++ b/packages/cli/snap-tests/check-lint-fail/snap.txt @@ -0,0 +1,18 @@ +[1]> vp check +info: vp fmt --check +Checking formatting... +All matched files use the correct format. +Finished in ms on 4 files using threads. +info: vp lint --type-aware --type-check + + ร— eslint(no-eval): eval can be harmful. + โ•ญโ”€[src/index.js:2:3] + 1 โ”‚ function hello() { + 2 โ”‚ eval("code"); + ยท  โ”€โ”€โ”€โ”€ + 3 โ”‚ return "hello"; + โ•ฐโ”€โ”€โ”€โ”€ + help: Avoid eval(). For JSON parsing use JSON.parse(); for dynamic property access use bracket notation (obj[key]); for other cases refactor to avoid evaluating strings as code. + +Found 0 warnings and 1 error. +Finished in ms on 1 file with rules using threads. diff --git a/packages/cli/snap-tests/check-lint-fail/src/index.js b/packages/cli/snap-tests/check-lint-fail/src/index.js new file mode 100644 index 0000000000..e916f931f1 --- /dev/null +++ b/packages/cli/snap-tests/check-lint-fail/src/index.js @@ -0,0 +1,6 @@ +function hello() { + eval("code"); + return "hello"; +} + +export { hello }; diff --git a/packages/cli/snap-tests/check-lint-fail/steps.json b/packages/cli/snap-tests/check-lint-fail/steps.json new file mode 100644 index 0000000000..d9c26d5a29 --- /dev/null +++ b/packages/cli/snap-tests/check-lint-fail/steps.json @@ -0,0 +1,6 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": ["vp check"] +} diff --git a/packages/cli/snap-tests/check-no-fmt/package.json b/packages/cli/snap-tests/check-no-fmt/package.json new file mode 100644 index 0000000000..6a77526d67 --- /dev/null +++ b/packages/cli/snap-tests/check-no-fmt/package.json @@ -0,0 +1,5 @@ +{ + "name": "check-no-fmt", + "version": "0.0.0", + "private": true +} diff --git a/packages/cli/snap-tests/check-no-fmt/snap.txt b/packages/cli/snap-tests/check-no-fmt/snap.txt new file mode 100644 index 0000000000..979ae626f4 --- /dev/null +++ b/packages/cli/snap-tests/check-no-fmt/snap.txt @@ -0,0 +1,4 @@ +> vp check --no-fmt +info: vp lint --type-aware --type-check +Found 0 warnings and 0 errors. +Finished in ms on 1 file with rules using threads. diff --git a/packages/cli/snap-tests/check-no-fmt/src/index.js b/packages/cli/snap-tests/check-no-fmt/src/index.js new file mode 100644 index 0000000000..f3bc62dfb5 --- /dev/null +++ b/packages/cli/snap-tests/check-no-fmt/src/index.js @@ -0,0 +1,5 @@ +function hello( ) { + return "hello" +} + +export { hello } diff --git a/packages/cli/snap-tests/check-no-fmt/steps.json b/packages/cli/snap-tests/check-no-fmt/steps.json new file mode 100644 index 0000000000..aba7793b58 --- /dev/null +++ b/packages/cli/snap-tests/check-no-fmt/steps.json @@ -0,0 +1,6 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": ["vp check --no-fmt"] +} diff --git a/packages/cli/snap-tests/check-no-lint/package.json b/packages/cli/snap-tests/check-no-lint/package.json new file mode 100644 index 0000000000..43c5c62da1 --- /dev/null +++ b/packages/cli/snap-tests/check-no-lint/package.json @@ -0,0 +1,5 @@ +{ + "name": "check-no-lint", + "version": "0.0.0", + "private": true +} diff --git a/packages/cli/snap-tests/check-no-lint/snap.txt b/packages/cli/snap-tests/check-no-lint/snap.txt new file mode 100644 index 0000000000..d7a0b52c64 --- /dev/null +++ b/packages/cli/snap-tests/check-no-lint/snap.txt @@ -0,0 +1,5 @@ +> vp check --no-lint +info: vp fmt --check +Checking formatting... +All matched files use the correct format. +Finished in ms on 3 files using threads. diff --git a/packages/cli/snap-tests/check-no-lint/src/index.js b/packages/cli/snap-tests/check-no-lint/src/index.js new file mode 100644 index 0000000000..e916f931f1 --- /dev/null +++ b/packages/cli/snap-tests/check-no-lint/src/index.js @@ -0,0 +1,6 @@ +function hello() { + eval("code"); + return "hello"; +} + +export { hello }; diff --git a/packages/cli/snap-tests/check-no-lint/steps.json b/packages/cli/snap-tests/check-no-lint/steps.json new file mode 100644 index 0000000000..8f1a7ce30d --- /dev/null +++ b/packages/cli/snap-tests/check-no-lint/steps.json @@ -0,0 +1,6 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": ["vp check --no-lint"] +} diff --git a/packages/cli/snap-tests/check-no-type-aware/package.json b/packages/cli/snap-tests/check-no-type-aware/package.json new file mode 100644 index 0000000000..985f7824d2 --- /dev/null +++ b/packages/cli/snap-tests/check-no-type-aware/package.json @@ -0,0 +1,5 @@ +{ + "name": "check-no-type-aware", + "version": "0.0.0", + "private": true +} diff --git a/packages/cli/snap-tests/check-no-type-aware/snap.txt b/packages/cli/snap-tests/check-no-type-aware/snap.txt new file mode 100644 index 0000000000..d4debbfbfd --- /dev/null +++ b/packages/cli/snap-tests/check-no-type-aware/snap.txt @@ -0,0 +1,8 @@ +> vp check --no-type-aware +info: vp fmt --check +Checking formatting... +All matched files use the correct format. +Finished in ms on 3 files using threads. +info: vp lint +Found 0 warnings and 0 errors. +Finished in ms on 1 file with rules using threads. diff --git a/packages/cli/snap-tests/check-no-type-aware/src/index.js b/packages/cli/snap-tests/check-no-type-aware/src/index.js new file mode 100644 index 0000000000..13305bd3e9 --- /dev/null +++ b/packages/cli/snap-tests/check-no-type-aware/src/index.js @@ -0,0 +1,5 @@ +function hello() { + return "hello"; +} + +export { hello }; diff --git a/packages/cli/snap-tests/check-no-type-aware/steps.json b/packages/cli/snap-tests/check-no-type-aware/steps.json new file mode 100644 index 0000000000..e5c768e88a --- /dev/null +++ b/packages/cli/snap-tests/check-no-type-aware/steps.json @@ -0,0 +1,6 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": ["vp check --no-type-aware"] +} diff --git a/packages/cli/snap-tests/check-no-type-check/package.json b/packages/cli/snap-tests/check-no-type-check/package.json new file mode 100644 index 0000000000..fb6ed410df --- /dev/null +++ b/packages/cli/snap-tests/check-no-type-check/package.json @@ -0,0 +1,5 @@ +{ + "name": "check-no-type-check", + "version": "0.0.0", + "private": true +} diff --git a/packages/cli/snap-tests/check-no-type-check/snap.txt b/packages/cli/snap-tests/check-no-type-check/snap.txt new file mode 100644 index 0000000000..7168050359 --- /dev/null +++ b/packages/cli/snap-tests/check-no-type-check/snap.txt @@ -0,0 +1,8 @@ +> vp check --no-type-check +info: vp fmt --check +Checking formatting... +All matched files use the correct format. +Finished in ms on 3 files using threads. +info: vp lint --type-aware +Found 0 warnings and 0 errors. +Finished in ms on 1 file with rules using threads. diff --git a/packages/cli/snap-tests/check-no-type-check/src/index.js b/packages/cli/snap-tests/check-no-type-check/src/index.js new file mode 100644 index 0000000000..13305bd3e9 --- /dev/null +++ b/packages/cli/snap-tests/check-no-type-check/src/index.js @@ -0,0 +1,5 @@ +function hello() { + return "hello"; +} + +export { hello }; diff --git a/packages/cli/snap-tests/check-no-type-check/steps.json b/packages/cli/snap-tests/check-no-type-check/steps.json new file mode 100644 index 0000000000..72bf1dbb08 --- /dev/null +++ b/packages/cli/snap-tests/check-no-type-check/steps.json @@ -0,0 +1,6 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": ["vp check --no-type-check"] +} diff --git a/packages/cli/snap-tests/check-pass/package.json b/packages/cli/snap-tests/check-pass/package.json new file mode 100644 index 0000000000..b64bd876a5 --- /dev/null +++ b/packages/cli/snap-tests/check-pass/package.json @@ -0,0 +1,5 @@ +{ + "name": "check-pass", + "version": "0.0.0", + "private": true +} diff --git a/packages/cli/snap-tests/check-pass/snap.txt b/packages/cli/snap-tests/check-pass/snap.txt new file mode 100644 index 0000000000..63398ca767 --- /dev/null +++ b/packages/cli/snap-tests/check-pass/snap.txt @@ -0,0 +1,8 @@ +> vp check +info: vp fmt --check +Checking formatting... +All matched files use the correct format. +Finished in ms on 3 files using threads. +info: vp lint --type-aware --type-check +Found 0 warnings and 0 errors. +Finished in ms on 1 file with rules using threads. diff --git a/packages/cli/snap-tests/check-pass/src/index.js b/packages/cli/snap-tests/check-pass/src/index.js new file mode 100644 index 0000000000..13305bd3e9 --- /dev/null +++ b/packages/cli/snap-tests/check-pass/src/index.js @@ -0,0 +1,5 @@ +function hello() { + return "hello"; +} + +export { hello }; diff --git a/packages/cli/snap-tests/check-pass/steps.json b/packages/cli/snap-tests/check-pass/steps.json new file mode 100644 index 0000000000..d9c26d5a29 --- /dev/null +++ b/packages/cli/snap-tests/check-pass/steps.json @@ -0,0 +1,6 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": ["vp check"] +} diff --git a/packages/cli/snap-tests/command-helper/snap.txt b/packages/cli/snap-tests/command-helper/snap.txt index c9cadc4837..7bf306fd3f 100644 --- a/packages/cli/snap-tests/command-helper/snap.txt +++ b/packages/cli/snap-tests/command-helper/snap.txt @@ -9,6 +9,7 @@ Vite+/ test Run tests lint Lint code fmt Format code + check Run format, lint, and type checks pack Build library run Run tasks exec Execute a command from local node_modules/.bin diff --git a/packages/cli/snap-tests/command-vp-alias/snap.txt b/packages/cli/snap-tests/command-vp-alias/snap.txt index c6293a591d..192cb1b61a 100644 --- a/packages/cli/snap-tests/command-vp-alias/snap.txt +++ b/packages/cli/snap-tests/command-vp-alias/snap.txt @@ -9,6 +9,7 @@ Vite+/ test Run tests lint Lint code fmt Format code + check Run format, lint, and type checks pack Build library run Run tasks exec Execute a command from local node_modules/.bin diff --git a/rfcs/check-command.md b/rfcs/check-command.md new file mode 100644 index 0000000000..e47bb90fcb --- /dev/null +++ b/rfcs/check-command.md @@ -0,0 +1,209 @@ +# RFC: `vp check` Command + +## Summary + +Add `vp check` as a built-in command that runs format verification, linting, and type checking in a single invocation. This provides a single "fast check" command for CI and local development, distinct from "slow checks" like test suites. + +## Motivation + +Currently, running a full code quality check requires chaining multiple commands: + +```bash +# From the monorepo template's "ready" script: +vp fmt && vp lint --type-aware && vp run test -r && vp run build -r +``` + +Pain points: + +- **No single command** for the most common pre-commit/CI check: "is my code correct?" +- Users must remember to pass `--type-aware` and `--type-check` to lint +- The `&&` chaining pattern is fragile and verbose +- No standardized "check" workflow across projects + +### Fast vs Slow Checks + +- **Fast checks** (seconds): type checking + linting + formatting โ€” static analysis, no code execution +- **Slow checks** (minutes): test suites (Vitest) โ€” code execution + +`vp check` targets the **fast checks** category. Tests are explicitly excluded โ€” use `vp test` for that. + +## Command Syntax + +```bash +# Run all fast checks (fmt --check + lint --type-aware --type-check) +vp check + +# Disable specific checks +vp check --no-fmt +vp check --no-lint +vp check --no-type-aware +vp check --no-type-check +``` + +### Options + +| Flag | Default | Description | +| ---------------------------------- | ------- | ------------------------------------------------------- | +| `--fmt` / `--no-fmt` | ON | Run format check (`vp fmt --check`) | +| `--lint` / `--no-lint` | ON | Run lint check (`vp lint`) | +| `--type-aware` / `--no-type-aware` | ON | Enable type-aware lint rules (oxlint `--type-aware`) | +| `--type-check` / `--no-type-check` | ON | Enable TypeScript type checking (oxlint `--type-check`) | + +**Flag dependency:** `--type-check` requires `--type-aware` as a prerequisite. + +- `--type-aware` enables lint rules that use type information (e.g., `no-floating-promises`) +- `--type-check` enables experimental TypeScript compiler-level type checking (requires type-aware) +- If `--no-type-aware` is set, `--type-check` is also implicitly disabled + +Both are enabled by default in `vp check` to provide comprehensive static analysis. + +## Behavior + +### Alpha (Non-Parallel, Sequential) + +Commands run **sequentially** with fail-fast semantics: + +``` +1. vp fmt --check (verify formatting, don't auto-fix) +2. vp lint --type-aware --type-check (lint + type checking) +``` + +If any step fails, `vp check` exits immediately with a non-zero exit code. + +### Future (Parallel) + +In a later iteration, steps could run in parallel and aggregate errors. + +## Decisions + +### Verify only, no auto-fix + +`vp check` is a **read-only verification** command. It never modifies files. + +- `vp fmt --check` reports unformatted files (doesn't auto-format) +- `vp lint` reports issues (doesn't auto-fix) +- To fix issues, users run `vp fmt` and `vp lint --fix` separately + +This keeps `vp check` safe for CI and predictable for local dev. + +### No `--fix` flag (alpha) + +For simplicity in the alpha, there's no `--fix` flag. Users compose fixes manually: + +```bash +vp fmt && vp lint --fix # fix what's fixable +vp check # verify everything passes +``` + +A `--fix` flag could be added later if needed. + +### No tests + +`vp check` does **not** run Vitest. The distinction is intentional: + +- `vp check` = fast static analysis (seconds) +- `vp test` = test execution (minutes) + +## Implementation Architecture + +### Rust Global CLI + +Add `Check` variant to `Commands` enum in `crates/vite_global_cli/src/cli.rs`: + +```rust +#[command(disable_help_flag = true)] +Check { + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, +}, +``` + +Route via delegation: + +```rust +Commands::Check { args } => commands::delegate::execute(cwd, "check", &args).await, +``` + +### NAPI Binding + +Add `Check` to `SynthesizableSubcommand` in `packages/cli/binding/src/cli.rs`. The check command internally resolves and runs fmt + lint sequentially, reusing existing resolvers. + +### TypeScript Side + +No new resolver needed โ€” `vp check` reuses existing `resolve-lint.ts` and `resolve-fmt.ts`. + +### Key Files to Modify + +1. `crates/vite_global_cli/src/cli.rs` โ€” Add `Check` command variant and routing +2. `packages/cli/binding/src/cli.rs` โ€” Add check subcommand handling (sequential fmt + lint) +3. `packages/cli/src/bin.ts` โ€” (if needed for routing) + +## CLI Help Output + +``` +Run format, lint, and type checks + +Usage: vp check [OPTIONS] + +Options: + --fmt Run format check [default: true] + --lint Run lint check [default: true] + --type-aware Enable type-aware linting [default: true] + --type-check Enable TypeScript type checking [default: true] + -h, --help Print help +``` + +## Relationship to Existing Commands + +| Command | Purpose | Speed | +| ----------------------------------- | ------------------------------------------------ | -------- | +| `vp fmt` | Format code (auto-fix) | Fast | +| `vp fmt --check` | Verify formatting | Fast | +| `vp lint` | Lint code | Fast | +| `vp lint --type-aware --type-check` | Lint + full type checking | Fast | +| `vp test` | Run test suite | Slow | +| `vp build` | Build project | Slow | +| **`vp check`** | **fmt --check + lint --type-aware --type-check** | **Fast** | + +With `vp check`, the monorepo template's "ready" script simplifies to: + +```json +"ready": "vp check && vp run test -r && vp run build -r" +``` + +## Comparison with Other Tools + +| Tool | Scope | +| ----------------- | ---------------------------------- | +| `cargo check` | Type checking only | +| `cargo clippy` | Lint only | +| **`biome check`** | **Format + lint (closest analog)** | +| `deno check` | Type checking only | + +## Snap Tests + +``` +packages/cli/snap-tests/check-basic/ + package.json + steps.json # { "steps": [{ "command": "vp check" }] } + src/index.ts # Clean file that passes all checks + snap.txt + +packages/cli/snap-tests/check-fmt-fail/ + package.json + steps.json # { "steps": [{ "command": "vp check" }] } + src/index.ts # Badly formatted file + snap.txt # Shows fmt --check failure, lint doesn't run (fail-fast) + +packages/cli/snap-tests/check-no-fmt/ + package.json + steps.json # { "steps": [{ "command": "vp check --no-fmt" }] } + snap.txt # Only lint runs +``` + +## Future Enhancements + +- Parallel execution of fmt and lint +- `--fix` flag to auto-fix fixable issues +- Workspace-aware mode (`vp check -r`) running checks across all packages +- Integration with `vp run` task graph for caching diff --git a/vite.config.ts b/vite.config.ts index de63270d31..b8c39661d9 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -69,6 +69,7 @@ export default defineConfig({ fmt: { ignorePatterns: [ '**/tmp/**', + 'packages/cli/snap-tests/check-*/**', 'packages/cli/snap-tests/fmt-ignore-patterns/src/ignored', 'ecosystem-ci/*/**', 'packages/test/**.cjs', From e13af176876084f7a7f8bff0479cff0de3ee9e0d Mon Sep 17 00:00:00 2001 From: MK Date: Thu, 26 Feb 2026 23:51:41 +0800 Subject: [PATCH 02/12] fix: replace toSorted() with sort() to fix tsgolint type-check errors tsgolint does not yet recognize Array#toSorted() from ES2023 lib, causing TS2550 errors under `vp check`. Both call sites sort freshly created arrays so in-place sort() is safe. --- packages/cli/src/utils/workspace.ts | 3 ++- packages/test/build.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/utils/workspace.ts b/packages/cli/src/utils/workspace.ts index 8356437db6..4d79aeef03 100644 --- a/packages/cli/src/utils/workspace.ts +++ b/packages/cli/src/utils/workspace.ts @@ -83,7 +83,8 @@ export async function detectWorkspace(rootDir: string): Promise(packageJsonFile); diff --git a/packages/test/build.ts b/packages/test/build.ts index e2cfaa7eb7..472372cb36 100644 --- a/packages/test/build.ts +++ b/packages/test/build.ts @@ -1138,7 +1138,8 @@ function rewriteImportsWithAst( } // Sort replacements in reverse order (end to start) to preserve positions - const replacements = [...replacementMap.values()].toSorted((a, b) => b[0] - a[0]); + // eslint-disable-next-line unicorn/no-array-sort -- safe: sorting a fresh spread copy + const replacements = [...replacementMap.values()].sort((a, b) => b[0] - a[0]); // Apply replacements let result_content = content; From 6defe28dab4220396ef10eb357a80a4221e5850f Mon Sep 17 00:00:00 2001 From: MK Date: Thu, 26 Feb 2026 23:54:02 +0800 Subject: [PATCH 03/12] chore: enable --type-check in lint script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e78eadf773..a9a620ddf7 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 --threads 4", + "lint": "vp lint --type-aware --type-check --threads 4", "test": "vp test run && pnpm -r snap-test", "fmt": "vp fmt", "test:unit": "vp test run", From c479fa6459bfa2da959960c05c6f60dfb4f84199 Mon Sep 17 00:00:00 2001 From: MK Date: Thu, 26 Feb 2026 23:54:58 +0800 Subject: [PATCH 04/12] ci: replace vp fmt --check + vp run lint with vp check --- .github/workflows/ci.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 497c41408d..d8bfa9ef62 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -230,11 +230,8 @@ jobs: vp --version vp -h - - name: Run CLI fmt - run: vp fmt --check - - - name: Run CLI lint - run: vp run lint + - name: Run CLI check + run: vp check - name: Test global package install (powershell) if: ${{ matrix.os == 'windows-latest' }} From b4e200fab52ef51f781d32409723981ab4fb787b Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 27 Feb 2026 15:43:53 +0800 Subject: [PATCH 05/12] feat(cli): add `--fix` flag to `vp check` command `vp check --fix` runs `vp fmt` (auto-format) then `vp lint --fix --type-aware --type-check` (auto-fix lint issues), replacing the manual `vp fmt && vp lint --fix` workflow. Also removes unused `create_install_synthetic_request` function. --- packages/cli/binding/src/cli.rs | 44 +++++++------------ .../cli/snap-tests/check-fix/package.json | 5 +++ packages/cli/snap-tests/check-fix/snap.txt | 5 +++ .../cli/snap-tests/check-fix/src/index.js | 5 +++ packages/cli/snap-tests/check-fix/steps.json | 6 +++ rfcs/check-command.md | 33 ++++++-------- 6 files changed, 49 insertions(+), 49 deletions(-) create mode 100644 packages/cli/snap-tests/check-fix/package.json create mode 100644 packages/cli/snap-tests/check-fix/snap.txt create mode 100644 packages/cli/snap-tests/check-fix/src/index.js create mode 100644 packages/cli/snap-tests/check-fix/steps.json diff --git a/packages/cli/binding/src/cli.rs b/packages/cli/binding/src/cli.rs index 74a4ff715c..0f852e4106 100644 --- a/packages/cli/binding/src/cli.rs +++ b/packages/cli/binding/src/cli.rs @@ -99,6 +99,9 @@ pub enum SynthesizableSubcommand { }, /// Run format, lint, and type checks Check { + /// Auto-fix format and lint issues + #[arg(long)] + fix: bool, /// Skip format check #[arg(long = "no-fmt")] no_fmt: bool, @@ -668,32 +671,6 @@ impl UserConfigLoader for VitePlusConfigLoader { } } -/// Create auto-install synthetic plan request -async fn create_install_synthetic_request( - cwd: &AbsolutePathBuf, -) -> Result { - let package_manager = vite_install::PackageManager::builder(cwd).build_with_default().await?; - let resolve_command = package_manager.resolve_install_command(&vec![]); - - let mut envs: FxHashMap, Arc> = std::env::vars_os() - .map(|(k, v)| (Arc::from(k.as_os_str()), Arc::from(v.as_os_str()))) - .collect(); - - for (k, v) in resolve_command.envs { - envs.insert(Arc::from(OsStr::new(&k)), Arc::from(OsStr::new(&v))); - } - - Ok(SyntheticPlanRequest { - program: Arc::::from(OsStr::new(&resolve_command.bin_path).to_os_string()), - args: resolve_command.args.into_iter().map(Str::from).collect(), - cache_config: UserCacheConfig::with_config(EnabledCacheConfig { - envs: None, - pass_through_envs: None, - }), - envs: Arc::new(envs), - }) -} - /// Resolve a single subcommand and execute it, returning its exit status. async fn resolve_and_execute( resolver: &mut SubcommandResolver, @@ -758,14 +735,20 @@ async fn execute_direct_subcommand( let cwd_arc: Arc = cwd.clone().into(); let status = match subcommand { - SynthesizableSubcommand::Check { no_fmt, no_lint, no_type_aware, no_type_check } => { + SynthesizableSubcommand::Check { fix, no_fmt, no_lint, no_type_aware, no_type_check } => { let mut status = ExitStatus::SUCCESS; if !no_fmt { - output::info("vp fmt --check"); + let args = if fix { vec![] } else { vec!["--check".to_string()] }; + if args.is_empty() { + output::info("vp fmt"); + } else { + let cmd = vite_str::format!("vp fmt {}", args.join(" ")); + output::info(&cmd); + } status = resolve_and_execute( &mut resolver, - SynthesizableSubcommand::Fmt { args: vec!["--check".to_string()] }, + SynthesizableSubcommand::Fmt { args }, &envs, cwd, &cwd_arc, @@ -779,6 +762,9 @@ async fn execute_direct_subcommand( if !no_lint { let mut args = Vec::new(); + if fix { + args.push("--fix".to_string()); + } if !no_type_aware { args.push("--type-aware".to_string()); // --type-check requires --type-aware as prerequisite diff --git a/packages/cli/snap-tests/check-fix/package.json b/packages/cli/snap-tests/check-fix/package.json new file mode 100644 index 0000000000..4649456492 --- /dev/null +++ b/packages/cli/snap-tests/check-fix/package.json @@ -0,0 +1,5 @@ +{ + "name": "check-fix", + "version": "0.0.0", + "private": true +} diff --git a/packages/cli/snap-tests/check-fix/snap.txt b/packages/cli/snap-tests/check-fix/snap.txt new file mode 100644 index 0000000000..fff55e3243 --- /dev/null +++ b/packages/cli/snap-tests/check-fix/snap.txt @@ -0,0 +1,5 @@ +> vp check --fix +info: vp fmt +info: vp lint --fix --type-aware --type-check +Found 0 warnings and 0 errors. +Finished in ms on 1 file with rules using threads. diff --git a/packages/cli/snap-tests/check-fix/src/index.js b/packages/cli/snap-tests/check-fix/src/index.js new file mode 100644 index 0000000000..eed174aaac --- /dev/null +++ b/packages/cli/snap-tests/check-fix/src/index.js @@ -0,0 +1,5 @@ +function hello( ) { + return "hello" +} + +export { hello } diff --git a/packages/cli/snap-tests/check-fix/steps.json b/packages/cli/snap-tests/check-fix/steps.json new file mode 100644 index 0000000000..b09548d9eb --- /dev/null +++ b/packages/cli/snap-tests/check-fix/steps.json @@ -0,0 +1,6 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": ["vp check --fix"] +} diff --git a/rfcs/check-command.md b/rfcs/check-command.md index e47bb90fcb..0ac8b515f7 100644 --- a/rfcs/check-command.md +++ b/rfcs/check-command.md @@ -33,6 +33,10 @@ Pain points: # Run all fast checks (fmt --check + lint --type-aware --type-check) vp check +# Auto-fix format and lint issues +vp check --fix +vp check --fix --no-lint # Only fix formatting + # Disable specific checks vp check --no-fmt vp check --no-lint @@ -44,6 +48,7 @@ vp check --no-type-check | Flag | Default | Description | | ---------------------------------- | ------- | ------------------------------------------------------- | +| `--fix` | OFF | Auto-fix format and lint issues | | `--fmt` / `--no-fmt` | ON | Run format check (`vp fmt --check`) | | `--lint` / `--no-lint` | ON | Run lint check (`vp lint`) | | `--type-aware` / `--no-type-aware` | ON | Enable type-aware lint rules (oxlint `--type-aware`) | @@ -59,8 +64,6 @@ Both are enabled by default in `vp check` to provide comprehensive static analys ## Behavior -### Alpha (Non-Parallel, Sequential) - Commands run **sequentially** with fail-fast semantics: ``` @@ -70,32 +73,23 @@ Commands run **sequentially** with fail-fast semantics: If any step fails, `vp check` exits immediately with a non-zero exit code. -### Future (Parallel) - -In a later iteration, steps could run in parallel and aggregate errors. - ## Decisions -### Verify only, no auto-fix +### Dual mode: verify and fix -`vp check` is a **read-only verification** command. It never modifies files. +By default, `vp check` is a **read-only verification** command. It never modifies files: - `vp fmt --check` reports unformatted files (doesn't auto-format) -- `vp lint` reports issues (doesn't auto-fix) -- To fix issues, users run `vp fmt` and `vp lint --fix` separately +- `vp lint --type-aware --type-check` reports issues (doesn't auto-fix) This keeps `vp check` safe for CI and predictable for local dev. -### No `--fix` flag (alpha) +With `--fix`, `vp check` switches to **auto-fix** mode: -For simplicity in the alpha, there's no `--fix` flag. Users compose fixes manually: - -```bash -vp fmt && vp lint --fix # fix what's fixable -vp check # verify everything passes -``` +- `vp fmt` auto-formats files +- `vp lint --fix --type-aware --type-check` auto-fixes lint issues -A `--fix` flag could be added later if needed. +This replaces the manual `vp fmt && vp lint --fix` workflow with a single command. ### No tests @@ -164,6 +158,7 @@ Options: | `vp test` | Run test suite | Slow | | `vp build` | Build project | Slow | | **`vp check`** | **fmt --check + lint --type-aware --type-check** | **Fast** | +| **`vp check --fix`** | **fmt + lint --fix --type-aware --type-check** | **Fast** | With `vp check`, the monorepo template's "ready" script simplifies to: @@ -203,7 +198,5 @@ packages/cli/snap-tests/check-no-fmt/ ## Future Enhancements -- Parallel execution of fmt and lint -- `--fix` flag to auto-fix fixable issues - Workspace-aware mode (`vp check -r`) running checks across all packages - Integration with `vp run` task graph for caching From a096b4619ffe3c36555d3094cee463c4682a5f59 Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 27 Feb 2026 15:47:28 +0800 Subject: [PATCH 06/12] fix(cli): re-run fmt after lint --fix to ensure consistent formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lint fixes (e.g. curly rule adding braces) can break formatting. Now `vp check --fix` runs: fmt โ†’ lint --fix โ†’ fmt, so a subsequent `vp check` always passes. Add check-fix-reformat snap test to verify the curly rule scenario. --- packages/cli/binding/src/cli.rs | 18 ++++++++++++++++++ .../check-fix-reformat/.oxlintrc.json | 5 +++++ .../snap-tests/check-fix-reformat/package.json | 5 +++++ .../cli/snap-tests/check-fix-reformat/snap.txt | 15 +++++++++++++++ .../snap-tests/check-fix-reformat/src/index.js | 6 ++++++ .../snap-tests/check-fix-reformat/steps.json | 9 +++++++++ packages/cli/snap-tests/check-fix/snap.txt | 1 + 7 files changed, 59 insertions(+) create mode 100644 packages/cli/snap-tests/check-fix-reformat/.oxlintrc.json create mode 100644 packages/cli/snap-tests/check-fix-reformat/package.json create mode 100644 packages/cli/snap-tests/check-fix-reformat/snap.txt create mode 100644 packages/cli/snap-tests/check-fix-reformat/src/index.js create mode 100644 packages/cli/snap-tests/check-fix-reformat/steps.json diff --git a/packages/cli/binding/src/cli.rs b/packages/cli/binding/src/cli.rs index 0f852e4106..9791a2bc13 100644 --- a/packages/cli/binding/src/cli.rs +++ b/packages/cli/binding/src/cli.rs @@ -786,6 +786,24 @@ async fn execute_direct_subcommand( &cwd_arc, ) .await?; + if status != ExitStatus::SUCCESS { + resolver.cleanup_temp_files().await; + return Ok(status); + } + } + + // Re-run fmt after lint --fix, since lint fixes can break formatting + // (e.g. the curly rule adding braces to if-statements) + if fix && !no_fmt && !no_lint { + output::info("vp fmt"); + status = resolve_and_execute( + &mut resolver, + SynthesizableSubcommand::Fmt { args: vec![] }, + &envs, + cwd, + &cwd_arc, + ) + .await?; } status diff --git a/packages/cli/snap-tests/check-fix-reformat/.oxlintrc.json b/packages/cli/snap-tests/check-fix-reformat/.oxlintrc.json new file mode 100644 index 0000000000..5718c8a663 --- /dev/null +++ b/packages/cli/snap-tests/check-fix-reformat/.oxlintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "curly": "error" + } +} diff --git a/packages/cli/snap-tests/check-fix-reformat/package.json b/packages/cli/snap-tests/check-fix-reformat/package.json new file mode 100644 index 0000000000..b48a3022ea --- /dev/null +++ b/packages/cli/snap-tests/check-fix-reformat/package.json @@ -0,0 +1,5 @@ +{ + "name": "check-fix-reformat", + "version": "0.0.0", + "private": true +} diff --git a/packages/cli/snap-tests/check-fix-reformat/snap.txt b/packages/cli/snap-tests/check-fix-reformat/snap.txt new file mode 100644 index 0000000000..daa0ae9f01 --- /dev/null +++ b/packages/cli/snap-tests/check-fix-reformat/snap.txt @@ -0,0 +1,15 @@ +> vp check --fix +info: vp fmt +info: vp lint --fix --type-aware --type-check +Found 0 warnings and 0 errors. +Finished in ms on 1 file with rules using threads. +info: vp fmt + +> vp check # should pass after fix +info: vp fmt --check +Checking formatting... +All matched files use the correct format. +Finished in ms on 4 files using threads. +info: vp lint --type-aware --type-check +Found 0 warnings and 0 errors. +Finished in ms on 1 file with rules using threads. diff --git a/packages/cli/snap-tests/check-fix-reformat/src/index.js b/packages/cli/snap-tests/check-fix-reformat/src/index.js new file mode 100644 index 0000000000..37d52f8389 --- /dev/null +++ b/packages/cli/snap-tests/check-fix-reformat/src/index.js @@ -0,0 +1,6 @@ +function hello(x) { + if (x) return "hello"; + return "world"; +} + +export { hello }; diff --git a/packages/cli/snap-tests/check-fix-reformat/steps.json b/packages/cli/snap-tests/check-fix-reformat/steps.json new file mode 100644 index 0000000000..d6b758b416 --- /dev/null +++ b/packages/cli/snap-tests/check-fix-reformat/steps.json @@ -0,0 +1,9 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp check --fix", + "vp check # should pass after fix" + ] +} diff --git a/packages/cli/snap-tests/check-fix/snap.txt b/packages/cli/snap-tests/check-fix/snap.txt index fff55e3243..9a072d5d5c 100644 --- a/packages/cli/snap-tests/check-fix/snap.txt +++ b/packages/cli/snap-tests/check-fix/snap.txt @@ -3,3 +3,4 @@ info: vp lint --fix --type-aware --type-check Found 0 warnings and 0 errors. Finished in ms on 1 file with rules using threads. +info: vp fmt From bd816a62702844aee496aae5b2c3d92e638ca278 Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 27 Feb 2026 15:49:40 +0800 Subject: [PATCH 07/12] chore: omit info log for the post-lint reformat step in vp check --fix --- packages/cli/binding/src/cli.rs | 1 - packages/cli/snap-tests/check-fix-reformat/snap.txt | 1 - packages/cli/snap-tests/check-fix/snap.txt | 1 - 3 files changed, 3 deletions(-) diff --git a/packages/cli/binding/src/cli.rs b/packages/cli/binding/src/cli.rs index 9791a2bc13..c426097a8c 100644 --- a/packages/cli/binding/src/cli.rs +++ b/packages/cli/binding/src/cli.rs @@ -795,7 +795,6 @@ async fn execute_direct_subcommand( // Re-run fmt after lint --fix, since lint fixes can break formatting // (e.g. the curly rule adding braces to if-statements) if fix && !no_fmt && !no_lint { - output::info("vp fmt"); status = resolve_and_execute( &mut resolver, SynthesizableSubcommand::Fmt { args: vec![] }, diff --git a/packages/cli/snap-tests/check-fix-reformat/snap.txt b/packages/cli/snap-tests/check-fix-reformat/snap.txt index daa0ae9f01..e9821197c1 100644 --- a/packages/cli/snap-tests/check-fix-reformat/snap.txt +++ b/packages/cli/snap-tests/check-fix-reformat/snap.txt @@ -3,7 +3,6 @@ info: vp lint --fix --type-aware --type-check Found 0 warnings and 0 errors. Finished in ms on 1 file with rules using threads. -info: vp fmt > vp check # should pass after fix info: vp fmt --check diff --git a/packages/cli/snap-tests/check-fix/snap.txt b/packages/cli/snap-tests/check-fix/snap.txt index 9a072d5d5c..fff55e3243 100644 --- a/packages/cli/snap-tests/check-fix/snap.txt +++ b/packages/cli/snap-tests/check-fix/snap.txt @@ -3,4 +3,3 @@ info: vp lint --fix --type-aware --type-check Found 0 warnings and 0 errors. Finished in ms on 1 file with rules using threads. -info: vp fmt From ff102586a44b4d7b996c7b0267cb38caeaf1a76a Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 27 Feb 2026 15:51:15 +0800 Subject: [PATCH 08/12] chore: remove Future Enhancements section from check-command RFC --- rfcs/check-command.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/rfcs/check-command.md b/rfcs/check-command.md index 0ac8b515f7..841951a226 100644 --- a/rfcs/check-command.md +++ b/rfcs/check-command.md @@ -196,7 +196,3 @@ packages/cli/snap-tests/check-no-fmt/ snap.txt # Only lint runs ``` -## Future Enhancements - -- Workspace-aware mode (`vp check -r`) running checks across all packages -- Integration with `vp run` task graph for caching From 1e43a30ea5bd17df37570974b04975f6224e7e3c Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 27 Feb 2026 15:54:06 +0800 Subject: [PATCH 09/12] chore: remove VITE_DISABLE_AUTO_INSTALL from check-fix snap tests --- packages/cli/snap-tests/check-fix-reformat/steps.json | 3 --- packages/cli/snap-tests/check-fix/steps.json | 3 --- 2 files changed, 6 deletions(-) diff --git a/packages/cli/snap-tests/check-fix-reformat/steps.json b/packages/cli/snap-tests/check-fix-reformat/steps.json index d6b758b416..98b725fd84 100644 --- a/packages/cli/snap-tests/check-fix-reformat/steps.json +++ b/packages/cli/snap-tests/check-fix-reformat/steps.json @@ -1,7 +1,4 @@ { - "env": { - "VITE_DISABLE_AUTO_INSTALL": "1" - }, "commands": [ "vp check --fix", "vp check # should pass after fix" diff --git a/packages/cli/snap-tests/check-fix/steps.json b/packages/cli/snap-tests/check-fix/steps.json index b09548d9eb..d6773eda8d 100644 --- a/packages/cli/snap-tests/check-fix/steps.json +++ b/packages/cli/snap-tests/check-fix/steps.json @@ -1,6 +1,3 @@ { - "env": { - "VITE_DISABLE_AUTO_INSTALL": "1" - }, "commands": ["vp check --fix"] } From 085ac5be15944e46403e6729b8f6e0b82257ba02 Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 27 Feb 2026 15:56:51 +0800 Subject: [PATCH 10/12] docs: use vp check --fix in Git Workflow section of CLAUDE.md --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 27dc6465b1..10f95666d0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -91,7 +91,7 @@ All user-facing output must go through shared output modules instead of raw prin ## Git Workflow -- Run `vp fmt` before committing to format code +- Run `vp check --fix` before committing to format and lint code ## Quick Reference From 9b8ceea48a1a5d50047454ed9381158f6997cd51 Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 27 Feb 2026 16:12:49 +0800 Subject: [PATCH 11/12] feat(cli): support file path arguments in vp check for lint-staged Add trailing file path arguments to `vp check` so lint-staged can pass staged files directly (e.g., `vp check --fix src/a.ts src/b.ts`). When paths are provided: - fmt receives `--no-error-on-unmatched-pattern` + paths - lint receives paths directly - The post-lint reformat step also receives the same flags Simplify lint-staged config from two separate commands to a single `vp check --fix`. --- package.json | 9 ++---- packages/cli/binding/src/cli.rs | 29 +++++++++++++++++-- .../snap-tests/check-fix-paths/package.json | 5 ++++ .../cli/snap-tests/check-fix-paths/snap.txt | 5 ++++ .../snap-tests/check-fix-paths/src/index.js | 5 ++++ .../cli/snap-tests/check-fix-paths/steps.json | 3 ++ rfcs/check-command.md | 25 +++++++++++++++- 7 files changed, 70 insertions(+), 11 deletions(-) create mode 100644 packages/cli/snap-tests/check-fix-paths/package.json create mode 100644 packages/cli/snap-tests/check-fix-paths/snap.txt create mode 100644 packages/cli/snap-tests/check-fix-paths/src/index.js create mode 100644 packages/cli/snap-tests/check-fix-paths/steps.json diff --git a/package.json b/package.json index a9a620ddf7..b35d64bf3d 100644 --- a/package.json +++ b/package.json @@ -37,13 +37,8 @@ "zod": "catalog:" }, "lint-staged": { - "*.@(js|ts|tsx)": [ - "vp run lint --fix", - "vp fmt --no-error-on-unmatched-pattern" - ], - "*.rs": [ - "cargo fmt --" - ] + "*.@(js|ts|tsx)": "vp check --fix", + "*.rs": "cargo fmt --" }, "engines": { "node": ">=22.18.0" diff --git a/packages/cli/binding/src/cli.rs b/packages/cli/binding/src/cli.rs index c426097a8c..280ceb7487 100644 --- a/packages/cli/binding/src/cli.rs +++ b/packages/cli/binding/src/cli.rs @@ -114,6 +114,9 @@ pub enum SynthesizableSubcommand { /// Disable TypeScript type checking #[arg(long = "no-type-check")] no_type_check: bool, + /// File paths to check (passed through to fmt and lint) + #[arg(trailing_var_arg = true)] + paths: Vec, }, } @@ -735,11 +738,23 @@ async fn execute_direct_subcommand( let cwd_arc: Arc = cwd.clone().into(); let status = match subcommand { - SynthesizableSubcommand::Check { fix, no_fmt, no_lint, no_type_aware, no_type_check } => { + SynthesizableSubcommand::Check { + fix, + no_fmt, + no_lint, + no_type_aware, + no_type_check, + paths, + } => { let mut status = ExitStatus::SUCCESS; + let has_paths = !paths.is_empty(); if !no_fmt { - let args = if fix { vec![] } else { vec!["--check".to_string()] }; + let mut args = if fix { vec![] } else { vec!["--check".to_string()] }; + if has_paths { + args.push("--no-error-on-unmatched-pattern".to_string()); + args.extend(paths.iter().cloned()); + } if args.is_empty() { output::info("vp fmt"); } else { @@ -772,6 +787,9 @@ async fn execute_direct_subcommand( args.push("--type-check".to_string()); } } + if has_paths { + args.extend(paths.iter().cloned()); + } if args.is_empty() { output::info("vp lint"); } else { @@ -795,9 +813,14 @@ async fn execute_direct_subcommand( // Re-run fmt after lint --fix, since lint fixes can break formatting // (e.g. the curly rule adding braces to if-statements) if fix && !no_fmt && !no_lint { + let mut args = Vec::new(); + if has_paths { + args.push("--no-error-on-unmatched-pattern".to_string()); + args.extend(paths.into_iter()); + } status = resolve_and_execute( &mut resolver, - SynthesizableSubcommand::Fmt { args: vec![] }, + SynthesizableSubcommand::Fmt { args }, &envs, cwd, &cwd_arc, diff --git a/packages/cli/snap-tests/check-fix-paths/package.json b/packages/cli/snap-tests/check-fix-paths/package.json new file mode 100644 index 0000000000..91b8e63f55 --- /dev/null +++ b/packages/cli/snap-tests/check-fix-paths/package.json @@ -0,0 +1,5 @@ +{ + "name": "check-fix-paths", + "version": "0.0.0", + "private": true +} diff --git a/packages/cli/snap-tests/check-fix-paths/snap.txt b/packages/cli/snap-tests/check-fix-paths/snap.txt new file mode 100644 index 0000000000..595b934f3c --- /dev/null +++ b/packages/cli/snap-tests/check-fix-paths/snap.txt @@ -0,0 +1,5 @@ +> vp check --fix src/index.js +info: vp fmt --no-error-on-unmatched-pattern src/index.js +info: vp lint --fix --type-aware --type-check src/index.js +Found 0 warnings and 0 errors. +Finished in ms on 1 file with rules using threads. diff --git a/packages/cli/snap-tests/check-fix-paths/src/index.js b/packages/cli/snap-tests/check-fix-paths/src/index.js new file mode 100644 index 0000000000..eed174aaac --- /dev/null +++ b/packages/cli/snap-tests/check-fix-paths/src/index.js @@ -0,0 +1,5 @@ +function hello( ) { + return "hello" +} + +export { hello } diff --git a/packages/cli/snap-tests/check-fix-paths/steps.json b/packages/cli/snap-tests/check-fix-paths/steps.json new file mode 100644 index 0000000000..94a0fa2c34 --- /dev/null +++ b/packages/cli/snap-tests/check-fix-paths/steps.json @@ -0,0 +1,3 @@ +{ + "commands": ["vp check --fix src/index.js"] +} diff --git a/rfcs/check-command.md b/rfcs/check-command.md index 841951a226..573cdab33b 100644 --- a/rfcs/check-command.md +++ b/rfcs/check-command.md @@ -62,6 +62,30 @@ vp check --no-type-check Both are enabled by default in `vp check` to provide comprehensive static analysis. +### File Path Arguments + +`vp check` accepts optional trailing file paths, which are passed through to `fmt` and `lint`: + +```bash +# Check only specific files +vp check --fix src/index.ts src/utils.ts +``` + +When file paths are provided: + +- `--no-error-on-unmatched-pattern` is automatically added to `fmt` args (prevents errors when paths don't match fmt patterns) +- Paths are appended to both `fmt` and `lint` sub-commands + +This enables lint-staged integration: + +```json +"lint-staged": { + "*.@(js|ts|tsx)": "vp check --fix" +} +``` + +lint-staged appends staged file paths automatically, so `vp check --fix` becomes e.g. `vp check --fix src/a.ts src/b.ts`. + ## Behavior Commands run **sequentially** with fail-fast semantics: @@ -195,4 +219,3 @@ packages/cli/snap-tests/check-no-fmt/ steps.json # { "steps": [{ "command": "vp check --no-fmt" }] } snap.txt # Only lint runs ``` - From 9cbf5b35b5af420fbac0ea134acaee04845cc1f0 Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 27 Feb 2026 16:26:51 +0800 Subject: [PATCH 12/12] chore: add markdown and yaml to lint-staged glob pattern --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b35d64bf3d..23a4321c29 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "zod": "catalog:" }, "lint-staged": { - "*.@(js|ts|tsx)": "vp check --fix", + "*.@(js|ts|tsx|md|yaml|yml)": "vp check --fix", "*.rs": "cargo fmt --" }, "engines": {