1use crate::compress::generic::GenericCompressor;
2use crate::compress::Compressor;
3
4pub struct NpmCompressor;
5
6impl Compressor for NpmCompressor {
7 fn matches(&self, command: &str) -> bool {
8 command
9 .split_whitespace()
10 .next()
11 .is_some_and(|head| head == "npm")
12 }
13
14 fn compress(&self, command: &str, output: &str) -> String {
15 match npm_subcommand(command).as_deref() {
16 Some("install" | "i" | "ci") => compress_install(output),
17 Some("run" | "test") => GenericCompressor::compress_output(output),
18 Some("audit") => compress_audit(output),
19 Some("publish") => compress_install(output),
20 _ => GenericCompressor::compress_output(output),
21 }
22 }
23}
24
25fn npm_subcommand(command: &str) -> Option<String> {
26 command
27 .split_whitespace()
28 .skip_while(|token| *token != "npm")
29 .skip(1)
30 .find(|token| !token.starts_with('-'))
31 .map(ToString::to_string)
32}
33
34fn compress_install(output: &str) -> String {
35 let lines: Vec<&str> = output.lines().collect();
36 let has_audit = lines
37 .iter()
38 .any(|line| line.trim_start().starts_with("audited "));
39 let has_final_summary = lines.iter().any(|line| is_final_summary(line));
40 let tail_start = lines.len().saturating_sub(5);
41 let mut result = Vec::new();
42 let mut deprecated_seen = 0usize;
43 let mut deprecated_omitted = 0usize;
44
45 for (index, line) in lines.iter().enumerate() {
46 if is_npm_progress(line) {
47 continue;
48 }
49 if line.trim_start().starts_with("npm WARN deprecated ") {
50 deprecated_seen += 1;
51 if deprecated_seen <= 5 {
52 result.push((*line).to_string());
53 } else {
54 deprecated_omitted += 1;
55 }
56 continue;
57 }
58 if has_audit && has_final_summary && line.trim_start().starts_with("added ") {
59 continue;
60 }
61 if index >= tail_start
62 || line.trim_start().starts_with("npm ERR!")
63 || is_final_summary(line)
64 {
65 result.push((*line).to_string());
66 }
67 }
68
69 if deprecated_omitted > 0 {
70 insert_after_deprecations(
71 &mut result,
72 format!("... and {deprecated_omitted} more deprecation warnings"),
73 );
74 }
75
76 trim_trailing_lines(&result.join("\n"))
77}
78
79fn is_npm_progress(line: &str) -> bool {
80 let trimmed = line.trim_start();
81 trimmed.starts_with("npm http fetch")
82 || trimmed.starts_with("npm timing")
83 || trimmed.starts_with("npm verb")
84}
85
86fn is_final_summary(line: &str) -> bool {
87 let trimmed = line.trim_start();
88 trimmed.starts_with("audited ")
89 || trimmed.starts_with("found ")
90 || trimmed.contains(" vulnerabilities")
91 || trimmed.contains(" packages are looking for funding")
92 || trimmed.starts_with("published ")
93 || trimmed.starts_with("+ ")
94}
95
96fn insert_after_deprecations(result: &mut Vec<String>, summary: String) {
97 let position = result
98 .iter()
99 .rposition(|line| line.trim_start().starts_with("npm WARN deprecated "))
100 .map_or(0, |index| index + 1);
101 result.insert(position, summary);
102}
103
104fn compress_audit(output: &str) -> String {
105 let mut result = Vec::new();
106 let mut vulnerabilities = 0usize;
107 let mut omitted = 0usize;
108
109 for line in output.lines() {
110 if is_audit_vulnerability_line(line) {
111 vulnerabilities += 1;
112 if vulnerabilities <= 10 {
113 result.push(line.to_string());
114 } else {
115 omitted += 1;
116 }
117 continue;
118 }
119 if is_audit_summary(line) || vulnerabilities <= 10 {
120 result.push(line.to_string());
121 }
122 }
123
124 if omitted > 0 {
125 result.push(format!("... and {omitted} more vulnerabilities"));
126 }
127
128 trim_trailing_lines(&result.join("\n"))
129}
130
131fn is_audit_vulnerability_line(line: &str) -> bool {
132 let trimmed = line.trim_start();
133 trimmed.starts_with("VULN ") || trimmed.starts_with("# ") && trimmed.contains(" - ")
134}
135
136fn is_audit_summary(line: &str) -> bool {
137 let trimmed = line.trim_start();
138 trimmed.starts_with("found ")
139 || trimmed.starts_with("npm audit fix")
140 || trimmed.contains(" vulnerabilities")
141}
142
143fn trim_trailing_lines(input: &str) -> String {
144 input
145 .lines()
146 .map(str::trim_end)
147 .collect::<Vec<_>>()
148 .join("\n")
149}