node_runtime: Respect npm release-age filters for managed npm installs (#56957)

Zed-managed npm installers were resolving a concrete latest version with
`npm info` and then installing `package@version`. That is brittle when
users
configure npm release-age filtering via `before` or `min-release-age`:
npm's
installer applies those rules during resolution, but our pinned install
target
could disagree with it, and therefore fail to install.

This changes managed npm installs to install `package@latest` and let
npm apply
its own resolver and user config. The local latest-version lookup
remains as a
best-effort cache freshness check, not as the exact install target.

Exact extension API installs remain unchanged because extensions
explicitly
request a package and version. If we want to revisit that we can.

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Closes #53611

Release Notes:

- Fixed npm-backed tool installs to better respect npm release-age
filters.
This commit is contained in:
Ben Brandt 2026-05-18 23:10:06 +02:00 committed by GitHub
parent 10fc0fb527
commit 2a00db06ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 405 additions and 119 deletions

1
Cargo.lock generated
View file

@ -11178,6 +11178,7 @@ dependencies = [
"async-std",
"async-tar",
"async-trait",
"chrono",
"futures 0.3.32",
"http_client",
"log",

View file

@ -1413,10 +1413,7 @@ async fn get_copilot_lsp(fs: Arc<dyn Fs>, 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?;
}

View file

@ -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<dyn LspAdapterDelegate>,
) -> impl Send + Future<Output = Result<lsp::LanguageServerBinary>> + 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 {

View file

@ -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<dyn LspAdapterDelegate>,
) -> impl Send + Future<Output = Result<LanguageServerBinary>> + 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?,

View file

@ -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<dyn LspAdapterDelegate>,
) -> impl Send + Future<Output = Result<LanguageServerBinary>> + 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?,

View file

@ -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<dyn LspAdapterDelegate>,
) -> impl Send + Future<Output = Result<LanguageServerBinary>> + 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<dyn LspAdapterDelegate>,
) -> impl Send + Future<Output = Result<LanguageServerBinary>> + 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 {

View file

@ -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<dyn LspAdapterDelegate>,
) -> impl Send + Future<Output = Result<LanguageServerBinary>> + 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?,

View file

@ -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<dyn LspAdapterDelegate>,
) -> impl Send + Future<Output = Result<LanguageServerBinary>> + 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?,

View file

@ -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<dyn LspAdapterDelegate>,
) -> impl Send + Future<Output = Result<LanguageServerBinary>> + 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?;

View file

@ -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<dyn LspAdapterDelegate>,
) -> impl Send + Future<Output = Result<LanguageServerBinary>> + 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<dyn LspAdapterDelegate>,
) -> impl Send + Future<Output = Option<LanguageServerBinary>> + 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),
})

View file

@ -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<dyn LspAdapterDelegate>,
) -> impl Send + Future<Output = Result<LanguageServerBinary>> + 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?,

View file

@ -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

View file

@ -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<Version> {
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::<Vec<_>>();
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<Version>,
#[serde(default)]
time: HashMap<String, String>,
}
#[derive(Debug, Deserialize, Default)]
@ -362,6 +417,95 @@ pub struct NpmInfoDistTags {
latest: Option<Version>,
}
#[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>,
) -> Result<Version> {
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<String, String>,
before: &DateTime<Utc>,
) -> Result<bool> {
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<String, String>,
before: &DateTime<Utc>,
) -> Result<bool> {
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<dyn NodeRuntimeTrait>;
@ -936,9 +1080,14 @@ fn npm_command_env(node_binary: Option<&Path>) -> HashMap<String, String> {
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(())
}
}

View file

@ -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<PathBuf> {
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

View file

@ -930,23 +930,11 @@ async fn install_prettier_packages(
plugins_to_install: HashSet<Arc<str>>,
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::<Vec<_>>();
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::<Vec<_>>();
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(())