npm_parser/
outdated.rs

1//! This parses the output of npm-outdated
2use std::collections::BTreeMap;
3use std::process::Command;
4use std::str::from_utf8;
5use tracing::{debug, warn};
6
7/// Outer structure for parsing npm-outdated output
8#[derive(Debug, serde::Serialize, serde::Deserialize)]
9pub struct NpmOutdatedData(pub BTreeMap<String, PackageStatus>);
10
11/// Inner, per-package structure when parsing npm-outdated output
12///
13/// Meaning of the fields is from [npm-outdated](https://docs.npmjs.com/cli/v7/commands/npm-outdated)
14#[derive(Debug, serde::Serialize, serde::Deserialize)]
15pub struct PackageStatus {
16    /// wanted is the maximum version of the package that satisfies the
17    /// semver range specified in package.json. If there's no available
18    /// semver range (i.e. you're running npm outdated --global, or
19    /// the package isn't included in package.json), then wanted shows
20    /// the currently-installed version.
21    pub wanted: String,
22    /// latest is the version of the package tagged as latest in the registry.
23    /// Running npm publish with no special configuration will publish the
24    /// package with a dist-tag of latest. This may or may not be the maximum
25    /// version of the package, or the most-recently published version of the
26    /// package, depending on how the package's developer manages the latest
27    /// dist-tag.
28    pub latest: String,
29    /// where in the physical tree the package is located.
30    pub location: Option<String>,
31    /// shows which package depends on the displayed dependency
32    ///
33    /// optional since it is new between npm version 6 and 8
34    pub dependent: Option<String>,
35    /// tells you whether this package is a dependency or a dev/peer/optional
36    /// dependency. Packages not included in package.json are always marked
37    /// dependencies.
38    #[serde(rename = "type")]
39    pub package_type: String,
40    /// the homepage value contained in the package's packument
41    ///
42    /// optional since it is not included in all npm versions
43    pub homepage: Option<String>,
44}
45
46/// What the exit code indicated about required updates
47#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
48pub enum IndicatedUpdateRequirement {
49    /// No update is required
50    UpToDate,
51    /// An update is required
52    UpdateRequired,
53}
54
55impl std::fmt::Display for IndicatedUpdateRequirement {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        match self {
58            Self::UpToDate => {
59                write!(f, "up-to-date")
60            }
61            Self::UpdateRequired => {
62                write!(f, "update-required")
63            }
64        }
65    }
66}
67
68/// main entry point for the npm-outdated call
69///
70/// # Errors
71///
72/// fails if the call to npm outdated fails or if the output could not be parsed
73pub fn outdated() -> Result<(IndicatedUpdateRequirement, NpmOutdatedData), crate::Error> {
74    let mut cmd = Command::new("npm");
75
76    cmd.args(["outdated", "--json", "--long"]);
77
78    let output = cmd.output()?;
79
80    if !output.status.success() {
81        warn!(
82            "npm outdated did not return with a successful exit code: {}",
83            output.status
84        );
85        debug!("stdout:\n{}", from_utf8(&output.stdout)?);
86        if !output.stderr.is_empty() {
87            warn!("stderr:\n{}", from_utf8(&output.stderr)?);
88        }
89    }
90
91    let update_requirement = if output.status.success() {
92        IndicatedUpdateRequirement::UpToDate
93    } else {
94        IndicatedUpdateRequirement::UpdateRequired
95    };
96
97    let json_str = from_utf8(&output.stdout)?;
98    let jd = &mut serde_json::Deserializer::from_str(json_str);
99    let data: NpmOutdatedData = serde_path_to_error::deserialize(jd)?;
100    Ok((update_requirement, data))
101}
102
103#[cfg(test)]
104mod test {
105    use super::*;
106    use crate::Error;
107
108    /// this test requires a package.json and package-lock.json in the main crate
109    /// directory (working dir of the tests)
110    #[test]
111    fn test_run_npm_outdated() -> Result<(), Error> {
112        outdated()?;
113        Ok(())
114    }
115}