Skip to content

Commit 61d318d

Browse files
authored
feat(cli): add vp check command for format, lint, and type checks (#643)
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.
1 parent 131de69 commit 61d318d

62 files changed

Lines changed: 693 additions & 40 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -230,11 +230,8 @@ jobs:
230230
vp --version
231231
vp -h
232232
233-
- name: Run CLI fmt
234-
run: vp fmt --check
235-
236-
- name: Run CLI lint
237-
run: vp run lint
233+
- name: Run CLI check
234+
run: vp check
238235

239236
- name: Test global package install (powershell)
240237
if: ${{ matrix.os == 'windows-latest' }}

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ All user-facing output must go through shared output modules instead of raw prin
9191

9292
## Git Workflow
9393

94-
- Run `vp fmt` before committing to format code
94+
- Run `vp check --fix` before committing to format and lint code
9595

9696
## Quick Reference
9797

crates/vite_global_cli/src/cli.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,14 @@ pub enum Commands {
569569
args: Vec<String>,
570570
},
571571

572+
/// Run format, lint, and type checks
573+
#[command(disable_help_flag = true)]
574+
Check {
575+
/// Additional arguments
576+
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
577+
args: Vec<String>,
578+
},
579+
572580
/// Build library
573581
#[command(disable_help_flag = true)]
574582
Pack {
@@ -1795,6 +1803,8 @@ pub async fn run_command(cwd: AbsolutePathBuf, args: Args) -> Result<ExitStatus,
17951803

17961804
Commands::Fmt { args } => commands::delegate::execute(cwd, "fmt", &args).await,
17971805

1806+
Commands::Check { args } => commands::delegate::execute(cwd, "check", &args).await,
1807+
17981808
Commands::Pack { args } => commands::delegate::execute(cwd, "pack", &args).await,
17991809

18001810
Commands::Run { args } => commands::run_or_delegate::execute(cwd, &args).await,
@@ -1857,6 +1867,7 @@ fn apply_custom_help(cmd: clap::Command) -> clap::Command {
18571867
{bold}test{reset} Run tests
18581868
{bold}lint{reset} Lint code
18591869
{bold}fmt{reset} Format code
1870+
{bold}check{reset} Run format, lint, and type checks
18601871
{bold}pack{reset} Build library
18611872
{bold}run{reset} Run tasks
18621873
{bold}exec{reset} Execute a command from local node_modules/.bin

package.json

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"bootstrap-cli:ci": "pnpm install-global-cli",
1010
"install-global-cli": "tool install-global-cli",
1111
"tsgo": "tsgo -b tsconfig.json",
12-
"lint": "vp lint --type-aware --threads 4",
12+
"lint": "vp lint --type-aware --type-check --threads 4",
1313
"test": "vp test run && pnpm -r snap-test",
1414
"fmt": "vp fmt",
1515
"test:unit": "vp test run",
@@ -37,13 +37,8 @@
3737
"zod": "catalog:"
3838
},
3939
"lint-staged": {
40-
"*.@(js|ts|tsx)": [
41-
"vp run lint --fix",
42-
"vp fmt --no-error-on-unmatched-pattern"
43-
],
44-
"*.rs": [
45-
"cargo fmt --"
46-
]
40+
"*.@(js|ts|tsx|md|yaml|yml)": "vp check --fix",
41+
"*.rs": "cargo fmt --"
4742
},
4843
"engines": {
4944
"node": ">=22.18.0"

packages/cli/AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ This project is using Vite+, a modern toolchain built on top of Vite, Rolldown,
1515
- lint - Lint code
1616
- test - Run tests
1717
- fmt - Format code
18+
- check - Run format, lint, and type checks
1819
- lib - Build library
1920
- migrate - Migrate an existing project to Vite+
2021
- create - Create a new monorepo package (in-project) or a new project (global)

packages/cli/binding/src/cli.rs

Lines changed: 161 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize};
1111
use tokio::fs::write;
1212
use vite_error::Error;
1313
use vite_path::{AbsolutePath, AbsolutePathBuf};
14-
use vite_shared::{PrependOptions, prepend_to_path_env};
14+
use vite_shared::{PrependOptions, output, prepend_to_path_env};
1515
use vite_str::Str;
1616
use vite_task::{
1717
Command, CommandHandler, ExitStatus, HandledCommand, ScriptCommand, Session, SessionCallbacks,
@@ -97,6 +97,27 @@ pub enum SynthesizableSubcommand {
9797
#[clap(allow_hyphen_values = true, trailing_var_arg = true)]
9898
args: Vec<String>,
9999
},
100+
/// Run format, lint, and type checks
101+
Check {
102+
/// Auto-fix format and lint issues
103+
#[arg(long)]
104+
fix: bool,
105+
/// Skip format check
106+
#[arg(long = "no-fmt")]
107+
no_fmt: bool,
108+
/// Skip lint check
109+
#[arg(long = "no-lint")]
110+
no_lint: bool,
111+
/// Disable type-aware linting
112+
#[arg(long = "no-type-aware")]
113+
no_type_aware: bool,
114+
/// Disable TypeScript type checking
115+
#[arg(long = "no-type-check")]
116+
no_type_check: bool,
117+
/// File paths to check (passed through to fmt and lint)
118+
#[arg(trailing_var_arg = true)]
119+
paths: Vec<String>,
120+
},
100121
}
101122

102123
/// Top-level CLI argument parser for vite-plus.
@@ -494,6 +515,11 @@ impl SubcommandResolver {
494515
envs: merge_resolved_envs(envs, resolved.envs),
495516
})
496517
}
518+
SynthesizableSubcommand::Check { .. } => {
519+
anyhow::bail!(
520+
"Check is a composite command and cannot be resolved to a single subcommand"
521+
);
522+
}
497523
SynthesizableSubcommand::Install { args } => {
498524
let package_manager =
499525
vite_install::PackageManager::builder(cwd).build_with_default().await?;
@@ -589,6 +615,10 @@ impl CommandHandler for VitePlusCommandHandler {
589615
let cli_args =
590616
CLIArgs::try_parse_from(iter::once("vp").chain(command.args.iter().map(Str::as_str)))?;
591617
match cli_args {
618+
CLIArgs::Synthesizable(SynthesizableSubcommand::Check { .. }) => {
619+
// Check is a composite command — run as a subprocess in task scripts
620+
Ok(HandledCommand::Verbatim)
621+
}
592622
CLIArgs::Synthesizable(subcmd) => {
593623
let resolved = self.resolver.resolve(subcmd, &command.envs, &command.cwd).await?;
594624
Ok(HandledCommand::Synthesized(resolved.into_synthetic_plan_request()))
@@ -644,31 +674,16 @@ impl UserConfigLoader for VitePlusConfigLoader {
644674
}
645675
}
646676

647-
/// Execute a synthesizable subcommand directly (not through vite-task Session).
648-
/// No caching, no task graph, no dependency resolution.
649-
async fn execute_direct_subcommand(
677+
/// Resolve a single subcommand and execute it, returning its exit status.
678+
async fn resolve_and_execute(
679+
resolver: &mut SubcommandResolver,
650680
subcommand: SynthesizableSubcommand,
681+
envs: &Arc<FxHashMap<Arc<OsStr>, Arc<OsStr>>>,
651682
cwd: &AbsolutePathBuf,
652-
options: Option<CliOptions>,
683+
cwd_arc: &Arc<AbsolutePath>,
653684
) -> Result<ExitStatus, Error> {
654-
let (workspace_root, _) = vite_workspace::find_workspace_root(cwd)?;
655-
let workspace_path: Arc<AbsolutePath> = workspace_root.path.into();
656-
657-
let mut resolver = if let Some(options) = options {
658-
SubcommandResolver::new(Arc::clone(&workspace_path)).with_cli_options(options)
659-
} else {
660-
SubcommandResolver::new(Arc::clone(&workspace_path))
661-
};
662-
663-
let envs: Arc<FxHashMap<Arc<OsStr>, Arc<OsStr>>> = Arc::new(
664-
std::env::vars_os()
665-
.map(|(k, v)| (Arc::from(k.as_os_str()), Arc::from(v.as_os_str())))
666-
.collect(),
667-
);
668-
let cwd_arc: Arc<AbsolutePath> = cwd.clone().into();
669-
670685
let resolved =
671-
resolver.resolve(subcommand, &envs, &cwd_arc).await.map_err(|e| Error::Anyhow(e))?;
686+
resolver.resolve(subcommand, envs, cwd_arc).await.map_err(|e| Error::Anyhow(e))?;
672687

673688
// Resolve the program path using `which` to handle Windows .cmd/.bat files (PATHEXT)
674689
let program_path = {
@@ -695,11 +710,132 @@ async fn execute_direct_subcommand(
695710
let mut child = cmd.spawn().map_err(|e| Error::Anyhow(e.into()))?;
696711

697712
let status = child.wait().await;
713+
let status = status.map_err(|e| Error::Anyhow(e.into()))?;
714+
Ok(ExitStatus(status.code().unwrap_or(1) as u8))
715+
}
716+
717+
/// Execute a synthesizable subcommand directly (not through vite-task Session).
718+
/// No caching, no task graph, no dependency resolution.
719+
async fn execute_direct_subcommand(
720+
subcommand: SynthesizableSubcommand,
721+
cwd: &AbsolutePathBuf,
722+
options: Option<CliOptions>,
723+
) -> Result<ExitStatus, Error> {
724+
let (workspace_root, _) = vite_workspace::find_workspace_root(cwd)?;
725+
let workspace_path: Arc<AbsolutePath> = workspace_root.path.into();
726+
727+
let mut resolver = if let Some(options) = options {
728+
SubcommandResolver::new(Arc::clone(&workspace_path)).with_cli_options(options)
729+
} else {
730+
SubcommandResolver::new(Arc::clone(&workspace_path))
731+
};
732+
733+
let envs: Arc<FxHashMap<Arc<OsStr>, Arc<OsStr>>> = Arc::new(
734+
std::env::vars_os()
735+
.map(|(k, v)| (Arc::from(k.as_os_str()), Arc::from(v.as_os_str())))
736+
.collect(),
737+
);
738+
let cwd_arc: Arc<AbsolutePath> = cwd.clone().into();
739+
740+
let status = match subcommand {
741+
SynthesizableSubcommand::Check {
742+
fix,
743+
no_fmt,
744+
no_lint,
745+
no_type_aware,
746+
no_type_check,
747+
paths,
748+
} => {
749+
let mut status = ExitStatus::SUCCESS;
750+
let has_paths = !paths.is_empty();
751+
752+
if !no_fmt {
753+
let mut args = if fix { vec![] } else { vec!["--check".to_string()] };
754+
if has_paths {
755+
args.push("--no-error-on-unmatched-pattern".to_string());
756+
args.extend(paths.iter().cloned());
757+
}
758+
if args.is_empty() {
759+
output::info("vp fmt");
760+
} else {
761+
let cmd = vite_str::format!("vp fmt {}", args.join(" "));
762+
output::info(&cmd);
763+
}
764+
status = resolve_and_execute(
765+
&mut resolver,
766+
SynthesizableSubcommand::Fmt { args },
767+
&envs,
768+
cwd,
769+
&cwd_arc,
770+
)
771+
.await?;
772+
if status != ExitStatus::SUCCESS {
773+
resolver.cleanup_temp_files().await;
774+
return Ok(status);
775+
}
776+
}
777+
778+
if !no_lint {
779+
let mut args = Vec::new();
780+
if fix {
781+
args.push("--fix".to_string());
782+
}
783+
if !no_type_aware {
784+
args.push("--type-aware".to_string());
785+
// --type-check requires --type-aware as prerequisite
786+
if !no_type_check {
787+
args.push("--type-check".to_string());
788+
}
789+
}
790+
if has_paths {
791+
args.extend(paths.iter().cloned());
792+
}
793+
if args.is_empty() {
794+
output::info("vp lint");
795+
} else {
796+
let cmd = vite_str::format!("vp lint {}", args.join(" "));
797+
output::info(&cmd);
798+
}
799+
status = resolve_and_execute(
800+
&mut resolver,
801+
SynthesizableSubcommand::Lint { args },
802+
&envs,
803+
cwd,
804+
&cwd_arc,
805+
)
806+
.await?;
807+
if status != ExitStatus::SUCCESS {
808+
resolver.cleanup_temp_files().await;
809+
return Ok(status);
810+
}
811+
}
812+
813+
// Re-run fmt after lint --fix, since lint fixes can break formatting
814+
// (e.g. the curly rule adding braces to if-statements)
815+
if fix && !no_fmt && !no_lint {
816+
let mut args = Vec::new();
817+
if has_paths {
818+
args.push("--no-error-on-unmatched-pattern".to_string());
819+
args.extend(paths.into_iter());
820+
}
821+
status = resolve_and_execute(
822+
&mut resolver,
823+
SynthesizableSubcommand::Fmt { args },
824+
&envs,
825+
cwd,
826+
&cwd_arc,
827+
)
828+
.await?;
829+
}
830+
831+
status
832+
}
833+
other => resolve_and_execute(&mut resolver, other, &envs, cwd, &cwd_arc).await?,
834+
};
698835

699836
resolver.cleanup_temp_files().await;
700837

701-
let status = status.map_err(|e| Error::Anyhow(e.into()))?;
702-
Ok(ExitStatus(status.code().unwrap_or(1) as u8))
838+
Ok(status)
703839
}
704840

705841
/// Execute a vite-task command (run, cache) through Session.
@@ -817,6 +953,7 @@ fn print_help() {
817953
{bold}test{reset} Run tests
818954
{bold}lint{reset} Lint code
819955
{bold}fmt{reset} Format code
956+
{bold}check{reset} Run format, lint, and type checks
820957
{bold}pack{reset} Build library
821958
{bold}run{reset} Run tasks
822959
{bold}exec{reset} Execute a command from local node_modules/.bin

packages/cli/snap-tests-global/cli-helper-message/snap.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Core Commands:
1010
test Run tests
1111
lint Lint code
1212
fmt Format code
13+
check Run format, lint, and type checks
1314
pack Build library
1415
run Run tasks
1516
exec Execute a command from local node_modules/.bin
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "check-all-skipped",
3+
"version": "0.0.0",
4+
"private": true
5+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
> vp check --no-fmt --no-lint
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"env": {
3+
"VITE_DISABLE_AUTO_INSTALL": "1"
4+
},
5+
"commands": ["vp check --no-fmt --no-lint"]
6+
}

0 commit comments

Comments
 (0)