Skip to main content

aft/compress/
npm.rs

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}