From ffec17703bac5b1990c433fb897aa1deebfff2f7 Mon Sep 17 00:00:00 2001 From: Adam Averay Date: Sun, 13 Sep 2026 14:01:04 +0100 Subject: [PATCH] fix(runtime): explain package.json parse errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a project’s `package.json` file contains invalid JSON, a generic invalid JSON error message was presented. Now the error output includes the file path to clarify which file caused the error. Fixes #2664. --- .../snapshots.toml | 8 +++ .../command_install_invalid_package_json.md | 19 +++++ crates/vp_js_runtime/src/error.rs | 5 ++ crates/vp_js_runtime/src/runtime.rs | 70 ++++++++++++++++++- 4 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_install_invalid_package_json/snapshots.toml create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_install_invalid_package_json/snapshots/command_install_invalid_package_json.md diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_install_invalid_package_json/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_install_invalid_package_json/snapshots.toml new file mode 100644 index 0000000000..1c22038b19 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_install_invalid_package_json/snapshots.toml @@ -0,0 +1,8 @@ +[[case]] +name = "command_install_invalid_package_json" +vp = "global" +steps = [ + { argv = ["vpt", "write-file", ".node-version", "22.18.0"], snapshot = false }, + { argv = ["vpt", "write-file", "package.json", "not-json"], snapshot = false }, + { argv = ["vp", "install"], comment = "should name the package.json that failed to parse", continue-on-failure = true }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_install_invalid_package_json/snapshots/command_install_invalid_package_json.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_install_invalid_package_json/snapshots/command_install_invalid_package_json.md new file mode 100644 index 0000000000..2bb8ad9941 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_install_invalid_package_json/snapshots/command_install_invalid_package_json.md @@ -0,0 +1,19 @@ +# command_install_invalid_package_json + +## `vpt write-file .node-version 22.18.0` + + +## `vpt write-file package.json not-json` + + +## `vp install` + +should name the package.json that failed to parse + +**Exit code:** 1 + +``` +VITE+ - The Unified Toolchain for the Web + +error: Failed to download Node.js runtime: Failed to parse /package.json: expected ident at line 1 column 2 +``` diff --git a/crates/vp_js_runtime/src/error.rs b/crates/vp_js_runtime/src/error.rs index 3840ffe92d..ae91a71751 100644 --- a/crates/vp_js_runtime/src/error.rs +++ b/crates/vp_js_runtime/src/error.rs @@ -1,4 +1,5 @@ use thiserror::Error; +use vt_path::AbsolutePath; use vt_str::Str; /// Errors that can occur during JavaScript runtime management @@ -40,6 +41,10 @@ pub enum Error { #[error("Failed to parse version index: {reason}")] VersionIndexParseFailed { reason: Str }, + /// Failed to parse package.json + #[error("Failed to parse {path}: {reason}")] + PackageJsonParseFailed { path: Box, reason: Str }, + /// No version matching the requirement found #[error("No version matching '{version_req}' found")] NoMatchingVersion { version_req: Str }, diff --git a/crates/vp_js_runtime/src/runtime.rs b/crates/vp_js_runtime/src/runtime.rs index 16bf269e7e..16bfd7870f 100644 --- a/crates/vp_js_runtime/src/runtime.rs +++ b/crates/vp_js_runtime/src/runtime.rs @@ -667,7 +667,11 @@ pub async fn read_package_json( } let content = tokio::fs::read_to_string(package_json_path).await?; - let pkg: PackageJson = serde_json::from_str(&content)?; + let pkg: PackageJson = + serde_json::from_str(&content).map_err(|e| Error::PackageJsonParseFailed { + path: package_json_path.as_absolute_path().into(), + reason: vt_str::format!("{e}"), + })?; Ok(Some(pkg)) } @@ -992,6 +996,70 @@ mod tests { .await; } + #[tokio::test] + async fn test_read_package_json_invalid_json_names_file() { + let temp_dir = TempDir::new().unwrap(); + let temp_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); + + // Write invalid package.json file + let package_json_path = temp_path.join("package.json"); + tokio::fs::write(&package_json_path, "not-json").await.unwrap(); + + let error = read_package_json(&package_json_path).await.unwrap_err(); + + // Should be the dedicated package.json error variant + assert!( + matches!(error, Error::PackageJsonParseFailed { .. }), + "unexpected error: {error:?}" + ); + + // Should name the file, line & column that failed to parse + assert_eq!( + error.to_string(), + format!( + "Failed to parse {}: expected ident at line 1 column 2", + package_json_path.as_absolute_path() + ) + ); + } + + #[tokio::test] + async fn test_download_runtime_for_project_invalid_package_json() { + let vp_home = TempDir::new().unwrap(); + vp_shared::EnvConfig::with_vars_async( + [(env_vars::VP_HOME, vp_home.path().as_os_str())], + |_| async { + let temp_dir = TempDir::new().unwrap(); + let temp_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); + + // Write invalid package.json file + tokio::fs::write(temp_path.join("package.json"), "not-json").await.unwrap(); + + // Write sibling .node-version to trigger proceeding to package.json re-read + tokio::fs::write(temp_path.join(".node-version"), "22.18.0\n").await.unwrap(); + + let error = download_runtime_for_project(&temp_path).await.unwrap_err(); + + // Should fail on the package.json re-read + assert!( + matches!(error, Error::PackageJsonParseFailed { .. }), + "unexpected error: {error:?}" + ); + + // Should name the file, line & column that failed to parse + let package_json_path = temp_path.join("package.json"); + assert_eq!( + error.to_string(), + format!( + "Failed to parse {}: expected ident at line 1 column 2", + package_json_path.as_absolute_path() + ) + ); + }, + ) + .await; + } + #[tokio::test] async fn test_download_runtime_for_project_inherits_parent_node_version() { let vp_home = shared_vp_home();