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' }} 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 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/package.json b/package.json index e78eadf773..23a4321c29 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", @@ -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|md|yaml|yml)": "vp check --fix", + "*.rs": "cargo fmt --" }, "engines": { "node": ">=22.18.0" 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..280ceb7487 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,27 @@ pub enum SynthesizableSubcommand { #[clap(allow_hyphen_values = true, trailing_var_arg = true)] args: Vec, }, + /// 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, + /// 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, + /// File paths to check (passed through to fmt and lint) + #[arg(trailing_var_arg = true)] + paths: Vec, + }, } /// Top-level CLI argument parser for vite-plus. @@ -494,6 +515,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 +615,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 +674,16 @@ 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( +/// 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, - options: Option, + cwd_arc: &Arc, ) -> 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 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 +710,132 @@ 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 { + 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 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 { + let cmd = vite_str::format!("vp fmt {}", args.join(" ")); + output::info(&cmd); + } + status = resolve_and_execute( + &mut resolver, + SynthesizableSubcommand::Fmt { args }, + &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 fix { + args.push("--fix".to_string()); + } + 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 has_paths { + args.extend(paths.iter().cloned()); + } + 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?; + 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 { + 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 }, + &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 +953,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-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/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..e9821197c1 --- /dev/null +++ b/packages/cli/snap-tests/check-fix-reformat/snap.txt @@ -0,0 +1,14 @@ +> 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. + +> 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..98b725fd84 --- /dev/null +++ b/packages/cli/snap-tests/check-fix-reformat/steps.json @@ -0,0 +1,6 @@ +{ + "commands": [ + "vp check --fix", + "vp check # should pass after fix" + ] +} 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..d6773eda8d --- /dev/null +++ b/packages/cli/snap-tests/check-fix/steps.json @@ -0,0 +1,3 @@ +{ + "commands": ["vp check --fix"] +} 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/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; diff --git a/rfcs/check-command.md b/rfcs/check-command.md new file mode 100644 index 0000000000..573cdab33b --- /dev/null +++ b/rfcs/check-command.md @@ -0,0 +1,221 @@ +# 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 + +# 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 +vp check --no-type-aware +vp check --no-type-check +``` + +### Options + +| 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`) | +| `--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. + +### 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: + +``` +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. + +## Decisions + +### Dual mode: verify and fix + +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 --type-aware --type-check` reports issues (doesn't auto-fix) + +This keeps `vp check` safe for CI and predictable for local dev. + +With `--fix`, `vp check` switches to **auto-fix** mode: + +- `vp fmt` auto-formats files +- `vp lint --fix --type-aware --type-check` auto-fixes lint issues + +This replaces the manual `vp fmt && vp lint --fix` workflow with a single command. + +### 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** | +| **`vp check --fix`** | **fmt + lint --fix --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 +``` 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',