From a0d687c24a2ed8d335504c83725396aeec17c6c5 Mon Sep 17 00:00:00 2001 From: Fabian <43351274+0xk1f0@users.noreply.github.com> Date: Tue, 23 Jul 2024 16:39:25 +0200 Subject: [PATCH] astro: Ensure Typescript is present (#14849) The current Astro Extension fails to load properly if it can't find a `tsserver.js` file in the current workspaces' `node_modules` folder. This happens pretty frequently, either if `typescript` is not installed in the project (which it isn't by default), or if `node_modules` is not in the workspace root. This PR adds a fallback method of installing `typescript` alongside the extensions' language server if it is not found in the workspaces' `node_modules`, as well as correctly setting the `tsdk` path in the initialization options. Release Notes: - N/A --------- Co-authored-by: Marshall Bowers --- Cargo.lock | 1 + extensions/astro/Cargo.toml | 1 + extensions/astro/extension.toml | 2 +- extensions/astro/src/astro.rs | 78 +++++++++++++++++++++++++++++++-- 4 files changed, 77 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 559b802cf2cb0aec99ddb25e5598850b5104f720..ad1b4405f2059ce3d3f11bfe9be630e45b90a38a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13855,6 +13855,7 @@ dependencies = [ name = "zed_astro" version = "0.0.3" dependencies = [ + "serde", "zed_extension_api 0.0.6", ] diff --git a/extensions/astro/Cargo.toml b/extensions/astro/Cargo.toml index d090873ccc0e504d9cac18be7292f34f8e172379..916518d040dc82aca7e29f34069e221c92a0bfa5 100644 --- a/extensions/astro/Cargo.toml +++ b/extensions/astro/Cargo.toml @@ -13,4 +13,5 @@ path = "src/astro.rs" crate-type = ["cdylib"] [dependencies] +serde = { version = "1.0", features = ["derive"] } zed_extension_api = "0.0.6" diff --git a/extensions/astro/extension.toml b/extensions/astro/extension.toml index e1228dc86e6b6cb961672070093768a5e4cad780..d3aab9c44882ecf9a73e7b3ca482ddafcf10515a 100644 --- a/extensions/astro/extension.toml +++ b/extensions/astro/extension.toml @@ -3,7 +3,7 @@ name = "Astro" description = "Astro support." version = "0.0.3" schema_version = 1 -authors = ["Alvaro Gaona "] +authors = ["Alvaro Gaona ", "0xk1f0 "] repository = "https://github.com/zed-industries/zed" [language_servers.astro-language-server] diff --git a/extensions/astro/src/astro.rs b/extensions/astro/src/astro.rs index c3bb859d22acd5bf08ea58c8b19aab09ef2d3e35..72c3646246a6f5668e7422c69c17bf44d44dae6b 100644 --- a/extensions/astro/src/astro.rs +++ b/extensions/astro/src/astro.rs @@ -1,11 +1,29 @@ +use std::collections::HashMap; use std::{env, fs}; + +use serde::Deserialize; use zed_extension_api::{self as zed, serde_json, Result}; const SERVER_PATH: &str = "node_modules/@astrojs/language-server/bin/nodeServer.js"; const PACKAGE_NAME: &str = "@astrojs/language-server"; +const TYPESCRIPT_PACKAGE_NAME: &str = "typescript"; + +/// The relative path to TypeScript's SDK. +const TYPESCRIPT_TSDK_PATH: &str = "node_modules/typescript/lib"; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct PackageJson { + #[serde(default)] + dependencies: HashMap, + #[serde(default)] + dev_dependencies: HashMap, +} + struct AstroExtension { did_find_server: bool, + typescript_tsdk_path: String, } impl AstroExtension { @@ -13,9 +31,14 @@ impl AstroExtension { fs::metadata(SERVER_PATH).map_or(false, |stat| stat.is_file()) } - fn server_script_path(&mut self, language_server_id: &zed::LanguageServerId) -> Result { + fn server_script_path( + &mut self, + language_server_id: &zed::LanguageServerId, + worktree: &zed::Worktree, + ) -> Result { let server_exists = self.server_exists(); if self.did_find_server && server_exists { + self.install_typescript_if_needed(worktree)?; return Ok(SERVER_PATH.to_string()); } @@ -49,24 +72,71 @@ impl AstroExtension { } } + self.install_typescript_if_needed(worktree)?; self.did_find_server = true; Ok(SERVER_PATH.to_string()) } + + /// Returns whether a local copy of TypeScript exists in the worktree. + fn typescript_exists_for_worktree(&self, worktree: &zed::Worktree) -> Result { + let package_json = worktree.read_text_file("package.json")?; + let package_json: PackageJson = serde_json::from_str(&package_json) + .map_err(|err| format!("failed to parse package.json: {err}"))?; + + let dev_dependencies = &package_json.dev_dependencies; + let dependencies = &package_json.dependencies; + + // Since the extension is not allowed to read the filesystem within the project + // except through the worktree (which does not contains `node_modules`), we check + // the `package.json` to see if `typescript` is listed in the dependencies. + Ok(dev_dependencies.contains_key(TYPESCRIPT_PACKAGE_NAME) + || dependencies.contains_key(TYPESCRIPT_PACKAGE_NAME)) + } + + fn install_typescript_if_needed(&mut self, worktree: &zed::Worktree) -> Result<()> { + if self + .typescript_exists_for_worktree(worktree) + .unwrap_or_default() + { + println!("found local TypeScript installation at '{TYPESCRIPT_TSDK_PATH}'"); + return Ok(()); + } + + let installed_typescript_version = + zed::npm_package_installed_version(TYPESCRIPT_PACKAGE_NAME)?; + let latest_typescript_version = zed::npm_package_latest_version(TYPESCRIPT_PACKAGE_NAME)?; + + if installed_typescript_version.as_ref() != Some(&latest_typescript_version) { + println!("installing {TYPESCRIPT_PACKAGE_NAME}@{latest_typescript_version}"); + zed::npm_install_package(TYPESCRIPT_PACKAGE_NAME, &latest_typescript_version)?; + } else { + println!("typescript already installed"); + } + + self.typescript_tsdk_path = env::current_dir() + .unwrap() + .join(TYPESCRIPT_TSDK_PATH) + .to_string_lossy() + .to_string(); + + Ok(()) + } } impl zed::Extension for AstroExtension { fn new() -> Self { Self { did_find_server: false, + typescript_tsdk_path: TYPESCRIPT_TSDK_PATH.to_owned(), } } fn language_server_command( &mut self, language_server_id: &zed::LanguageServerId, - _worktree: &zed::Worktree, + worktree: &zed::Worktree, ) -> Result { - let server_path = self.server_script_path(language_server_id)?; + let server_path = self.server_script_path(language_server_id, worktree)?; Ok(zed::Command { command: zed::node_binary_path()?, args: vec![ @@ -89,7 +159,7 @@ impl zed::Extension for AstroExtension { Ok(Some(serde_json::json!({ "provideFormatter": true, "typescript": { - "tsdk": "node_modules/typescript/lib" + "tsdk": self.typescript_tsdk_path } }))) }