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