diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index e8d18840e0..e34a0b7dbf 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -12,7 +12,7 @@ use language::{ use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName, Uri}; use node_runtime::{NodeRuntime, VersionStrategy}; use project::{Fs, lsp_store::language_server_settings}; -use semver::Version; +use semver::{Version, VersionReq}; use serde_json::{Value, json}; use smol::lock::RwLock; use std::{ @@ -601,6 +601,9 @@ fn replace_test_name_parameters(test_name: &str) -> String { PATTERN.split(test_name).map(regex::escape).join("(.+?)") } +static TYPESCRIPT_VERSION_REQ: LazyLock = + LazyLock::new(|| VersionReq::parse("^6").expect("Failed to parse TypeScript version req")); + pub struct TypeScriptLspAdapter { fs: Arc, node: NodeRuntime, @@ -634,7 +637,12 @@ impl TypeScriptLspAdapter { if self .fs - .is_dir(&adapter.worktree_root_path().join(tsdk_path)) + .is_file( + &adapter + .worktree_root_path() + .join(tsdk_path) + .join("tsserver.js"), + ) .await { Some(tsdk_path) @@ -661,7 +669,10 @@ impl LspInstaller for TypeScriptLspAdapter { Ok(TypeScriptVersions { typescript_version: self .node - .npm_package_latest_version(Self::PACKAGE_NAME) + .npm_package_latest_version_with_requirement( + Self::PACKAGE_NAME, + Some(&TYPESCRIPT_VERSION_REQ), + ) .await?, server_version: self .node @@ -684,12 +695,13 @@ impl LspInstaller for TypeScriptLspAdapter { async move { let server_path = container_dir.join(Self::NEW_SERVER_PATH); + // Pin rather than Latest so an unusable TypeScript 7.x install gets downgraded. if node .should_install_npm_package( Self::PACKAGE_NAME, &server_path, &container_dir, - VersionStrategy::Latest(&typescript_version), + VersionStrategy::Pin(&typescript_version), ) .await { @@ -718,7 +730,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,10 +738,14 @@ 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(); - node.npm_install_latest_packages( + node.npm_install_packages( &container_dir, - &[Self::PACKAGE_NAME, Self::SERVER_PACKAGE_NAME], + &[ + (Self::PACKAGE_NAME, typescript_version.as_str()), + (Self::SERVER_PACKAGE_NAME, "latest"), + ], ) .await?; diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index 035c86f8ad..6a2d1d7c3d 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -5,7 +5,7 @@ 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; +use semver::{Version, VersionReq}; use serde::Deserialize; use smol::io::BufReader; use smol::{fs, lock::Mutex}; @@ -253,6 +253,15 @@ impl NodeRuntime { } pub async fn npm_package_latest_version(&self, name: &str) -> Result { + 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 { let http = self.0.lock().await.http.clone(); let instance = self.instance().await; let output = instance @@ -285,9 +294,10 @@ impl NodeRuntime { .log_err() .flatten(); let latest_dist_tag = info.dist_tags.latest.clone(); - let selected_version = select_npm_package_version(name, info, before.as_deref())?; + let selected_version = + select_npm_package_version(name, info, before.as_deref(), version_requirement)?; log::debug!( - "selected latest npm package version package={name:?} before={before:?} dist_tag_latest={latest_dist_tag:?} selected={selected_version}" + "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) } @@ -481,7 +491,19 @@ fn select_npm_package_version( package_name: &str, mut info: NpmInfo, before: Option<&str>, + version_requirement: Option<&VersionReq>, ) -> Result { + if let Some(version_requirement) = version_requirement { + info.versions + .retain(|version| version_requirement.matches(version)); + info.versions.sort(); + info.dist_tags.latest = info + .dist_tags + .latest + .take() + .filter(|version| version_requirement.matches(version)); + } + if let Some(before) = before && !info.time.is_empty() { @@ -502,6 +524,7 @@ fn select_npm_package_version( latest_version, &info.time, &before_timestamp, + version_requirement.is_some(), )? { return Ok(version.clone()); } @@ -521,8 +544,9 @@ fn is_allowed_npm_version_before( latest_version: Option<&Version>, published_at_by_version: &HashMap, before: &DateTime, + allow_prereleases: bool, ) -> Result { - if !version.pre.is_empty() + if (!allow_prereleases && !version.pre.is_empty()) || latest_version.is_some_and(|latest_version| version > latest_version) { return Ok(false); @@ -1121,7 +1145,7 @@ mod tests { use anyhow::{Result, bail}; use http_client::Url; - use semver::Version; + use semver::{Version, VersionReq}; use super::{ NpmInfo, VersionStrategy, build_npm_command_args, deserialize_npm_info_from_response, @@ -1245,7 +1269,7 @@ mod tests { )?; assert_eq!( - select_npm_package_version("test-package", info, None)?, + select_npm_package_version("test-package", info, None, None)?, Version::parse("3.0.0")? ); Ok(()) @@ -1270,7 +1294,12 @@ mod tests { )?; assert_eq!( - select_npm_package_version("test-package", info, Some("2024-02-15T00:00:00.000Z"))?, + select_npm_package_version( + "test-package", + info, + Some("2024-02-15T00:00:00.000Z"), + None + )?, Version::parse("2.0.0")? ); Ok(()) @@ -1291,7 +1320,12 @@ mod tests { )?; assert_eq!( - select_npm_package_version("test-package", info, Some("2024-02-15T00:00:00.000Z"))?, + select_npm_package_version( + "test-package", + info, + Some("2024-02-15T00:00:00.000Z"), + None + )?, Version::parse("2.0.0")? ); Ok(()) @@ -1312,7 +1346,12 @@ mod tests { )?; assert_eq!( - select_npm_package_version("test-package", info, Some("2024-02-15T00:00:00.000Z"))?, + select_npm_package_version( + "test-package", + info, + Some("2024-02-15T00:00:00.000Z"), + None + )?, Version::parse("2.0.0")? ); Ok(()) @@ -1332,7 +1371,12 @@ mod tests { )?; assert_eq!( - select_npm_package_version("test-package", info, Some("2024-02-15T00:00:00.000Z"))?, + select_npm_package_version( + "test-package", + info, + Some("2024-02-15T00:00:00.000Z"), + None + )?, Version::parse("2.0.0-beta.1")? ); Ok(()) @@ -1353,7 +1397,12 @@ mod tests { )?; assert_eq!( - select_npm_package_version("test-package", info, Some("2024-02-15T00:00:00.000Z"))?, + select_npm_package_version( + "test-package", + info, + Some("2024-02-15T00:00:00.000Z"), + None + )?, Version::parse("1.0.0")? ); Ok(()) @@ -1374,7 +1423,12 @@ mod tests { )?; assert_eq!( - select_npm_package_version("test-package", info, Some("2024-02-15T00:00:00.000Z"))?, + select_npm_package_version( + "test-package", + info, + Some("2024-02-15T00:00:00.000Z"), + None + )?, Version::parse("1.0.0")? ); Ok(()) @@ -1393,9 +1447,12 @@ mod tests { }"#, )?; - let Err(error) = - select_npm_package_version("test-package", info, Some("2023-12-01T00:00:00.000Z")) - else { + let Err(error) = select_npm_package_version( + "test-package", + info, + Some("2023-12-01T00:00:00.000Z"), + None, + ) else { bail!("expected cutoff to reject all package versions"); }; assert_eq!( @@ -1405,6 +1462,112 @@ mod tests { Ok(()) } + #[test] + fn test_select_npm_package_version_selects_latest_matching_requirement() -> Result<()> { + let info: NpmInfo = serde_json::from_str( + r#"{ + "dist-tags": { "latest": "7.0.0" }, + "versions": ["6.0.3", "7.0.0", "5.9.3", "6.0.2"] + }"#, + )?; + let version_requirement = VersionReq::parse("^6")?; + + assert_eq!( + select_npm_package_version("test-package", info, None, Some(&version_requirement))?, + Version::parse("6.0.3")? + ); + Ok(()) + } + + #[test] + fn test_select_npm_package_version_applies_before_to_matching_versions() -> Result<()> { + let info: NpmInfo = serde_json::from_str( + r#"{ + "dist-tags": { "latest": "7.0.0" }, + "versions": ["6.0.3", "7.0.0", "6.0.2"], + "time": { + "6.0.2": "2024-02-01T00:00:00.000Z", + "6.0.3": "2024-03-01T00:00:00.000Z", + "7.0.0": "2024-04-01T00:00:00.000Z" + } + }"#, + )?; + let version_requirement = VersionReq::parse("^6")?; + + assert_eq!( + select_npm_package_version( + "test-package", + info, + Some("2024-02-15T00:00:00.000Z"), + Some(&version_requirement), + )?, + Version::parse("6.0.2")? + ); + Ok(()) + } + + #[test] + fn test_select_npm_package_version_allows_requested_prerelease_before_cutoff() -> Result<()> { + let info: NpmInfo = serde_json::from_str( + r#"{ + "dist-tags": { "latest": "7.0.0" }, + "versions": ["7.1.0-beta.1", "7.1.0-beta.2", "7.0.0"], + "time": { + "7.0.0": "2024-01-01T00:00:00.000Z", + "7.1.0-beta.1": "2024-02-01T00:00:00.000Z", + "7.1.0-beta.2": "2024-03-01T00:00:00.000Z" + } + }"#, + )?; + let version_requirement = VersionReq::parse(">=7.1.0-beta.1, <7.1.0")?; + + assert_eq!( + select_npm_package_version( + "test-package", + info, + Some("2024-02-15T00:00:00.000Z"), + Some(&version_requirement), + )?, + Version::parse("7.1.0-beta.1")? + ); + Ok(()) + } + + #[test] + fn test_select_npm_package_version_errors_without_matching_version() -> Result<()> { + let info: NpmInfo = serde_json::from_str( + r#"{ + "dist-tags": { "latest": "7.0.0" }, + "versions": ["5.9.3", "7.0.0"] + }"#, + )?; + let version_requirement = VersionReq::parse("^6")?; + + let error = + select_npm_package_version("test-package", info, None, Some(&version_requirement)) + .expect_err("expected version requirement to reject all package versions"); + assert_eq!( + error.to_string(), + "no version found for npm package test-package" + ); + Ok(()) + } + + #[test] + fn test_pinned_version_strategy_replaces_different_installed_version() -> Result<()> { + let pinned_version = Version::parse("6.0.3")?; + + assert!(!should_install_npm_package_version( + &pinned_version, + VersionStrategy::Pin(&pinned_version) + )); + assert!(should_install_npm_package_version( + &Version::parse("7.0.0")?, + VersionStrategy::Pin(&pinned_version) + )); + Ok(()) + } + #[test] fn test_deserialize_npm_info_npm11_format() -> Result<()> { let json = r#"{