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}