diff --git a/crates/turborepo-lockfiles/fixtures/gh_8849.lock b/crates/turborepo-lockfiles/fixtures/gh_8849.lock new file mode 100644 index 0000000000000..9fde66b81238e --- /dev/null +++ b/crates/turborepo-lockfiles/fixtures/gh_8849.lock @@ -0,0 +1,95 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +prettier@^3.2.5: + version "3.3.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105" + integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew== + +"string-width-cjs@npm:string-width@4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" + integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + +string-width@4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" + integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + +strip-ansi@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +turbo-darwin-64@2.0.9: + version "2.0.9" + resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-2.0.9.tgz#dc7bb92060a41b92155195dba5850c9669fa765a" + integrity sha512-owlGsOaExuVGBUfrnJwjkL1BWlvefjSKczEAcpLx4BI7Oh6ttakOi+JyomkPkFlYElRpjbvlR2gP8WIn6M/+xQ== + +turbo-darwin-arm64@2.0.9: + version "2.0.9" + resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-2.0.9.tgz#6e5ce2c0f03999c6ec0116d5525841107da3078b" + integrity sha512-XAXkKkePth5ZPPE/9G9tTnPQx0C8UTkGWmNGYkpmGgRr8NedW+HrPsi9N0HcjzzIH9A4TpNYvtiV+WcwdaEjKA== + +turbo-linux-64@2.0.9: + version "2.0.9" + resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-2.0.9.tgz#e00e5e1b1cffab23c58888e7c397e108dc24fe2f" + integrity sha512-l9wSgEjrCFM1aG16zItBsZ206ZlhSSx1owB8Cgskfv0XyIXRGHRkluihiaxkp+UeU5WoEfz4EN5toc+ICA0q0w== + +turbo-linux-arm64@2.0.9: + version "2.0.9" + resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-2.0.9.tgz#d240e4f0a784d03f1a79fd9e6c4e83abd9aa57c7" + integrity sha512-gRnjxXRne18B27SwxXMqL3fJu7jw/8kBrOBTBNRSmZZiG1Uu3nbnP7b4lgrA/bCku6C0Wligwqurvtpq6+nFHA== + +turbo-windows-64@2.0.9: + version "2.0.9" + resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-2.0.9.tgz#d52835302e722cc7de670b90aca55ce2b3516879" + integrity sha512-ZVo0apxUvaRq4Vm1qhsfqKKhtRgReYlBVf9MQvVU1O9AoyydEQvLDO1ryqpXDZWpcHoFxHAQc9msjAMtE5K2lA== + +turbo-windows-arm64@2.0.9: + version "2.0.9" + resolved "https://registry.yarnpkg.com/turbo-windows-arm64/-/turbo-windows-arm64-2.0.9.tgz#45f0aa685514ec1cc753a559924e003b22b24bb7" + integrity sha512-sGRz7c5Pey6y7y9OKi8ypbWNuIRPF9y8xcMqL56OZifSUSo+X2EOsOleR9MKxQXVaqHPGOUKWsE6y8hxBi9pag== + +turbo@^2.0.9: + version "2.0.9" + resolved "https://registry.yarnpkg.com/turbo/-/turbo-2.0.9.tgz#fa0ab576c4cb9a8fc9db648e9ac9adfe10a22ae5" + integrity sha512-QaLaUL1CqblSKKPgLrFW3lZWkWG4pGBQNW+q1ScJB5v1D/nFWtsrD/yZljW/bdawg90ihi4/ftQJ3h6fz1FamA== + optionalDependencies: + turbo-darwin-64 "2.0.9" + turbo-darwin-arm64 "2.0.9" + turbo-linux-64 "2.0.9" + turbo-linux-arm64 "2.0.9" + turbo-windows-64 "2.0.9" + turbo-windows-arm64 "2.0.9" + +typescript@^5.4.5: + version "5.5.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba" + integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== diff --git a/crates/turborepo-lockfiles/src/yarn1/mod.rs b/crates/turborepo-lockfiles/src/yarn1/mod.rs index f93f1c6d837c4..ff7f6c2152dbc 100644 --- a/crates/turborepo-lockfiles/src/yarn1/mod.rs +++ b/crates/turborepo-lockfiles/src/yarn1/mod.rs @@ -163,9 +163,11 @@ mod test { const MINIMAL: &str = include_str!("../../fixtures/yarn1.lock"); const FULL: &str = include_str!("../../fixtures/yarn1full.lock"); + const GH_8849: &str = include_str!("../../fixtures/gh_8849.lock"); #[test_case(MINIMAL ; "minimal lockfile")] #[test_case(FULL ; "full lockfile")] + #[test_case(GH_8849 ; "gh 8849")] fn test_roundtrip(input: &str) { let lockfile = Yarn1Lockfile::from_str(input).unwrap(); assert_eq!(input, lockfile.to_string()); diff --git a/crates/turborepo-lockfiles/src/yarn1/ser.rs b/crates/turborepo-lockfiles/src/yarn1/ser.rs index 57f6be81fe4ac..377ffa67d9128 100644 --- a/crates/turborepo-lockfiles/src/yarn1/ser.rs +++ b/crates/turborepo-lockfiles/src/yarn1/ser.rs @@ -8,14 +8,32 @@ use super::{Entry, Yarn1Lockfile}; const INDENT: &str = " "; +fn reverse_seen_keys<'a>( + seen_keys: &'a HashMap<&'a str, String>, +) -> HashMap<&'a str, HashSet<&'a str>> { + let mut reverse_lookup = HashMap::new(); + for (key, value) in seen_keys.iter() { + let keys: &mut HashSet<&str> = reverse_lookup.entry(value.as_str()).or_default(); + keys.insert(key); + } + reverse_lookup +} + impl Yarn1Lockfile { - fn reverse_lookup(&self) -> HashMap<&Entry, HashSet<&str>> { - let mut reverse_lookup = HashMap::new(); - for (key, value) in self.inner.iter() { - let keys: &mut HashSet<&str> = reverse_lookup.entry(value).or_default(); - keys.insert(key); + // Map from keys to seen keys + // A "seen key" just entry.resolved with the key's package name appended to it + // See https://github.com/yarnpkg/yarn/pull/9023/ + fn seen_keys(&self) -> HashMap<&str, String> { + let mut seen_keys = HashMap::new(); + for (key, entry) in &self.inner { + let Some(resolved) = entry.resolved.as_deref() else { + continue; + }; + let pkg_name = Pattern::new(key).name; + let seen_key = format!("{resolved}#{pkg_name}"); + seen_keys.insert(key.as_str(), seen_key); } - reverse_lookup + seen_keys } } @@ -25,18 +43,31 @@ impl fmt::Display for Yarn1Lockfile { "# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.\n# yarn lockfile \ v1\n\n", )?; - let reverse_lookup = self.reverse_lookup(); + let seen_keys = self.seen_keys(); + // A map from seen_keys to keys + let reverse_lookup = reverse_seen_keys(&seen_keys); let mut added_keys: HashSet<&str> = HashSet::with_capacity(self.inner.len()); for (key, entry) in self.inner.iter() { - if added_keys.contains(key.as_str()) { + let seen_key = seen_keys.get(key.as_str()); + let seen_pattern = seen_key.map_or(false, |key| added_keys.contains(key.as_str())); + if seen_pattern { continue; } - let all_keys = reverse_lookup - .get(entry) - .expect("entry in lockfile should appear as a key in reverse lookup"); - added_keys.extend(all_keys); - let mut keys = all_keys.iter().copied().collect::>(); + let mut keys = match seen_key { + Some(seen_key) => { + added_keys.insert(seen_key); + let all_keys = reverse_lookup + .get(seen_key.as_str()) + .expect("entry in lockfile should appear as a key in reverse lookup"); + all_keys.iter().copied().collect::>() + } + None => { + // If there isn't a seen key, then there won't be any merged entries so we can + // just add the key as is + vec![key.as_str()] + } + }; // Keys must be sorted before they get wrapped keys.sort(); @@ -111,6 +142,52 @@ impl fmt::Display for Entry { } } +#[allow(dead_code)] +struct Pattern { + name: String, + range: String, + has_version: bool, +} + +impl Pattern { + // This is an exact port of JS code. It is intentionally keeps JS-isms to make + // patching easier in the future https://github.com/yarnpkg/yarn/blob/3c3ef8278121c0598c61caf8023d9bb2af888152/src/util/normalize-pattern.js + fn new(pattern: &str) -> Self { + let mut name = pattern; + let mut range = "latest".to_owned(); + let mut has_version = false; + let mut is_scoped = false; + if name.starts_with('@') { + is_scoped = true; + name = &name[1..]; + } + + let mut parts: Vec<_> = name.split('@').collect(); + if parts.len() > 1 { + name = parts.remove(0); + range = parts.join("@"); + + if !range.is_empty() { + has_version = true; + } else { + range = "*".to_owned(); + } + } + + let name = if is_scoped { + format!("@{name}") + } else { + name.to_owned() + }; + + Self { + name, + range, + has_version, + } + } +} + #[derive(Debug, Clone, Copy)] enum LeadingNewline { First,