From 8c4550cb95d88a1841a1d6cfcf5e092d313745a1 Mon Sep 17 00:00:00 2001 From: Vitaly Slobodin Date: Mon, 13 Jul 2026 00:23:53 +0200 Subject: [PATCH] node_runtime: Fix npm v12 output deserialization (#60798) # Objective - Fix npm v12 output deserialization - Fixes #60705 ## Solution Initially I was thinking about checking the `npm` version and then parsing its output accordingly but that would include a call to `npm --version`. Zed's mojo is to be blazingly fast so just checking the structure of the output should be enough: 1. `npm info --json` in v12 produces an array with 1 object: `[ { "name": "something" } ]` 2. `npm info --json` in v11 and older produces an object: `{ "name": "something" }` So the trick is to check the output: if it's an array, we have npm v12, otherwise npm v11. ## Testing - Did you test these changes? If so, how? 1. Ensure you have `npm` v12 installed. 2. Install a Node.js based language server, for example, `markdownlint` or `oxfmt` 3. Open a project and trigger start of a language server - Are there any parts that need more testing? no - How can other people (reviewers) test your changes? Is there anything specific they need to know? Use steps above - If relevant, what platforms did you test these changes on, and are there any important ones you can't test? I've tested this on macOS and Linux. ## Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [ ] The content adheres to Zed's UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable ## Showcase N/A --- Release Notes: - Fixed npm v12 output deserialization --- crates/node_runtime/src/node_runtime.rs | 68 +++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 3 deletions(-) diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index e960634a84..035c86f8ad 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -273,7 +273,12 @@ impl NodeRuntime { ) .await?; - let info: NpmInfo = serde_json::from_slice(&output.stdout)?; + let info: NpmInfo = deserialize_npm_info_from_response(&output.stdout).map_err(|e| { + anyhow::anyhow!( + "failed to parse npm info response: {e}\nstdout: {}", + String::from_utf8_lossy(&output.stdout) + ) + })?; let before = npm_config_before(instance.as_ref(), http.proxy()) .await .context("getting npm before config") @@ -412,6 +417,21 @@ pub struct NpmInfo { time: HashMap, } +/// Parse NpmInfo from npm info --json output, handling both v11 and >= v12 formats. +fn deserialize_npm_info_from_response(data: &[u8]) -> Result { + let value: serde_json::Value = serde_json::from_slice(data)?; + + // npm >= 12 returns an array with one object: [ { ... } ] + if let serde_json::Value::Array(arr) = &value { + if arr.len() == 1 { + return NpmInfo::deserialize(&arr[0]); + } + } + + // npm <= v11 returns a bare JSON object: { ... } + NpmInfo::deserialize(value) +} + #[derive(Debug, Deserialize, Default)] pub struct NpmInfoDistTags { latest: Option, @@ -1104,8 +1124,8 @@ mod tests { use semver::Version; use super::{ - NpmInfo, VersionStrategy, build_npm_command_args, proxy_argument, - select_npm_package_version, should_install_npm_package_version, + NpmInfo, VersionStrategy, build_npm_command_args, deserialize_npm_info_from_response, + proxy_argument, select_npm_package_version, should_install_npm_package_version, }; // Map localhost to 127.0.0.1 @@ -1384,4 +1404,46 @@ mod tests { ); Ok(()) } + + #[test] + fn test_deserialize_npm_info_npm11_format() -> Result<()> { + let json = r#"{ + "dist-tags": { "latest": "3.0.0" }, + "versions": ["1.0.0", "2.0.0", "3.0.0"] + }"#; + + let info = deserialize_npm_info_from_response(json.as_bytes())?; + assert_eq!(info.dist_tags.latest, Some(Version::parse("3.0.0")?)); + assert_eq!( + info.versions, + vec![ + Version::parse("1.0.0")?, + Version::parse("2.0.0")?, + Version::parse("3.0.0")? + ] + ); + Ok(()) + } + + #[test] + fn test_deserialize_npm_v12_format() -> Result<()> { + let json = r#"[ + { + "dist-tags": { "latest": "3.0.0" }, + "versions": ["1.0.0", "2.0.0", "3.0.0"] + } + ]"#; + + let info = deserialize_npm_info_from_response(json.as_bytes())?; + assert_eq!(info.dist_tags.latest, Some(Version::parse("3.0.0")?)); + assert_eq!( + info.versions, + vec![ + Version::parse("1.0.0")?, + Version::parse("2.0.0")?, + Version::parse("3.0.0")? + ] + ); + Ok(()) + } }