Skip to main content

osdk_core/backend/
npm_cli.rs

1use std::path::PathBuf;
2
3use async_trait::async_trait;
4
5use crate::backend::{Backend, Ctx, InstallCtx};
6use crate::error::{Error, Result};
7use crate::pipeline::{self, ArchiveKind, InstallPlan, PipelineCtx};
8use crate::platform::Os;
9use crate::source::Source;
10use crate::version::{ToolVersion, VersionInfo};
11
12pub struct NpmBackend;
13
14#[async_trait]
15impl Backend for NpmBackend {
16    fn id(&self) -> &str {
17        "npm"
18    }
19
20    fn default_sources(&self) -> Vec<Source> {
21        vec![
22            Source::mirror("npmmirror", "https://registry.npmmirror.com/", 5)
23                .with_index("https://registry.npmmirror.com/npm"),
24            Source::official("npm", "https://registry.npmjs.org/")
25                .with_index("https://registry.npmjs.org/npm"),
26        ]
27    }
28
29    fn probe_url(&self, _ctx: &Ctx, source: &Source) -> Option<String> {
30        source.index_url.clone()
31    }
32
33    async fn list_remote_versions(&self, ctx: &Ctx) -> Result<Vec<VersionInfo>> {
34        let sources = crate::source::select::ranked_source_list(ctx, self).await?;
35        let versions = crate::npm::list_versions(ctx, &sources, "npm").await?;
36        Ok(versions
37            .into_iter()
38            .map(|version| VersionInfo {
39                stable: !version.contains('-'),
40                version,
41                lts: None,
42            })
43            .collect())
44    }
45
46    async fn install(&self, ictx: &InstallCtx<'_>, tv: &ToolVersion) -> Result<()> {
47        let ctx = ictx.ctx;
48        let plan = if let Some(plan) = pipeline::locked_install_plan(self.id(), tv, true)? {
49            plan
50        } else {
51            let sources = crate::source::select::ranked_source_list(ctx, self).await?;
52            let dist = crate::npm::resolve_dist(ctx, &sources, "npm", &tv.version).await?;
53            InstallPlan {
54                tool: self.id().into(),
55                version: tv.version.clone(),
56                urls: dist.urls,
57                file_name: format!("npm-{}.tgz", tv.version),
58                kind: ArchiveKind::TarGz,
59                checksum: dist.checksum,
60                strip_root: true,
61                subdir: None,
62            }
63        };
64        let pipeline_ctx = PipelineCtx {
65            client: &ctx.client,
66            dirs: &ctx.dirs,
67            cas: &ctx.cas,
68            link_mode: ctx.config.settings.link_mode,
69            show_progress: ctx.show_progress,
70            offline: ctx.config.settings.offline,
71            require_checksums: true,
72        };
73        let install = pipeline::run(&plan, &pipeline_ctx).await?;
74        let entry = install.join("bin/npm-cli.js");
75        let npx_entry = install.join("bin/npx-cli.js");
76        if !entry.is_file() || !npx_entry.is_file() {
77            let _ = std::fs::remove_dir_all(&install);
78            return Err(Error::other(format!(
79                "npm {} archive is missing bin/npm-cli.js or bin/npx-cli.js",
80                tv.version
81            )));
82        }
83        write_launchers(&install.join("bin"), &entry, &npx_entry, ctx.platform.os)?;
84        Ok(())
85    }
86
87    fn bin_paths(&self, ctx: &Ctx, tv: &ToolVersion) -> Result<Vec<PathBuf>> {
88        Ok(vec![ctx
89            .dirs
90            .install_path(self.id(), &tv.version)
91            .join("bin")])
92    }
93
94    fn exec_env(
95        &self,
96        ctx: &Ctx,
97        _tv: &ToolVersion,
98    ) -> Result<std::collections::BTreeMap<String, String>> {
99        Ok(crate::cache::manager_exec_env(
100            &ctx.dirs.cache,
101            &[("npm_config_cache", "npm")],
102        ))
103    }
104
105    fn bin_names(&self, _ctx: &Ctx, _tv: &ToolVersion) -> Result<Vec<String>> {
106        Ok(vec!["npm".into(), "npx".into()])
107    }
108}
109
110#[cfg(unix)]
111fn write_launchers(
112    bin_dir: &std::path::Path,
113    npm: &std::path::Path,
114    npx: &std::path::Path,
115    _os: Os,
116) -> Result<()> {
117    use std::os::unix::fs::PermissionsExt;
118    for (name, script) in [("npm", npm), ("npx", npx)] {
119        let path = bin_dir.join(name);
120        let contents = format!("#!/bin/sh\nexec node \"{}\" \"$@\"\n", script.display());
121        std::fs::write(&path, contents).map_err(|error| Error::io(&path, error))?;
122        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755))
123            .map_err(|error| Error::io(&path, error))?;
124    }
125    Ok(())
126}
127
128#[cfg(windows)]
129fn write_launchers(
130    bin_dir: &std::path::Path,
131    npm: &std::path::Path,
132    npx: &std::path::Path,
133    _os: Os,
134) -> Result<()> {
135    for (name, script) in [("npm", npm), ("npx", npx)] {
136        let path = bin_dir.join(format!("{name}.cmd"));
137        let contents = format!("@echo off\r\nnode \"{}\" %*\r\n", script.display());
138        std::fs::write(&path, contents).map_err(|error| Error::io(&path, error))?;
139    }
140    Ok(())
141}
142
143#[cfg(test)]
144mod tests {
145    #[cfg(unix)]
146    use super::*;
147
148    #[cfg(unix)]
149    #[test]
150    fn launchers_call_node_from_path() {
151        let temp = tempfile::tempdir().unwrap();
152        let bin = temp.path().join("bin");
153        std::fs::create_dir_all(&bin).unwrap();
154        let npm = bin.join("npm-cli.js");
155        let npx = bin.join("npx-cli.js");
156        std::fs::write(&npm, "").unwrap();
157        std::fs::write(&npx, "").unwrap();
158        write_launchers(&bin, &npm, &npx, Os::Linux).unwrap();
159        let launcher = std::fs::read_to_string(bin.join("npm")).unwrap();
160        assert!(launcher.contains("exec node"));
161        assert!(launcher.contains("npm-cli.js"));
162    }
163}