diff --git a/vendor/aube/crates/aube-manifest/src/lib.rs b/vendor/aube/crates/aube-manifest/src/lib.rs index 55864808c..f964daf45 100644 --- a/vendor/aube/crates/aube-manifest/src/lib.rs +++ b/vendor/aube/crates/aube-manifest/src/lib.rs @@ -388,7 +388,22 @@ impl PackageJson { /// [`Error::Parse`] with the source content and a span so `miette`'s /// `fancy` handler renders a pointer at the offending byte. pub fn parse(path: &Path, content: String) -> Result { - parse_json(path, content) + match parse_json(path, content.clone()) { + Ok(manifest) => Ok(manifest), + Err(original) => { + // `JSON.parse`, npm and pnpm accept duplicate object keys and keep the last value. + // Serde's struct deserializer rejects duplicate known fields before it can apply + // that de-facto package.json behavior. Preserve the fast typed parse above for the + // common case; only normalize through a generic JSON object after it fails. + let normalized = content.strip_prefix('\u{FEFF}').unwrap_or(&content); + if let Ok(value) = serde_json::from_str::(normalized) + && let Ok(manifest) = serde_json::from_value(value) + { + return Ok(manifest); + } + Err(original) + } + } } /// True when `peerDependenciesMeta..optional` is set. @@ -1458,6 +1473,34 @@ mod tests { serde_json::from_str(json).unwrap() } + #[test] + fn package_json_duplicate_fields_keep_the_last_value() { + let manifest = PackageJson::parse( + Path::new("package.json"), + r#"{ + "name": "first", + "scripts": {"install": "old"}, + "dependencies": {"left-pad": "1.1.0"}, + "name": "last", + "scripts": {"postinstall": "new"}, + "dependencies": {"left-pad": "1.3.0"} + }"# + .to_string(), + ) + .unwrap(); + + assert_eq!(manifest.name.as_deref(), Some("last")); + assert_eq!(manifest.scripts.len(), 1); + assert_eq!( + manifest.scripts.get("postinstall").map(String::as_str), + Some("new") + ); + assert_eq!( + manifest.dependencies.get("left-pad").map(String::as_str), + Some("1.3.0") + ); + } + /// `npm_package_env` mirrors pnpm's exact flattening: name, version, /// and deep `engines`/`config`/`bin` — and nothing else. #[test]