Skip to content
Draft
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
1 change: 1 addition & 0 deletions crates/vite_global_cli/src/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,7 @@ fn delegated_help_doc(command: &str) -> Option<HelpDoc> {
row("--fix", "Auto-fix format and lint issues"),
row("--no-fmt", "Skip format check"),
row("--no-lint", "Skip lint check"),
row("--no-type-check", "Skip type check"),
row(
"--no-error-on-unmatched-pattern",
"Do not exit with error when pattern is unmatched",
Expand Down
10 changes: 10 additions & 0 deletions docs/guide/check.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ vp check
vp check --fix # Format and run autofixers.
```

## Phase skip flags

`vp check` runs three phases — format, lint rules, and type check — all enabled by default. Each phase can be skipped independently:

| Flag | Skips |
| ---- | ----- |
| `--no-fmt` | Format check |
| `--no-lint` | Lint rules (type check still runs if enabled in config) |
| `--no-type-check` | Type check |

## Configuration

`vp check` uses the same configuration you already define for linting and formatting:
Expand Down
65 changes: 26 additions & 39 deletions packages/cli/binding/src/check/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,41 +39,59 @@ pub(super) struct LintFailure {
pub(super) enum LintMessageKind {
LintOnly,
LintAndTypeCheck,
// Used when lint rules are skipped but type-check still runs.
TypeCheckOnly,
}

impl LintMessageKind {
pub(super) fn from_lint_config(lint_config: Option<&serde_json::Value>) -> Self {
let type_check_enabled = lint_config
.and_then(|config| config.get("options"))
.and_then(|options| options.get("typeCheck"))
.and_then(serde_json::Value::as_bool)
.unwrap_or(false);

if type_check_enabled { Self::LintAndTypeCheck } else { Self::LintOnly }
// Selects a variant from the `(lint, type-check)` enabled tuple callers have
// already resolved, avoiding a second config lookup inside the helper.
pub(super) fn from_flags(lint_enabled: bool, type_check_enabled: bool) -> Self {
match (lint_enabled, type_check_enabled) {
(true, true) => Self::LintAndTypeCheck,
(true, false) => Self::LintOnly,
(false, true) => Self::TypeCheckOnly,
(false, false) => {
unreachable!(
"from_flags called with (false, false) — caller must ensure lint or type-check runs before selecting a message kind"
)
}
}
}

pub(super) fn success_label(self) -> &'static str {
match self {
Self::LintOnly => "Found no warnings or lint errors",
Self::LintAndTypeCheck => "Found no warnings, lint errors, or type errors",
Self::TypeCheckOnly => "Found no type errors",
}
}

pub(super) fn warning_heading(self) -> &'static str {
match self {
Self::LintOnly => "Lint warnings found",
Self::LintAndTypeCheck => "Lint or type warnings found",
Self::TypeCheckOnly => "Type warnings found",
}
}

pub(super) fn issue_heading(self) -> &'static str {
match self {
Self::LintOnly => "Lint issues found",
Self::LintAndTypeCheck => "Lint or type issues found",
Self::TypeCheckOnly => "Type errors found",
}
}
}

pub(super) fn lint_config_type_check_enabled(lint_config: Option<&serde_json::Value>) -> bool {
lint_config
.and_then(|config| config.get("options"))
.and_then(|options| options.get("typeCheck"))
.and_then(serde_json::Value::as_bool)
.unwrap_or(false)
}

fn parse_check_summary(line: &str) -> Option<CheckSummary> {
let rest = line.strip_prefix("Finished in ")?;
let (duration, rest) = rest.split_once(" on ")?;
Expand Down Expand Up @@ -222,34 +240,3 @@ pub(super) fn analyze_lint_output(output: &str) -> Option<Result<LintSuccess, Li

Some(Err(LintFailure { summary, warnings, errors, diagnostics }))
}

#[cfg(test)]
mod tests {
use serde_json::json;

use super::LintMessageKind;

#[test]
fn lint_message_kind_defaults_to_lint_only_without_typecheck() {
assert_eq!(LintMessageKind::from_lint_config(None), LintMessageKind::LintOnly);
assert_eq!(
LintMessageKind::from_lint_config(Some(&json!({ "options": {} }))),
LintMessageKind::LintOnly
);
}

#[test]
fn lint_message_kind_detects_typecheck_from_vite_config() {
let kind = LintMessageKind::from_lint_config(Some(&json!({
"options": {
"typeAware": true,
"typeCheck": true
}
})));

assert_eq!(kind, LintMessageKind::LintAndTypeCheck);
assert_eq!(kind.success_label(), "Found no warnings, lint errors, or type errors");
assert_eq!(kind.warning_heading(), "Lint or type warnings found");
assert_eq!(kind.issue_heading(), "Lint or type issues found");
}
}
95 changes: 78 additions & 17 deletions packages/cli/binding/src/check/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod analysis;
mod sidecar;

use std::{ffi::OsStr, sync::Arc, time::Instant};

Expand All @@ -8,9 +9,13 @@ use vite_path::{AbsolutePath, AbsolutePathBuf};
use vite_shared::output;
use vite_task::ExitStatus;

use self::analysis::{
LintMessageKind, analyze_fmt_check_output, analyze_lint_output, format_count, format_elapsed,
print_error_block, print_pass_line, print_stdout_block, print_summary_line,
use self::{
analysis::{
LintMessageKind, analyze_fmt_check_output, analyze_lint_output, format_count,
format_elapsed, lint_config_type_check_enabled, print_error_block, print_pass_line,
print_stdout_block, print_summary_line,
},
sidecar::write_no_type_check_sidecar,
};
use crate::cli::{
CapturedCommandOutput, SubcommandResolver, SynthesizableSubcommand, resolve_and_capture_output,
Expand All @@ -22,20 +27,13 @@ pub(crate) async fn execute_check(
fix: bool,
no_fmt: bool,
no_lint: bool,
no_type_check: bool,
no_error_on_unmatched_pattern: bool,
paths: Vec<String>,
envs: &Arc<FxHashMap<Arc<OsStr>, Arc<OsStr>>>,
cwd: &AbsolutePathBuf,
cwd_arc: &Arc<AbsolutePath>,
) -> Result<ExitStatus, Error> {
if no_fmt && no_lint {
output::error("No checks enabled");
print_summary_line(
"`vp check` did not run because both `--no-fmt` and `--no-lint` were set",
);
return Ok(ExitStatus(1));
}

let mut status = ExitStatus::SUCCESS;
let has_paths = !paths.is_empty();
// In --fix mode with file paths (the lint-staged use case), implicitly suppress
Expand All @@ -46,6 +44,52 @@ pub(crate) async fn execute_check(
let mut deferred_lint_pass: Option<(String, String)> = None;
let resolved_vite_config = resolver.resolve_universal_vite_config().await?;

// Per-phase enabled booleans derived from the raw flags plus the resolved
// config's `typeCheck` setting. `run_lint_phase` drives whether the lint
// subprocess starts at all — true when lint rules should run, or when
// type-check should run via oxlint's type-check-only path.
let config_type_check_enabled =
lint_config_type_check_enabled(resolved_vite_config.lint.as_ref());
let type_check_enabled = !no_type_check && config_type_check_enabled;
let lint_enabled = !no_lint;
let run_lint_phase = lint_enabled || type_check_enabled;

if no_fmt && !run_lint_phase {
output::error("No checks enabled");
print_summary_line(
"`vp check` did not run because all checks were disabled by the provided flags",
);
return Ok(ExitStatus(1));
}

// Reject `--fix --no-lint` when the project enables type-check. With lint
// rules skipped, oxlint would take the type-check-only path which it
// itself refuses to combine with `--fix`. Running fmt first and then
// hitting that rejection would leave the working tree partially formatted
// (a real hazard inside lint-staged). Failing up-front keeps the
// invocation transactional.
if fix && !lint_enabled && type_check_enabled {
output::error(
"`vp check --fix --no-lint` cannot be combined with type-check enabled in config",
);
print_summary_line(
"type-check diagnostics are read-only and cannot be auto-fixed. Add `--no-type-check` to format-only fix, or drop `--no-lint` to run lint fixes.",
);
return Ok(ExitStatus(1));
}

// Build the `--no-type-check` sidecar up front, before any fmt side effects.
// If temp-dir write fails (full tmpfs, read-only mount, permission denied),
// we surface the error before fmt modifies files, mirroring the
// transactional guarantee of the hard-error guard above. The returned
// guard lives through both fmt and lint phases and is dropped at function
// exit, cleaning up the temp file.
let sidecar = if lint_enabled && no_type_check && config_type_check_enabled {
write_no_type_check_sidecar(&resolved_vite_config)?
} else {
None
};

if !no_fmt {
let mut args = if fix { vec![] } else { vec!["--check".to_string()] };
if suppress_unmatched {
Expand Down Expand Up @@ -109,7 +153,7 @@ pub(crate) async fn execute_check(
}
}

if fix && no_lint && status == ExitStatus::SUCCESS {
if fix && !run_lint_phase && status == ExitStatus::SUCCESS {
print_pass_line(
"Formatting completed for checked files",
Some(&format!("({})", format_elapsed(fmt_start.elapsed()))),
Expand All @@ -127,13 +171,23 @@ pub(crate) async fn execute_check(
}
}

if !no_lint {
let lint_message_kind =
LintMessageKind::from_lint_config(resolved_vite_config.lint.as_ref());
if run_lint_phase {
let lint_message_kind = LintMessageKind::from_flags(lint_enabled, type_check_enabled);
let mut args = Vec::new();
if fix {
// Hard-error guard above rejects (fix && !lint_enabled && type_check_enabled),
// so when this branch runs with `fix`, lint_enabled is always true. The
// `lint_enabled` check is defense-in-depth against future guard changes.
if fix && lint_enabled {
args.push("--fix".to_string());
}
// `--type-check-only` suppresses lint rules and runs only type-check
// diagnostics. oxlint accepts this as a hidden flag (oxc#21184). When
// config `typeCheck` is false this flag forces type-check ON, so we
// only emit it on the `--no-lint` + `typeCheck: true` path and skip
// the lint phase entirely when type_check_enabled is false.
if !lint_enabled && type_check_enabled {
args.push("--type-check-only".to_string());
}
// `vp check` parses oxlint's human-readable summary output to print
// unified pass/fail lines. When `GITHUB_ACTIONS=true`, oxlint auto-switches
// to the GitHub reporter, which omits that summary on success and makes the
Expand All @@ -146,10 +200,17 @@ pub(crate) async fn execute_check(
if has_paths {
args.extend(paths.iter().cloned());
}

// `sidecar` was built up front to surface temp-dir write failures
// before fmt made any changes. Borrow its config here to route oxlint
// through the override when present; otherwise use the resolved
// config unchanged.
let lint_vite_config = sidecar.as_ref().map(|s| &s.config).unwrap_or(&resolved_vite_config);

let captured = resolve_and_capture_output(
resolver,
SynthesizableSubcommand::Lint { args },
Some(&resolved_vite_config),
Some(lint_vite_config),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#1378 The -c parameter specifying oxlint configuration will be removed, so the sidecar configuration approach will have issues. Going forward, the configuration resolution logic for oxlint and oxfmt will no longer be controlled by Vite+, and will be handed back to oxlint/fmt themselves to handle.

cc @leaysgur @camc314 Do you have any better suggestions?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will be handed back to oxlint/fmt themselves to handle.

I think this is the only option.

Otherwise, Vite+ would end up handling everything, including searching for nested configs.
(That might be a valid decision in itself, but as far as I know, there are currently no plans to do so?)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#1378 The -c parameter specifying oxlint configuration will be removed, so the sidecar configuration approach will have issues. Going forward, the configuration resolution logic for oxlint and oxfmt will no longer be controlled by Vite+, and will be handed back to oxlint/fmt themselves to handle.

cc @leaysgur @camc314 Do you have any better suggestions?

Thanks for the heads up on #1378. Since the sidecar + -c approach won't survive that change, what would be the recommended direction for implementing --no-type-check going forward?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fengmk2 what's the use case for the --no-type-check flag?

envs,
cwd,
cwd_arc,
Expand Down
Loading
Loading