hni 0.0.3

ni-compatible package manager command router with node shim
Documentation
use std::{
    collections::BTreeMap,
    fs,
    path::{Path, PathBuf},
};

use anyhow::{Result, anyhow};
use serde::Deserialize;

#[derive(Debug, Clone, Deserialize, Default)]
pub struct PackageJson {
    pub name: Option<String>,
    #[serde(rename = "packageManager")]
    pub package_manager: Option<String>,
    #[serde(rename = "devEngines")]
    pub dev_engines: Option<DevEngines>,
    #[serde(default)]
    pub bin: PackageBin,
    pub scripts: Option<BTreeMap<String, String>>,
    #[serde(rename = "scripts-info")]
    pub scripts_info: Option<BTreeMap<String, String>>,
    pub dependencies: Option<BTreeMap<String, String>>,
    #[serde(rename = "devDependencies")]
    pub dev_dependencies: Option<BTreeMap<String, String>>,
    #[serde(rename = "peerDependencies")]
    pub peer_dependencies: Option<BTreeMap<String, String>>,
    #[serde(rename = "optionalDependencies")]
    pub optional_dependencies: Option<BTreeMap<String, String>>,
}

#[derive(Debug, Clone, Deserialize, Default)]
pub struct DevEngines {
    #[serde(rename = "packageManager")]
    pub package_manager: Option<DeclaredPackageManagerSpec>,
}

#[derive(Debug, Clone, Deserialize, Default)]
pub struct DeclaredPackageManager {
    pub name: Option<String>,
    pub version: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum DeclaredPackageManagerSpec {
    Single(DeclaredPackageManager),
    Multiple(Vec<DeclaredPackageManager>),
}

#[derive(Debug, Clone, Deserialize, Default)]
#[serde(untagged)]
pub enum PackageBin {
    #[default]
    None,
    Single(String),
    Map(BTreeMap<String, String>),
}

impl PackageJson {
    pub fn bin_command_path(&self, command: &str) -> Option<&str> {
        match &self.bin {
            PackageBin::None => None,
            PackageBin::Single(path) => {
                let package_name = self.name.as_deref()?;
                let short_name = package_name
                    .rsplit_once('/')
                    .map(|(_, tail)| tail)
                    .unwrap_or(package_name);
                if short_name == command {
                    Some(path.as_str())
                } else {
                    None
                }
            }
            PackageBin::Map(map) => map.get(command).map(String::as_str),
        }
    }
}

pub fn package_json_path(cwd: &Path) -> PathBuf {
    cwd.join("package.json")
}

pub fn read_package_json(cwd: &Path) -> Result<Option<PackageJson>> {
    let path = package_json_path(cwd);
    let raw = match crate::core::profile::measure("package_json.read_file", || fs::read(&path)) {
        Ok(content) => content,
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
        Err(e) => {
            return Err(anyhow!(
                "config error: failed to read {}: {e}",
                path.display()
            ));
        }
    };

    let parsed: PackageJson =
        crate::core::profile::measure("package_json.parse_serde", || serde_json::from_slice(&raw))
            .map_err(|error| {
                anyhow!("config error: failed to parse {}: {error}", path.display())
            })?;
    Ok(Some(parsed))
}