Skip to main content

lean_ctx/core/patterns/
next_build.rs

1use regex::Regex;
2use std::sync::OnceLock;
3
4static ROUTE_RE: OnceLock<Regex> = OnceLock::new();
5static SIZE_RE: OnceLock<Regex> = OnceLock::new();
6static BUILD_TIME_RE: OnceLock<Regex> = OnceLock::new();
7static VITE_CHUNK_RE: OnceLock<Regex> = OnceLock::new();
8
9fn route_re() -> &'static Regex {
10    ROUTE_RE.get_or_init(|| Regex::new(r"^[○●λƒ◐]\s+(/\S*)").unwrap())
11}
12fn size_re() -> &'static Regex {
13    SIZE_RE.get_or_init(|| Regex::new(r"(\d+\.?\d*)\s*(kB|MB|B)\b").unwrap())
14}
15fn build_time_re() -> &'static Regex {
16    BUILD_TIME_RE.get_or_init(|| {
17        Regex::new(r"(?:compiled|built|done)\s+(?:in\s+)?(\d+\.?\d*\s*[ms]+)").unwrap()
18    })
19}
20fn vite_chunk_re() -> &'static Regex {
21    VITE_CHUNK_RE.get_or_init(|| Regex::new(r"dist/(\S+)\s+(\d+\.?\d*\s*[kKMm]?B)").unwrap())
22}
23
24pub fn compress(command: &str, output: &str) -> Option<String> {
25    if command.contains("vite") {
26        return Some(compress_vite(output));
27    }
28    Some(compress_next(output))
29}
30
31fn compress_next(output: &str) -> String {
32    let trimmed = output.trim();
33    if trimmed.is_empty() {
34        return "ok".to_string();
35    }
36
37    let mut routes = Vec::new();
38    let mut total_size = 0f64;
39    let mut build_time = String::new();
40    let mut errors = Vec::new();
41
42    for line in trimmed.lines() {
43        if let Some(caps) = route_re().captures(line) {
44            let route = &caps[1];
45            let size = extract_size(line);
46            routes.push(format!("{route} ({size})"));
47        }
48        if let Some(caps) = build_time_re().captures(line) {
49            build_time = caps[1].to_string();
50        }
51        if line.to_lowercase().contains("error") && !line.contains("0 error") {
52            errors.push(line.trim().to_string());
53        }
54        if let Some(caps) = size_re().captures(line) {
55            let val: f64 = caps[1].parse().unwrap_or(0.0);
56            let unit = &caps[2];
57            total_size += match unit {
58                "MB" => val * 1024.0,
59                "kB" => val,
60                _ => val / 1024.0,
61            };
62        }
63    }
64
65    if !errors.is_empty() {
66        return format!("BUILD ERROR:\n{}", errors.join("\n"));
67    }
68
69    let mut parts = Vec::new();
70    if !build_time.is_empty() {
71        parts.push(format!("built ({build_time})"));
72    } else {
73        parts.push("built".to_string());
74    }
75
76    if !routes.is_empty() {
77        parts.push(format!("{} routes:", routes.len()));
78        for r in routes.iter().take(15) {
79            parts.push(format!("  {r}"));
80        }
81        if routes.len() > 15 {
82            parts.push(format!("  ... +{} more", routes.len() - 15));
83        }
84    }
85
86    if total_size > 0.0 {
87        if total_size > 1024.0 {
88            parts.push(format!("total: {:.1} MB", total_size / 1024.0));
89        } else {
90            parts.push(format!("total: {:.0} kB", total_size));
91        }
92    }
93
94    if parts.len() == 1 && parts[0] == "built" {
95        return compact_output(trimmed, 10);
96    }
97
98    parts.join("\n")
99}
100
101fn compress_vite(output: &str) -> String {
102    let trimmed = output.trim();
103    if trimmed.is_empty() {
104        return "ok".to_string();
105    }
106
107    let mut chunks = Vec::new();
108    let mut build_time = String::new();
109
110    for line in trimmed.lines() {
111        if let Some(caps) = vite_chunk_re().captures(line) {
112            chunks.push(format!("{}: {}", &caps[1], &caps[2]));
113        }
114        if let Some(caps) = build_time_re().captures(line) {
115            build_time = caps[1].to_string();
116        }
117    }
118
119    let mut parts = Vec::new();
120    if !build_time.is_empty() {
121        parts.push(format!("built ({build_time})"));
122    } else {
123        parts.push("built".to_string());
124    }
125
126    if !chunks.is_empty() {
127        parts.push(format!("{} chunks:", chunks.len()));
128        for c in chunks.iter().take(10) {
129            parts.push(format!("  {c}"));
130        }
131        if chunks.len() > 10 {
132            parts.push(format!("  ... +{} more", chunks.len() - 10));
133        }
134    }
135
136    if parts.len() == 1 {
137        return compact_output(trimmed, 10);
138    }
139    parts.join("\n")
140}
141
142fn extract_size(line: &str) -> String {
143    if let Some(caps) = size_re().captures(line) {
144        format!("{} {}", &caps[1], &caps[2])
145    } else {
146        "?".to_string()
147    }
148}
149
150fn compact_output(text: &str, max: usize) -> String {
151    let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
152    if lines.len() <= max {
153        return lines.join("\n");
154    }
155    format!(
156        "{}\n... ({} more lines)",
157        lines[..max].join("\n"),
158        lines.len() - max
159    )
160}