use forge::signal::compactor;
use once_cell::sync::Lazy;
use regex::Regex;
static WATCH_STATUS_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?m)^\[\d{2}:\d{2}:\d{2}\] (Starting compilation|File change detected)[^\n]*\n?")
.unwrap()
});
static TSUP_PROGRESS_RE: Lazy<Regex> = Lazy::new(||
Regex::new(r"(?m)^(DTS Build|CLI Building|ESM|CJS)\s[^\n]+\n?").unwrap());
static ESBUILD_META_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^\s+(node_modules/[^\s]+ \d+(\.\d+)? [km]b)\n?").unwrap());
pub fn compress_tsc(raw: &str, exit_code: i32) -> String {
let cleaned = compactor::normalise(raw);
let s = WATCH_STATUS_RE.replace_all(&cleaned, "");
if exit_code == 0 {
let kept: Vec<&str> = s
.lines()
.filter(|l| l.contains("error TS") || l.contains("warning TS") || l.is_empty())
.collect();
let out = kept.join("\n");
if out.trim().is_empty() {
return "tsc: no errors".to_string();
}
return compactor::collapse_blanks(&out);
}
let errors: Vec<&str> = s
.lines()
.filter(|l| l.contains("error TS") || l.contains("warning TS"))
.take(60)
.collect();
let total: usize = s.lines().filter(|l| l.contains("error TS")).count();
let mut out = errors.join("\n");
if total > 60 {
out.push_str(&format!(
"\n... [{} more errors — run tsc to see all] ...",
total - 60
));
}
out
}
pub fn compress_tsup(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = TSUP_PROGRESS_RE.replace_all(&cleaned, "");
let kept: Vec<&str> = s
.lines()
.filter(|l| {
let l = l.trim();
!l.is_empty()
&& (l.contains("dist/")
|| l.contains("Built in")
|| l.contains("error")
|| l.starts_with("✓")
|| l.starts_with("✗"))
})
.collect();
if kept.is_empty() {
return compactor::collapse_blanks(&s);
}
kept.join("\n")
}
pub fn compress_esbuild(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = ESBUILD_META_RE.replace_all(&cleaned, "");
compactor::collapse_blanks(&s)
}
pub fn compress_typescript(prog: &str, subcmd: &str, raw: &str, exit_code: i32) -> String {
match prog {
"tsc" => compress_tsc(raw, exit_code),
"tsup" | "unbuild" => compress_tsup(raw),
"esbuild" => compress_esbuild(raw),
_ => {
if subcmd.contains("tsc") {
return compress_tsc(raw, exit_code);
}
compactor::normalise(raw)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tsc_success_no_errors_returns_clean_message() {
let out = compress_tsc("", 0);
assert!(out.contains("no errors") || out.is_empty(), "{out}");
}
#[test]
fn tsc_failure_keeps_error_lines() {
let raw = "src/main.ts(10,5): error TS2345: Argument of type 'string'\nsrc/lib.ts(3,1): error TS2304: Cannot find name\n";
let out = compress_tsc(raw, 1);
assert!(out.contains("TS2345"), "{out}");
assert!(out.contains("TS2304"), "{out}");
}
#[test]
fn tsc_truncates_many_errors() {
let raw = (0..80)
.map(|i| format!("src/f{i}.ts(1,1): error TS2304: msg\n"))
.collect::<String>();
let out = compress_tsc(&raw, 1);
assert!(out.contains("more errors"), "{out}");
}
#[test]
fn tsup_keeps_dist_lines() {
let raw =
"DTS Build start\ndist/index.js 42.1 kb\ndist/index.d.ts 8.2 kb\nBuilt in 1.2s\n";
let out = compress_tsup(raw);
assert!(out.contains("dist/"), "{out}");
}
}