From 440affd44393f2f392dea3f3e09df0dd2855979c Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Tue, 14 Jul 2026 18:51:41 +0530 Subject: [PATCH] languages: Pin TypeScript to 6.x for typescript-language-server (#60970) Closes https://github.com/zed-industries/zed/issues/60618 TypeScript 7 no longer ships `tsserver.js`, which `typescript-language-server` depends on. This PR does two things: 1. Pins the `typescript` package Zed auto-installs for `typescript-language-server` to `^6`. For projects without their own TypeScript in `node_modules` (which rely on the copy Zed installs), an existing 7.x install gets downgraded to the latest 6.x, making the server usable again. 2. Only hands the workspace's TypeScript to the server as a tsdk when it actually contains `tsserver.js`, falling back to Zed's installed 6.x otherwise. So if you upgraded your project to TypeScript 7, the language server won't break. It keeps working against the 6.x TypeScript Zed ships. Note that vtsls, the default TypeScript language server, is unaffected by the TypeScript 7 release. It bundles its own compatible TypeScript. See #60951 for details. This PR only affects users who have manually configured `typescript-language-server` as their language server. Release Notes: - Fixed `typescript-language-server` breaking after the TypeScript 7 release. For projects that have upgraded to TypeScript 7, install the `tsgo` extension to use TypeScript's native language server. --- crates/languages/src/typescript.rs | 30 +++- crates/node_runtime/src/node_runtime.rs | 193 ++++++++++++++++++++++-- 2 files changed, 201 insertions(+), 22 deletions(-) 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#"{