diff --git a/Cargo.lock b/Cargo.lock index b46f8c8f265..51a1d750fa4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11178,6 +11178,7 @@ dependencies = [ "async-std", "async-tar", "async-trait", + "chrono", "futures 0.3.32", "http_client", "log", diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 4fa41fc8cb4..a48bf3c1a43 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -1413,10 +1413,7 @@ async fn get_copilot_lsp(fs: Arc, node_runtime: NodeRuntime) -> anyhow:: .await; if should_install { node_runtime - .npm_install_packages( - paths::copilot_dir(), - &[(PACKAGE_NAME, &latest_version.to_string())], - ) + .npm_install_latest_packages(paths::copilot_dir(), &[PACKAGE_NAME]) .await?; } diff --git a/crates/languages/src/bash.rs b/crates/languages/src/bash.rs index 438090e2aa9..2f550e87c7f 100644 --- a/crates/languages/src/bash.rs +++ b/crates/languages/src/bash.rs @@ -141,7 +141,7 @@ impl LspInstaller for BashLspAdapter { fn fetch_server_binary( &self, - latest_version: Self::BinaryVersion, + _latest_version: Self::BinaryVersion, container_dir: std::path::PathBuf, delegate: &Arc, ) -> impl Send + Future> + use<> { @@ -152,13 +152,9 @@ impl LspInstaller for BashLspAdapter { let server_path = container_dir .join("node_modules") .join(Self::NODE_MODULE_RELATIVE_SERVER_PATH); - let latest_version = latest_version.to_string(); - node.npm_install_packages( - &container_dir, - &[(Self::PACKAGE_NAME, latest_version.as_str())], - ) - .await?; + node.npm_install_latest_packages(&container_dir, &[Self::PACKAGE_NAME]) + .await?; let env = delegate.shell_env().await; Ok(LanguageServerBinary { diff --git a/crates/languages/src/css.rs b/crates/languages/src/css.rs index dfa0bc9fd3d..4506481a17b 100644 --- a/crates/languages/src/css.rs +++ b/crates/languages/src/css.rs @@ -67,7 +67,7 @@ impl LspInstaller for CssLspAdapter { fn fetch_server_binary( &self, - latest_version: Self::BinaryVersion, + _latest_version: Self::BinaryVersion, container_dir: PathBuf, _: &Arc, ) -> impl Send + Future> + use<> { @@ -75,13 +75,9 @@ impl LspInstaller for CssLspAdapter { async move { let server_path = container_dir.join(SERVER_PATH); - let latest_version = latest_version.to_string(); - node.npm_install_packages( - &container_dir, - &[(Self::PACKAGE_NAME, latest_version.as_str())], - ) - .await?; + node.npm_install_latest_packages(&container_dir, &[Self::PACKAGE_NAME]) + .await?; Ok(LanguageServerBinary { path: node.binary_path().await?, diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 9cd6c1565ad..8389fd65f65 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -213,7 +213,7 @@ impl LspInstaller for JsonLspAdapter { fn fetch_server_binary( &self, - latest_version: Self::BinaryVersion, + _latest_version: Self::BinaryVersion, container_dir: PathBuf, _: &Arc, ) -> impl Send + Future> + use<> { @@ -221,13 +221,9 @@ impl LspInstaller for JsonLspAdapter { async move { let server_path = container_dir.join(SERVER_PATH); - let latest_version = latest_version.to_string(); - node.npm_install_packages( - &container_dir, - &[(Self::PACKAGE_NAME, latest_version.as_str())], - ) - .await?; + node.npm_install_latest_packages(&container_dir, &[Self::PACKAGE_NAME]) + .await?; Ok(LanguageServerBinary { path: node.binary_path().await?, diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 483430bd75d..5d2024d3b8d 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -786,7 +786,7 @@ impl LspInstaller for PyrightLspAdapter { fn fetch_server_binary( &self, - latest_version: Self::BinaryVersion, + _latest_version: Self::BinaryVersion, container_dir: PathBuf, delegate: &Arc, ) -> impl Send + Future> + use<> { @@ -795,13 +795,8 @@ impl LspInstaller for PyrightLspAdapter { async move { let server_path = container_dir.join(Self::SERVER_PATH); - let latest_version = latest_version.to_string(); - - node.npm_install_packages( - &container_dir, - &[(Self::SERVER_NAME.as_ref(), latest_version.as_str())], - ) - .await?; + node.npm_install_latest_packages(&container_dir, &[Self::SERVER_NAME.as_ref()]) + .await?; let env = delegate.shell_env().await; Ok(LanguageServerBinary { @@ -2252,7 +2247,7 @@ impl LspInstaller for BasedPyrightLspAdapter { fn fetch_server_binary( &self, - latest_version: Self::BinaryVersion, + _latest_version: Self::BinaryVersion, container_dir: PathBuf, delegate: &Arc, ) -> impl Send + Future> + use<> { @@ -2261,13 +2256,8 @@ impl LspInstaller for BasedPyrightLspAdapter { async move { let server_path = container_dir.join(Self::SERVER_PATH); - let latest_version = latest_version.to_string(); - - node.npm_install_packages( - &container_dir, - &[(Self::SERVER_NAME.as_ref(), latest_version.as_str())], - ) - .await?; + node.npm_install_latest_packages(&container_dir, &[Self::SERVER_NAME.as_ref()]) + .await?; let env = delegate.shell_env().await; Ok(LanguageServerBinary { diff --git a/crates/languages/src/tailwind.rs b/crates/languages/src/tailwind.rs index 41fa248a935..6d4211b58c8 100644 --- a/crates/languages/src/tailwind.rs +++ b/crates/languages/src/tailwind.rs @@ -72,7 +72,7 @@ impl LspInstaller for TailwindLspAdapter { fn fetch_server_binary( &self, - latest_version: Self::BinaryVersion, + _latest_version: Self::BinaryVersion, container_dir: PathBuf, _: &Arc, ) -> impl Send + Future> + use<> { @@ -80,13 +80,9 @@ impl LspInstaller for TailwindLspAdapter { async move { let server_path = container_dir.join(SERVER_PATH); - let latest_version = latest_version.to_string(); - node.npm_install_packages( - &container_dir, - &[(Self::PACKAGE_NAME, latest_version.as_str())], - ) - .await?; + node.npm_install_latest_packages(&container_dir, &[Self::PACKAGE_NAME]) + .await?; Ok(LanguageServerBinary { path: node.binary_path().await?, diff --git a/crates/languages/src/tailwindcss.rs b/crates/languages/src/tailwindcss.rs index dcc9e8bf4ef..0e9ac9af40f 100644 --- a/crates/languages/src/tailwindcss.rs +++ b/crates/languages/src/tailwindcss.rs @@ -68,7 +68,7 @@ impl LspInstaller for TailwindCssLspAdapter { fn fetch_server_binary( &self, - latest_version: Self::BinaryVersion, + _latest_version: Self::BinaryVersion, container_dir: PathBuf, _: &Arc, ) -> impl Send + Future> + use<> { @@ -76,13 +76,9 @@ impl LspInstaller for TailwindCssLspAdapter { async move { let server_path = container_dir.join(SERVER_PATH); - let latest_version = latest_version.to_string(); - node.npm_install_packages( - &container_dir, - &[(Self::PACKAGE_NAME, latest_version.as_str())], - ) - .await?; + node.npm_install_latest_packages(&container_dir, &[Self::PACKAGE_NAME]) + .await?; Ok(LanguageServerBinary { path: node.binary_path().await?, diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index d6889d8cbb8..4d37898eca1 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -718,7 +718,7 @@ impl LspInstaller for TypeScriptLspAdapter { fn fetch_server_binary( &self, - latest_version: Self::BinaryVersion, + _latest_version: Self::BinaryVersion, container_dir: PathBuf, _: &Arc, ) -> impl Send + Future> + use<> { @@ -726,15 +726,10 @@ impl LspInstaller for TypeScriptLspAdapter { async move { let server_path = container_dir.join(Self::NEW_SERVER_PATH); - let typescript_version = latest_version.typescript_version.to_string(); - let server_version = latest_version.server_version.to_string(); - node.npm_install_packages( + node.npm_install_latest_packages( &container_dir, - &[ - (Self::PACKAGE_NAME, typescript_version.as_str()), - (Self::SERVER_PACKAGE_NAME, server_version.as_str()), - ], + &[Self::PACKAGE_NAME, Self::SERVER_PACKAGE_NAME], ) .await?; diff --git a/crates/languages/src/vtsls.rs b/crates/languages/src/vtsls.rs index 4bc4401ff30..c46ea39a4f1 100644 --- a/crates/languages/src/vtsls.rs +++ b/crates/languages/src/vtsls.rs @@ -126,7 +126,7 @@ impl LspInstaller for VtslsLspAdapter { fn fetch_server_binary( &self, - latest_version: Self::BinaryVersion, + _latest_version: Self::BinaryVersion, container_dir: PathBuf, _: &Arc, ) -> impl Send + Future> + use<> { @@ -135,21 +135,44 @@ impl LspInstaller for VtslsLspAdapter { async move { let server_path = container_dir.join(Self::SERVER_PATH); - let typescript_version = latest_version.typescript_version.to_string(); - let server_version = latest_version.server_version.to_string(); + node.npm_install_latest_packages( + &container_dir, + &[Self::PACKAGE_NAME, Self::TYPESCRIPT_PACKAGE_NAME], + ) + .await?; - let mut packages_to_install = Vec::new(); + Ok(LanguageServerBinary { + path: node.binary_path().await?, + env: None, + arguments: typescript_server_binary_arguments(&server_path), + }) + } + } + + fn check_if_version_installed( + &self, + version: &Self::BinaryVersion, + container_dir: &PathBuf, + _: &Arc, + ) -> impl Send + Future> + use<> { + let node = self.node.clone(); + let typescript_version = version.typescript_version.clone(); + let server_version = version.server_version.clone(); + let container_dir = container_dir.clone(); + + async move { + let server_path = container_dir.join(Self::SERVER_PATH); if node .should_install_npm_package( Self::PACKAGE_NAME, &server_path, &container_dir, - VersionStrategy::Latest(&latest_version.server_version), + VersionStrategy::Latest(&server_version), ) .await { - packages_to_install.push((Self::PACKAGE_NAME, server_version.as_str())); + return None; } if node @@ -157,19 +180,15 @@ impl LspInstaller for VtslsLspAdapter { Self::TYPESCRIPT_PACKAGE_NAME, &container_dir.join(Self::TYPESCRIPT_TSDK_PATH), &container_dir, - VersionStrategy::Latest(&latest_version.typescript_version), + VersionStrategy::Latest(&typescript_version), ) .await { - packages_to_install - .push((Self::TYPESCRIPT_PACKAGE_NAME, typescript_version.as_str())); + return None; } - node.npm_install_packages(&container_dir, &packages_to_install) - .await?; - - Ok(LanguageServerBinary { - path: node.binary_path().await?, + Some(LanguageServerBinary { + path: node.binary_path().await.ok()?, env: None, arguments: typescript_server_binary_arguments(&server_path), }) diff --git a/crates/languages/src/yaml.rs b/crates/languages/src/yaml.rs index 22781acf25a..de9b11b03dc 100644 --- a/crates/languages/src/yaml.rs +++ b/crates/languages/src/yaml.rs @@ -68,7 +68,7 @@ impl LspInstaller for YamlLspAdapter { fn fetch_server_binary( &self, - latest_version: Self::BinaryVersion, + _latest_version: Self::BinaryVersion, container_dir: PathBuf, _: &Arc, ) -> impl Send + Future> + use<> { @@ -76,13 +76,9 @@ impl LspInstaller for YamlLspAdapter { async move { let server_path = container_dir.join(SERVER_PATH); - let latest_version = latest_version.to_string(); - node.npm_install_packages( - &container_dir, - &[(Self::PACKAGE_NAME, latest_version.as_str())], - ) - .await?; + node.npm_install_latest_packages(&container_dir, &[Self::PACKAGE_NAME]) + .await?; Ok(LanguageServerBinary { path: node.binary_path().await?, diff --git a/crates/node_runtime/Cargo.toml b/crates/node_runtime/Cargo.toml index dfa40ad666e..25f7b2997e5 100644 --- a/crates/node_runtime/Cargo.toml +++ b/crates/node_runtime/Cargo.toml @@ -20,6 +20,7 @@ anyhow.workspace = true async-compression.workspace = true async-tar.workspace = true async-trait.workspace = true +chrono.workspace = true futures.workspace = true http_client.workspace = true log.workspace = true diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index 9d4bfe9cffb..7ce29532644 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -1,6 +1,7 @@ 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; @@ -253,9 +254,8 @@ impl NodeRuntime { pub async fn npm_package_latest_version(&self, name: &str) -> Result { let http = self.0.lock().await.http.clone(); - let output = self - .instance() - .await + let instance = self.instance().await; + let output = instance .run_npm_subcommand( None, http.proxy(), @@ -273,11 +273,18 @@ impl NodeRuntime { ) .await?; - let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?; - info.dist_tags - .latest - .or_else(|| info.versions.pop()) - .with_context(|| format!("no version found for npm package {name}")) + let info: NpmInfo = serde_json::from_slice(&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())?; + log::debug!( + "selected latest npm package version package={name:?} before={before:?} dist_tag_latest={latest_dist_tag:?} selected={selected_version}" + ); + Ok(selected_version) } pub async fn npm_install_packages( @@ -289,6 +296,11 @@ impl NodeRuntime { return Ok(()); } + log::debug!( + "installing npm packages directory={} packages={packages:?}", + directory.display() + ); + let packages: Vec<_> = packages .iter() .map(|(name, version)| format!("{name}@{version}")) @@ -314,6 +326,23 @@ impl NodeRuntime { 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::>(); + self.npm_install_packages(directory, &packages).await + } + pub async fn should_install_npm_package( &self, package_name: &str, @@ -325,6 +354,10 @@ impl NodeRuntime { // 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; } @@ -334,13 +367,33 @@ impl NodeRuntime { .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; }; - match version_strategy { - VersionStrategy::Pin(pinned_version) => &installed_version != pinned_version, - VersionStrategy::Latest(latest_version) => &installed_version < latest_version, - } + 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, } } @@ -355,6 +408,8 @@ pub struct NpmInfo { #[serde(default)] dist_tags: NpmInfoDistTags, versions: Vec, + #[serde(default)] + time: HashMap, } #[derive(Debug, Deserialize, Default)] @@ -362,6 +417,95 @@ pub struct NpmInfoDistTags { latest: Option, } +#[derive(Debug, Deserialize)] +struct NpmConfig { + #[serde(default)] + before: Option, +} + +async fn npm_config_before( + node_runtime: &dyn NodeRuntimeTrait, + proxy: Option<&Url>, +) -> Result> { + // `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>, +) -> Result { + if let Some(before) = before + && !info.time.is_empty() + { + let before_timestamp = DateTime::parse_from_rfc3339(before) + .with_context(|| format!("parsing npm before config timestamp {before:?}"))? + .with_timezone(&Utc); + let latest_version = info.dist_tags.latest.as_ref(); + + if let Some(version) = latest_version + && npm_version_was_published_before(version, &info.time, &before_timestamp)? + { + return Ok(version.clone()); + } + + for version in info.versions.iter().rev() { + if is_allowed_npm_version_before( + version, + latest_version, + &info.time, + &before_timestamp, + )? { + return Ok(version.clone()); + } + } + + bail!("no version found for npm package {package_name} before {before}"); + } + + info.dist_tags + .latest + .or_else(|| info.versions.pop()) + .with_context(|| format!("no version found for npm package {package_name}")) +} + +fn is_allowed_npm_version_before( + version: &Version, + latest_version: Option<&Version>, + published_at_by_version: &HashMap, + before: &DateTime, +) -> Result { + if !version.pre.is_empty() + || latest_version.is_some_and(|latest_version| version > latest_version) + { + return Ok(false); + } + + npm_version_was_published_before(version, published_at_by_version, before) +} + +fn npm_version_was_published_before( + version: &Version, + published_at_by_version: &HashMap, + before: &DateTime, +) -> Result { + let Some(published_at) = published_at_by_version.get(&version.to_string()) else { + return Ok(false); + }; + let published_at = DateTime::parse_from_rfc3339(published_at) + .with_context(|| format!("parsing npm release timestamp for version {version}"))? + .with_timezone(&Utc); + Ok(&published_at <= before) +} + #[async_trait::async_trait] trait NodeRuntimeTrait: Send + Sync { fn boxed_clone(&self) -> Box; @@ -936,9 +1080,14 @@ fn npm_command_env(node_binary: Option<&Path>) -> HashMap { mod tests { use std::path::Path; + use anyhow::{Result, bail}; use http_client::Url; + use semver::Version; - use super::{build_npm_command_args, proxy_argument}; + use super::{ + NpmInfo, VersionStrategy, build_npm_command_args, proxy_argument, + select_npm_package_version, should_install_npm_package_version, + }; // Map localhost to 127.0.0.1 // NodeRuntime without environment information can not parse `localhost` correctly. @@ -1021,4 +1170,174 @@ mod tests { ] ); } + + #[test] + fn test_latest_version_strategy_accepts_newer_installed_versions() -> Result<()> { + let target_version = Version::parse("2.0.0")?; + + assert!(!should_install_npm_package_version( + &Version::parse("2.0.0")?, + VersionStrategy::Latest(&target_version) + )); + assert!(should_install_npm_package_version( + &Version::parse("1.0.0")?, + VersionStrategy::Latest(&target_version) + )); + assert!(!should_install_npm_package_version( + &Version::parse("3.0.0")?, + VersionStrategy::Latest(&target_version) + )); + + Ok(()) + } + + #[test] + fn test_select_npm_package_version_uses_dist_tag_without_before() -> Result<()> { + let info: NpmInfo = serde_json::from_str( + r#"{ + "dist-tags": { "latest": "3.0.0" }, + "versions": ["1.0.0", "2.0.0", "3.0.0"], + "time": { + "1.0.0": "2024-01-01T00:00:00.000Z", + "2.0.0": "2024-02-01T00:00:00.000Z", + "3.0.0": "2024-03-01T00:00:00.000Z" + } + }"#, + )?; + + assert_eq!( + select_npm_package_version("test-package", info, None)?, + Version::parse("3.0.0")? + ); + Ok(()) + } + + #[test] + fn test_select_npm_package_version_uses_latest_before_npm_before_config() -> Result<()> { + let info: NpmInfo = serde_json::from_str( + r#"{ + "dist-tags": { "latest": "3.0.0" }, + "versions": ["1.0.0", "2.0.0", "3.0.0"], + "time": { + "1.0.0": "2024-01-01T00:00:00.000Z", + "2.0.0": "2024-02-01T00:00:00.000Z", + "3.0.0": "2024-03-01T00:00:00.000Z" + } + }"#, + )?; + + assert_eq!( + select_npm_package_version("test-package", info, Some("2024-02-15T00:00:00.000Z"))?, + Version::parse("2.0.0")? + ); + Ok(()) + } + + #[test] + fn test_select_npm_package_version_keeps_allowed_latest_dist_tag() -> Result<()> { + let info: NpmInfo = serde_json::from_str( + r#"{ + "dist-tags": { "latest": "2.0.0" }, + "versions": ["1.0.0", "2.0.0", "3.0.0"], + "time": { + "1.0.0": "2024-01-01T00:00:00.000Z", + "2.0.0": "2024-02-01T00:00:00.000Z", + "3.0.0": "2024-03-01T00:00:00.000Z" + } + }"#, + )?; + + assert_eq!( + select_npm_package_version("test-package", info, Some("2024-02-15T00:00:00.000Z"))?, + Version::parse("2.0.0")? + ); + Ok(()) + } + + #[test] + fn test_select_npm_package_version_keeps_allowed_prerelease_latest_dist_tag() -> Result<()> { + let info: NpmInfo = serde_json::from_str( + r#"{ + "dist-tags": { "latest": "2.0.0-beta.1" }, + "versions": ["1.0.0", "2.0.0-beta.1"], + "time": { + "1.0.0": "2024-01-01T00:00:00.000Z", + "2.0.0-beta.1": "2024-02-01T00:00:00.000Z" + } + }"#, + )?; + + assert_eq!( + select_npm_package_version("test-package", info, Some("2024-02-15T00:00:00.000Z"))?, + Version::parse("2.0.0-beta.1")? + ); + Ok(()) + } + + #[test] + fn test_select_npm_package_version_ignores_prereleases_before_cutoff() -> Result<()> { + let info: NpmInfo = serde_json::from_str( + r#"{ + "dist-tags": { "latest": "2.0.0" }, + "versions": ["1.0.0", "2.0.0-beta.1", "2.0.0"], + "time": { + "1.0.0": "2024-01-01T00:00:00.000Z", + "2.0.0-beta.1": "2024-02-01T00:00:00.000Z", + "2.0.0": "2024-03-01T00:00:00.000Z" + } + }"#, + )?; + + assert_eq!( + select_npm_package_version("test-package", info, Some("2024-02-15T00:00:00.000Z"))?, + Version::parse("1.0.0")? + ); + Ok(()) + } + + #[test] + fn test_select_npm_package_version_ignores_versions_above_latest_dist_tag() -> Result<()> { + let info: NpmInfo = serde_json::from_str( + r#"{ + "dist-tags": { "latest": "2.0.0" }, + "versions": ["1.0.0", "2.0.0", "3.0.0"], + "time": { + "1.0.0": "2024-01-01T00:00:00.000Z", + "2.0.0": "2024-03-01T00:00:00.000Z", + "3.0.0": "2024-02-01T00:00:00.000Z" + } + }"#, + )?; + + assert_eq!( + select_npm_package_version("test-package", info, Some("2024-02-15T00:00:00.000Z"))?, + Version::parse("1.0.0")? + ); + Ok(()) + } + + #[test] + fn test_select_npm_package_version_errors_when_no_version_matches_before() -> Result<()> { + let info: NpmInfo = serde_json::from_str( + r#"{ + "dist-tags": { "latest": "2.0.0" }, + "versions": ["1.0.0", "2.0.0"], + "time": { + "1.0.0": "2024-01-01T00:00:00.000Z", + "2.0.0": "2024-02-01T00:00:00.000Z" + } + }"#, + )?; + + let Err(error) = + select_npm_package_version("test-package", info, Some("2023-12-01T00:00:00.000Z")) + else { + bail!("expected cutoff to reject all package versions"); + }; + assert_eq!( + error.to_string(), + "no version found for npm package test-package before 2023-12-01T00:00:00.000Z" + ); + Ok(()) + } } diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 39578eaf8f0..fc5f56395cc 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -3145,7 +3145,7 @@ async fn get_or_install_companion(node: NodeRuntime, cx: &mut AsyncApp) -> Resul async fn install_latest_version(dir: PathBuf, node: NodeRuntime) -> Result { let temp_dir = tempfile::tempdir().context("creating temporary directory")?; - node.npm_install_packages(temp_dir.path(), &[(PACKAGE_NAME, "latest")]) + node.npm_install_latest_packages(temp_dir.path(), &[PACKAGE_NAME]) .await .context("installing latest companion package")?; let version = node diff --git a/crates/project/src/prettier_store.rs b/crates/project/src/prettier_store.rs index faa2cca7986..8d9399dce64 100644 --- a/crates/project/src/prettier_store.rs +++ b/crates/project/src/prettier_store.rs @@ -930,23 +930,11 @@ async fn install_prettier_packages( plugins_to_install: HashSet>, node: NodeRuntime, ) -> anyhow::Result<()> { - let packages_to_versions = future::try_join_all( - plugins_to_install - .iter() - .chain(Some(&"prettier".into())) - .map(|package_name| async { - let returned_package_name = package_name.to_string(); - let latest_version = node - .npm_package_latest_version(package_name) - .await - .with_context(|| { - format!("fetching latest npm version for package {returned_package_name}") - })?; - anyhow::Ok((returned_package_name, latest_version.to_string())) - }), - ) - .await - .context("fetching latest npm versions")?; + let packages_to_install = plugins_to_install + .iter() + .map(|package_name| package_name.to_string()) + .chain(Some("prettier".to_string())) + .collect::>(); let default_prettier_dir = default_prettier_dir().as_path(); match fs.metadata(default_prettier_dir).await.with_context(|| { @@ -962,12 +950,12 @@ async fn install_prettier_packages( .with_context(|| format!("creating default prettier dir {default_prettier_dir:?}"))?, } - log::info!("Installing default prettier and plugins: {packages_to_versions:?}"); - let borrowed_packages = packages_to_versions + log::info!("Installing default prettier and plugins: {packages_to_install:?}"); + let borrowed_packages = packages_to_install .iter() - .map(|(package, version)| (package.as_str(), version.as_str())) + .map(|package_name| package_name.as_str()) .collect::>(); - node.npm_install_packages(default_prettier_dir, &borrowed_packages) + node.npm_install_latest_packages(default_prettier_dir, &borrowed_packages) .await .context("fetching formatter packages")?; anyhow::Ok(())