Skip to main content

lean_ctx/core/patterns/
next_build.rs

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