Skip to main content

aft/compress/
pnpm.rs

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