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 107 108 109 110 111
//! This parses the output of npm-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
///
/// optional since it is new between npm version 6 and 8
pub dependent: Option<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
///
/// optional since it is not included in all npm versions
pub homepage: Option<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 jd = &mut serde_json::Deserializer::from_str(json_str);
let data: NpmOutdatedData = serde_path_to_error::deserialize(jd)?;
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(())
}
}