1use crate::compress::generic::GenericCompressor;
2use crate::compress::Compressor;
3
4pub struct BunCompressor;
5
6impl Compressor for BunCompressor {
7 fn matches(&self, command: &str) -> bool {
8 command
9 .split_whitespace()
10 .next()
11 .is_some_and(|head| head == "bun")
12 }
13
14 fn compress(&self, command: &str, output: &str) -> String {
15 match bun_subcommand(command).as_deref() {
16 Some("install" | "add" | "remove") => compress_package(output),
17 Some("run" | "test") => GenericCompressor::compress_output(output),
18 Some("build") => compress_build(output),
19 _ => GenericCompressor::compress_output(output),
20 }
21 }
22}
23
24fn bun_subcommand(command: &str) -> Option<String> {
25 command
26 .split_whitespace()
27 .skip_while(|token| *token != "bun")
28 .skip(1)
29 .find(|token| !token.starts_with('-'))
30 .map(ToString::to_string)
31}
32
33fn compress_package(output: &str) -> String {
34 let mut result = Vec::new();
35 for line in output.lines() {
36 if is_bun_progress(line) {
37 continue;
38 }
39 let trimmed = line.trim_start();
40 if trimmed.contains("packages installed")
41 || trimmed.contains("package installed")
42 || trimmed.starts_with("error:")
43 || trimmed.starts_with("bun install error:")
44 || trimmed.starts_with("Saved lockfile")
45 {
46 result.push(line.to_string());
47 }
48 }
49 trim_trailing_lines(&result.join("\n"))
50}
51
52fn compress_build(output: &str) -> String {
53 let mut result = Vec::new();
54 let mut timing_seen = 0usize;
55 let mut timing_omitted = 0usize;
56 for line in output.lines() {
57 if is_timing_line(line) {
58 timing_seen += 1;
59 if timing_seen > 10 {
60 timing_omitted += 1;
61 continue;
62 }
63 }
64 result.push(line.to_string());
65 }
66 if timing_omitted > 0 {
67 result.push(format!("... and {timing_omitted} more timing lines"));
68 }
69 trim_trailing_lines(&result.join("\n"))
70}
71
72fn is_bun_progress(line: &str) -> bool {
73 let trimmed = line.trim();
74 trimmed == "."
75 || trimmed.chars().all(|char| char == '.')
76 || trimmed.starts_with("Resolving")
77 || trimmed.starts_with("Resolved")
78 || trimmed.starts_with("Downloaded")
79 || trimmed.starts_with("Extracted")
80}
81
82fn is_timing_line(line: &str) -> bool {
83 let trimmed = line.trim_start();
84 trimmed.starts_with('[') && trimmed.contains(" ms]")
85}
86
87fn trim_trailing_lines(input: &str) -> String {
88 input
89 .lines()
90 .map(str::trim_end)
91 .collect::<Vec<_>>()
92 .join("\n")
93}