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
|
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 })
|
Ok(ResolvedLspCommand { command, params })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ use std::sync::Arc;
|
|||||||
#[cfg(feature = "local_fs")]
|
#[cfg(feature = "local_fs")]
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
#[cfg(feature = "local_fs")]
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::language_server_candidate::{LanguageServerCandidate, LanguageServerMetadata};
|
use crate::language_server_candidate::{LanguageServerCandidate, LanguageServerMetadata};
|
||||||
#[cfg(feature = "local_fs")]
|
#[cfg(feature = "local_fs")]
|
||||||
@@ -20,6 +22,97 @@ impl TypeScriptLanguageServerCandidate {
|
|||||||
Self { client }
|
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.
|
/// Returns a CustomBinaryConfig that runs the server via npx.
|
||||||
/// npx handles downloading/caching the package automatically.
|
/// npx handles downloading/caching the package automatically.
|
||||||
#[cfg(feature = "local_fs")]
|
#[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.");
|
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)");
|
log::info!("typescript-language-server will run via npx (no pre-install needed)");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ use std::sync::Arc;
|
|||||||
use command::r#async::Command;
|
use command::r#async::Command;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
use serde_json::Value;
|
||||||
use strum::IntoEnumIterator;
|
use strum::IntoEnumIterator;
|
||||||
use strum_macros::EnumIter;
|
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> {
|
pub fn candidate(&self, client: Arc<http_client::Client>) -> Box<dyn LanguageServerCandidate> {
|
||||||
match self {
|
match self {
|
||||||
LSPServerType::RustAnalyzer => Box::new(RustAnalyzerCandidate::new(client)),
|
LSPServerType::RustAnalyzer => Box::new(RustAnalyzerCandidate::new(client)),
|
||||||
|
|||||||
Reference in New Issue
Block a user