Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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' }}
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
11 changes: 11 additions & 0 deletions crates/vite_global_cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,14 @@ pub enum Commands {
args: Vec<String>,
},

/// 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<String>,
},

/// Build library
#[command(disable_help_flag = true)]
Pack {
Expand Down Expand Up @@ -1795,6 +1803,8 @@ pub async fn run_command(cwd: AbsolutePathBuf, args: Args) -> Result<ExitStatus,

Commands::Fmt { args } => 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,
Expand Down Expand Up @@ -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
Expand Down
11 changes: 3 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions packages/cli/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
185 changes: 161 additions & 24 deletions packages/cli/binding/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -97,6 +97,27 @@ pub enum SynthesizableSubcommand {
#[clap(allow_hyphen_values = true, trailing_var_arg = true)]
args: Vec<String>,
},
/// 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<String>,
},
}

/// Top-level CLI argument parser for vite-plus.
Expand Down Expand Up @@ -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?;
Expand Down Expand Up @@ -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()))
Expand Down Expand Up @@ -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<FxHashMap<Arc<OsStr>, Arc<OsStr>>>,
cwd: &AbsolutePathBuf,
options: Option<CliOptions>,
cwd_arc: &Arc<AbsolutePath>,
) -> Result<ExitStatus, Error> {
let (workspace_root, _) = vite_workspace::find_workspace_root(cwd)?;
let workspace_path: Arc<AbsolutePath> = 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<FxHashMap<Arc<OsStr>, Arc<OsStr>>> = 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<AbsolutePath> = 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 = {
Expand All @@ -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<CliOptions>,
) -> Result<ExitStatus, Error> {
let (workspace_root, _) = vite_workspace::find_workspace_root(cwd)?;
let workspace_path: Arc<AbsolutePath> = 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<FxHashMap<Arc<OsStr>, Arc<OsStr>>> = 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<AbsolutePath> = 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.
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/cli/snap-tests-global/cli-helper-message/snap.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/snap-tests/check-all-skipped/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "check-all-skipped",
"version": "0.0.0",
"private": true
}
1 change: 1 addition & 0 deletions packages/cli/snap-tests/check-all-skipped/snap.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
> vp check --no-fmt --no-lint
6 changes: 6 additions & 0 deletions packages/cli/snap-tests/check-all-skipped/steps.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"env": {
"VITE_DISABLE_AUTO_INSTALL": "1"
},
"commands": ["vp check --no-fmt --no-lint"]
}
5 changes: 5 additions & 0 deletions packages/cli/snap-tests/check-fail-fast/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "check-fail-fast",
"version": "0.0.0",
"private": true
}
7 changes: 7 additions & 0 deletions packages/cli/snap-tests/check-fail-fast/snap.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[1]> vp check
info: vp fmt --check
Checking formatting...
src/index.js (<variable>ms)

Format issues found in above 1 files. Run without `--check` to fix.
Finished in <variable>ms on 3 files using <variable> threads.
6 changes: 6 additions & 0 deletions packages/cli/snap-tests/check-fail-fast/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
function hello( ) {
eval( "code" )
return "hello"
}

export { hello }
6 changes: 6 additions & 0 deletions packages/cli/snap-tests/check-fail-fast/steps.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"env": {
"VITE_DISABLE_AUTO_INSTALL": "1"
},
"commands": ["vp check"]
}
5 changes: 5 additions & 0 deletions packages/cli/snap-tests/check-fix-paths/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "check-fix-paths",
"version": "0.0.0",
"private": true
}
5 changes: 5 additions & 0 deletions packages/cli/snap-tests/check-fix-paths/snap.txt
Original file line number Diff line number Diff line change
@@ -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 <variable>ms on 1 file with <variable> rules using <variable> threads.
5 changes: 5 additions & 0 deletions packages/cli/snap-tests/check-fix-paths/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function hello( ) {
return "hello"
}

export { hello }
Loading
Loading