Skip to main content

lean_ctx_patterns/
next_build.rs

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