1use anyhow::{Context as _, Result, anyhow, bail};
2use async_compression::futures::bufread::GzipDecoder;
3use async_tar::Archive;
4use futures::{AsyncReadExt, FutureExt as _, channel::oneshot, future::Shared};
5use http_client::{HttpClient, Url};
6use log::Level;
7use semver::Version;
8use serde::Deserialize;
9use smol::io::BufReader;
10use smol::{fs, lock::Mutex};
11use std::fmt::Display;
12use std::{
13 env::{self, consts},
14 ffi::OsString,
15 io,
16 path::{Path, PathBuf},
17 process::Output,
18 sync::Arc,
19};
20use util::ResultExt;
21use util::archive::extract_zip;
22
23const NODE_CA_CERTS_ENV_VAR: &str = "NODE_EXTRA_CA_CERTS";
24
25#[derive(Clone, Debug, Default, Eq, PartialEq)]
26pub struct NodeBinaryOptions {
27 pub allow_path_lookup: bool,
28 pub allow_binary_download: bool,
29 pub use_paths: Option<(PathBuf, PathBuf)>,
30}
31
32#[derive(Default)]
33pub enum VersionCheck {
34 /// Check whether the installed and requested version have a mismatch
35 VersionMismatch,
36 /// Only check whether the currently installed version is older than the newest one
37 #[default]
38 OlderVersion,
39}
40
41#[derive(Clone)]
42pub struct NodeRuntime(Arc<Mutex<NodeRuntimeState>>);
43
44struct NodeRuntimeState {
45 http: Arc<dyn HttpClient>,
46 instance: Option<Box<dyn NodeRuntimeTrait>>,
47 last_options: Option<NodeBinaryOptions>,
48 options: watch::Receiver<Option<NodeBinaryOptions>>,
49 shell_env_loaded: Shared<oneshot::Receiver<()>>,
50}
51
52impl NodeRuntime {
53 pub fn new(
54 http: Arc<dyn HttpClient>,
55 shell_env_loaded: Option<oneshot::Receiver<()>>,
56 options: watch::Receiver<Option<NodeBinaryOptions>>,
57 ) -> Self {
58 NodeRuntime(Arc::new(Mutex::new(NodeRuntimeState {
59 http,
60 instance: None,
61 last_options: None,
62 options,
63 shell_env_loaded: shell_env_loaded.unwrap_or(oneshot::channel().1).shared(),
64 })))
65 }
66
67 pub fn unavailable() -> Self {
68 NodeRuntime(Arc::new(Mutex::new(NodeRuntimeState {
69 http: Arc::new(http_client::BlockedHttpClient),
70 instance: None,
71 last_options: None,
72 options: watch::channel(Some(NodeBinaryOptions::default())).1,
73 shell_env_loaded: oneshot::channel().1.shared(),
74 })))
75 }
76
77 async fn instance(&self) -> Box<dyn NodeRuntimeTrait> {
78 let mut state = self.0.lock().await;
79
80 let options = loop {
81 match state.options.borrow().as_ref() {
82 Some(options) => break options.clone(),
83 None => {}
84 }
85 match state.options.changed().await {
86 Ok(()) => {}
87 // failure case not cached
88 Err(err) => {
89 return Box::new(UnavailableNodeRuntime {
90 error_message: err.to_string().into(),
91 });
92 }
93 }
94 };
95
96 if state.last_options.as_ref() != Some(&options) {
97 state.instance.take();
98 }
99 if let Some(instance) = state.instance.as_ref() {
100 return instance.boxed_clone();
101 }
102
103 if let Some((node, npm)) = options.use_paths.as_ref() {
104 let instance = match SystemNodeRuntime::new(node.clone(), npm.clone()).await {
105 Ok(instance) => {
106 log::info!("using Node.js from `node.path` in settings: {:?}", instance);
107 Box::new(instance)
108 }
109 Err(err) => {
110 // failure case not cached, since it's cheap to check again
111 return Box::new(UnavailableNodeRuntime {
112 error_message: format!(
113 "failure checking Node.js from `node.path` in settings ({}): {:?}",
114 node.display(),
115 err
116 )
117 .into(),
118 });
119 }
120 };
121 state.instance = Some(instance.boxed_clone());
122 state.last_options = Some(options);
123 return instance;
124 }
125
126 let system_node_error = if options.allow_path_lookup {
127 state.shell_env_loaded.clone().await.ok();
128 match SystemNodeRuntime::detect().await {
129 Ok(instance) => {
130 log::info!("using Node.js found on PATH: {:?}", instance);
131 state.instance = Some(instance.boxed_clone());
132 state.last_options = Some(options);
133 return Box::new(instance);
134 }
135 Err(err) => Some(err),
136 }
137 } else {
138 None
139 };
140
141 let instance = if options.allow_binary_download {
142 let (log_level, why_using_managed) = match system_node_error {
143 Some(err @ DetectError::Other(_)) => (Level::Warn, err.to_string()),
144 Some(err @ DetectError::NotInPath(_)) => (Level::Info, err.to_string()),
145 None => (
146 Level::Info,
147 "`node.ignore_system_version` is `true` in settings".to_string(),
148 ),
149 };
150 match ManagedNodeRuntime::install_if_needed(&state.http).await {
151 Ok(instance) => {
152 log::log!(
153 log_level,
154 "using Zed managed Node.js at {} since {}",
155 instance.installation_path.display(),
156 why_using_managed
157 );
158 Box::new(instance) as Box<dyn NodeRuntimeTrait>
159 }
160 Err(err) => {
161 // failure case is cached, since downloading + installing may be expensive. The
162 // downside of this is that it may fail due to an intermittent network issue.
163 //
164 // TODO: Have `install_if_needed` indicate which failure cases are retryable
165 // and/or have shared tracking of when internet is available.
166 Box::new(UnavailableNodeRuntime {
167 error_message: format!(
168 "failure while downloading and/or installing Zed managed Node.js, \
169 restart Zed to retry: {}",
170 err
171 )
172 .into(),
173 }) as Box<dyn NodeRuntimeTrait>
174 }
175 }
176 } else if let Some(system_node_error) = system_node_error {
177 // failure case not cached, since it's cheap to check again
178 //
179 // TODO: When support is added for setting `options.allow_binary_download`, update this
180 // error message.
181 return Box::new(UnavailableNodeRuntime {
182 error_message: format!(
183 "failure while checking system Node.js from PATH: {}",
184 system_node_error
185 )
186 .into(),
187 });
188 } else {
189 // failure case is cached because it will always happen with these options
190 //
191 // TODO: When support is added for setting `options.allow_binary_download`, update this
192 // error message.
193 Box::new(UnavailableNodeRuntime {
194 error_message: "`node` settings do not allow any way to use Node.js"
195 .to_string()
196 .into(),
197 })
198 };
199
200 state.instance = Some(instance.boxed_clone());
201 state.last_options = Some(options);
202 return instance;
203 }
204
205 pub async fn binary_path(&self) -> Result<PathBuf> {
206 self.instance().await.binary_path()
207 }
208
209 pub async fn run_npm_subcommand(
210 &self,
211 directory: &Path,
212 subcommand: &str,
213 args: &[&str],
214 ) -> Result<Output> {
215 let http = self.0.lock().await.http.clone();
216 self.instance()
217 .await
218 .run_npm_subcommand(Some(directory), http.proxy(), subcommand, args)
219 .await
220 }
221
222 pub async fn npm_package_installed_version(
223 &self,
224 local_package_directory: &Path,
225 name: &str,
226 ) -> Result<Option<String>> {
227 self.instance()
228 .await
229 .npm_package_installed_version(local_package_directory, name)
230 .await
231 }
232
233 pub async fn npm_package_latest_version(&self, name: &str) -> Result<String> {
234 let http = self.0.lock().await.http.clone();
235 let output = self
236 .instance()
237 .await
238 .run_npm_subcommand(
239 None,
240 http.proxy(),
241 "info",
242 &[
243 name,
244 "--json",
245 "--fetch-retry-mintimeout",
246 "2000",
247 "--fetch-retry-maxtimeout",
248 "5000",
249 "--fetch-timeout",
250 "5000",
251 ],
252 )
253 .await?;
254
255 let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?;
256 info.dist_tags
257 .latest
258 .or_else(|| info.versions.pop())
259 .with_context(|| format!("no version found for npm package {name}"))
260 }
261
262 pub async fn npm_install_packages(
263 &self,
264 directory: &Path,
265 packages: &[(&str, &str)],
266 ) -> Result<()> {
267 if packages.is_empty() {
268 return Ok(());
269 }
270
271 let packages: Vec<_> = packages
272 .iter()
273 .map(|(name, version)| format!("{name}@{version}"))
274 .collect();
275
276 let mut arguments: Vec<_> = packages.iter().map(|p| p.as_str()).collect();
277 arguments.extend_from_slice(&[
278 "--save-exact",
279 "--fetch-retry-mintimeout",
280 "2000",
281 "--fetch-retry-maxtimeout",
282 "5000",
283 "--fetch-timeout",
284 "5000",
285 ]);
286
287 // This is also wrong because the directory is wrong.
288 self.run_npm_subcommand(directory, "install", &arguments)
289 .await?;
290 Ok(())
291 }
292
293 pub async fn should_install_npm_package(
294 &self,
295 package_name: &str,
296 local_executable_path: &Path,
297 local_package_directory: &Path,
298 latest_version: &str,
299 version_check: VersionCheck,
300 ) -> bool {
301 // In the case of the local system not having the package installed,
302 // or in the instances where we fail to parse package.json data,
303 // we attempt to install the package.
304 if fs::metadata(local_executable_path).await.is_err() {
305 return true;
306 }
307
308 let Some(installed_version) = self
309 .npm_package_installed_version(local_package_directory, package_name)
310 .await
311 .log_err()
312 .flatten()
313 else {
314 return true;
315 };
316
317 let Some(installed_version) = Version::parse(&installed_version).log_err() else {
318 return true;
319 };
320 let Some(latest_version) = Version::parse(latest_version).log_err() else {
321 return true;
322 };
323
324 match version_check {
325 VersionCheck::VersionMismatch => installed_version != latest_version,
326 VersionCheck::OlderVersion => installed_version < latest_version,
327 }
328 }
329}
330
331enum ArchiveType {
332 TarGz,
333 Zip,
334}
335
336#[derive(Debug, Deserialize)]
337#[serde(rename_all = "kebab-case")]
338pub struct NpmInfo {
339 #[serde(default)]
340 dist_tags: NpmInfoDistTags,
341 versions: Vec<String>,
342}
343
344#[derive(Debug, Deserialize, Default)]
345pub struct NpmInfoDistTags {
346 latest: Option<String>,
347}
348
349#[async_trait::async_trait]
350trait NodeRuntimeTrait: Send + Sync {
351 fn boxed_clone(&self) -> Box<dyn NodeRuntimeTrait>;
352 fn binary_path(&self) -> Result<PathBuf>;
353
354 async fn run_npm_subcommand(
355 &self,
356 directory: Option<&Path>,
357 proxy: Option<&Url>,
358 subcommand: &str,
359 args: &[&str],
360 ) -> Result<Output>;
361
362 async fn npm_package_installed_version(
363 &self,
364 local_package_directory: &Path,
365 name: &str,
366 ) -> Result<Option<String>>;
367}
368
369#[derive(Clone)]
370struct ManagedNodeRuntime {
371 installation_path: PathBuf,
372}
373
374impl ManagedNodeRuntime {
375 const VERSION: &str = "v22.5.1";
376
377 #[cfg(not(windows))]
378 const NODE_PATH: &str = "bin/node";
379 #[cfg(windows)]
380 const NODE_PATH: &str = "node.exe";
381
382 #[cfg(not(windows))]
383 const NPM_PATH: &str = "bin/npm";
384 #[cfg(windows)]
385 const NPM_PATH: &str = "node_modules/npm/bin/npm-cli.js";
386
387 async fn install_if_needed(http: &Arc<dyn HttpClient>) -> Result<Self> {
388 log::info!("Node runtime install_if_needed");
389
390 let os = match consts::OS {
391 "macos" => "darwin",
392 "linux" => "linux",
393 "windows" => "win",
394 other => bail!("Running on unsupported os: {other}"),
395 };
396
397 let arch = match consts::ARCH {
398 "x86_64" => "x64",
399 "aarch64" => "arm64",
400 other => bail!("Running on unsupported architecture: {other}"),
401 };
402
403 let version = Self::VERSION;
404 let folder_name = format!("node-{version}-{os}-{arch}");
405 let node_containing_dir = paths::data_dir().join("node");
406 let node_dir = node_containing_dir.join(folder_name);
407 let node_binary = node_dir.join(Self::NODE_PATH);
408 let npm_file = node_dir.join(Self::NPM_PATH);
409 let node_ca_certs = env::var(NODE_CA_CERTS_ENV_VAR).unwrap_or_else(|_| String::new());
410
411 let valid = if fs::metadata(&node_binary).await.is_ok() {
412 let result = util::command::new_smol_command(&node_binary)
413 .env_clear()
414 .env(NODE_CA_CERTS_ENV_VAR, node_ca_certs)
415 .arg(npm_file)
416 .arg("--version")
417 .args(["--cache".into(), node_dir.join("cache")])
418 .args(["--userconfig".into(), node_dir.join("blank_user_npmrc")])
419 .args(["--globalconfig".into(), node_dir.join("blank_global_npmrc")])
420 .output()
421 .await;
422 match result {
423 Ok(output) => {
424 if output.status.success() {
425 true
426 } else {
427 log::warn!(
428 "Zed managed Node.js binary at {} failed check with output: {:?}",
429 node_binary.display(),
430 output
431 );
432 false
433 }
434 }
435 Err(err) => {
436 log::warn!(
437 "Zed managed Node.js binary at {} failed check, so re-downloading it. \
438 Error: {}",
439 node_binary.display(),
440 err
441 );
442 false
443 }
444 }
445 } else {
446 false
447 };
448
449 if !valid {
450 _ = fs::remove_dir_all(&node_containing_dir).await;
451 fs::create_dir(&node_containing_dir)
452 .await
453 .context("error creating node containing dir")?;
454
455 let archive_type = match consts::OS {
456 "macos" | "linux" => ArchiveType::TarGz,
457 "windows" => ArchiveType::Zip,
458 other => bail!("Running on unsupported os: {other}"),
459 };
460
461 let version = Self::VERSION;
462 let file_name = format!(
463 "node-{version}-{os}-{arch}.{extension}",
464 extension = match archive_type {
465 ArchiveType::TarGz => "tar.gz",
466 ArchiveType::Zip => "zip",
467 }
468 );
469
470 let url = format!("https://nodejs.org/dist/{version}/{file_name}");
471 log::info!("Downloading Node.js binary from {url}");
472 let mut response = http
473 .get(&url, Default::default(), true)
474 .await
475 .context("error downloading Node binary tarball")?;
476 log::info!("Download of Node.js complete, extracting...");
477
478 let body = response.body_mut();
479 match archive_type {
480 ArchiveType::TarGz => {
481 let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
482 let archive = Archive::new(decompressed_bytes);
483 archive.unpack(&node_containing_dir).await?;
484 }
485 ArchiveType::Zip => extract_zip(&node_containing_dir, body).await?,
486 }
487 log::info!("Extracted Node.js to {}", node_containing_dir.display())
488 }
489
490 // Note: Not in the `if !valid {}` so we can populate these for existing installations
491 _ = fs::create_dir(node_dir.join("cache")).await;
492 _ = fs::write(node_dir.join("blank_user_npmrc"), []).await;
493 _ = fs::write(node_dir.join("blank_global_npmrc"), []).await;
494
495 anyhow::Ok(ManagedNodeRuntime {
496 installation_path: node_dir,
497 })
498 }
499}
500
501fn path_with_node_binary_prepended(node_binary: &Path) -> Option<OsString> {
502 let existing_path = env::var_os("PATH");
503 let node_bin_dir = node_binary.parent().map(|dir| dir.as_os_str());
504 match (existing_path, node_bin_dir) {
505 (Some(existing_path), Some(node_bin_dir)) => {
506 if let Ok(joined) = env::join_paths(
507 [PathBuf::from(node_bin_dir)]
508 .into_iter()
509 .chain(env::split_paths(&existing_path)),
510 ) {
511 Some(joined)
512 } else {
513 Some(existing_path)
514 }
515 }
516 (Some(existing_path), None) => Some(existing_path),
517 (None, Some(node_bin_dir)) => Some(node_bin_dir.to_owned()),
518 _ => None,
519 }
520}
521
522#[async_trait::async_trait]
523impl NodeRuntimeTrait for ManagedNodeRuntime {
524 fn boxed_clone(&self) -> Box<dyn NodeRuntimeTrait> {
525 Box::new(self.clone())
526 }
527
528 fn binary_path(&self) -> Result<PathBuf> {
529 Ok(self.installation_path.join(Self::NODE_PATH))
530 }
531
532 async fn run_npm_subcommand(
533 &self,
534 directory: Option<&Path>,
535 proxy: Option<&Url>,
536 subcommand: &str,
537 args: &[&str],
538 ) -> Result<Output> {
539 let attempt = || async move {
540 let node_binary = self.installation_path.join(Self::NODE_PATH);
541 let npm_file = self.installation_path.join(Self::NPM_PATH);
542 let env_path = path_with_node_binary_prepended(&node_binary).unwrap_or_default();
543
544 anyhow::ensure!(
545 smol::fs::metadata(&node_binary).await.is_ok(),
546 "missing node binary file"
547 );
548 anyhow::ensure!(
549 smol::fs::metadata(&npm_file).await.is_ok(),
550 "missing npm file"
551 );
552
553 let node_ca_certs = env::var(NODE_CA_CERTS_ENV_VAR).unwrap_or_else(|_| String::new());
554
555 let mut command = util::command::new_smol_command(node_binary);
556 command.env_clear();
557 command.env("PATH", env_path);
558 command.env(NODE_CA_CERTS_ENV_VAR, node_ca_certs);
559 command.arg(npm_file).arg(subcommand);
560 command.args(["--cache".into(), self.installation_path.join("cache")]);
561 command.args([
562 "--userconfig".into(),
563 self.installation_path.join("blank_user_npmrc"),
564 ]);
565 command.args([
566 "--globalconfig".into(),
567 self.installation_path.join("blank_global_npmrc"),
568 ]);
569 command.args(args);
570 configure_npm_command(&mut command, directory, proxy);
571 command.output().await.map_err(|e| anyhow!("{e}"))
572 };
573
574 let mut output = attempt().await;
575 if output.is_err() {
576 output = attempt().await;
577 anyhow::ensure!(
578 output.is_ok(),
579 "failed to launch npm subcommand {subcommand} subcommand\nerr: {:?}",
580 output.err()
581 );
582 }
583
584 if let Ok(output) = &output {
585 anyhow::ensure!(
586 output.status.success(),
587 "failed to execute npm {subcommand} subcommand:\nstdout: {:?}\nstderr: {:?}",
588 String::from_utf8_lossy(&output.stdout),
589 String::from_utf8_lossy(&output.stderr)
590 );
591 }
592
593 output.map_err(|e| anyhow!("{e}"))
594 }
595 async fn npm_package_installed_version(
596 &self,
597 local_package_directory: &Path,
598 name: &str,
599 ) -> Result<Option<String>> {
600 read_package_installed_version(local_package_directory.join("node_modules"), name).await
601 }
602}
603
604#[derive(Debug, Clone)]
605pub struct SystemNodeRuntime {
606 node: PathBuf,
607 npm: PathBuf,
608 global_node_modules: PathBuf,
609 scratch_dir: PathBuf,
610}
611
612impl SystemNodeRuntime {
613 const MIN_VERSION: semver::Version = Version::new(20, 0, 0);
614 async fn new(node: PathBuf, npm: PathBuf) -> Result<Self> {
615 let output = util::command::new_smol_command(&node)
616 .arg("--version")
617 .output()
618 .await
619 .with_context(|| format!("running node from {:?}", node))?;
620 if !output.status.success() {
621 anyhow::bail!(
622 "failed to run node --version. stdout: {}, stderr: {}",
623 String::from_utf8_lossy(&output.stdout),
624 String::from_utf8_lossy(&output.stderr),
625 );
626 }
627 let version_str = String::from_utf8_lossy(&output.stdout);
628 let version = semver::Version::parse(version_str.trim().trim_start_matches('v'))?;
629 if version < Self::MIN_VERSION {
630 anyhow::bail!(
631 "node at {} is too old. want: {}, got: {}",
632 node.to_string_lossy(),
633 Self::MIN_VERSION,
634 version
635 )
636 }
637
638 let scratch_dir = paths::data_dir().join("node");
639 fs::create_dir(&scratch_dir).await.ok();
640 fs::create_dir(scratch_dir.join("cache")).await.ok();
641
642 let mut this = Self {
643 node,
644 npm,
645 global_node_modules: PathBuf::default(),
646 scratch_dir,
647 };
648 let output = this.run_npm_subcommand(None, None, "root", &["-g"]).await?;
649 this.global_node_modules =
650 PathBuf::from(String::from_utf8_lossy(&output.stdout).to_string());
651
652 Ok(this)
653 }
654
655 async fn detect() -> std::result::Result<Self, DetectError> {
656 let node = which::which("node").map_err(DetectError::NotInPath)?;
657 let npm = which::which("npm").map_err(DetectError::NotInPath)?;
658 Self::new(node, npm).await.map_err(DetectError::Other)
659 }
660}
661
662enum DetectError {
663 NotInPath(which::Error),
664 Other(anyhow::Error),
665}
666
667impl Display for DetectError {
668 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
669 match self {
670 DetectError::NotInPath(err) => {
671 write!(f, "system Node.js wasn't found on PATH: {}", err)
672 }
673 DetectError::Other(err) => {
674 write!(f, "checking system Node.js failed with error: {}", err)
675 }
676 }
677 }
678}
679
680#[async_trait::async_trait]
681impl NodeRuntimeTrait for SystemNodeRuntime {
682 fn boxed_clone(&self) -> Box<dyn NodeRuntimeTrait> {
683 Box::new(self.clone())
684 }
685
686 fn binary_path(&self) -> Result<PathBuf> {
687 Ok(self.node.clone())
688 }
689
690 async fn run_npm_subcommand(
691 &self,
692 directory: Option<&Path>,
693 proxy: Option<&Url>,
694 subcommand: &str,
695 args: &[&str],
696 ) -> anyhow::Result<Output> {
697 let node_ca_certs = env::var(NODE_CA_CERTS_ENV_VAR).unwrap_or_else(|_| String::new());
698 let mut command = util::command::new_smol_command(self.npm.clone());
699 let path = path_with_node_binary_prepended(&self.node).unwrap_or_default();
700 command
701 .env_clear()
702 .env("PATH", path)
703 .env(NODE_CA_CERTS_ENV_VAR, node_ca_certs)
704 .arg(subcommand)
705 .args(["--cache".into(), self.scratch_dir.join("cache")])
706 .args(args);
707 configure_npm_command(&mut command, directory, proxy);
708 let output = command.output().await?;
709 anyhow::ensure!(
710 output.status.success(),
711 "failed to execute npm {subcommand} subcommand:\nstdout: {:?}\nstderr: {:?}",
712 String::from_utf8_lossy(&output.stdout),
713 String::from_utf8_lossy(&output.stderr)
714 );
715 Ok(output)
716 }
717
718 async fn npm_package_installed_version(
719 &self,
720 local_package_directory: &Path,
721 name: &str,
722 ) -> Result<Option<String>> {
723 read_package_installed_version(local_package_directory.join("node_modules"), name).await
724 // todo: allow returning a globally installed version (requires callers not to hard-code the path)
725 }
726}
727
728pub async fn read_package_installed_version(
729 node_module_directory: PathBuf,
730 name: &str,
731) -> Result<Option<String>> {
732 let package_json_path = node_module_directory.join(name).join("package.json");
733
734 let mut file = match fs::File::open(package_json_path).await {
735 Ok(file) => file,
736 Err(err) => {
737 if err.kind() == io::ErrorKind::NotFound {
738 return Ok(None);
739 }
740
741 Err(err)?
742 }
743 };
744
745 #[derive(Deserialize)]
746 struct PackageJson {
747 version: String,
748 }
749
750 let mut contents = String::new();
751 file.read_to_string(&mut contents).await?;
752 let package_json: PackageJson = serde_json::from_str(&contents)?;
753 Ok(Some(package_json.version))
754}
755
756#[derive(Clone)]
757pub struct UnavailableNodeRuntime {
758 error_message: Arc<String>,
759}
760
761#[async_trait::async_trait]
762impl NodeRuntimeTrait for UnavailableNodeRuntime {
763 fn boxed_clone(&self) -> Box<dyn NodeRuntimeTrait> {
764 Box::new(self.clone())
765 }
766 fn binary_path(&self) -> Result<PathBuf> {
767 bail!("{}", self.error_message)
768 }
769
770 async fn run_npm_subcommand(
771 &self,
772 _: Option<&Path>,
773 _: Option<&Url>,
774 _: &str,
775 _: &[&str],
776 ) -> anyhow::Result<Output> {
777 bail!("{}", self.error_message)
778 }
779
780 async fn npm_package_installed_version(
781 &self,
782 _local_package_directory: &Path,
783 _: &str,
784 ) -> Result<Option<String>> {
785 bail!("{}", self.error_message)
786 }
787}
788
789fn configure_npm_command(
790 command: &mut smol::process::Command,
791 directory: Option<&Path>,
792 proxy: Option<&Url>,
793) {
794 if let Some(directory) = directory {
795 command.current_dir(directory);
796 command.args(["--prefix".into(), directory.to_path_buf()]);
797 }
798
799 if let Some(proxy) = proxy {
800 // Map proxy settings from `http://localhost:10809` to `http://127.0.0.1:10809`
801 // NodeRuntime without environment information can not parse `localhost`
802 // correctly.
803 // TODO: map to `[::1]` if we are using ipv6
804 let proxy = proxy
805 .to_string()
806 .to_ascii_lowercase()
807 .replace("localhost", "127.0.0.1");
808
809 command.args(["--proxy", &proxy]);
810 }
811
812 #[cfg(windows)]
813 {
814 // SYSTEMROOT is a critical environment variables for Windows.
815 if let Some(val) = env::var("SYSTEMROOT")
816 .context("Missing environment variable: SYSTEMROOT!")
817 .log_err()
818 {
819 command.env("SYSTEMROOT", val);
820 }
821 // Without ComSpec, the post-install will always fail.
822 if let Some(val) = env::var("ComSpec")
823 .context("Missing environment variable: ComSpec!")
824 .log_err()
825 {
826 command.env("ComSpec", val);
827 }
828 }
829}