node_runtime.rs

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