proto_node/
resolve.rs

1use crate::NodeLanguage;
2use proto_core::{
3    async_trait, is_offline, is_semantic_version, load_versions_manifest, parse_version,
4    remove_v_prefix, Describable, ProtoError, Resolvable, Tool, VersionManifest,
5    VersionManifestEntry,
6};
7use serde::Deserialize;
8use std::collections::BTreeMap;
9use tracing::debug;
10
11#[derive(Deserialize)]
12#[serde(untagged)]
13enum NodeLTS {
14    Name(String),
15    State(bool),
16}
17
18#[derive(Deserialize)]
19struct NodeDistVersion {
20    lts: NodeLTS,
21    version: String, // Starts with v
22}
23
24#[async_trait]
25impl Resolvable<'_> for NodeLanguage {
26    fn get_resolved_version(&self) -> &str {
27        match self.version.as_ref() {
28            Some(version) => version,
29            None => "latest",
30        }
31    }
32
33    async fn load_version_manifest(&self) -> Result<VersionManifest, ProtoError> {
34        let mut aliases = BTreeMap::new();
35        let mut versions = BTreeMap::new();
36        let response: Vec<NodeDistVersion> =
37            load_versions_manifest("https://nodejs.org/dist/index.json").await?;
38
39        for (index, item) in response.iter().enumerate() {
40            // First item is always the latest
41            if index == 0 {
42                aliases.insert("latest".into(), item.version.clone());
43            }
44
45            let mut entry = VersionManifestEntry {
46                alias: None,
47                version: remove_v_prefix(&item.version),
48            };
49
50            if let NodeLTS::Name(alias) = &item.lts {
51                let alias = alias.to_lowercase();
52
53                // The first encounter of an lts in general is the latest stable
54                if !aliases.contains_key("stable") {
55                    aliases.insert("stable".into(), item.version.clone());
56                }
57
58                // The first encounter of an lts is the latest version for that alias
59                if !aliases.contains_key(&alias) {
60                    aliases.insert(alias.clone(), item.version.clone());
61                }
62
63                entry.alias = Some(alias);
64            }
65
66            versions.insert(entry.version.clone(), entry);
67        }
68
69        let mut manifest = VersionManifest { aliases, versions };
70
71        manifest.inherit_aliases(&self.get_manifest()?.aliases);
72
73        Ok(manifest)
74    }
75
76    async fn resolve_version(&mut self, initial_version: &str) -> Result<String, ProtoError> {
77        if let Some(version) = &self.version {
78            return Ok(version.to_owned());
79        }
80
81        let initial_version = remove_v_prefix(initial_version).to_lowercase();
82
83        // If offline but we have a fully qualified semantic version,
84        // exit early and assume the version is legitimate
85        if is_semantic_version(&initial_version) && is_offline() {
86            self.set_version(&initial_version);
87
88            return Ok(initial_version);
89        }
90
91        debug!(
92            tool = self.get_id(),
93            initial_version = initial_version,
94            "Resolving a semantic version for \"{}\"",
95            initial_version
96        );
97
98        let manifest = self.load_version_manifest().await?;
99        let candidate;
100
101        // Latest version is always at the top
102        if initial_version == "node" || initial_version == "latest" {
103            candidate = manifest.get_version_from_alias("latest")?;
104
105        // Stable version is the first with an LTS
106        } else if initial_version == "stable"
107            || initial_version == "lts-*"
108            || initial_version == "lts/*"
109        {
110            candidate = manifest.get_version_from_alias("stable")?;
111
112            // Find the first version with a matching LTS
113        } else if initial_version.starts_with("lts-") || initial_version.starts_with("lts/") {
114            candidate = manifest.get_version_from_alias(&initial_version[4..])?;
115
116            // Either an alias or version
117        } else {
118            candidate = manifest.find_version(&initial_version)?;
119        }
120
121        let version = parse_version(candidate)?.to_string();
122
123        debug!(
124            tool = self.get_id(),
125            version = candidate,
126            "Resolved to {}",
127            version
128        );
129
130        self.set_version(&version);
131
132        Ok(version)
133    }
134
135    fn set_version(&mut self, version: &str) {
136        self.version = Some(version.to_owned());
137    }
138}