use forge::signal::compactor;
use once_cell::sync::Lazy;
use regex::Regex;
static VITE_TRANSFORM_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^transforming \(\d+\)[^\n]*\n?").unwrap());
#[allow(dead_code)]
static VITE_MODULES_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^[✓✗] \d+ modules transformed\.\n?").unwrap());
static NEXT_CREATING_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^Creating an optimized production build[^\n]*\n?").unwrap());
static NEXT_SPINNER_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^\s+[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏][^\n]*\n?").unwrap());
static PROGRESS_BAR_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^\[=+>?=*\] \d+%[^\n]*\n?").unwrap());
static ESBUILD_ENTRY_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^\s+entry:[^\n]+\n?").unwrap());
pub fn compress_vite_build(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = VITE_TRANSFORM_RE.replace_all(&cleaned, "");
let s = PROGRESS_BAR_RE.replace_all(&s, "");
compactor::collapse_blanks(&s)
}
pub fn compress_next_build(raw: &str, exit_code: i32) -> String {
let cleaned = compactor::normalise(raw);
let s = NEXT_CREATING_RE.replace_all(&cleaned, "");
let s = NEXT_SPINNER_RE.replace_all(&s, "");
let s = PROGRESS_BAR_RE.replace_all(&s, "");
if exit_code == 0 {
let useful: Vec<&str> = s
.lines()
.filter(|l| {
let t = l.trim();
!t.is_empty()
&& (l.starts_with("Route")
|| l.starts_with(" ")
|| t.starts_with("First Load")
|| t.contains("kB")
|| t.starts_with("Ready")
|| t.starts_with("info")
|| t.contains("Compiled")
|| t.contains("error")
|| t.contains("warn"))
})
.collect();
if !useful.is_empty() {
return useful.join("\n");
}
}
compactor::collapse_blanks(&s)
}
pub fn compress_esbuild(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = ESBUILD_ENTRY_RE.replace_all(&cleaned, "");
let s = PROGRESS_BAR_RE.replace_all(&s, "");
compactor::collapse_blanks(&s)
}
pub fn compress_turbo(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let noise: &[&str] = &["• Packages in scope", "• Running ", ">>> Finished"];
let lines: Vec<&str> = cleaned
.lines()
.filter(|l| {
let t = l.trim();
!t.is_empty() && !noise.iter().any(|n| t.starts_with(n))
})
.collect();
if lines.len() <= 50 {
return lines.join("\n");
}
format!(
"{}\n... [{} more lines]",
lines[..50].join("\n"),
lines.len() - 50
)
}
pub fn compress_vite(subcmd: &str, raw: &str, _exit_code: i32) -> String {
match subcmd.trim() {
"build" => compress_vite_build(raw),
_ => compactor::normalise(raw),
}
}
pub fn compress_next(subcmd: &str, raw: &str, exit_code: i32) -> String {
match subcmd.trim() {
"build" => compress_next_build(raw, exit_code),
_ => compactor::normalise(raw),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn vite_build_strips_transform_progress() {
let raw = "vite v5.0.0 building for production...\ntransforming (1) src/main.ts\ntransforming (234) src/App.tsx\ntransforming (512) src/utils.ts\n✓ 512 modules transformed.\ndist/index.html 0.46 kB\ndist/assets/index-abc.js 142.3 kB\n";
let out = compress_vite_build(raw);
assert!(!out.contains("transforming (1)"), "{out}");
assert!(!out.contains("transforming (234)"), "{out}");
assert!(out.contains("dist/"), "{out}");
}
#[test]
fn next_build_success_keeps_route_table() {
let raw = "Creating an optimized production build...\nCompiled successfully\n\nRoute (app) Size First Load JS\n ○ / 142 B 87.5 kB\n ○ /about 200 B 87.6 kB\n\nReady in 4.2s\n";
let out = compress_next_build(raw, 0);
assert!(!out.contains("Creating an optimized"), "{out}");
assert!(out.contains("Route") || out.contains("Ready"), "{out}");
}
#[test]
fn esbuild_strips_entry_verbose() {
let raw = " entry: src/index.ts\n entry: src/worker.ts\n\n dist/index.js 142kb\n dist/worker.js 12kb\n";
let out = compress_esbuild(raw);
assert!(!out.contains("entry: src/"), "{out}");
assert!(out.contains("dist/"), "{out}");
}
}