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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
use crate::NodeLanguage;
use proto_core::{
    async_trait, is_offline, is_semantic_version, load_versions_manifest, parse_version,
    remove_v_prefix, Manifest, ProtoError, Resolvable, Tool, VersionManifest, VersionManifestEntry,
};
use serde::Deserialize;
use std::collections::BTreeMap;
use tracing::debug;

#[derive(Deserialize)]
#[serde(untagged)]
enum NodeLTS {
    Name(String),
    State(bool),
}

#[derive(Deserialize)]
struct NodeDistVersion {
    lts: NodeLTS,
    version: String, // Starts with v
}

#[async_trait]
impl Resolvable<'_> for NodeLanguage {
    fn get_resolved_version(&self) -> &str {
        match self.version.as_ref() {
            Some(version) => version,
            None => "latest",
        }
    }

    async fn load_version_manifest(&self) -> Result<VersionManifest, ProtoError> {
        let mut aliases = BTreeMap::new();
        let mut versions = BTreeMap::new();
        let response: Vec<NodeDistVersion> =
            load_versions_manifest("https://nodejs.org/dist/index.json").await?;

        for (index, item) in response.iter().enumerate() {
            // First item is always the latest
            if index == 0 {
                aliases.insert("latest".into(), item.version.clone());
            }

            let mut entry = VersionManifestEntry {
                alias: None,
                version: remove_v_prefix(&item.version),
            };

            if let NodeLTS::Name(alias) = &item.lts {
                let alias = alias.to_lowercase();

                // The first encounter of an lts in general is the latest stable
                if !aliases.contains_key("stable") {
                    aliases.insert("stable".into(), item.version.clone());
                }

                // The first encounter of an lts is the latest version for that alias
                if !aliases.contains_key(&alias) {
                    aliases.insert(alias.clone(), item.version.clone());
                }

                entry.alias = Some(alias);
            }

            versions.insert(entry.version.clone(), entry);
        }

        let mut manifest = VersionManifest { aliases, versions };

        manifest.inherit_aliases(&Manifest::load(self.get_manifest_path())?.aliases);

        Ok(manifest)
    }

    async fn resolve_version(&mut self, initial_version: &str) -> Result<String, ProtoError> {
        if let Some(version) = &self.version {
            return Ok(version.to_owned());
        }

        let initial_version = remove_v_prefix(initial_version).to_lowercase();

        // If offline but we have a fully qualified semantic version,
        // exit early and assume the version is legitimate
        if is_semantic_version(&initial_version) && is_offline() {
            self.set_version(&initial_version);

            return Ok(initial_version);
        }

        debug!("Resolving a semantic version for \"{}\"", initial_version);

        let manifest = self.load_version_manifest().await?;
        let candidate;

        // Latest version is always at the top
        if initial_version == "node" || initial_version == "latest" {
            candidate = manifest.get_version_from_alias("latest")?;

        // Stable version is the first with an LTS
        } else if initial_version == "stable"
            || initial_version == "lts-*"
            || initial_version == "lts/*"
        {
            candidate = manifest.get_version_from_alias("stable")?;

            // Find the first version with a matching LTS
        } else if initial_version.starts_with("lts-") || initial_version.starts_with("lts/") {
            candidate = manifest.get_version_from_alias(&initial_version[4..])?;

            // Either an alias or version
        } else {
            candidate = manifest.find_version(&initial_version)?;
        }

        let version = parse_version(candidate)?.to_string();

        debug!("Resolved to {}", version);

        self.set_version(&version);

        Ok(version)
    }

    fn set_version(&mut self, version: &str) {
        self.version = Some(version.to_owned());
    }
}