Skip to main content

aft/compress/
pnpm.rs

1use crate::compress::generic::GenericCompressor;
2use crate::compress::{CompressionResult, Compressor, Specificity};
3
4pub struct PnpmCompressor;
5
6impl Compressor for PnpmCompressor {
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 == "pnpm")
16    }
17
18    fn compress_with_exit_code(
19        &self,
20        command: &str,
21        output: &str,
22        _exit_code: Option<i32>,
23    ) -> CompressionResult {
24        match pnpm_subcommand(command).as_deref() {
25            Some("install" | "i" | "add" | "remove") => compress_package(output).into(),
26            Some("run" | "test" | "build") => GenericCompressor::compress_output(output).into(),
27            _ => GenericCompressor::compress_output(output).into(),
28        }
29    }
30}
31
32/// Known pnpm subcommands. Same rationale as bun.rs::BUN_SUBCOMMANDS —
33/// using a whitelist instead of "first non-flag" avoids returning flag
34/// values like `--filter <pattern>` as the subcommand for command lines
35/// such as `pnpm --filter ./packages/foo test`.
36const PNPM_SUBCOMMANDS: &[&str] = &[
37    "install",
38    "i",
39    "add",
40    "remove",
41    "rm",
42    "uninstall",
43    "un",
44    "update",
45    "up",
46    "upgrade",
47    "outdated",
48    "audit",
49    "outdated-of",
50    "publish",
51    "pack",
52    "run",
53    "test",
54    "t",
55    "exec",
56    "x",
57    "dlx",
58    "create",
59    "init",
60    "build",
61    "start",
62    "link",
63    "unlink",
64    "view",
65    "info",
66    "show",
67    "config",
68    "help",
69    "version",
70    "ls",
71    "list",
72    "list-modules",
73    "list-bin",
74    "ping",
75    "whoami",
76    "login",
77    "logout",
78    "deploy",
79    "dedupe",
80    "fetch",
81    "import",
82    "patch",
83    "patch-commit",
84    "patch-remove",
85    "prune",
86    "rebuild",
87    "recursive",
88    "root",
89    "store",
90    "why",
91    "doctor",
92    "env",
93    "server",
94    "setup",
95];
96
97fn pnpm_subcommand(command: &str) -> Option<String> {
98    command
99        .split_whitespace()
100        .skip_while(|token| *token != "pnpm")
101        .skip(1)
102        .find(|token| PNPM_SUBCOMMANDS.contains(token))
103        .map(ToString::to_string)
104}
105
106fn compress_package(output: &str) -> String {
107    let mut result = Vec::new();
108    let mut progress_seen = 0usize;
109    let mut up_to_date_seen = false;
110
111    for line in output.lines() {
112        let trimmed = line.trim_start();
113        if trimmed.starts_with("Progress: resolved ") {
114            progress_seen += 1;
115            if progress_seen > 2 {
116                continue;
117            }
118        }
119        if trimmed == "Already up-to-date" {
120            if up_to_date_seen {
121                continue;
122            }
123            up_to_date_seen = true;
124        }
125        if trimmed.contains("WARN GET_NO_AUTH")
126            || trimmed.starts_with("ERR_PNPM_")
127            || trimmed.starts_with("Progress: resolved ")
128            || trimmed == "Already up-to-date"
129            || trimmed.starts_with("dependencies:")
130            || trimmed.starts_with("devDependencies:")
131            || trimmed.starts_with("Done in ")
132        {
133            result.push(line.to_string());
134        }
135    }
136
137    trim_trailing_lines(&result.join("\n"))
138}
139
140fn trim_trailing_lines(input: &str) -> String {
141    input
142        .lines()
143        .map(str::trim_end)
144        .collect::<Vec<_>>()
145        .join("\n")
146}