1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
//! This parses the output of composer-outdated
use std::collections::BTreeMap;
use std::process::Command;
use std::str::from_utf8;
use tracing::{debug, warn};

/// Outer structure for parsing npm-outdated output
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct NpmOutdatedData(pub BTreeMap<String, PackageStatus>);

/// Inner, per-package structure when parsing npm-outdated output
///
/// Meaning of the fields is from [npm-outdated](https://docs.npmjs.com/cli/v7/commands/npm-outdated)
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct PackageStatus {
    /// wanted is the maximum version of the package that satisfies the
    /// semver range specified in package.json. If there's no available
    /// semver range (i.e. you're running npm outdated --global, or
    /// the package isn't included in package.json), then wanted shows
    /// the currently-installed version.
    pub wanted: String,
    /// latest is the version of the package tagged as latest in the registry.
    /// Running npm publish with no special configuration will publish the
    /// package with a dist-tag of latest. This may or may not be the maximum
    /// version of the package, or the most-recently published version of the
    /// package, depending on how the package's developer manages the latest
    /// dist-tag.
    pub latest: String,
    /// where in the physical tree the package is located.
    pub location: Option<String>,
    /// shows which package depends on the displayed dependency
    pub dependent: String,
    /// tells you whether this package is a dependency or a dev/peer/optional
    /// dependency. Packages not included in package.json are always marked
    /// dependencies.
    #[serde(rename = "type")]
    pub package_type: String,
    /// the homepage value contained in the package's packument
    pub homepage: String,
}

/// What the exit code indicated about required updates
#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum IndicatedUpdateRequirement {
    /// No update is required
    UpToDate,
    /// An update is required
    UpdateRequired,
}

impl std::fmt::Display for IndicatedUpdateRequirement {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            IndicatedUpdateRequirement::UpToDate => {
                write!(f, "up-to-date")
            }
            IndicatedUpdateRequirement::UpdateRequired => {
                write!(f, "update-required")
            }
        }
    }
}

/// main entry point for the npm-oudated call
pub fn outdated() -> Result<(IndicatedUpdateRequirement, NpmOutdatedData), crate::Error> {
    let mut cmd = Command::new("npm");

    cmd.args(["outdated", "--json", "--long"]);

    let output = cmd.output()?;

    if !output.status.success() {
        warn!(
            "npm outdated did not return with a successful exit code: {}",
            output.status
        );
        debug!("stdout:\n{}", from_utf8(&output.stdout)?);
        if !output.stderr.is_empty() {
            warn!("stderr:\n{}", from_utf8(&output.stderr)?);
        }
    }

    let update_requirement = if output.status.success() {
        IndicatedUpdateRequirement::UpToDate
    } else {
        IndicatedUpdateRequirement::UpdateRequired
    };

    let json_str = from_utf8(&output.stdout)?;
    let data: NpmOutdatedData = serde_json::from_str(json_str)?;
    Ok((update_requirement, data))
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::Error;

    /// this test requires a package.json and package-lock.json in the main crate
    /// directory (working dir of the tests)
    #[test]
    fn test_run_npm_outdated() -> Result<(), Error> {
        outdated()?;
        Ok(())
    }
}