Skip to content
Open
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
4 changes: 3 additions & 1 deletion crates/vite_task/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ pub mod session;

// Public exports for vite_task_bin
pub use cli::{CacheSubcommand, Command, RunCommand, RunFlags};
pub use session::{CommandHandler, ExitStatus, HandledCommand, Session, SessionConfig};
pub use session::{
CommandHandler, ExitStatus, HandledCommand, Session, SessionConfig, TaskErrorHints,
};
pub use vite_task_graph::{
config::{
self,
Expand Down
114 changes: 108 additions & 6 deletions crates/vite_task/src/session/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@ mod execute;
pub(crate) mod reporter;

// Re-export types that are part of the public API
use std::{ffi::OsStr, fmt::Debug, io::IsTerminal, sync::Arc};
use std::{
ffi::OsStr,
fmt::Debug,
io::{IsTerminal, Write as _},
sync::Arc,
};

use cache::ExecutionCache;
pub use cache::{CacheMiss, FingerprintMismatch};
use clap::Parser as _;
use once_cell::sync::OnceCell;
use owo_colors::{OwoColorize as _, Stream, Style};
pub use reporter::ExitStatus;
use reporter::{
GroupedReporterBuilder, InterleavedReporterBuilder, LabeledReporterBuilder,
Expand Down Expand Up @@ -80,6 +86,11 @@ impl TaskGraphLoader for LazyTaskGraph<'_> {
}
}

pub struct TaskErrorHints {
pub task_list_hint: Str,
pub task_source_hint: Str,
}

pub struct SessionConfig<'a> {
pub command_handler: &'a mut (dyn CommandHandler + 'a),
pub user_config_loader: &'a mut (dyn UserConfigLoader + 'a),
Expand Down Expand Up @@ -110,6 +121,9 @@ pub trait CommandHandler: Debug {
&mut self,
command: &mut ScriptCommand,
) -> anyhow::Result<HandledCommand>;

/// Returns hints shown to the user when a task is not found.
fn task_error_hints(&self) -> TaskErrorHints;
}

#[derive(derive_more::Debug)]
Expand Down Expand Up @@ -165,6 +179,7 @@ pub struct Session<'a> {
plan_request_parser: PlanRequestParser<'a>,

program_name: Str,
task_error_hints: TaskErrorHints,

