miho 8.1.0

Repository management tools
use super::default_version;
use crate::agent::Agent;
use crate::dependency::{self, DependencyKind, DependencyTree};
use crate::package::Package;
use crate::package::manifest::{Handler, Manifest, ManifestBox};
use crate::version::ComparatorExt;
use anyhow::Result;
use itertools::Itertools;
use semver::{Comparator, Version};
use serde::Deserialize;
use serde_json::Value;
use std::collections::HashMap;
use std::fs;
use std::path::Path;

#[derive(Debug, Deserialize)]
#[serde(rename_all(deserialize = "camelCase"))]
pub(super) struct PackageJson {
  #[serde(default)]
  name: String,

  #[serde(default = "default_version")]
  version: String,

  #[serde(default)]
  package_manager: Option<String>,

  #[serde(default)]
  dependencies: HashMap<String, String>,

  #[serde(default)]
  dev_dependencies: HashMap<String, String>,

  #[serde(default)]
  peer_dependencies: HashMap<String, String>,
}

impl Manifest for PackageJson {
  type Value = serde_json::Value;

  fn read(path: impl AsRef<Path>) -> Result<ManifestBox> {
    let contents = fs::read_to_string(path)?;
    let manifest: PackageJson = serde_json::from_str(&contents)?;
    Ok(Box::new(manifest))
  }

  fn read_as_value(path: impl AsRef<Path>) -> Result<Self::Value> {
    let contents = fs::read_to_string(path)?;
    Ok(serde_json::from_str::<Self::Value>(&contents)?)
  }
}

impl Handler for PackageJson {
  fn agent(&self) -> Agent {
    match &self.package_manager {
      Some(pm) if pm.starts_with("pnpm") => Agent::Pnpm,
      _ => Agent::Npm,
    }
  }

  fn bump(&self, package: &Package, version: Version) -> Result<()> {
    let mut manifest = PackageJson::read_as_value(&package.path)?;
    manifest["version"] = Value::String(version.to_string());

    let mut contents = serde_json::to_string_pretty(&manifest)?;
    if !contents.ends_with('\n') {
      contents.push('\n');
    }

    fs::write(&package.path, contents)?;

    Ok(())
  }

  fn dependency_tree(&self) -> DependencyTree {
    let mut tree = DependencyTree::new(self.agent());

    macro_rules! add {
      ($deps:expr, $kind:ident) => {
        if !$deps.is_empty() {
          tree.add_many($deps, DependencyKind::$kind);
        }
      };
    }

    add!(&self.dependencies, Normal);
    add!(&self.dev_dependencies, Development);
    add!(&self.peer_dependencies, Peer);

    if let Some(pm) = &self.package_manager
      && let Some((name, version)) = pm.split('@').next_tuple()
      && let Ok(comparator) = Comparator::parse(version)
    {
      tree.add(name, comparator, DependencyKind::PackageManager);
    }

    tree
  }

  fn name(&self) -> &str {
    self.name.as_str()
  }

  fn update(&self, package: &Package, targets: &[dependency::Target]) -> Result<()> {
    let mut manifest = PackageJson::read_as_value(&package.path)?;

    for target in targets {
      let key = match target.dependency.kind {
        DependencyKind::Normal => "dependencies",
        DependencyKind::Development => "devDependencies",
        DependencyKind::Peer => "peerDependencies",
        DependencyKind::PackageManager => "packageManager",
        DependencyKind::Build => continue,
      };

      if target.dependency.kind.is_package_manager() {
        let agent = package.agent().to_string().to_lowercase();
        let version = target.comparator.as_version()?;
        manifest[key] = Value::String(format!("{agent}@{version}"));
      } else if let Some(deps) = manifest
        .get_mut(key)
        .and_then(Value::as_object_mut)
      {
        let comparator = Value::String(target.comparator.to_string());
        deps.insert(target.dependency.name.clone(), comparator);
      }
    }

    let mut contents = serde_json::to_string_pretty(&manifest)?;
    if !contents.ends_with('\n') {
      contents.push('\n');
    }

    fs::write(&package.path, contents)?;

    Ok(())
  }

  fn version(&self) -> Result<Version> {
    Version::parse(&self.version).map_err(Into::into)
  }
}