Skip to content

Commit

Permalink
feat: add a new flag for checking global packages
Browse files Browse the repository at this point in the history
  • Loading branch information
flaviodelgrosso committed Oct 4, 2024
1 parent 397d5ca commit e5dd1ce
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 59 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@
- [x] Autocomplete
- [x] Colored updatable packages based on semver diff
- [x] CLI utility flags
- [x] Check global packages

## Roadmap

- [ ] Monorepo support ⚠️
- [ ] Check global packages ⚠️
- [ ] Single packages update with filters ⚠️
- [ ] Non-interactive mode with different display formatting and infos (publish time, semver grouping ) ⚠️

Expand All @@ -43,6 +43,7 @@ pushapp

| Option | Description |
|-------------------------------------|--------------------------------------|
| `-g`, `--global` | Check global packages |
| `-d`, `--development` | Check only `devDependencies` |
| `-p`, `--production` | Check only `dependencies` |
| `-o`, `--optional` | Check only `optionalDependencies` |
Expand Down
6 changes: 5 additions & 1 deletion src/cli/args.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use clap::Parser;

#[derive(Parser, Debug)]
#[derive(Parser, Debug, Default)]
#[command(version, about, long_about = None)]
#[allow(clippy::struct_excessive_bools)]
pub struct Args {
/// Check only "devDependencies"
#[clap(short, long)]
Expand All @@ -12,4 +13,7 @@ pub struct Args {
/// Check only "optionalDependencies"
#[clap(short, long)]
pub optional: bool,
/// Check global packages
#[clap(short, long)]
pub global: bool,
}
115 changes: 72 additions & 43 deletions src/cli/package_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use serde::Deserialize;
use std::collections::HashMap;
use std::env;
use std::path::{Path, PathBuf};
use tokio::process::Command;
use std::process::Command;

use super::{
args::Args,
Expand All @@ -25,6 +25,16 @@ pub struct PackageJson {
pub package_manager: Option<String>,
}

#[derive(Deserialize, Debug)]
pub struct GlobalPackage {
pub version: String,
}

#[derive(Deserialize, Debug)]
pub struct GlobalList {
pub dependencies: HashMap<String, GlobalPackage>,
}

#[derive(Debug, Default)]
#[allow(clippy::module_name_repetitions)]
pub struct PackageJsonManager {
Expand Down Expand Up @@ -63,69 +73,89 @@ impl PackageJsonManager {
}
}

pub fn all_deps_iter(&self, args: &Args) -> impl Iterator<Item = (&String, &String)> {
// Build a vector of the selected dependencies based on CLI arguments
let mut selected_deps: Vec<&Option<PackageDependencies>> = Vec::new();
pub fn get_local_deps(&self, args: &Args) -> PackageDependencies {
let mut combined_deps = PackageDependencies::new();

// Apply logic based on the provided flags
if args.production || (!args.development && !args.optional) {
selected_deps.push(&self.json.dependencies);
if let Some(dependencies) = &self.json.dependencies {
combined_deps.extend(dependencies.clone());
}
}

if args.development || (!args.production && !args.optional) {
selected_deps.push(&self.json.dev_dependencies);
if let Some(dev_dependencies) = &self.json.dev_dependencies {
combined_deps.extend(dev_dependencies.clone());
}
}

if args.optional || (!args.production && !args.development) {
selected_deps.push(&self.json.optional_dependencies);
if let Some(optional_dependencies) = &self.json.optional_dependencies {
combined_deps.extend(optional_dependencies.clone());
}
}

selected_deps
.into_iter()
.flat_map(|deps_option| deps_option.iter().flat_map(|deps| deps.iter()))
combined_deps
}

pub fn get_global_deps() -> Result<PackageDependencies> {
// Run the `npm list -g --depth=0` command to get the global packages and their versions
let output = Command::new("npm")
.arg("ls")
.arg("--json")
.arg("-g")
.arg("--depth=0")
.output()?;

let output_str = String::from_utf8(output.stdout)?;

let global_list: GlobalList = serde_json::from_str(&output_str)?;

let packages = global_list
.dependencies
.iter()
.map(|(name, package)| (name.clone(), package.version.clone()))
.collect();

Ok(packages)
}

/// Detect the package manager used in the project and return it with the install command.
fn detect_package_manager(&self) -> (String, String) {
let package_manager = self.json.package_manager.as_deref().unwrap_or("npm");
fn detect_package_manager(&self, args: &Args) -> String {
if args.global {
return "npm".to_string();
}

// Split at '@' and get the package manager name
let package_manager_name = package_manager.split('@').next().unwrap_or("npm");
let package_manager_field = self.json.package_manager.as_deref().unwrap_or("npm");

// Determine the command based on the package manager
let command = match package_manager_name {
"npm" => "install",
_ => "add",
};
// Split at '@' and get the package manager name
let package_manager = package_manager_field.split('@').next().unwrap_or("npm");

(package_manager_name.to_string(), command.to_string())
package_manager.to_string()
}

pub async fn install_deps(&self, updates: Vec<PackageInfo>) -> Result<()> {
let (package_manager, command) = self.detect_package_manager();
pub fn install_deps(&self, updates: &[PackageInfo], args: &Args) -> Result<()> {
let package_manager = self.detect_package_manager(args);

let install_args = updates
.iter()
.map(|package| format!("{}@{}", package.pkg_name, package.latest_version))
.collect::<Vec<String>>();

#[cfg(not(debug_assertions))]
let status = Command::new(package_manager)
.arg(command)
.args(install_args)
.status()
.await?;

#[cfg(debug_assertions)]
let status = Command::new("echo")
.arg(format!(
"Would have run: {} {} {}",
package_manager,
command,
install_args.join(" ")
))
.status()
.await?;
// Determine the command based on the package manager
let command = match package_manager.as_str() {
"npm" => "install",
_ => "add",
};

let mut cmd = Command::new(package_manager);
cmd.arg(command).args(install_args);

if args.global {
cmd.arg("-g");
}

let status = cmd.status()?;
if status.success() {
println!("{}", "Packages successfully updated!".bright_green());
} else {
Expand Down Expand Up @@ -172,9 +202,8 @@ mod tests {
..Default::default()
};

assert_eq!(
manager.detect_package_manager(),
("pnpm".to_owned(), "add".to_owned())
);
let args = Args::default();

assert_eq!(manager.detect_package_manager(&args), "pnpm");
}
}
29 changes: 17 additions & 12 deletions src/cli/updater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use tokio::task::{self, JoinHandle};
use super::{
args::Args,
package_info::{normalize_version, PackageInfo},
package_json::PackageJsonManager,
package_json::{PackageDependencies, PackageJsonManager},
prompt::display_update,
registry::RegistryClient,
};
Expand All @@ -33,7 +33,13 @@ impl UpdateChecker {
pub async fn run(&self) -> Result<()> {
println!("🔍 {}", "Checking updates...".bright_yellow());

let tasks = self.fetch_updates();
let deps = if self.args.global {
self::PackageJsonManager::get_global_deps()?
} else {
self.pkg_manager.get_local_deps(&self.args)
};

let tasks = self.fetch_updates(&deps);
if tasks.is_empty() {
println!("{}", "📦 No dependencies found.".bright_red());
return Ok(());
Expand All @@ -45,13 +51,15 @@ impl UpdateChecker {
);

let updatable_packages = self.process_update_stream(tasks).await;
self.handle_updatable_packages(updatable_packages).await
self.handle_updatable_packages(updatable_packages)
}

fn fetch_updates(&self) -> FuturesUnordered<JoinHandle<Option<PackageInfo>>> {
self
.pkg_manager
.all_deps_iter(&self.args)
fn fetch_updates(
&self,
deps: &PackageDependencies,
) -> FuturesUnordered<JoinHandle<Option<PackageInfo>>> {
deps
.iter()
.map(|(name, version)| {
let client = Arc::clone(&self.client);
let name = name.to_string();
Expand Down Expand Up @@ -93,10 +101,7 @@ impl UpdateChecker {
pkg_infos
}

async fn handle_updatable_packages(
&self,
mut updatable_packages: Vec<PackageInfo>,
) -> Result<()> {
fn handle_updatable_packages(&self, mut updatable_packages: Vec<PackageInfo>) -> Result<()> {
if updatable_packages.is_empty() {
println!("{}", "There are no updates available.".bright_blue());
return Ok(());
Expand All @@ -106,7 +111,7 @@ impl UpdateChecker {

match display_update(updatable_packages) {
Some(selected) => {
self.pkg_manager.install_deps(selected).await?;
self.pkg_manager.install_deps(&selected, &self.args)?;
}
None => {
println!("{}", "\nNo packages were updated.".bright_yellow());
Expand Down
6 changes: 4 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ async fn main() -> Result<()> {
let args = Args::parse();

let mut pkg_manager = PackageJsonManager::new();
pkg_manager.locate_closest()?;
pkg_manager.read()?;
if !args.global {
pkg_manager.locate_closest()?;
pkg_manager.read()?;
}

let update_checker = UpdateChecker::new(args, pkg_manager);
update_checker.run().await?;
Expand Down

0 comments on commit e5dd1ce

Please sign in to comment.