npm.rs 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. // Copyright 2019-2024 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. use anyhow::Context;
  5. use crate::helpers::cross_command;
  6. use std::{fmt::Display, path::Path, process::Command};
  7. #[derive(Debug, PartialEq, Eq, Clone, Copy)]
  8. pub enum PackageManager {
  9. Npm,
  10. Pnpm,
  11. Yarn,
  12. YarnBerry,
  13. Bun,
  14. }
  15. impl Display for PackageManager {
  16. fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  17. write!(
  18. f,
  19. "{}",
  20. match self {
  21. PackageManager::Npm => "npm",
  22. PackageManager::Pnpm => "pnpm",
  23. PackageManager::Yarn => "yarn",
  24. PackageManager::YarnBerry => "yarn berry",
  25. PackageManager::Bun => "bun",
  26. }
  27. )
  28. }
  29. }
  30. impl PackageManager {
  31. pub fn from_project<P: AsRef<Path>>(path: P) -> Vec<Self> {
  32. let mut use_npm = false;
  33. let mut use_pnpm = false;
  34. let mut use_yarn = false;
  35. if let Ok(entries) = std::fs::read_dir(path) {
  36. for entry in entries.flatten() {
  37. let path = entry.path();
  38. let name = path.file_name().unwrap().to_string_lossy();
  39. if name.as_ref() == "package-lock.json" {
  40. use_npm = true;
  41. } else if name.as_ref() == "pnpm-lock.yaml" {
  42. use_pnpm = true;
  43. } else if name.as_ref() == "yarn.lock" {
  44. use_yarn = true;
  45. }
  46. }
  47. }
  48. if !use_npm && !use_pnpm && !use_yarn {
  49. return Vec::new();
  50. }
  51. let mut found = Vec::new();
  52. if use_npm {
  53. found.push(PackageManager::Npm);
  54. }
  55. if use_pnpm {
  56. found.push(PackageManager::Pnpm);
  57. }
  58. if use_yarn {
  59. found.push(PackageManager::Yarn);
  60. }
  61. found
  62. }
  63. fn cross_command(&self) -> Command {
  64. match self {
  65. PackageManager::Yarn => cross_command("yarn"),
  66. PackageManager::YarnBerry => cross_command("yarn"),
  67. PackageManager::Npm => cross_command("npm"),
  68. PackageManager::Pnpm => cross_command("pnpm"),
  69. PackageManager::Bun => cross_command("bun"),
  70. }
  71. }
  72. pub fn install<P: AsRef<Path>>(&self, dependencies: &[String], app_dir: P) -> crate::Result<()> {
  73. let dependencies_str = if dependencies.len() > 1 {
  74. "dependencies"
  75. } else {
  76. "dependency"
  77. };
  78. log::info!(
  79. "Installing NPM {dependencies_str} {}...",
  80. dependencies
  81. .iter()
  82. .map(|d| format!("\"{d}\""))
  83. .collect::<Vec<_>>()
  84. .join(", ")
  85. );
  86. let status = self
  87. .cross_command()
  88. .arg("add")
  89. .args(dependencies)
  90. .current_dir(app_dir)
  91. .status()
  92. .with_context(|| format!("failed to run {self}"))?;
  93. if !status.success() {
  94. anyhow::bail!("Failed to install NPM {dependencies_str}");
  95. }
  96. Ok(())
  97. }
  98. pub fn current_package_version<P: AsRef<Path>>(
  99. &self,
  100. name: &str,
  101. app_dir: P,
  102. ) -> crate::Result<Option<String>> {
  103. let (output, regex) = match self {
  104. PackageManager::Yarn => (
  105. cross_command("yarn")
  106. .args(["list", "--pattern"])
  107. .arg(name)
  108. .args(["--depth", "0"])
  109. .current_dir(app_dir)
  110. .output()?,
  111. None,
  112. ),
  113. PackageManager::YarnBerry => (
  114. cross_command("yarn")
  115. .arg("info")
  116. .arg(name)
  117. .arg("--json")
  118. .current_dir(app_dir)
  119. .output()?,
  120. Some(regex::Regex::new("\"Version\":\"([\\da-zA-Z\\-\\.]+)\"").unwrap()),
  121. ),
  122. PackageManager::Npm => (
  123. cross_command("npm")
  124. .arg("list")
  125. .arg(name)
  126. .args(["version", "--depth", "0"])
  127. .current_dir(app_dir)
  128. .output()?,
  129. None,
  130. ),
  131. PackageManager::Pnpm => (
  132. cross_command("pnpm")
  133. .arg("list")
  134. .arg(name)
  135. .args(["--parseable", "--depth", "0"])
  136. .current_dir(app_dir)
  137. .output()?,
  138. None,
  139. ),
  140. // Bun doesn't support `list` command
  141. PackageManager::Bun => (
  142. cross_command("npm")
  143. .arg("list")
  144. .arg(name)
  145. .args(["version", "--depth", "0"])
  146. .current_dir(app_dir)
  147. .output()?,
  148. None,
  149. ),
  150. };
  151. if output.status.success() {
  152. let stdout = String::from_utf8_lossy(&output.stdout);
  153. let regex = regex.unwrap_or_else(|| regex::Regex::new("@(\\d[\\da-zA-Z\\-\\.]+)").unwrap());
  154. Ok(
  155. regex
  156. .captures_iter(&stdout)
  157. .last()
  158. .and_then(|cap| cap.get(1).map(|v| v.as_str().to_string())),
  159. )
  160. } else {
  161. Ok(None)
  162. }
  163. }
  164. }