use anyhow::{Context as _, Result, anyhow, bail};use async_compression::futures::bufread::GzipDecoder;use async_tar::Archive;use chrono::{DateTime, Utc};use futures::{AsyncReadExt, FutureExt as _, channel::oneshot, future::Shared};use http_client::{Host, HttpClient, Url};use log::Level;use semver::{Version, VersionReq};use serde::Deserialize;use smol::io::BufReader;use smol::{fs, lock::Mutex};use std::collections::HashMap;use std::fmt::Display;use std::{ env::{self, consts}, ffi::OsString, io, net::{IpAddr, Ipv4Addr}, path::{Path, PathBuf}, process::Output, sync::Arc,};use util::ResultExt;use util::archive::extract_zip; const NODE_CA_CERTS_ENV_VAR: &str = "NODE_EXTRA_CA_CERTS"; #[derive(Clone, Debug, Default, Eq, PartialEq)]pub struct NodeBinaryOptions { pub allow_path_lookup: bool, pub allow_binary_download: bool, pub use_paths: Option<(PathBuf, PathBuf)>,} /// Use this when you need to launch npm as a long-lived process (for example, an agent server),/// so the invocation and environment stay consistent with the Node runtime's proxy and CA setup.#[derive(Clone, Debug)]pub struct NpmCommand { pub path: PathBuf, pub args: Vec<String>, pub env: HashMap<String, String>,} pub enum VersionStrategy<'a> { /// Install if current version doesn't match pinned version Pin(&'a Version), /// Install if current version is older than latest version Latest(&'a Version),} #[derive(Clone)]pub struct NodeRuntime(Arc<Mutex<NodeRuntimeState>>); struct NodeRuntimeState { http: Arc<dyn HttpClient>, instance: Option<Box<dyn NodeRuntimeTrait>>, last_options: Option<NodeBinaryOptions>, options: watch::Receiver<Option<NodeBinaryOptions>>, shell_env_loaded: Shared<oneshot::Receiver<()>>,} impl NodeRuntime { pub fn new( http: Arc<dyn HttpClient>, shell_env_loaded: Option<oneshot::Receiver<()>>, options: watch::Receiver<Option<NodeBinaryOptions>>, ) -> Self { NodeRuntime(Arc::new(Mutex::new(NodeRuntimeState { http, instance: None, last_options: None, options, shell_env_loaded: shell_env_loaded.unwrap_or(oneshot::channel().1).shared(), }))) } pub fn unavailable() -> Self { NodeRuntime(Arc::new(Mutex::new(NodeRuntimeState { http: Arc::new(http_client::BlockedHttpClient), instance: None, last_options: None, options: watch::channel(Some(NodeBinaryOptions::default())).1, shell_env_loaded: oneshot::channel().1.shared(), }))) } async fn instance(&self) -> Box<dyn NodeRuntimeTrait> { let mut state = self.0.lock().await; let options = loop { if let Some(options) = state.options.borrow().as_ref() { break options.clone(); } match state.options.changed().await { Ok(()) => {} // failure case not cached Err(err) => { return Box::new(UnavailableNodeRuntime { error_message: err.to_string().into(), }) as Box<dyn NodeRuntimeTrait>; } } }; if state.last_options.as_ref() != Some(&options) { state.instance.take(); } if let Some(instance) = state.instance.as_ref() { return instance.boxed_clone(); } if let Some((node, npm)) = options.use_paths.as_ref() { let instance = match SystemNodeRuntime::new(node.clone(), npm.clone()).await { Ok(instance) => { log::info!("using Node.js from `node.path` in settings: {:?}", instance); Box::new(instance) } Err(err) => { // failure case not cached, since it's cheap to check again return Box::new(UnavailableNodeRuntime { error_message: format!( "failure checking Node.js from `node.path` in settings ({}): {:?}", node.display(), err ) .into(), }); } }; state.instance = Some(instance.boxed_clone()); state.last_options = Some(options); return instance; } let system_node_error = if options.allow_path_lookup { state.shell_env_loaded.clone().await.ok(); match SystemNodeRuntime::detect().await { Ok(instance) => { log::info!("using Node.js found on PATH: {:?}", instance); state.instance = Some(instance.boxed_clone()); state.last_options = Some(options); return Box::new(instance) as Box<dyn NodeRuntimeTrait>; } Err(err) => Some(err), } } else { None }; let instance = if options.allow_binary_download { let (log_level, why_using_managed) = match system_node_error { Some(err @ DetectError::Other(_)) => (Level::Warn, err.to_string()), Some(err @ DetectError::NotInPath(_)) => (Level::Info, err.to_string()), None => ( Level::Info, "`node.ignore_system_version` is `true` in settings".to_string(), ), }; match ManagedNodeRuntime::install_if_needed(&state.http).await { Ok(instance) => { log::log!( log_level, "using Zed managed Node.js at {} since {}", instance.installation_path.display(), why_using_managed ); Box::new(instance) as Box<dyn NodeRuntimeTrait> } Err(err) => { // failure case is cached, since downloading + installing may be expensive. The // downside of this is that it may fail due to an intermittent network issue. // // TODO: Have `install_if_needed` indicate which failure cases are retryable // and/or have shared tracking of when internet is available. Box::new(UnavailableNodeRuntime { error_message: format!( "failure while downloading and/or installing Zed managed Node.js, \ restart Zed to retry: {}", err ) .into(), }) as Box<dyn NodeRuntimeTrait> } } } else if let Some(system_node_error) = system_node_error { // failure case not cached, since it's cheap to check again // // TODO: When support is added for setting `options.allow_binary_download`, update this // error message. return Box::new(UnavailableNodeRuntime { error_message: format!( "failure while checking system Node.js from PATH: {}", system_node_error ) .into(), }); } else { // failure case is cached because it will always happen with these options // // TODO: When support is added for setting `options.allow_binary_download`, update this // error message. Box::new(UnavailableNodeRuntime { error_message: "`node` settings do not allow any way to use Node.js" .to_string() .into(), }) }; state.instance = Some(instance.boxed_clone()); state.last_options = Some(options); instance } pub async fn binary_path(&self) -> Result<PathBuf> { self.instance().await.binary_path() } pub async fn run_npm_subcommand( &self, directory: Option<&Path>, subcommand: &str, args: &[&str], ) -> Result<Output> { let http = self.0.lock().await.http.clone(); self.instance() .await .run_npm_subcommand(directory, http.proxy(), subcommand, args) .await } pub async fn npm_package_installed_version( &self, local_package_directory: &Path, name: &str, ) -> Result<Option<Version>> { self.instance() .await .npm_package_installed_version(local_package_directory, name) .await } pub async fn npm_command( &self, prefix_dir: Option<&Path>, subcommand: &str, args: &[&str], ) -> Result<NpmCommand> { let http = self.0.lock().await.http.clone(); self.instance() .await .npm_command(prefix_dir, http.proxy(), subcommand, args) .await } pub async fn npm_package_latest_version(&self, name: &str) -> Result<Version> { self.npm_package_latest_version_with_requirement(name, None) .await } pub async fn npm_package_latest_version_with_requirement( &self, name: &str, version_requirement: Option<&VersionReq>, ) -> Result<Version> { let http = self.0.lock().await.http.clone(); let instance = self.instance().await; let output = instance .run_npm_subcommand( None, http.proxy(), "info", &[ name, "--json", "--fetch-retry-mintimeout", "2000", "--fetch-retry-maxtimeout", "5000", "--fetch-timeout", "5000", ], ) .await?; let info: NpmInfo = deserialize_npm_info_from_response(&output.stdout).map_err(|e| { anyhow::anyhow!( "failed to parse npm info response: {e}\nstdout: {}", String::from_utf8_lossy(&output.stdout) ) })?; let before = npm_config_before(instance.as_ref(), http.proxy()) .await .context("getting npm before config") .log_err() .flatten(); let latest_dist_tag = info.dist_tags.latest.clone(); let selected_version = select_npm_package_version(name, info, before.as_deref(), version_requirement)?; log::debug!( "selected latest npm package version package={name:?} version_requirement={version_requirement:?} before={before:?} dist_tag_latest={latest_dist_tag:?} selected={selected_version}" ); Ok(selected_version) } pub async fn npm_install_packages( &self, directory: &Path, packages: &[(&str, &str)], ) -> Result<()> { if packages.is_empty() { return Ok(()); } log::debug!( "installing npm packages directory={} packages={packages:?}", directory.display() ); let packages: Vec<_> = packages .iter() .map(|(name, version)| format!("{name}@{version}")) .collect(); let arguments: Vec<_> = packages .iter() .map(|p| p.as_str()) .chain([ "--save-exact", "--fetch-retry-mintimeout", "2000", "--fetch-retry-maxtimeout", "5000", "--fetch-timeout", "5000", ]) .collect(); // This is also wrong because the directory is wrong. self.run_npm_subcommand(Some(directory), "install", &arguments) .await?; Ok(()) } pub async fn npm_install_latest_packages( &self, directory: &Path, package_names: &[&str], ) -> Result<()> { // Let npm apply user config such as `before` and `min-release-age` during resolution. log::debug!( "installing latest npm packages directory={} packages={package_names:?}", directory.display() ); let packages = package_names .iter() .map(|package_name| (*package_name, "latest")) .collect::<Vec<_>>(); self.npm_install_packages(directory, &packages).await } pub async fn should_install_npm_package( &self, package_name: &str, local_executable_path: &Path, local_package_directory: &Path, version_strategy: VersionStrategy<'_>, ) -> bool { // In the case of the local system not having the package installed, // or in the instances where we fail to parse package.json data, // we attempt to install the package. if fs::metadata(local_executable_path).await.is_err() { log::debug!( "npm package cache miss package={package_name:?} reason=missing-executable executable={}", local_executable_path.display() ); return true; } let Some(installed_version) = self .npm_package_installed_version(local_package_directory, package_name) .await .log_err() .flatten() else { log::debug!( "npm package cache miss package={package_name:?} reason=missing-installed-version package_dir={}", local_package_directory.display() ); return true; }; let version_strategy_label = match &version_strategy { VersionStrategy::Pin(version) => format!("pin:{version}"), VersionStrategy::Latest(version) => format!("latest:{version}"), }; let should_install = should_install_npm_package_version(&installed_version, version_strategy); log::debug!( "npm package cache check package={package_name:?} installed={installed_version} strategy={version_strategy_label} should_install={should_install}" ); should_install }} fn should_install_npm_package_version( installed_version: &Version, version_strategy: VersionStrategy<'_>,) -> bool { match version_strategy { VersionStrategy::Pin(pinned_version) => installed_version != pinned_version, VersionStrategy::Latest(latest_version) => installed_version < latest_version, }} enum ArchiveType { TarGz, Zip,} #[derive(Debug, Deserialize)]#[serde(rename_all = "kebab-case")]pub struct NpmInfo { #[serde(default)] dist_tags: NpmInfoDistTags, versions: Vec<Version>, #[serde(default, deserialize_with = "deserialize_npm_info_time")] time: HashMap<String, String>,} /// Parse NpmInfo from npm info --json output, handling both v11 and >= v12 formats.fn deserialize_npm_info_from_response(data: &[u8]) -> Result<NpmInfo, serde_json::Error> { let value: serde_json::Value = serde_json::from_slice(data)?; // npm >= 12 returns an array with one object: [ { ... } ] if let serde_json::Value::Array(arr) = &value { if arr.len() == 1 { return NpmInfo::deserialize(&arr[0]); } } // npm <= v11 returns a bare JSON object: { ... } NpmInfo::deserialize(value)} #[derive(Debug, Deserialize, Default)]pub struct NpmInfoDistTags { latest: Option<Version>,} // Some registries put non-string values in the `time` map: JFrog Artifactory emits// `"unpublished": null`, and npm itself reports `unpublished` as an object when a// package has had versions unpublished. Only version keys map to the RFC 3339 strings// we read, so keep the string entries and drop the rest rather than failing to parse// the entire `npm info` response (which would block language server installation).fn deserialize_npm_info_time<'de, D>(deserializer: D) -> Result<HashMap<String, String>, D::Error>where D: serde::Deserializer<'de>,{ let entries = HashMap::<String, serde_json::Value>::deserialize(deserializer)?; Ok(entries .into_iter() .filter_map(|(key, value)| match value { serde_json::Value::String(value) => Some((key, value)), _ => None, }) .collect())} #[derive(Debug, Deserialize)]struct NpmConfig { #[serde(default)] before: Option<String>,} async fn npm_config_before( node_runtime: &dyn NodeRuntimeTrait, proxy: Option<&Url>,) -> Result<Option<String>> { // `npm config get before` renders Date values for display. The JSON config output keeps the // computed cutoff in the same ISO format used by `npm info --json` release times. let output = node_runtime .run_npm_subcommand(None, proxy, "config", &["list", "--json"]) .await?; let config: NpmConfig = serde_json::from_slice(&output.stdout)?; Ok(config .before .filter(|before| !before.trim().is_empty() && before != "null"))} fn select_npm_package_version( package_name: &str, mut info: NpmInfo, before: Option<&str>, version_requirement: Option<&VersionReq>,) -> Result<Version> { if let Some(version_requirement) = version_requirement { info.versions .retain(|version| version_requirement.matches(version)); info.versions.sort(); info.dist_tags.latest = infoLines 1–500 of 1660. Load more
use anyhow::{Context as _, Result, anyhow, bail}; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use chrono::{DateTime, Utc}; use futures::{AsyncReadExt, FutureExt as _, channel::oneshot, future::Shared}; use http_client::{Host, HttpClient, Url}; use log::Level; use semver::{Version, VersionReq}; use serde::Deserialize; use smol::io::BufReader; use smol::{fs, lock::Mutex}; use std::collections::HashMap; use std::fmt::Display; use std::{ env::{self, consts}, ffi::OsString, io, net::{IpAddr, Ipv4Addr}, path::{Path, PathBuf}, process::Output, sync::Arc, }; use util::ResultExt; use util::archive::extract_zip; const NODE_CA_CERTS_ENV_VAR: &str = "NODE_EXTRA_CA_CERTS"; #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct NodeBinaryOptions { pub allow_path_lookup: bool, pub allow_binary_download: bool, pub use_paths: Option<(PathBuf, PathBuf)>, } /// Use this when you need to launch npm as a long-lived process (for example, an agent server), /// so the invocation and environment stay consistent with the Node runtime's proxy and CA setup. #[derive(Clone, Debug)] pub struct NpmCommand { pub path: PathBuf, pub args: Vec<String>, pub env: HashMap<String, String>, } pub enum VersionStrategy<'a> { /// Install if current version doesn't match pinned version Pin(&'a Version), /// Install if current version is older than latest version Latest(&'a Version), } #[derive(Clone)] pub struct NodeRuntime(Arc<Mutex<NodeRuntimeState>>); struct NodeRuntimeState { http: Arc<dyn HttpClient>, instance: Option<Box<dyn NodeRuntimeTrait>>, last_options: Option<NodeBinaryOptions>, options: watch::Receiver<Option<NodeBinaryOptions>>, shell_env_loaded: Shared<oneshot::Receiver<()>>, } impl NodeRuntime { pub fn new( http: Arc<dyn HttpClient>, shell_env_loaded: Option<oneshot::Receiver<()>>, options: watch::Receiver<Option<NodeBinaryOptions>>, ) -> Self { NodeRuntime(Arc::new(Mutex::new(NodeRuntimeState { http, instance: None, last_options: None, options, shell_env_loaded: shell_env_loaded.unwrap_or(oneshot::channel().1).shared(), }))) } pub fn unavailable() -> Self { NodeRuntime(Arc::new(Mutex::new(NodeRuntimeState { http: Arc::new(http_client::BlockedHttpClient), instance: None, last_options: None, options: watch::channel(Some(NodeBinaryOptions::default())).1, shell_env_loaded: oneshot::channel().1.shared(), }))) } async fn instance(&self) -> Box<dyn NodeRuntimeTrait> { let mut state = self.0.lock().await; let options = loop { if let Some(options) = state.options.borrow().as_ref() { break options.clone(); } match state.options.changed().await { Ok(()) => {} // failure case not cached Err(err) => { return Box::new(UnavailableNodeRuntime { error_message: err.to_string().into(), }) as Box<dyn NodeRuntimeTrait>; } } }; if state.last_options.as_ref() != Some(&options) { state.instance.take(); } if let Some(instance) = state.instance.as_ref() { return instance.boxed_clone(); } if let Some((node, npm)) = options.use_paths.as_ref() { let instance = match SystemNodeRuntime::new(node.clone(), npm.clone()).await { Ok(instance) => { log::info!("using Node.js from `node.path` in settings: {:?}", instance); Box::new(instance) } Err(err) => { // failure case not cached, since it's cheap to check again return Box::new(UnavailableNodeRuntime { error_message: format!( "failure checking Node.js from `node.path` in settings ({}): {:?}", node.display(), err ) .into(), }); } }; state.instance = Some(instance.boxed_clone()); state.last_options = Some(options); return instance; } let system_node_error = if options.allow_path_lookup { state.shell_env_loaded.clone().await.ok(); match SystemNodeRuntime::detect().await { Ok(instance) => { log::info!("using Node.js found on PATH: {:?}", instance); state.instance = Some(instance.boxed_clone()); state.last_options = Some(options); return Box::new(instance) as Box<dyn NodeRuntimeTrait>; } Err(err) => Some(err), } } else { None }; let instance = if options.allow_binary_download { let (log_level, why_using_managed) = match system_node_error { Some(err @ DetectError::Other(_)) => (Level::Warn, err.to_string()), Some(err @ DetectError::NotInPath(_)) => (Level::Info, err.to_string()), None => ( Level::Info, "`node.ignore_system_version` is `true` in settings".to_string(), ), }; match ManagedNodeRuntime::install_if_needed(&state.http).await { Ok(instance) => { log::log!( log_level, "using Zed managed Node.js at {} since {}", instance.installation_path.display(), why_using_managed ); Box::new(instance) as Box<dyn NodeRuntimeTrait> } Err(err) => { // failure case is cached, since downloading + installing may be expensive. The // downside of this is that it may fail due to an intermittent network issue. // // TODO: Have `install_if_needed` indicate which failure cases are retryable // and/or have shared tracking of when internet is available. Box::new(UnavailableNodeRuntime { error_message: format!( "failure while downloading and/or installing Zed managed Node.js, \ restart Zed to retry: {}", err ) .into(), }) as Box<dyn NodeRuntimeTrait> } } } else if let Some(system_node_error) = system_node_error { // failure case not cached, since it's cheap to check again // // TODO: When support is added for setting `options.allow_binary_download`, update this // error message. return Box::new(UnavailableNodeRuntime { error_message: format!( "failure while checking system Node.js from PATH: {}", system_node_error ) .into(), }); } else { // failure case is cached because it will always happen with these options // // TODO: When support is added for setting `options.allow_binary_download`, update this // error message. Box::new(UnavailableNodeRuntime { error_message: "`node` settings do not allow any way to use Node.js" .to_string() .into(), }) }; state.instance = Some(instance.boxed_clone()); state.last_options = Some(options); instance } pub async fn binary_path(&self) -> Result<PathBuf> { self.instance().await.binary_path() } pub async fn run_npm_subcommand( &self, directory: Option<&Path>, subcommand: &str, args: &[&str], ) -> Result<Output> { let http = self.0.lock().await.http.clone(); self.instance() .await .run_npm_subcommand(directory, http.proxy(), subcommand, args) .await } pub async fn npm_package_installed_version( &self, local_package_directory: &Path, name: &str, ) -> Result<Option<Version>> { self.instance() .await .npm_package_installed_version(local_package_directory, name) .await } pub async fn npm_command( &self, prefix_dir: Option<&Path>, subcommand: &str, args: &[&str], ) -> Result<NpmCommand> { let http = self.0.lock().await.http.clone(); self.instance() .await .npm_command(prefix_dir, http.proxy(), subcommand, args) .await } pub async fn npm_package_latest_version(&self, name: &str) -> Result<Version> { self.npm_package_latest_version_with_requirement(name, None) .await } pub async fn npm_package_latest_version_with_requirement( &self, name: &str, version_requirement: Option<&VersionReq>, ) -> Result<Version> { let http = self.0.lock().await.http.clone(); let instance = self.instance().await; let output = instance .run_npm_subcommand( None, http.proxy(), "info", &[ name, "--json", "--fetch-retry-mintimeout", "2000", "--fetch-retry-maxtimeout", "5000", "--fetch-timeout", "5000", ], ) .await?; let info: NpmInfo = deserialize_npm_info_from_response(&output.stdout).map_err(|e| { anyhow::anyhow!( "failed to parse npm info response: {e}\nstdout: {}", String::from_utf8_lossy(&output.stdout) ) })?; let before = npm_config_before(instance.as_ref(), http.proxy()) .await .context("getting npm before config") .log_err() .flatten(); let latest_dist_tag = info.dist_tags.latest.clone(); let selected_version = select_npm_package_version(name, info, before.as_deref(), version_requirement)?; log::debug!( "selected latest npm package version package={name:?} version_requirement={version_requirement:?} before={before:?} dist_tag_latest={latest_dist_tag:?} selected={selected_version}" ); Ok(selected_version) } pub async fn npm_install_packages( &self, directory: &Path, packages: &[(&str, &str)], ) -> Result<()> { if packages.is_empty() { return Ok(()); } log::debug!( "installing npm packages directory={} packages={packages:?}", directory.display() ); let packages: Vec<_> = packages .iter() .map(|(name, version)| format!("{name}@{version}")) .collect(); let arguments: Vec<_> = packages .iter() .map(|p| p.as_str()) .chain([ "--save-exact", "--fetch-retry-mintimeout", "2000", "--fetch-retry-maxtimeout", "5000", "--fetch-timeout", "5000", ]) .collect(); // This is also wrong because the directory is wrong. self.run_npm_subcommand(Some(directory), "install", &arguments) .await?; Ok(()) } pub async fn npm_install_latest_packages( &self, directory: &Path, package_names: &[&str], ) -> Result<()> { // Let npm apply user config such as `before` and `min-release-age` during resolution. log::debug!( "installing latest npm packages directory={} packages={package_names:?}", directory.display() ); let packages = package_names .iter() .map(|package_name| (*package_name, "latest")) .collect::<Vec<_>>(); self.npm_install_packages(directory, &packages).await } pub async fn should_install_npm_package( &self, package_name: &str, local_executable_path: &Path, local_package_directory: &Path, version_strategy: VersionStrategy<'_>, ) -> bool { // In the case of the local system not having the package installed, // or in the instances where we fail to parse package.json data, // we attempt to install the package. if fs::metadata(local_executable_path).await.is_err() { log::debug!( "npm package cache miss package={package_name:?} reason=missing-executable executable={}", local_executable_path.display() ); return true; } let Some(installed_version) = self .npm_package_installed_version(local_package_directory, package_name) .await .log_err() .flatten() else { log::debug!( "npm package cache miss package={package_name:?} reason=missing-installed-version package_dir={}", local_package_directory.display() ); return true; }; let version_strategy_label = match &version_strategy { VersionStrategy::Pin(version) => format!("pin:{version}"), VersionStrategy::Latest(version) => format!("latest:{version}"), }; let should_install = should_install_npm_package_version(&installed_version, version_strategy); log::debug!( "npm package cache check package={package_name:?} installed={installed_version} strategy={version_strategy_label} should_install={should_install}" ); should_install } } fn should_install_npm_package_version( installed_version: &Version, version_strategy: VersionStrategy<'_>, ) -> bool { match version_strategy { VersionStrategy::Pin(pinned_version) => installed_version != pinned_version, VersionStrategy::Latest(latest_version) => installed_version < latest_version, } } enum ArchiveType { TarGz, Zip, } #[derive(Debug, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct NpmInfo { #[serde(default)] dist_tags: NpmInfoDistTags, versions: Vec<Version>, #[serde(default, deserialize_with = "deserialize_npm_info_time")] time: HashMap<String, String>, } /// Parse NpmInfo from npm info --json output, handling both v11 and >= v12 formats. fn deserialize_npm_info_from_response(data: &[u8]) -> Result<NpmInfo, serde_json::Error> { let value: serde_json::Value = serde_json::from_slice(data)?; // npm >= 12 returns an array with one object: [ { ... } ] if let serde_json::Value::Array(arr) = &value { if arr.len() == 1 { return NpmInfo::deserialize(&arr[0]); } } // npm <= v11 returns a bare JSON object: { ... } NpmInfo::deserialize(value) } #[derive(Debug, Deserialize, Default)] pub struct NpmInfoDistTags { latest: Option<Version>, } // Some registries put non-string values in the `time` map: JFrog Artifactory emits // `"unpublished": null`, and npm itself reports `unpublished` as an object when a // package has had versions unpublished. Only version keys map to the RFC 3339 strings // we read, so keep the string entries and drop the rest rather than failing to parse // the entire `npm info` response (which would block language server installation). fn deserialize_npm_info_time<'de, D>(deserializer: D) -> Result<HashMap<String, String>, D::Error> where D: serde::Deserializer<'de>, { let entries = HashMap::<String, serde_json::Value>::deserialize(deserializer)?; Ok(entries .into_iter() .filter_map(|(key, value)| match value { serde_json::Value::String(value) => Some((key, value)), _ => None, }) .collect()) } #[derive(Debug, Deserialize)] struct NpmConfig { #[serde(default)] before: Option<String>, } async fn npm_config_before( node_runtime: &dyn NodeRuntimeTrait, proxy: Option<&Url>, ) -> Result<Option<String>> { // `npm config get before` renders Date values for display. The JSON config output keeps the // computed cutoff in the same ISO format used by `npm info --json` release times. let output = node_runtime .run_npm_subcommand(None, proxy, "config", &["list", "--json"]) .await?; let config: NpmConfig = serde_json::from_slice(&output.stdout)?; Ok(config .before .filter(|before| !before.trim().is_empty() && before != "null")) } fn select_npm_package_version( package_name: &str, mut info: NpmInfo, before: Option<&str>, version_requirement: Option<&VersionReq>, ) -> Result<Version> { if let Some(version_requirement) = version_requirement { info.versions .retain(|version| version_requirement.matches(version)); info.versions.sort(); info.dist_tags.latest = info