lean_ctx/core/patterns/
next_build.rs1use 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}