/// Cache is lazily initialized to avoid `SQLite` race conditions when multiple
/// processes (e.g., parallel `vt lib` commands) start simultaneously.
Expand Down Expand Up @@ -227,6 +242,7 @@ impl<'a> Session<'a> {
prepend_path_env(&mut envs, &workspace_node_modules_bin)?;

// Cache is lazily initialized on first access to avoid SQLite race conditions
let task_error_hints = config.command_handler.task_error_hints();
Ok(Self {
workspace_path: Arc::clone(&workspace_root.path),
lazy_task_graph: LazyTaskGraph::Uninitialized {
Expand All @@ -237,6 +253,7 @@ impl<'a> Session<'a> {
cwd,
plan_request_parser: PlanRequestParser { command_handler: config.command_handler },
program_name: config.program_name,
task_error_hints,
cache: OnceCell::new(),
cache_path,
})
Expand All @@ -246,13 +263,17 @@ impl<'a> Session<'a> {
///
/// # Errors
///
/// Returns an error if planning or execution fails.
/// Prints user-facing CLI errors and returns an exit status.
#[tracing::instrument(level = "debug", skip_all)]
pub async fn main(mut self, command: Command) -> anyhow::Result<ExitStatus> {
pub async fn main(mut self, command: Command) -> ExitStatus {
let verbose = matches!(&command, Command::Run(run) if run.flags.verbose);
match self.main_inner(command).await {
Ok(()) => Ok(ExitStatus::SUCCESS),
Err(SessionError::EarlyExit(status)) => Ok(status),
Err(SessionError::Anyhow(err)) => Err(err),
Ok(()) => ExitStatus::SUCCESS,
Err(SessionError::EarlyExit(status)) => status,
Err(SessionError::Anyhow(err)) => {
self.print_cli_error(&err, verbose);
ExitStatus::FAILURE
}
}
}

Expand Down Expand Up @@ -343,6 +364,87 @@ impl<'a> Session<'a> {
Ok(())
}

fn print_cli_error(&self, err: &anyhow::Error, verbose: bool) {
let mut stderr = std::io::stderr();

if let Some(plan_error) = err.downcast_ref::<vite_task_plan::Error>() {
if let Some(nested) = plan_error.nested_missing_task() {
let _ = writeln!(
stderr,
"{}",
vite_str::format!("Task \"{}\" not found.", nested.task_name)
.if_supports_color(Stream::Stderr, |text| {
text.style(Style::new().red().bold())
})
);
let _ = writeln!(
stderr,
"{} depends on \"{}\" via `{}`.",
nested.parent_task, nested.task_name, nested.command
);
let _ = writeln!(stderr, "Next steps:");
let _ = writeln!(stderr, " {}", self.task_error_hints.task_list_hint);
let _ = writeln!(stderr, " {}", self.task_error_hints.task_source_hint);
if verbose {
let _ = writeln!(stderr);
let _ = writeln!(
stderr,
"{}",
"Raw details:".if_supports_color(Stream::Stderr, |text| text.bold())
);
let _ = writeln!(stderr, "{err:#}");
let _ = writeln!(stderr, "Exit code: {}", ExitStatus::FAILURE.0);
}
let _ = stderr.flush();
return;
}

if let vite_task_plan::Error::NoTasksMatched(task_name) = plan_error {
let _ = writeln!(
stderr,
"{}",
vite_str::format!("Task \"{}\" not found.", task_name)
.if_supports_color(Stream::Stderr, |text| text
.style(Style::new().red().bold()))
);
let _ = writeln!(stderr, "Next steps:");
let _ = writeln!(stderr, " {}", self.task_error_hints.task_list_hint);
let _ = writeln!(stderr, " {}", self.task_error_hints.task_source_hint);
if verbose {
let _ = writeln!(stderr);
let _ = writeln!(
stderr,
"{}",
"Raw details:".if_supports_color(Stream::Stderr, |text| text.bold())
);
let _ = writeln!(stderr, "{err:#}");
let _ = writeln!(stderr, "Exit code: {}", ExitStatus::FAILURE.0);
}
let _ = stderr.flush();
return;
}
}

let _ = writeln!(
stderr,
"{} {}",
"✗".if_supports_color(Stream::Stderr, |text| text.style(Style::new().red().bold())),
err.to_string()
.if_supports_color(Stream::Stderr, |text| text.style(Style::new().red()))
);
if verbose {
let _ = writeln!(stderr);
let _ = writeln!(
stderr,
"{}",
"Raw details:".if_supports_color(Stream::Stderr, |text| text.bold())
);
let _ = writeln!(stderr, "{err:#}");
let _ = writeln!(stderr, "Exit code: {}", ExitStatus::FAILURE.0);
}
let _ = stderr.flush();
}

/// Show the task selector or list, and return a plan request for the selected task.
///
/// In interactive mode, shows a fuzzy-searchable selection list. On selection,
Expand Down
13 changes: 11 additions & 2 deletions crates/vite_task_bin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ use clap::Parser;
use vite_path::AbsolutePath;
use vite_str::Str;
use vite_task::{
Command, EnabledCacheConfig, HandledCommand, ScriptCommand, SessionConfig, UserCacheConfig,
get_path_env, plan_request::SyntheticPlanRequest,
Command, EnabledCacheConfig, HandledCommand, ScriptCommand, SessionConfig, TaskErrorHints,
UserCacheConfig, get_path_env, plan_request::SyntheticPlanRequest,
};

#[derive(Debug, Default)]
Expand Down Expand Up @@ -105,6 +105,15 @@ impl vite_task::CommandHandler for CommandHandler {
Args::Task(parsed) => Ok(HandledCommand::ViteTaskCommand(parsed)),
}
}

fn task_error_hints(&self) -> TaskErrorHints {
TaskErrorHints {
task_list_hint: Str::from("Run `vt run` to browse available tasks."),
task_source_hint: Str::from(
"Check `vite-task.json` and `package.json` scripts where tasks are defined.",
),
}
}
}

/// A `UserConfigLoader` implementation that only loads `vite-task.json`.
Expand Down
2 changes: 1 addition & 1 deletion crates/vite_task_bin/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ async fn run() -> anyhow::Result<ExitStatus> {
let args = Command::parse();
let mut owned_config = OwnedSessionConfig::default();
let session = Session::init(owned_config.as_config())?;
session.main(args).await
Ok(session.main(args).await)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ source: crates/vite_task_bin/tests/e2e_snapshots/main.rs
expression: e2e_outputs
---
[1]> vt run task-a # task-a -> task-b -> task-a cycle
Error: Cycle dependency detected: error-cycle-dependency-test#task-a -> error-cycle-dependency-test#task-b -> error-cycle-dependency-test#task-a
Cycle dependency detected: error-cycle-dependency-test#task-a -> error-cycle-dependency-test#task-b -> error-cycle-dependency-test#task-a
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "nested-task-not-found-test",
"private": true,
"workspaces": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[[e2e]]
name = "nested task not found"
steps = ["vt run repro"]

[[e2e]]
name = "nested task not found verbose"
steps = ["vt run repro --verbose"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
source: crates/vite_task_bin/tests/e2e_snapshots/main.rs
expression: e2e_outputs
---
[1]> vt run repro --verbose
Task "missing-task" not found.
nested-task-not-found-test#repro depends on "missing-task" via `vt run missing-task`.
Next steps:
Run `vt run` to browse available tasks.
Check `vite-task.json` and `package.json` scripts where tasks are defined.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
source: crates/vite_task_bin/tests/e2e_snapshots/main.rs
expression: e2e_outputs
---
[1]> vt run repro
Task "missing-task" not found.
nested-task-not-found-test#repro depends on "missing-task" via `vt run missing-task`.
Next steps:
Run `vt run` to browse available tasks.
Check `vite-task.json` and `package.json` scripts where tasks are defined.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"tasks": {
"repro": {
"command": "vt run missing-task"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@ source: crates/vite_task_bin/tests/e2e_snapshots/main.rs
expression: e2e_outputs
---
[1]> echo '' | vt run -r buid
Error: Task "buid" not found
Task "buid" not found.
Next steps:
Run `vt run` to browse available tasks.
Check `vite-task.json` and `package.json` scripts where tasks are defined.
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ info:
cwd: packages/app
---
[1]> vt run -r
Error: No task specifier provided for 'run' command
No task specifier provided for 'run' command
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@ info:
cwd: packages/app
---
[1]> vt run -t buid
Error: Task "buid" not found
Task "buid" not found.
Next steps:
Run `vt run` to browse available tasks.
Check `vite-task.json` and `package.json` scripts where tasks are defined.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ source: crates/vite_task_bin/tests/e2e_snapshots/main.rs
expression: e2e_outputs
---
[1]> vt run run-typo-task
Error: Failed to plan tasks from `vt run nonexistent-xyz` in task task-select-test#run-typo-task

Caused by:
Task "nonexistent-xyz" not found
Task "nonexistent-xyz" not found.
task-select-test#run-typo-task depends on "nonexistent-xyz" via `vt run nonexistent-xyz`.
Next steps:
Run `vt run` to browse available tasks.
Check `vite-task.json` and `package.json` scripts where tasks are defined.
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@ info:
cwd: packages/app
---
[1]> vt run --verbose
Error: No task specifier provided for 'run' command
✗ No task specifier provided for 'run' command

Raw details:
No task specifier provided for 'run' command
Exit code: 1
22 changes: 22 additions & 0 deletions crates/vite_task_plan/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ use vite_task_graph::display::TaskDisplay;

use crate::{context::TaskRecursionError, envs::ResolveEnvError};

#[derive(Debug, Clone)]
pub struct NestedMissingTaskError<'a> {
pub parent_task: &'a TaskDisplay,
pub command: &'a Str,
pub task_name: &'a Str,
}

#[derive(Debug, thiserror::Error)]
pub enum CdCommandError {
#[error("No home directory found for 'cd' command with no arguments")]
Expand Down Expand Up @@ -162,3 +169,18 @@ pub enum Error {
#[error("Cycle dependency detected: {}", _0.iter().map(std::string::ToString::to_string).collect::<Vec<_>>().join(" -> "))]
CycleDependencyDetected(Vec<TaskDisplay>),
}

impl Error {
#[must_use]
pub fn nested_missing_task(&self) -> Option<NestedMissingTaskError<'_>> {
match self {
Self::NestPlan { task_display, command, error } => match error.as_ref() {
Self::NoTasksMatched(task_name) => {
Some(NestedMissingTaskError { parent_task: task_display, command, task_name })
}
inner => inner.nested_missing_task(),
},
_ => None,
}
}
}
Loading