From abef1a7a148d4bba16271fe1c365019ab3f5bbe9 Mon Sep 17 00:00:00 2001 From: Ankur Goyal Date: Sat, 4 Apr 2026 16:55:08 -0700 Subject: [PATCH 1/3] fail faster without vite node --- src/eval.rs | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/eval.rs b/src/eval.rs index d108c61..d48bcbd 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -2147,13 +2147,9 @@ fn build_vite_node_fallback_command(runner: &Path, files: &[String]) -> Result Date: Sat, 4 Apr 2026 17:08:09 -0700 Subject: [PATCH 2/3] do not collect watch dependencies unless running --watch mode --- src/eval.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/eval.rs b/src/eval.rs index d48bcbd..fcd4e1f 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -430,6 +430,7 @@ pub async fn run(base: BaseArgs, args: EvalArgs) -> Result<()> { &files, args.no_send_logs, &options, + false, ) .await?; if !output.status.success() { @@ -464,6 +465,7 @@ async fn run_eval_files_watch( files, no_send_logs, options, + true, ) .await { @@ -546,6 +548,7 @@ async fn run_eval_files_once( files: &[String], no_send_logs: bool, options: &EvalRunOptions, + collect_dependencies: bool, ) -> Result { let plan = build_eval_plan(files, language_override, runner_override)?; let console_policy = match plan.retry_policy { @@ -590,12 +593,17 @@ async fn run_eval_files_once( eprintln!("Hint: If this eval uses ESM features (like top-level await), try `--runner vite-node`."); } - let mut dependencies = - normalize_watch_paths(output.dependency_files.into_iter().map(PathBuf::from))?; - if plan.language == EvalLanguage::JavaScript { - let static_dependencies = collect_js_static_dependencies(files)?; - dependencies = merge_watch_paths(&dependencies, &static_dependencies); - } + let dependencies = if collect_dependencies { + let mut dependencies = + normalize_watch_paths(output.dependency_files.into_iter().map(PathBuf::from))?; + if plan.language == EvalLanguage::JavaScript { + let static_dependencies = collect_js_static_dependencies(files)?; + dependencies = merge_watch_paths(&dependencies, &static_dependencies); + } + dependencies + } else { + Vec::new() + }; Ok(EvalRunOutput { status: output.status, From 14ec874c949c967ec2e8830e91502e5e6aa49c02 Mon Sep 17 00:00:00 2001 From: Ankur Goyal Date: Sat, 4 Apr 2026 17:37:12 -0700 Subject: [PATCH 3/3] improve --- src/eval.rs | 105 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 78 insertions(+), 27 deletions(-) diff --git a/src/eval.rs b/src/eval.rs index fcd4e1f..128a51e 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -593,6 +593,10 @@ async fn run_eval_files_once( eprintln!("Hint: If this eval uses ESM features (like top-level await), try `--runner vite-node`."); } + if let Some(message) = missing_vite_node_retry_message(&output) { + anyhow::bail!(message); + } + let dependencies = if collect_dependencies { let mut dependencies = normalize_watch_paths(output.dependency_files.into_iter().map(PathBuf::from))?; @@ -909,6 +913,26 @@ fn should_retry_esm(plan: &EvalPlan<'_>, output: &EvalAttemptOutput) -> bool { .any(|line| is_esm_interop_error(line)) } +fn missing_vite_node_retry_message(output: &EvalAttemptOutput) -> Option<&'static str> { + if output.runner_kind != RunnerKind::ViteNode { + return None; + } + + let missing_runner = output + .stderr_lines + .iter() + .chain(&output.error_messages) + .any(|line| line.contains("vite-node: command not found")); + let exited_with_command_not_found = output.status.code() == Some(127); + if !missing_runner && !exited_with_command_not_found { + return None; + } + + Some( + "The eval could not be retried in ESM mode. The initial `tsx` load hit an ESM/CJS interop error while loading the eval (for example an ESM-only dependency), and the `vite-node` fallback exited before the eval started. This usually means `vite-node` is not installed in this workspace or available on PATH. Install `vite-node` in the eval workspace, for example `pnpm add -D vite-node`, then rerun `bt eval`.", + ) +} + fn has_ts_eval_files(files: &[String]) -> bool { files.iter().any(|file| { let ext = Path::new(file) @@ -2155,9 +2179,13 @@ fn build_vite_node_fallback_command(runner: &Path, files: &[String]) -> Result ExitStatus { + use std::os::unix::process::ExitStatusExt; + ExitStatus::from_raw(code << 8) + } + + #[cfg(windows)] + fn exit_status(code: i32) -> ExitStatus { + use std::os::windows::process::ExitStatusExt; + ExitStatus::from_raw(code as u32) + } + + fn success_status() -> ExitStatus { + exit_status(0) + } + fn get_command_env(command: &Command, key: &str) -> Option { command.as_std().get_envs().find_map(|(env_key, value)| { if env_key == OsStr::new(key) { @@ -3546,30 +3590,6 @@ mod tests { let _ = std::fs::remove_dir_all(&dir); } - #[test] - fn build_vite_node_fallback_command_errors_when_vite_node_is_missing() { - let _guard = env_test_lock().lock().expect("env test lock"); - let previous_path = clear_env_var("PATH"); - - let dir = make_temp_dir("missing-vite-node"); - let eval_dir = dir.join("evals"); - std::fs::create_dir_all(&eval_dir).expect("eval dir should be created"); - - let file = eval_dir.join("sample.eval.ts"); - std::fs::write(&file, "").expect("eval file should be written"); - - let err = build_vite_node_fallback_command( - Path::new("runner.ts"), - &[file.to_string_lossy().to_string()], - ) - .expect_err("missing vite-node should error"); - - assert!(err.to_string().contains("ESM retry requires vite-node")); - - restore_env_var("PATH", previous_path); - let _ = std::fs::remove_dir_all(&dir); - } - #[test] fn expand_eval_file_globs_expands_flat_glob() { let dir = make_temp_dir("glob-flat"); @@ -4042,6 +4062,37 @@ mod tests { assert!(!should_set_node_heap_size(RunnerKind::Bun)); } + #[test] + fn missing_vite_node_retry_message_is_user_facing() { + let output = EvalAttemptOutput { + status: success_status(), + dependency_files: Vec::new(), + error_messages: Vec::new(), + stderr_lines: vec!["sh: vite-node: command not found".to_string()], + runner_kind: RunnerKind::ViteNode, + }; + + let message = missing_vite_node_retry_message(&output).expect("missing vite-node message"); + assert!(message.contains("could not be retried in ESM mode")); + assert!(message.contains("pnpm add -D vite-node")); + assert!(message.contains("`tsx` load hit an ESM/CJS interop error")); + } + + #[test] + fn missing_vite_node_retry_message_uses_exit_code_127_fallback() { + let output = EvalAttemptOutput { + status: exit_status(127), + dependency_files: Vec::new(), + error_messages: Vec::new(), + stderr_lines: Vec::new(), + runner_kind: RunnerKind::ViteNode, + }; + + let message = missing_vite_node_retry_message(&output).expect("missing vite-node message"); + assert!(message.contains("`vite-node` fallback exited before the eval started")); + assert!(message.contains("pnpm add -D vite-node")); + } + #[test] fn build_sse_socket_path_is_unique_for_consecutive_calls() { let first = build_sse_socket_path().expect("first socket path");