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(&self, command: &str, output: &str) -> CompressionResult {
19 match pnpm_subcommand(command).as_deref() {
20 Some("install" | "i" | "add" | "remove") => compress_package(output).into(),
21 Some("run" | "test" | "build") => GenericCompressor::compress_output(output).into(),
22 _ => GenericCompressor::compress_output(output).into(),
23 }
24 }
25}
26
27const 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}