Files
openidea/crates/ide-sdk/src/angular.rs
Mykhailo Smolinskyiandnntndfrk 7a46774fda fix(angular): find ngserver installed through version managers (#260)
The Angular language server (ngserver) was not found when Node and global npm packages came from a version manager (nvm, fnm, volta, asdf, mise) whose bin directories are not on the GUI app's PATH. Tool lookup now searches those directories too through a shared ide-core tool_path module used by the LSP launcher, SDK queries and tool detection; explicit relative commands resolve against the project root, empty PATH entries are ignored, and a non-executable file earlier on PATH no longer shadows the real command.

The process query test that flaked under load now uses a generous deadline for the success case, with the timeout covered by a separate test of a child that never exits.

Co-authored-by: nntndfrk <[email protected]>
2026-10-30 12:23:25 +08:00

94 lines
3.9 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Angular Language Server (`ngserver`): its package, where TypeScript and
//! `@angular/language-service` come from, and the project's Angular version.
use crate::npm::{package_dir, shim_package};
use std::collections::BTreeMap;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
pub(crate) const SERVER: &str = "angular-language-server";
const PACKAGE: &str = "@angular/language-server";
/// `@angular/language-server`'s directory for an `ngserver` executable found on
/// `path` and run in `cwd` with `env`.
pub(crate) fn server_package(bin: &Path, path: &OsStr, cwd: &Path, env: &BTreeMap<String, String>) -> Option<PathBuf> {
package_dir(bin, PACKAGE).or_else(|| shim_package(bin, PACKAGE, path, cwd, env))
}
/// Why an `ngserver` whose package isn't found can't start: it loads TypeScript
/// and `@angular/language-service` from there.
pub(crate) fn missing_package_message(bin: &Path) -> String {
format!(
"Can't find the @angular/language-server package of {}, which brings the TypeScript and Angular \
language service the server loads. Reinstall it in Settings › Languages & Frameworks › Language \
Servers or with `npm i -g @angular/language-server`.",
bin.display()
)
}
/// Why a server installed in a directory whose path has a comma can't start.
pub(crate) fn comma_message(package: &str) -> String {
format!(
"The Angular Language Server can't load from {package}: its path has a comma. Install it in a \
directory without one."
)
}
/// The version of the `@angular/core` that `dir` resolves (from its own or an
/// ancestor's `node_modules`, like Node).
pub(crate) fn core_version(dir: &Path) -> Option<String> {
dir.ancestors().find_map(|a| {
let text = std::fs::read_to_string(a.join("node_modules/@angular/core/package.json")).ok()?;
let json: serde_json::Value = serde_json::from_str(&text).ok()?;
json.get("version")?.as_str().map(str::to_owned)
})
}
/// Arguments telling the server the project's Angular `version`: templates of
/// projects before 17 have no blocks (`@if`), before 18.1 no `@let`.
pub(crate) fn version_args(version: &str) -> Vec<String> {
let mut parts = version.split(['.', '-', '+']).map(|p| p.parse::<u32>().ok());
let (Some(Some(major)), Some(Some(minor))) = (parts.next(), parts.next()) else { return Vec::new() };
// 0.0.0: a local build of Angular.
if major == 0 {
return Vec::new();
}
let mut args = vec!["--angularCoreVersion".to_owned(), version.to_owned()];
if major < 17 {
args.push("--disableBlockSyntax".into());
}
if (major, minor) < (18, 1) {
args.push("--disableLetSyntax".into());
}
args
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn template_syntax_follows_the_angular_version() {
let args = |v: &str| version_args(v).into_iter().skip(2).collect::<Vec<_>>();
assert_eq!(version_args("22.2.3")[..2], ["--angularCoreVersion", "22.2.3"]);
assert_eq!(args("22.0.0-next.1"), Vec::<String>::new());
assert_eq!(args("18.1.0"), Vec::<String>::new());
assert_eq!(args("18.0.5"), ["--disableLetSyntax"]);
assert_eq!(args("17.3.12"), ["--disableLetSyntax"]);
assert_eq!(args("16.2.0"), ["--disableBlockSyntax", "--disableLetSyntax"]);
assert!(version_args("0.0.0-PLACEHOLDER").is_empty());
assert!(version_args("next").is_empty());
}
#[test]
fn core_version_is_found_in_an_ancestor() {
let tmp = tempfile::tempdir().unwrap();
let core = tmp.path().join("node_modules/@angular/core");
std::fs::create_dir_all(&core).unwrap();
std::fs::write(core.join("package.json"), r#"{"name":"@angular/core","version":"19.2.1"}"#).unwrap();
let app = tmp.path().join("apps/web");
std::fs::create_dir_all(&app).unwrap();
assert_eq!(core_version(&app).as_deref(), Some("19.2.1"));
}
}