mirror of
https://github.com/zed-industries/zed.git
synced 2026-05-23 12:37:09 +00:00
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:
parent
10fc0fb527
commit
2a00db06ce
15 changed files with 405 additions and 119 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -11178,6 +11178,7 @@ dependencies = [
|
|||
"async-std",
|
||||
"async-tar",
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"futures 0.3.32",
|
||||
"http_client",
|
||||
"log",
|
||||
|
|
|
|||
|
|
@ -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?;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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?;
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue