From ed0839bd60c9c04759324c4690f1913f0a0b333e Mon Sep 17 00:00:00 2001 From: cijiugechu Date: Tue, 10 Feb 2026 23:33:30 +0800 Subject: [PATCH 1/5] Improve parser error context for manifests, lockfiles, and config --- packages/zpm-config/src/lib.rs | 24 ++++++++++++++++++++++-- packages/zpm/src/error.rs | 21 +++++++++++++++------ packages/zpm/src/lockfile.rs | 7 +++++-- packages/zpm/src/manifest/helpers.rs | 15 ++++++++++++--- packages/zpm/src/project.rs | 9 ++++++--- 5 files changed, 60 insertions(+), 16 deletions(-) diff --git a/packages/zpm-config/src/lib.rs b/packages/zpm-config/src/lib.rs index b4b361d3..a05ec559 100644 --- a/packages/zpm-config/src/lib.rs +++ b/packages/zpm-config/src/lib.rs @@ -793,6 +793,18 @@ pub enum ConfigurationError { #[error(transparent)] SerdeError(#[from] Arc), + + #[error("Invalid user configuration file ({}): {message}", path.to_print_string())] + UserConfigParseError { + path: Path, + message: String, + }, + + #[error("Invalid project configuration file ({}): {message}", path.to_print_string())] + ProjectConfigParseError { + path: Path, + message: String, + }, } impl From for ConfigurationError { @@ -882,7 +894,11 @@ impl Configuration { .fs_read_text_with_size(metadata.len())?; let user_config: intermediate::Settings - = serde_yaml::from_str(&user_config_text)?; + = serde_yaml::from_str(&user_config_text) + .map_err(|error| ConfigurationError::UserConfigParseError { + path: user_config_path.clone(), + message: error.to_string(), + })?; intermediate_user_config = Partial::Value(user_config); } @@ -906,7 +922,11 @@ impl Configuration { .fs_read_text_with_size(metadata.len())?; let project_config: intermediate::Settings - = serde_yaml::from_str(&project_config_text)?; + = serde_yaml::from_str(&project_config_text) + .map_err(|error| ConfigurationError::ProjectConfigParseError { + path: project_config_path.clone(), + message: error.to_string(), + })?; intermediate_project_config = Partial::Value(project_config); } diff --git a/packages/zpm/src/error.rs b/packages/zpm/src/error.rs index 1cc8ab57..b3c29af6 100644 --- a/packages/zpm/src/error.rs +++ b/packages/zpm/src/error.rs @@ -165,8 +165,11 @@ pub enum Error { #[error("Package manifest not found ({})", .0.to_print_string())] ManifestNotFound(Path), - #[error("Package manifest failed to parse ({}): {}", .0.to_print_string(), .1)] - ManifestParseError(Path, Arc), + #[error("Package manifest failed to parse ({}): {reason}", path.to_print_string())] + ManifestParseError { + path: Path, + reason: String, + }, #[error("Invalid descriptor ({0})")] InvalidDescriptor(String), @@ -249,8 +252,11 @@ pub enum Error { #[error("An error occured while reading the lockfile from disk")] LockfileReadError(Arc), - #[error("An error occured while parsing the lockfile: {0}")] - LockfileParseError(zpm_parsers::Error), + #[error("An error occured while parsing the lockfile ({}): {reason}", path.to_print_string())] + LockfileParseError { + path: Path, + reason: String, + }, #[error("Can't perform this operation without a git root")] NoGitRoot, @@ -261,8 +267,11 @@ pub enum Error { #[error("No merge base could be found between any of HEAD and {args}", args = .0.join(", "))] NoMergeBaseFound(Vec), - #[error("An error occured while parsing the Yarn Berry lockfile: {0}")] - LegacyLockfileParseError(Arc), + #[error("An error occured while parsing the Yarn Berry lockfile ({}): {reason}", path.to_print_string())] + LegacyLockfileParseError { + path: Path, + reason: String, + }, #[error("Failed to read pnpm node_modules directory")] PnpmNodeModulesReadError, diff --git a/packages/zpm/src/lockfile.rs b/packages/zpm/src/lockfile.rs index b6eeb323..4eec5030 100644 --- a/packages/zpm/src/lockfile.rs +++ b/packages/zpm/src/lockfile.rs @@ -249,13 +249,16 @@ struct LegacyBerryLockfilePayload { entries: TolerantMap, LegacyBerryLockfileEntry>, } -pub fn from_legacy_berry_lockfile(data: &str) -> Result { +pub fn from_legacy_berry_lockfile(data: &str, lockfile_path: &Path) -> Result { if data.starts_with("# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.") { return Err(Error::LockfileV1Error); } let payload: LegacyBerryLockfilePayload = serde_yaml::from_str(data) - .map_err(|err| Error::LegacyLockfileParseError(Arc::new(err)))?; + .map_err(|error| Error::LegacyLockfileParseError { + path: lockfile_path.clone(), + reason: error.to_string(), + })?; let mut lockfile = Lockfile::new(); diff --git a/packages/zpm/src/manifest/helpers.rs b/packages/zpm/src/manifest/helpers.rs index beacf50b..1cc91ca2 100644 --- a/packages/zpm/src/manifest/helpers.rs +++ b/packages/zpm/src/manifest/helpers.rs @@ -1,5 +1,3 @@ -use std::sync::Arc; - use zpm_parsers::JsonDocument; use zpm_utils::{IoResultExt, Path}; @@ -21,7 +19,18 @@ pub fn read_manifest_with_size(abs_path: &Path, size: u64) -> Result Result { diff --git a/packages/zpm/src/project.rs b/packages/zpm/src/project.rs index e5656b00..20aa68d0 100644 --- a/packages/zpm/src/project.rs +++ b/packages/zpm/src/project.rs @@ -296,12 +296,15 @@ impl Project { } if src.starts_with('#') { - return from_legacy_berry_lockfile(&src); + return from_legacy_berry_lockfile(&src, lockfile_path); } let lockfile: Lockfile = JsonDocument::hydrate_from_str(&src) - .map_err(|e| Error::LockfileParseError(e))?; + .map_err(|error| Error::LockfileParseError { + path: lockfile_path.clone(), + reason: error.to_string(), + })?; Ok(lockfile) } @@ -780,7 +783,7 @@ impl Project { let mut lockfile = self.lockfile(); - if let Err(Error::LockfileParseError(_)) = lockfile { + if let Err(Error::LockfileParseError { .. }) = lockfile { let lockfile_path = self.lockfile_path(); From bd0009b4018196c481c0d3b4f03d215a110ce784 Mon Sep 17 00:00:00 2001 From: cijiugechu Date: Tue, 10 Feb 2026 23:58:58 +0800 Subject: [PATCH 2/5] Improve remote manifest parse errors and aggregate duplicates --- packages/zpm/src/content_flags.rs | 5 +- packages/zpm/src/error.rs | 76 ++++++++++++++++++++++++++ packages/zpm/src/fetchers/folder.rs | 5 +- packages/zpm/src/fetchers/git.rs | 5 +- packages/zpm/src/fetchers/patch.rs | 10 ++-- packages/zpm/src/fetchers/tarball.rs | 5 +- packages/zpm/src/fetchers/url.rs | 5 +- packages/zpm/src/report.rs | 80 +++++++++++++++++++++++++++- packages/zpm/src/resolvers/npm.rs | 32 +++++++++-- 9 files changed, 205 insertions(+), 18 deletions(-) diff --git a/packages/zpm/src/content_flags.rs b/packages/zpm/src/content_flags.rs index b8a58729..1b04503f 100644 --- a/packages/zpm/src/content_flags.rs +++ b/packages/zpm/src/content_flags.rs @@ -9,7 +9,7 @@ use zpm_primitives::{Ident, Locator, Reference}; use zpm_utils::{Path, Requirements}; use crate::{ - build, error::Error, fetchers::PackageData, manifest::bin::BinField + build, error::{Error, remote_manifest_parse_error}, fetchers::PackageData, manifest::bin::BinField }; static UNPLUG_SCRIPTS: &[&str] = &["preinstall", "install", "postinstall"]; @@ -156,7 +156,8 @@ impl ContentFlags { = zpm_formats::zip::first_entry_from_zip(&package_bytes)?; let meta_manifest: Manifest - = JsonDocument::hydrate_from_slice(&first_entry.data)?; + = JsonDocument::hydrate_from_slice(&first_entry.data) + .map_err(|error| remote_manifest_parse_error(locator, "cached package archive", "package.json", error))?; let mut build_commands = UNPLUG_SCRIPTS.iter() .filter_map(|k| meta_manifest.scripts.get(*k).map(|s| (k, s))) diff --git a/packages/zpm/src/error.rs b/packages/zpm/src/error.rs index b3c29af6..5d4ecd20 100644 --- a/packages/zpm/src/error.rs +++ b/packages/zpm/src/error.rs @@ -12,6 +12,50 @@ fn render_backtrace(backtrace: &std::backtrace::Backtrace) -> String { } } +fn render_remote_manifest_reason(raw_reason: &str) -> String { + let first_line = raw_reason.lines().next().unwrap_or(raw_reason).trim(); + + if let Some(rest) = first_line.strip_prefix("invalid type: ") + && let Some((actual, expected_tail)) = rest.split_once(", expected ") + { + if let Some((expected, line_col)) = expected_tail.split_once(" at line ") { + return format!( + "type mismatch: expected {}, got {} (line {})", + expected.trim(), + actual.trim(), + line_col.trim(), + ); + } + + return format!( + "type mismatch: expected {}, got {}", + expected_tail.trim(), + actual.trim(), + ); + } + + first_line.to_string() +} + +pub fn remote_manifest_parse_error( + locator: &Locator, + origin: impl Into, + path: impl Into, + parser_error: zpm_parsers::Error, +) -> Error { + let raw_reason = match &parser_error { + zpm_parsers::Error::InvalidSyntax(message) => message.clone(), + _ => parser_error.to_string(), + }; + + Error::RemoteManifestParseError { + locator: locator.clone(), + origin: origin.into(), + path: path.into(), + reason: render_remote_manifest_reason(&raw_reason), + } +} + pub async fn set_timeout(timeout: std::time::Duration, f: F) -> Result { let res = tokio::time::timeout(timeout, f).await .map_err(|_| Error::TaskTimeout)?; @@ -144,6 +188,14 @@ pub enum Error { #[error("File parsing error ({0})")] FileParsingError(#[from] zpm_parsers::Error), + #[error("Invalid package metadata in {path} ({origin}): {reason}")] + RemoteManifestParseError { + locator: Locator, + origin: String, + path: String, + reason: String, + }, + #[error("Semver error ({0})")] SemverError(#[from] zpm_semver::Error), @@ -509,6 +561,30 @@ pub enum Error { SilentError, } +#[cfg(test)] +mod tests { + use super::render_remote_manifest_reason; + + #[test] + fn renders_manifest_type_mismatch_compactly() { + let reason = render_remote_manifest_reason( + "invalid type: string \"glibc\", expected a sequence at line 1 column 1687", + ); + + assert_eq!( + reason, + "type mismatch: expected a sequence, got string \"glibc\" (line 1 column 1687)" + ); + } + + #[test] + fn keeps_first_line_for_multiline_reasons() { + let reason = render_remote_manifest_reason("foo\nbar\nbaz"); + + assert_eq!(reason, "foo"); + } +} + impl Error { pub fn ignore bool>(self, f: F) -> Result, Error> { match f(&self) { diff --git a/packages/zpm/src/fetchers/folder.rs b/packages/zpm/src/fetchers/folder.rs index 457afe68..640809a6 100644 --- a/packages/zpm/src/fetchers/folder.rs +++ b/packages/zpm/src/fetchers/folder.rs @@ -2,7 +2,7 @@ use zpm_parsers::JsonDocument; use zpm_primitives::{FolderReference, Locator}; use crate::{ - error::Error, install::{FetchResult, InstallContext, InstallOpResult}, manifest::RemoteManifest, npm::NpmEntryExt, resolvers::Resolution + error::{Error, remote_manifest_parse_error}, install::{FetchResult, InstallContext, InstallOpResult}, manifest::RemoteManifest, npm::NpmEntryExt, resolvers::Resolution }; use super::PackageData; @@ -58,7 +58,8 @@ pub async fn fetch_locator<'a>(context: &InstallContext<'a>, locator: &Locator, = zpm_formats::zip::first_entry_from_zip(&pkg_blob.data)?; let remote_manifest: RemoteManifest - = JsonDocument::hydrate_from_slice(&first_entry.data)?; + = JsonDocument::hydrate_from_slice(&first_entry.data) + .map_err(|error| remote_manifest_parse_error(locator, "package archive", "package.json", error))?; let resolution = Resolution::from_remote_manifest(locator.clone(), remote_manifest); diff --git a/packages/zpm/src/fetchers/git.rs b/packages/zpm/src/fetchers/git.rs index b8f95ba1..c8790660 100644 --- a/packages/zpm/src/fetchers/git.rs +++ b/packages/zpm/src/fetchers/git.rs @@ -3,7 +3,7 @@ use zpm_parsers::JsonDocument; use zpm_primitives::{GitReference, Locator}; use crate::{ - error::Error, git, install::{FetchResult, InstallContext}, manifest::RemoteManifest, npm::NpmEntryExt, prepare, resolvers::Resolution + error::{Error, remote_manifest_parse_error}, git, install::{FetchResult, InstallContext}, manifest::RemoteManifest, npm::NpmEntryExt, prepare, resolvers::Resolution }; use super::PackageData; @@ -62,7 +62,8 @@ pub async fn fetch_locator<'a>(context: &InstallContext<'a>, locator: &Locator, = zpm_formats::zip::first_entry_from_zip(&pkg_blob.data)?; let remote_manifest: RemoteManifest - = JsonDocument::hydrate_from_slice(&first_entry.data)?; + = JsonDocument::hydrate_from_slice(&first_entry.data) + .map_err(|error| remote_manifest_parse_error(locator, "package archive", "package.json", error))?; let resolution = Resolution::from_remote_manifest(locator.clone(), remote_manifest); diff --git a/packages/zpm/src/fetchers/patch.rs b/packages/zpm/src/fetchers/patch.rs index 187411d7..4666da2a 100644 --- a/packages/zpm/src/fetchers/patch.rs +++ b/packages/zpm/src/fetchers/patch.rs @@ -4,7 +4,7 @@ use zpm_primitives::{Ident, Locator, PatchReference}; use zpm_utils::Hash64; use crate::{ - error::Error, install::{FetchResult, InstallContext, InstallOpResult}, manifest::RemoteManifest, misc::unpack_brotli_data, npm::NpmEntryExt, patch::apply::apply_patch, resolvers::Resolution + error::{Error, remote_manifest_parse_error}, install::{FetchResult, InstallContext, InstallOpResult}, manifest::RemoteManifest, misc::unpack_brotli_data, npm::NpmEntryExt, patch::apply::apply_patch, resolvers::Resolution }; use super::PackageData; @@ -97,6 +97,8 @@ pub async fn fetch_locator<'a>(context: &InstallContext<'a>, locator: &Locator, let package_subdir = locator.ident.nm_subdir(); + let locator_for_patch_source + = locator.clone(); let package_subdir_for_entries = package_subdir.clone(); @@ -142,7 +144,8 @@ pub async fn fetch_locator<'a>(context: &InstallContext<'a>, locator: &Locator, .ok_or(Error::MissingPackageManifest)?; let manifest: RemoteManifest - = JsonDocument::hydrate_from_slice(&package_json_entry.data)?; + = JsonDocument::hydrate_from_slice(&package_json_entry.data) + .map_err(|error| remote_manifest_parse_error(&locator_for_patch_source, "patch source archive", "package.json", error))?; let package_version = manifest.version @@ -174,7 +177,8 @@ pub async fn fetch_locator<'a>(context: &InstallContext<'a>, locator: &Locator, = zpm_formats::zip::first_entry_from_zip(&cached_blob.data)?; let manifest: RemoteManifest - = JsonDocument::hydrate_from_slice(&package_json_entry.data)?; + = JsonDocument::hydrate_from_slice(&package_json_entry.data) + .map_err(|error| remote_manifest_parse_error(&locator, "patched package archive", "package.json", error))?; let resolution = Resolution::from_remote_manifest(locator.clone(), manifest); diff --git a/packages/zpm/src/fetchers/tarball.rs b/packages/zpm/src/fetchers/tarball.rs index 1fc6b444..ef0aa845 100644 --- a/packages/zpm/src/fetchers/tarball.rs +++ b/packages/zpm/src/fetchers/tarball.rs @@ -3,7 +3,7 @@ use zpm_parsers::JsonDocument; use zpm_primitives::{Locator, TarballReference}; use crate::{ - error::Error, install::{FetchResult, InstallContext, InstallOpResult}, manifest::RemoteManifest, npm::NpmEntryExt, resolvers::Resolution + error::{Error, remote_manifest_parse_error}, install::{FetchResult, InstallContext, InstallOpResult}, manifest::RemoteManifest, npm::NpmEntryExt, resolvers::Resolution }; use super::PackageData; @@ -63,7 +63,8 @@ pub async fn fetch_locator<'a>(context: &InstallContext<'a>, locator: &Locator, = zpm_formats::zip::first_entry_from_zip(&cached_blob.data)?; let manifest: RemoteManifest - = JsonDocument::hydrate_from_slice(&first_entry.data)?; + = JsonDocument::hydrate_from_slice(&first_entry.data) + .map_err(|error| remote_manifest_parse_error(locator, "package archive", "package.json", error))?; let resolution = Resolution::from_remote_manifest(locator.clone(), manifest); diff --git a/packages/zpm/src/fetchers/url.rs b/packages/zpm/src/fetchers/url.rs index d53f5a39..7080eef0 100644 --- a/packages/zpm/src/fetchers/url.rs +++ b/packages/zpm/src/fetchers/url.rs @@ -5,7 +5,7 @@ use zpm_parsers::JsonDocument; use zpm_primitives::{Locator, UrlReference}; use crate::{ - error::Error, + error::{Error, remote_manifest_parse_error}, http_npm::{self, AuthorizationMode, GetAuthorizationOptions}, install::{FetchResult, InstallContext}, manifest::RemoteManifest, @@ -91,7 +91,8 @@ pub async fn fetch_locator<'a>(context: &InstallContext<'a>, locator: &Locator, = zpm_formats::zip::first_entry_from_zip(&cached_blob.data)?; let manifest: RemoteManifest - = JsonDocument::hydrate_from_slice(&first_entry.data)?; + = JsonDocument::hydrate_from_slice(&first_entry.data) + .map_err(|error| remote_manifest_parse_error(locator, "package archive", "package.json", error))?; let resolution = Resolution::from_remote_manifest(locator.clone(), manifest); diff --git a/packages/zpm/src/report.rs b/packages/zpm/src/report.rs index 01234b7f..2b6eb6f2 100644 --- a/packages/zpm/src/report.rs +++ b/packages/zpm/src/report.rs @@ -1,4 +1,4 @@ -use std::{cell::RefCell, future::Future, io::{self, Write}, sync::{Arc, LazyLock, atomic::AtomicU32, mpsc}, thread::JoinHandle, time::{Duration, SystemTime}}; +use std::{cell::RefCell, collections::{BTreeMap, BTreeSet}, future::Future, io::{self, Write}, sync::{Arc, LazyLock, atomic::AtomicU32, mpsc}, thread::JoinHandle, time::{Duration, SystemTime}}; use colored::{Color, Colorize}; use dialoguer::{Input, Password}; @@ -168,11 +168,23 @@ pub struct ReportCounters { pub enum ReportMessage { Line(Severity, String), LogFile(Path), + RemoteManifestParse { + locator: String, + origin: String, + path: String, + reason: String, + }, PushSection(String), PopSection, Prompt(PromptType), } +#[derive(Debug, Default)] +struct RemoteManifestErrorAggregate { + count: u32, + locators: BTreeSet, +} + struct Reporter { config: StreamReportConfig, @@ -184,6 +196,7 @@ struct Reporter { start_time: Option, buffered_lines: Option>, log_paths: Vec, + remote_manifest_errors: BTreeMap<(String, String, String), RemoteManifestErrorAggregate>, spinner_idx: Option, prompt_tx: mpsc::Sender, } @@ -202,6 +215,7 @@ impl Reporter { start_time: None, buffered_lines, log_paths: Vec::new(), + remote_manifest_errors: BTreeMap::new(), spinner_idx: None, prompt_tx, } @@ -287,6 +301,10 @@ impl Reporter { self.log_paths.push(log_path); }, + ReportMessage::RemoteManifestParse { locator, origin, path, reason } => { + self.on_remote_manifest_parse(locator, origin, path, reason); + }, + ReportMessage::PushSection(name) => { self.on_push_section(writer, &name); }, @@ -308,6 +326,47 @@ impl Reporter { } fn on_end(&mut self, writer: &mut T) { + if !self.remote_manifest_errors.is_empty() { + let mut entries = self.remote_manifest_errors + .iter() + .map(|((origin, path, reason), aggregate)| { + (origin.clone(), path.clone(), reason.clone(), aggregate.count, aggregate.locators.clone()) + }) + .collect::>(); + + entries.sort_by(|left, right| right.3.cmp(&left.3)); + + for (origin, path, reason, count, locators) in entries { + let summary = if count == 1 { + format!("Invalid package metadata in {} ({}): {}", path, origin, reason) + } else { + format!("{} packages have invalid package metadata in {} ({}): {}", count, path, origin, reason) + }; + + self.on_line(writer, Severity::Error, &summary); + + let sample = locators + .into_iter() + .take(3) + .collect_vec(); + + if !sample.is_empty() { + let hidden_count = count.saturating_sub(sample.len() as u32); + let suffix = if hidden_count > 0 { + format!(" (+{} more)", hidden_count) + } else { + String::new() + }; + + self.on_line( + writer, + Severity::Error, + &format!("Affected packages: {}{}", sample.join(", "), suffix), + ); + } + } + } + for log_path in &self.log_paths { writeln!(writer, "\n{}\n", log_path.to_print_string()).unwrap(); @@ -330,6 +389,15 @@ impl Reporter { self.write_line(writer, message, severity); } + fn on_remote_manifest_parse(&mut self, locator: String, origin: String, path: String, reason: String) { + let aggregate = self.remote_manifest_errors + .entry((origin, path, reason)) + .or_default(); + + aggregate.count += 1; + aggregate.locators.insert(locator); + } + fn on_push_section(&mut self, writer: &mut T, name: &str) { self.level += 1; @@ -535,6 +603,16 @@ impl StreamReport { } pub fn error(&self, error: Error) { + if let Error::RemoteManifestParseError { locator, origin, path, reason } = &error { + self.report(ReportMessage::RemoteManifestParse { + locator: locator.to_print_string(), + origin: origin.clone(), + path: path.clone(), + reason: reason.clone(), + }); + return; + } + if !matches!(error, Error::SilentError) { self.report(ReportMessage::Line(Severity::Error, self.with_content_prefix(error.to_string()))); } diff --git a/packages/zpm/src/resolvers/npm.rs b/packages/zpm/src/resolvers/npm.rs index a24229fe..9b19adc8 100644 --- a/packages/zpm/src/resolvers/npm.rs +++ b/packages/zpm/src/resolvers/npm.rs @@ -9,7 +9,7 @@ use zpm_primitives::{AnonymousSemverRange, Descriptor, Ident, Locator, Reference use zpm_utils::UrlEncoded; use crate::{ - error::Error, + error::{Error, remote_manifest_parse_error}, http_npm, install::{InstallContext, InstallOpResult, IntoResolutionResult, ResolutionResult}, manifest::RemoteManifest, @@ -213,8 +213,18 @@ pub async fn resolve_semver_descriptor(context: &InstallContext<'_>, descriptor: continue; } + let locator = Locator::new( + package_ident.clone(), + RegistryReference { + ident: package_ident.clone(), + version: version.clone(), + url: None, + }.into(), + ); + let manifest - = JsonDocument::hydrate_from_value(manifest)?; + = JsonDocument::hydrate_from_value(manifest) + .map_err(|error| remote_manifest_parse_error(&locator, "npm registry metadata", "package.json", error))?; return build_resolution_result(context, descriptor, package_ident, version.clone(), manifest); } @@ -285,7 +295,20 @@ pub async fn resolve_tag_descriptor(context: &InstallContext<'_>, descriptor: &D .ok_or_else(|| Error::NoCandidatesFound(AnonymousSemverRange {range: zpm_semver::Range::lte(latest_version.clone())}.into()))?; let manifest - = JsonDocument::hydrate_from_value(&manifest)?; + = JsonDocument::hydrate_from_value(&manifest) + .map_err(|error| remote_manifest_parse_error( + &Locator::new( + package_ident.clone(), + RegistryReference { + ident: package_ident.clone(), + version: version.clone(), + url: None, + }.into(), + ), + "npm registry metadata", + "package.json", + error, + ))?; build_resolution_result(context, descriptor, package_ident, version, manifest) } @@ -319,7 +342,8 @@ pub async fn resolve_locator(context: &InstallContext<'_>, locator: &Locator, pa }).await?; let mut manifest: RemoteManifestWithScripts - = JsonDocument::hydrate_from_slice(&bytes[..])?; + = JsonDocument::hydrate_from_slice(&bytes[..]) + .map_err(|error| remote_manifest_parse_error(locator, "npm registry metadata", "package.json", error))?; fix_manifest(&mut manifest); From 732cdbf90d451e3fbbbce7b4eb4eee1f610addb7 Mon Sep 17 00:00:00 2001 From: cijiugechu Date: Tue, 10 Feb 2026 23:59:23 +0800 Subject: [PATCH 3/5] Fix legacy lockfile alias locator keying --- packages/zpm/src/lockfile.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/zpm/src/lockfile.rs b/packages/zpm/src/lockfile.rs index 4eec5030..c32dc9b8 100644 --- a/packages/zpm/src/lockfile.rs +++ b/packages/zpm/src/lockfile.rs @@ -294,12 +294,12 @@ pub fn from_legacy_berry_lockfile(data: &str, lockfile_path: &Path) -> Result Date: Wed, 11 Feb 2026 00:13:53 +0800 Subject: [PATCH 4/5] Use EcoString for parser/config error payload fields --- packages/zpm-config/src/lib.rs | 10 +++++----- packages/zpm/src/error.rs | 20 ++++++++++---------- packages/zpm/src/lockfile.rs | 2 +- packages/zpm/src/manifest/helpers.rs | 2 +- packages/zpm/src/project.rs | 2 +- packages/zpm/src/report.rs | 6 +++--- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/zpm-config/src/lib.rs b/packages/zpm-config/src/lib.rs index a05ec559..01e355b1 100644 --- a/packages/zpm-config/src/lib.rs +++ b/packages/zpm-config/src/lib.rs @@ -1,7 +1,7 @@ use std::{collections::BTreeMap, fmt::Display, ops::Deref, sync::Arc, time::UNIX_EPOCH}; use serde::{Deserialize, Deserializer, Serialize, Serializer, de}; -use zpm_utils::{AbstractValue, Container, Cpu, DataType, FromFileString, IoResultExt, LastModifiedAt, Libc, Os, Path, RawString, Serialized, System, ToFileString, ToHumanString, tree}; +use zpm_utils::{AbstractValue, Container, Cpu, DataType, EcoString, FromFileString, IoResultExt, LastModifiedAt, Libc, Os, Path, RawString, Serialized, System, ToFileString, ToHumanString, tree}; #[derive(Debug, Clone)] pub struct ConfigurationContext { @@ -797,13 +797,13 @@ pub enum ConfigurationError { #[error("Invalid user configuration file ({}): {message}", path.to_print_string())] UserConfigParseError { path: Path, - message: String, + message: EcoString, }, #[error("Invalid project configuration file ({}): {message}", path.to_print_string())] ProjectConfigParseError { path: Path, - message: String, + message: EcoString, }, } @@ -897,7 +897,7 @@ impl Configuration { = serde_yaml::from_str(&user_config_text) .map_err(|error| ConfigurationError::UserConfigParseError { path: user_config_path.clone(), - message: error.to_string(), + message: error.to_string().into(), })?; intermediate_user_config = Partial::Value(user_config); @@ -925,7 +925,7 @@ impl Configuration { = serde_yaml::from_str(&project_config_text) .map_err(|error| ConfigurationError::ProjectConfigParseError { path: project_config_path.clone(), - message: error.to_string(), + message: error.to_string().into(), })?; intermediate_project_config = Partial::Value(project_config); diff --git a/packages/zpm/src/error.rs b/packages/zpm/src/error.rs index 5d4ecd20..0dbde5c9 100644 --- a/packages/zpm/src/error.rs +++ b/packages/zpm/src/error.rs @@ -1,7 +1,7 @@ use std::{future::Future, sync::Arc}; use zpm_primitives::{Descriptor, Ident, Locator, Range}; -use zpm_utils::{DataType, Path, ToHumanString}; +use zpm_utils::{DataType, EcoString, Path, ToHumanString}; use tokio::task::JoinError; fn render_backtrace(backtrace: &std::backtrace::Backtrace) -> String { @@ -39,8 +39,8 @@ fn render_remote_manifest_reason(raw_reason: &str) -> String { pub fn remote_manifest_parse_error( locator: &Locator, - origin: impl Into, - path: impl Into, + origin: impl Into, + path: impl Into, parser_error: zpm_parsers::Error, ) -> Error { let raw_reason = match &parser_error { @@ -52,7 +52,7 @@ pub fn remote_manifest_parse_error( locator: locator.clone(), origin: origin.into(), path: path.into(), - reason: render_remote_manifest_reason(&raw_reason), + reason: render_remote_manifest_reason(&raw_reason).into(), } } @@ -191,9 +191,9 @@ pub enum Error { #[error("Invalid package metadata in {path} ({origin}): {reason}")] RemoteManifestParseError { locator: Locator, - origin: String, - path: String, - reason: String, + origin: EcoString, + path: EcoString, + reason: EcoString, }, #[error("Semver error ({0})")] @@ -220,7 +220,7 @@ pub enum Error { #[error("Package manifest failed to parse ({}): {reason}", path.to_print_string())] ManifestParseError { path: Path, - reason: String, + reason: EcoString, }, #[error("Invalid descriptor ({0})")] @@ -307,7 +307,7 @@ pub enum Error { #[error("An error occured while parsing the lockfile ({}): {reason}", path.to_print_string())] LockfileParseError { path: Path, - reason: String, + reason: EcoString, }, #[error("Can't perform this operation without a git root")] @@ -322,7 +322,7 @@ pub enum Error { #[error("An error occured while parsing the Yarn Berry lockfile ({}): {reason}", path.to_print_string())] LegacyLockfileParseError { path: Path, - reason: String, + reason: EcoString, }, #[error("Failed to read pnpm node_modules directory")] diff --git a/packages/zpm/src/lockfile.rs b/packages/zpm/src/lockfile.rs index c32dc9b8..b77a6e78 100644 --- a/packages/zpm/src/lockfile.rs +++ b/packages/zpm/src/lockfile.rs @@ -257,7 +257,7 @@ pub fn from_legacy_berry_lockfile(data: &str, lockfile_path: &Path) -> Result Result Date: Wed, 11 Feb 2026 00:53:37 +0800 Subject: [PATCH 5/5] Fix remote manifest aggregate counts to use unique locators --- packages/zpm/src/report.rs | 83 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 3 deletions(-) diff --git a/packages/zpm/src/report.rs b/packages/zpm/src/report.rs index 1a1c4ebb..c80c6198 100644 --- a/packages/zpm/src/report.rs +++ b/packages/zpm/src/report.rs @@ -181,7 +181,6 @@ pub enum ReportMessage { #[derive(Debug, Default)] struct RemoteManifestErrorAggregate { - count: u32, locators: BTreeSet, } @@ -330,7 +329,13 @@ impl Reporter { let mut entries = self.remote_manifest_errors .iter() .map(|((origin, path, reason), aggregate)| { - (origin.clone(), path.clone(), reason.clone(), aggregate.count, aggregate.locators.clone()) + ( + origin.clone(), + path.clone(), + reason.clone(), + aggregate.locators.len() as u32, + aggregate.locators.clone(), + ) }) .collect::>(); @@ -394,7 +399,6 @@ impl Reporter { .entry((origin, path, reason)) .or_default(); - aggregate.count += 1; aggregate.locators.insert(locator); } @@ -680,3 +684,76 @@ impl StreamReport { self.handle.join().unwrap(); } } + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use super::{ReportCounters, Reporter, StreamReportConfig}; + + fn new_reporter() -> Reporter { + let (prompt_tx, _prompt_rx) = std::sync::mpsc::channel(); + + Reporter::new( + StreamReportConfig { + enable_progress_bars: false, + enable_timers: false, + include_version: false, + silent_or_error: false, + }, + Arc::new(ReportCounters::default()), + prompt_tx, + ) + } + + #[test] + fn remote_manifest_summary_counts_unique_locators() { + let mut reporter = new_reporter(); + + reporter.on_remote_manifest_parse( + "sass-embedded-linux-x64@npm:1.97.3".to_string(), + "npm registry metadata".to_string(), + "package.json".to_string(), + "type mismatch: expected a sequence, got string \"glibc\" (line 1 column 1672)".to_string(), + ); + reporter.on_remote_manifest_parse( + "sass-embedded-linux-x64@npm:1.97.3".to_string(), + "npm registry metadata".to_string(), + "package.json".to_string(), + "type mismatch: expected a sequence, got string \"glibc\" (line 1 column 1672)".to_string(), + ); + + let mut output = Vec::new(); + reporter.on_end(&mut output); + let output = String::from_utf8(output).expect("report output should be valid utf8"); + + assert!(output.contains("Invalid package metadata in package.json (npm registry metadata): type mismatch: expected a sequence, got string \"glibc\" (line 1 column 1672)")); + assert!(!output.contains("2 packages have invalid package metadata")); + } + + #[test] + fn remote_manifest_hidden_count_uses_unique_locator_count() { + let mut reporter = new_reporter(); + + for locator in [ + "sass-embedded-linux-x64@npm:1.97.3", + "sass-embedded-linux-x64@npm:1.97.3", + "sass-embedded-linux-arm64@npm:1.97.3", + "sass-embedded-linux-arm64@npm:1.97.3", + ] { + reporter.on_remote_manifest_parse( + locator.to_string(), + "npm registry metadata".to_string(), + "package.json".to_string(), + "type mismatch: expected a sequence, got string \"glibc\" (line 1 column 1672)".to_string(), + ); + } + + let mut output = Vec::new(); + reporter.on_end(&mut output); + let output = String::from_utf8(output).expect("report output should be valid utf8"); + + assert!(output.contains("2 packages have invalid package metadata in package.json (npm registry metadata):")); + assert!(!output.contains("(+2 more)")); + } +}