diff --git a/Cargo.lock b/Cargo.lock index d3172c867..08f30143f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7618,6 +7618,7 @@ dependencies = [ "reqwest 0.12.24", "rolldown", "rolldown_common", + "rolldown_error", "rolldown_utils", "rustyline", "serde", diff --git a/Cargo.toml b/Cargo.toml index 006d5afc6..b3fa90b7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -259,6 +259,7 @@ regex = "1" reqwest = { version = "0.12", features = ["stream", "json"] } rolldown = { git = "https://github.com/rolldown/rolldown.git", tag = "v1.0.0-rc.3" } rolldown_common = { git = "https://github.com/rolldown/rolldown.git", tag = "v1.0.0-rc.3" } +rolldown_error = { git = "https://github.com/rolldown/rolldown.git", tag = "v1.0.0-rc.3" } rolldown_utils = { git = "https://github.com/rolldown/rolldown.git", tag = "v1.0.0-rc.3" } ron = "0.8" rusqlite = { version = "0.29.0", features = ["bundled", "column_decltype"] } diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 3348e1a26..c2ac45c40 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -81,6 +81,7 @@ glob.workspace = true dialoguer = { workspace = true, features = ["fuzzy-select"] } rolldown.workspace = true rolldown_common.workspace = true +rolldown_error.workspace = true rolldown_utils.workspace = true xmltree.workspace = true quick-xml.workspace = true diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 175bcf14f..9e40d2804 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -74,3 +74,14 @@ pub async fn exec_subcommand( } .map(|()| ExitCode::SUCCESS) } + +/// An error type indicating that the process should exit silently with the +/// given `ExitCode`. +#[derive(thiserror::Error, Debug)] +#[error("exit with {0:?}")] +pub struct ExitWithCode(pub ExitCode); + +impl ExitWithCode { + /// Basic unsuccessful termination. + pub const FAILURE: Self = ExitWithCode(ExitCode::FAILURE); +} diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 5d768046a..6f2e4d23f 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -43,7 +43,9 @@ async fn main() -> anyhow::Result { .unwrap_or_else(|| paths.cli_config_dir.cli_toml()); let config = Config::load(cli_toml)?; - exec_subcommand(config, &paths, root_dir, cmd, subcommand_args).await + exec_subcommand(config, &paths, root_dir, cmd, subcommand_args) + .await + .or_else(|e| e.downcast::().map(|e| e.0)) } #[cfg(feature = "markdown-docs")] diff --git a/crates/cli/src/tasks/javascript.rs b/crates/cli/src/tasks/javascript.rs index b6af1b2f4..68af61faa 100644 --- a/crates/cli/src/tasks/javascript.rs +++ b/crates/cli/src/tasks/javascript.rs @@ -1,14 +1,19 @@ use anyhow::Context; use regex::Regex; -use rolldown::{Bundler, BundlerOptions, Either, SourceMapType}; +use rolldown::{BundleOutput, Bundler, BundlerOptions, Either, SourceMapType}; +use rolldown_error::{BuildDiagnostic, DiagnosableResolveError, DiagnosticOptions, Severity}; use rolldown_utils::indexmap::FxIndexMap; use rolldown_utils::js_regex::HybridRegex; use rolldown_utils::pattern_filter::StringOrRegex; use std::fs; +use std::io::IsTerminal; use std::path::{Path, PathBuf}; +use std::process::ExitCode; use std::sync::{Arc, OnceLock}; use tokio::runtime::{Builder, Handle, Runtime}; +use crate::ExitWithCode; + static RUNTIME: OnceLock = OnceLock::new(); fn runtime() -> &'static Runtime { @@ -38,6 +43,33 @@ where pub(crate) fn build_javascript(project_path: &Path, build_debug: bool) -> anyhow::Result { let cwd = fs::canonicalize(project_path)?; + + let mut tsc_path = cwd.join("node_modules/.bin/tsc"); + if cfg!(windows) { + tsc_path.set_extension(".cmd"); + } + if tsc_path.exists() { + let status = std::process::Command::new(tsc_path) + .arg("--noEmit") + .current_dir(&cwd) + .status() + .context("Failed to execute tsc")?; + if !status.success() { + if let Some(code) = status.code() { + if let Ok(code) = u8::try_from(code).map(ExitCode::from) { + anyhow::bail!(ExitWithCode(code)); + } + } + // For an abnormal exit, show the details of the status. + anyhow::bail!("tsc exited with {status}"); + } + } else { + eprintln!( + "tsc not found in node_modules. Make sure you have the `typescript` package \ + as a dev-dependency and that your dependencies are installed." + ) + } + let mut bundler = Bundler::new(BundlerOptions { input: Some(vec!["./src/index.ts".to_string().into()]), cwd: Some(cwd.clone()), @@ -188,14 +220,42 @@ pub(crate) fn build_javascript(project_path: &Path, build_debug: bool) -> anyhow strict_execution_order: None, })?; - let bundle_output = run_blocking(async move { bundler.write().await })?; + let bundle_result = run_blocking(async move { bundler.write().await }); - bundle_output.warnings.into_iter().for_each(|w| { - eprintln!("Rolldown warning: {w}"); - }); + let (mut bundle_result, diagnostics) = match bundle_result { + Ok(BundleOutput { warnings, assets }) => (Some(assets), warnings), + Err(errors) => (None, errors.into_vec()), + }; - let output_chunk = bundle_output - .assets + let color = std::io::stderr().is_terminal(); + let diag_options = DiagnosticOptions { cwd }; + for mut diag in diagnostics { + // if an import failed to resolve, force it to be an error. + if let Some(err) = diag.downcast_mut::() { + err.help = Some("Module not found".into()); + // `BuildDiagnostic` doesn't let us change its severity to error. Instead, we + // construct a fresh `BuildDiagnostic` (which will have Severity::Error), then + // swap in the real `DiagnosableResolveError`. + let mut new_diag = BuildDiagnostic::resolve_error( + Default::default(), + Default::default(), + rolldown_error::DiagnosableArcstr::String(Default::default()), + Default::default(), + err.diagnostic_kind, + Default::default(), + ); + std::mem::swap(new_diag.downcast_mut::().unwrap(), err); + diag = new_diag; + } + // if there are any errors, we want to bail after printing + if diag.severity() == Severity::Error { + bundle_result = None; + } + eprintln!("{}", diag.to_diagnostic_with(&diag_options).convert_to_string(color)); + } + let bundle_assets = bundle_result.ok_or(ExitWithCode::FAILURE)?; + + let output_chunk = bundle_assets .into_iter() .find_map(|chunk| match chunk { rolldown_common::Output::Chunk(chunk) if chunk.is_entry && chunk.filename == "bundle.js" => Some(chunk),