Skip to main content

cli/lib/package_managers/
mod.rs

1//! Package manager detection and install command construction.
2
3use 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}