fix: resolve TypeScript LSP initialization failure
typescript-language-server requires a valid TypeScript installation to function. Previously, no initializationOptions were sent during the LSP initialize request, causing the server to fail with: 'Could not find a valid TypeScript installation. Please ensure that the typescript dependency is installed in the workspace or that a valid tsserver.path is specified.' This fix: - Adds initializationOptions.tsserver.path resolution that searches for TypeScript in: workspace node_modules, global npm install, and npx cache - Wires initialization_options into the LSP startup flow via LSPServerType - Updates the install step to proactively install TypeScript globally if not found locally
This commit is contained in:
@@ -205,7 +205,16 @@ impl LspServerConfig {
|
||||
custom_binary_config
|
||||
);
|
||||
|
||||
let params = default_init_params(&self.initial_workspace, self.client_name)?;
|
||||
let mut params = default_init_params(&self.initial_workspace, self.client_name)?;
|
||||
|
||||
// Resolve server-specific initialization options (e.g., tsserver.path for TypeScript)
|
||||
let init_options = self
|
||||
.server_type
|
||||
.initialization_options(&self.initial_workspace, executor.path_env_var())
|
||||
.await;
|
||||
if let Some(options) = init_options {
|
||||
params.initialization_options = Some(options);
|
||||
}
|
||||
|
||||
Ok(ResolvedLspCommand { command, params })
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ use std::sync::Arc;
|
||||
#[cfg(feature = "local_fs")]
|
||||
use anyhow::Context;
|
||||
use async_trait::async_trait;
|
||||
#[cfg(feature = "local_fs")]
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::language_server_candidate::{LanguageServerCandidate, LanguageServerMetadata};
|
||||
#[cfg(feature = "local_fs")]
|
||||
@@ -20,6 +22,97 @@ impl TypeScriptLanguageServerCandidate {
|
||||
Self { client }
|
||||
}
|
||||
|
||||
/// Resolves `initializationOptions` for `typescript-language-server`.
|
||||
///
|
||||
/// The server requires a valid TypeScript installation to function. It searches for
|
||||
/// `tsserver` in this order:
|
||||
/// 1. The workspace's `node_modules/typescript/lib` (local install)
|
||||
/// 2. A globally-installed TypeScript package (via `npm root -g`)
|
||||
///
|
||||
/// Returns `initializationOptions.tsserver.path` pointing to the TypeScript lib directory
|
||||
/// so the language server can locate `tsserver.js`.
|
||||
#[cfg(feature = "local_fs")]
|
||||
pub async fn resolve_initialization_options(
|
||||
workspace_root: &Path,
|
||||
path_env_var: Option<&str>,
|
||||
) -> Option<Value> {
|
||||
// 1. Check workspace-local node_modules
|
||||
let local_ts_lib = workspace_root.join("node_modules/typescript/lib");
|
||||
if local_ts_lib.join("tsserver.js").is_file() {
|
||||
log::info!(
|
||||
"typescript-language-server: using workspace typescript at {}",
|
||||
local_ts_lib.display()
|
||||
);
|
||||
return Some(serde_json::json!({
|
||||
"tsserver": {
|
||||
"path": local_ts_lib.to_string_lossy()
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// 2. Check global npm install location
|
||||
if let Some(path_env) = path_env_var {
|
||||
let mut cmd = command::r#async::Command::new("npm");
|
||||
cmd.env("PATH", path_env);
|
||||
cmd.args(["root", "-g"]);
|
||||
if let Ok(output) = cmd.output().await {
|
||||
if output.status.success() {
|
||||
let global_root = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
let global_ts_lib =
|
||||
std::path::PathBuf::from(&global_root).join("typescript/lib");
|
||||
if global_ts_lib.join("tsserver.js").is_file() {
|
||||
log::info!(
|
||||
"typescript-language-server: using global typescript at {}",
|
||||
global_ts_lib.display()
|
||||
);
|
||||
return Some(serde_json::json!({
|
||||
"tsserver": {
|
||||
"path": global_ts_lib.to_string_lossy()
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Try npx-managed typescript location (npx caches packages)
|
||||
if let Some(path_env) = path_env_var {
|
||||
let mut cmd = command::r#async::Command::new("npx");
|
||||
cmd.env("PATH", path_env);
|
||||
cmd.args(["--yes", "--package", "typescript", "node", "-e", "console.log(require('typescript').sys.getExecutingFilePath())"]);
|
||||
// Set cwd to workspace so npx resolves in the right context
|
||||
cmd.current_dir(workspace_root);
|
||||
if let Ok(output) = cmd.output().await {
|
||||
if output.status.success() {
|
||||
let tsserver_path = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
if !tsserver_path.is_empty() {
|
||||
// The executing file path is something like .../typescript/lib/tsserver.js
|
||||
// We need the parent directory (the lib dir)
|
||||
if let Some(lib_dir) = std::path::Path::new(&tsserver_path).parent() {
|
||||
if lib_dir.join("tsserver.js").is_file() {
|
||||
log::info!(
|
||||
"typescript-language-server: using npx-resolved typescript at {}",
|
||||
lib_dir.display()
|
||||
);
|
||||
return Some(serde_json::json!({
|
||||
"tsserver": {
|
||||
"path": lib_dir.to_string_lossy()
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::warn!(
|
||||
"typescript-language-server: could not find a TypeScript installation for {}",
|
||||
workspace_root.display()
|
||||
);
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns a CustomBinaryConfig that runs the server via npx.
|
||||
/// npx handles downloading/caching the package automatically.
|
||||
#[cfg(feature = "local_fs")]
|
||||
@@ -89,6 +182,35 @@ impl LanguageServerCandidate for TypeScriptLanguageServerCandidate {
|
||||
anyhow::bail!("npx is not working. Ensure Node.js and npm are installed.");
|
||||
}
|
||||
|
||||
// Pre-warm: ensure the typescript package is available so the LSP server
|
||||
// doesn't fail at initialization time with "Could not find a valid TypeScript
|
||||
// installation". This uses `npm list` to check workspace-local or global installs,
|
||||
// and falls back to installing typescript globally if not found.
|
||||
let has_typescript = {
|
||||
let output = executor
|
||||
.command("npm")
|
||||
.args(["list", "typescript", "--depth=0"])
|
||||
.output()
|
||||
.await;
|
||||
output.map(|o| o.status.success()).unwrap_or(false)
|
||||
};
|
||||
|
||||
if !has_typescript {
|
||||
log::info!("typescript not found locally, installing globally for LSP support");
|
||||
let output = executor
|
||||
.command("npm")
|
||||
.args(["install", "-g", "typescript"])
|
||||
.output()
|
||||
.await
|
||||
.context("Failed to install typescript globally")?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
log::warn!("Failed to install typescript globally: {stderr}");
|
||||
// Non-fatal: the workspace might have it in node_modules already
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("typescript-language-server will run via npx (no pre-install needed)");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ use std::sync::Arc;
|
||||
use command::r#async::Command;
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use serde_json::Value;
|
||||
use strum::IntoEnumIterator;
|
||||
use strum_macros::EnumIter;
|
||||
|
||||
@@ -197,6 +199,30 @@ impl LSPServerType {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns server-specific `initializationOptions` to send during LSP initialization.
|
||||
///
|
||||
/// For most servers this returns `None`. For `typescript-language-server`, it resolves
|
||||
/// the path to `tsserver` from the workspace's `node_modules` or a globally-installed
|
||||
/// TypeScript package so the server doesn't fail with "Could not find a valid TypeScript
|
||||
/// installation".
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub async fn initialization_options(
|
||||
&self,
|
||||
workspace_root: &std::path::Path,
|
||||
path_env_var: Option<&str>,
|
||||
) -> Option<Value> {
|
||||
match self {
|
||||
LSPServerType::TypeScriptLanguageServer => {
|
||||
TypeScriptLanguageServerCandidate::resolve_initialization_options(
|
||||
workspace_root,
|
||||
path_env_var,
|
||||
)
|
||||
.await
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn candidate(&self, client: Arc<http_client::Client>) -> Box<dyn LanguageServerCandidate> {
|
||||
match self {
|
||||
LSPServerType::RustAnalyzer => Box::new(RustAnalyzerCandidate::new(client)),
|
||||
|
||||
Reference in New Issue
Block a user