Skip to main content

aft/compress/
npm.rs

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}