cli/lib/package_managers/
mod.rs1use std::fmt;
4use std::fs;
5use std::io;
6use std::path::{Path, PathBuf};
7use std::str::FromStr;
8
9use crate::runners::{ProcessRunner, Runner, RunnerCommand, RunnerFactory, RunnerKind};
10
11pub mod abstract_package_manager;
12pub mod npm_package_manager;
13pub mod package_manager;
14pub mod package_manager_commands;
15pub mod package_manager_factory;
16pub mod pnpm_package_manager;
17pub mod project_dependency;
18pub mod yarn_package_manager;
19
20#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
21pub enum PackageManager {
22 Npm,
23 Yarn,
24 Pnpm,
25}
26
27impl PackageManager {
28 pub const fn as_str(self) -> &'static str {
29 match self {
30 Self::Npm => "npm",
31 Self::Yarn => "yarn",
32 Self::Pnpm => "pnpm",
33 }
34 }
35
36 pub const fn display_name(self) -> &'static str {
37 match self {
38 Self::Npm => "NPM",
39 Self::Yarn => "YARN",
40 Self::Pnpm => "PNPM",
41 }
42 }
43
44 pub const fn runner_kind(self) -> RunnerKind {
45 match self {
46 Self::Npm => RunnerKind::Npm,
47 Self::Yarn => RunnerKind::Yarn,
48 Self::Pnpm => RunnerKind::Pnpm,
49 }
50 }
51
52 pub const fn commands(self) -> PackageManagerCommands {
53 match self {
54 Self::Npm => PackageManagerCommands {
55 install: "install",
56 add: "install",
57 update: "update",
58 remove: "uninstall",
59 save_flag: "--save",
60 save_dev_flag: "--save-dev",
61 silent_flag: "--silent",
62 },
63 Self::Yarn => PackageManagerCommands {
64 install: "install",
65 add: "add",
66 update: "upgrade",
67 remove: "remove",
68 save_flag: "",
69 save_dev_flag: "-D",
70 silent_flag: "--silent",
71 },
72 Self::Pnpm => PackageManagerCommands {
73 install: "install --strict-peer-dependencies=false",
74 add: "install --strict-peer-dependencies=false",
75 update: "update",
76 remove: "uninstall",
77 save_flag: "--save",
78 save_dev_flag: "--save-dev",
79 silent_flag: "--reporter=silent",
80 },
81 }
82 }
83}
84
85impl fmt::Display for PackageManager {
86 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
87 formatter.write_str(self.as_str())
88 }
89}
90
91impl FromStr for PackageManager {
92 type Err = PackageManagerError;
93
94 fn from_str(value: &str) -> Result<Self, Self::Err> {
95 match value {
96 "npm" => Ok(Self::Npm),
97 "yarn" => Ok(Self::Yarn),
98 "pnpm" => Ok(Self::Pnpm),
99 _ => Err(PackageManagerError::Unsupported(value.to_owned())),
100 }
101 }
102}
103
104#[derive(Clone, Copy, Debug, Eq, PartialEq)]
105pub struct PackageManagerCommands {
106 pub install: &'static str,
107 pub add: &'static str,
108 pub update: &'static str,
109 pub remove: &'static str,
110 pub save_flag: &'static str,
111 pub save_dev_flag: &'static str,
112 pub silent_flag: &'static str,
113}
114
115#[derive(Clone, Debug, Eq, PartialEq)]
116pub struct ProjectDependency {
117 pub name: String,
118 pub version: String,
119}
120
121#[derive(Clone, Debug, Eq, PartialEq)]
122pub struct PackageManagerClient {
123 manager: PackageManager,
124 runner: ProcessRunner,
125}
126
127impl PackageManagerClient {
128 pub fn new(manager: PackageManager) -> Self {
129 Self {
130 manager,
131 runner: RunnerFactory::create(manager.runner_kind()),
132 }
133 }
134
135 pub const fn manager(&self) -> PackageManager {
136 self.manager
137 }
138
139 pub const fn name(&self) -> &'static str {
140 self.manager.display_name()
141 }
142
143 pub const fn commands(&self) -> PackageManagerCommands {
144 self.manager.commands()
145 }
146
147 pub fn install_command(&self, project_directory: impl Into<PathBuf>) -> RunnerCommand {
148 let cli = self.commands();
149 let command = join_non_empty([cli.install, cli.silent_flag]);
150 self.runner
151 .describe(command, true, Some(project_directory.into()))
152 }
153
154 pub fn version_command(&self) -> RunnerCommand {
155 self.runner.describe("--version", true, None)
156 }
157
158 pub fn add_production_command(&self, dependencies: &[&str], tag: &str) -> RunnerCommand {
159 let cli = self.commands();
160 let command = join_non_empty([cli.add, cli.save_flag]);
161 let args = dependencies_with_tag(dependencies, tag);
162 self.runner
163 .describe(format!("{command} {args}"), true, None)
164 }
165
166 pub fn add_development_command(&self, dependencies: &[&str], tag: &str) -> RunnerCommand {
167 let cli = self.commands();
168 let args = dependencies_with_tag(dependencies, tag);
169 self.runner.describe(
170 format!("{} {} {}", cli.add, cli.save_dev_flag, args),
171 true,
172 None,
173 )
174 }
175
176 pub fn update_production_command(&self, dependencies: &[&str]) -> RunnerCommand {
177 self.update_command(dependencies)
178 }
179
180 pub fn update_development_command(&self, dependencies: &[&str]) -> RunnerCommand {
181 self.update_command(dependencies)
182 }
183
184 pub fn delete_production_command(&self, dependencies: &[&str]) -> RunnerCommand {
185 let cli = self.commands();
186 let command = join_non_empty([cli.remove, cli.save_flag]);
187 self.runner
188 .describe(format!("{command} {}", dependencies.join(" ")), true, None)
189 }
190
191 pub fn delete_development_command(&self, dependencies: &[&str]) -> RunnerCommand {
192 let cli = self.commands();
193 self.runner.describe(
194 format!(
195 "{} {} {}",
196 cli.remove,
197 cli.save_dev_flag,
198 dependencies.join(" ")
199 ),
200 true,
201 None,
202 )
203 }
204
205 pub fn raw_full_command(&self, command: impl AsRef<str>) -> String {
206 self.runner.raw_full_command(command)
207 }
208
209 fn update_command(&self, dependencies: &[&str]) -> RunnerCommand {
210 self.runner.describe(
211 format!("{} {}", self.commands().update, dependencies.join(" ")),
212 true,
213 None,
214 )
215 }
216}
217
218#[derive(Clone, Copy, Debug, Default)]
219pub struct PackageManagerFactory;
220
221impl PackageManagerFactory {
222 pub fn create(name: impl AsRef<str>) -> Result<PackageManagerClient, PackageManagerError> {
223 Ok(PackageManagerClient::new(name.as_ref().parse()?))
224 }
225
226 pub fn create_manager(manager: PackageManager) -> PackageManagerClient {
227 PackageManagerClient::new(manager)
228 }
229
230 pub fn find_in_dir(directory: impl AsRef<Path>) -> PackageManagerClient {
231 let manager = detect_package_manager(directory).unwrap_or(PackageManager::Npm);
232 Self::create_manager(manager)
233 }
234}
235
236pub fn detect_package_manager(directory: impl AsRef<Path>) -> io::Result<PackageManager> {
237 let entries = fs::read_dir(directory)?;
238 let mut has_yarn_lock_file = false;
239 let mut has_pnpm_lock_file = false;
240
241 for entry in entries {
242 let file_name = entry?.file_name();
243 if file_name == "yarn.lock" {
244 has_yarn_lock_file = true;
245 } else if file_name == "pnpm-lock.yaml" {
246 has_pnpm_lock_file = true;
247 }
248 }
249
250 if has_yarn_lock_file {
251 Ok(PackageManager::Yarn)
252 } else if has_pnpm_lock_file {
253 Ok(PackageManager::Pnpm)
254 } else {
255 Ok(PackageManager::Npm)
256 }
257}
258
259#[derive(Clone, Debug, Eq, PartialEq)]
260pub enum PackageManagerError {
261 Unsupported(String),
262}
263
264impl fmt::Display for PackageManagerError {
265 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
266 match self {
267 Self::Unsupported(name) => write!(formatter, "Package manager {name} is not managed."),
268 }
269 }
270}
271
272impl std::error::Error for PackageManagerError {}
273
274fn dependencies_with_tag(dependencies: &[&str], tag: &str) -> String {
275 dependencies
276 .iter()
277 .map(|dependency| format!("{dependency}@{tag}"))
278 .collect::<Vec<_>>()
279 .join(" ")
280}
281
282fn join_non_empty<const N: usize>(parts: [&str; N]) -> String {
283 parts
284 .into_iter()
285 .filter(|part| !part.is_empty())
286 .collect::<Vec<_>>()
287 .join(" ")
288}