Skip to content

Commit 02ac381

Browse files
fix(prune): fix Yarn1 entries getting merged erroneously (#9627)
### Description Fixes #8849 This is a port of yarnpkg/yarn#9023 our codebase. Previously, Yarn would collapse all identical entries to share a slot in the lockfile with the keys joined by `,` e.g. `next@latest, [email protected]`. We copied that logic which resulted in `string-width-cjs@npm:[email protected]` and `[email protected]` getting collapsed as they have identical data: ``` 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" ``` This behavior was altered in the PR linked above where now it will no longer group keys for identical entries if they have different package names in the keys. So since `string-width-cjs != string-width` these will have separate slots. This PR intentionally isn't super Rust-y so we're able to better update our behavior to match Yarn in case we need to update. ### Testing Instructions Added a unit test from reproduction to ensure that we no longer merge the problematic entries.
1 parent c8d4946 commit 02ac381

File tree

3 files changed

+187
-13
lines changed

3 files changed

+187
-13
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2+
# yarn lockfile v1
3+
4+
5+
ansi-regex@^5.0.1:
6+
version "5.0.1"
7+
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
8+
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
9+
10+
emoji-regex@^8.0.0:
11+
version "8.0.0"
12+
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
13+
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
14+
15+
is-fullwidth-code-point@^3.0.0:
16+
version "3.0.0"
17+
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
18+
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
19+
20+
prettier@^3.2.5:
21+
version "3.3.3"
22+
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105"
23+
integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==
24+
25+
"string-width-cjs@npm:[email protected]":
26+
version "4.2.0"
27+
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5"
28+
integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==
29+
dependencies:
30+
emoji-regex "^8.0.0"
31+
is-fullwidth-code-point "^3.0.0"
32+
strip-ansi "^6.0.0"
33+
34+
35+
version "4.2.0"
36+
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5"
37+
integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==
38+
dependencies:
39+
emoji-regex "^8.0.0"
40+
is-fullwidth-code-point "^3.0.0"
41+
strip-ansi "^6.0.0"
42+
43+
strip-ansi@^6.0.0:
44+
version "6.0.1"
45+
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
46+
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
47+
dependencies:
48+
ansi-regex "^5.0.1"
49+
50+
51+
version "2.0.9"
52+
resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-2.0.9.tgz#dc7bb92060a41b92155195dba5850c9669fa765a"
53+
integrity sha512-owlGsOaExuVGBUfrnJwjkL1BWlvefjSKczEAcpLx4BI7Oh6ttakOi+JyomkPkFlYElRpjbvlR2gP8WIn6M/+xQ==
54+
55+
56+
version "2.0.9"
57+
resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-2.0.9.tgz#6e5ce2c0f03999c6ec0116d5525841107da3078b"
58+
integrity sha512-XAXkKkePth5ZPPE/9G9tTnPQx0C8UTkGWmNGYkpmGgRr8NedW+HrPsi9N0HcjzzIH9A4TpNYvtiV+WcwdaEjKA==
59+
60+
61+
version "2.0.9"
62+
resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-2.0.9.tgz#e00e5e1b1cffab23c58888e7c397e108dc24fe2f"
63+
integrity sha512-l9wSgEjrCFM1aG16zItBsZ206ZlhSSx1owB8Cgskfv0XyIXRGHRkluihiaxkp+UeU5WoEfz4EN5toc+ICA0q0w==
64+
65+
66+
version "2.0.9"
67+
resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-2.0.9.tgz#d240e4f0a784d03f1a79fd9e6c4e83abd9aa57c7"
68+
integrity sha512-gRnjxXRne18B27SwxXMqL3fJu7jw/8kBrOBTBNRSmZZiG1Uu3nbnP7b4lgrA/bCku6C0Wligwqurvtpq6+nFHA==
69+
70+
71+
version "2.0.9"
72+
resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-2.0.9.tgz#d52835302e722cc7de670b90aca55ce2b3516879"
73+
integrity sha512-ZVo0apxUvaRq4Vm1qhsfqKKhtRgReYlBVf9MQvVU1O9AoyydEQvLDO1ryqpXDZWpcHoFxHAQc9msjAMtE5K2lA==
74+
75+
76+
version "2.0.9"
77+
resolved "https://registry.yarnpkg.com/turbo-windows-arm64/-/turbo-windows-arm64-2.0.9.tgz#45f0aa685514ec1cc753a559924e003b22b24bb7"
78+
integrity sha512-sGRz7c5Pey6y7y9OKi8ypbWNuIRPF9y8xcMqL56OZifSUSo+X2EOsOleR9MKxQXVaqHPGOUKWsE6y8hxBi9pag==
79+
80+
turbo@^2.0.9:
81+
version "2.0.9"
82+
resolved "https://registry.yarnpkg.com/turbo/-/turbo-2.0.9.tgz#fa0ab576c4cb9a8fc9db648e9ac9adfe10a22ae5"
83+
integrity sha512-QaLaUL1CqblSKKPgLrFW3lZWkWG4pGBQNW+q1ScJB5v1D/nFWtsrD/yZljW/bdawg90ihi4/ftQJ3h6fz1FamA==
84+
optionalDependencies:
85+
turbo-darwin-64 "2.0.9"
86+
turbo-darwin-arm64 "2.0.9"
87+
turbo-linux-64 "2.0.9"
88+
turbo-linux-arm64 "2.0.9"
89+
turbo-windows-64 "2.0.9"
90+
turbo-windows-arm64 "2.0.9"
91+
92+
typescript@^5.4.5:
93+
version "5.5.4"
94+
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba"
95+
integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==

crates/turborepo-lockfiles/src/yarn1/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -163,9 +163,11 @@ mod test {
163163

164164
const MINIMAL: &str = include_str!("../../fixtures/yarn1.lock");
165165
const FULL: &str = include_str!("../../fixtures/yarn1full.lock");
166+
const GH_8849: &str = include_str!("../../fixtures/gh_8849.lock");
166167

167168
#[test_case(MINIMAL ; "minimal lockfile")]
168169
#[test_case(FULL ; "full lockfile")]
170+
#[test_case(GH_8849 ; "gh 8849")]
169171
fn test_roundtrip(input: &str) {
170172
let lockfile = Yarn1Lockfile::from_str(input).unwrap();
171173
assert_eq!(input, lockfile.to_string());

crates/turborepo-lockfiles/src/yarn1/ser.rs

+90-13
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,32 @@ use super::{Entry, Yarn1Lockfile};
88

99
const INDENT: &str = " ";
1010

11+
fn reverse_seen_keys<'a>(
12+
seen_keys: &'a HashMap<&'a str, String>,
13+
) -> HashMap<&'a str, HashSet<&'a str>> {
14+
let mut reverse_lookup = HashMap::new();
15+
for (key, value) in seen_keys.iter() {
16+
let keys: &mut HashSet<&str> = reverse_lookup.entry(value.as_str()).or_default();
17+
keys.insert(key);
18+
}
19+
reverse_lookup
20+
}
21+
1122
impl Yarn1Lockfile {
12-
fn reverse_lookup(&self) -> HashMap<&Entry, HashSet<&str>> {
13-
let mut reverse_lookup = HashMap::new();
14-
for (key, value) in self.inner.iter() {
15-
let keys: &mut HashSet<&str> = reverse_lookup.entry(value).or_default();
16-
keys.insert(key);
23+
// Map from keys to seen keys
24+
// A "seen key" just entry.resolved with the key's package name appended to it
25+
// See https://github.com/yarnpkg/yarn/pull/9023/
26+
fn seen_keys(&self) -> HashMap<&str, String> {
27+
let mut seen_keys = HashMap::new();
28+
for (key, entry) in &self.inner {
29+
let Some(resolved) = entry.resolved.as_deref() else {
30+
continue;
31+
};
32+
let pkg_name = Pattern::new(key).name;
33+
let seen_key = format!("{resolved}#{pkg_name}");
34+
seen_keys.insert(key.as_str(), seen_key);
1735
}
18-
reverse_lookup
36+
seen_keys
1937
}
2038
}
2139

@@ -25,18 +43,31 @@ impl fmt::Display for Yarn1Lockfile {
2543
"# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.\n# yarn lockfile \
2644
v1\n\n",
2745
)?;
28-
let reverse_lookup = self.reverse_lookup();
46+
let seen_keys = self.seen_keys();
47+
// A map from seen_keys to keys
48+
let reverse_lookup = reverse_seen_keys(&seen_keys);
2949
let mut added_keys: HashSet<&str> = HashSet::with_capacity(self.inner.len());
3050
for (key, entry) in self.inner.iter() {
31-
if added_keys.contains(key.as_str()) {
51+
let seen_key = seen_keys.get(key.as_str());
52+
let seen_pattern = seen_key.map_or(false, |key| added_keys.contains(key.as_str()));
53+
if seen_pattern {
3254
continue;
3355
}
3456

35-
let all_keys = reverse_lookup
36-
.get(entry)
37-
.expect("entry in lockfile should appear as a key in reverse lookup");
38-
added_keys.extend(all_keys);
39-
let mut keys = all_keys.iter().copied().collect::<Vec<_>>();
57+
let mut keys = match seen_key {
58+
Some(seen_key) => {
59+
added_keys.insert(seen_key);
60+
let all_keys = reverse_lookup
61+
.get(seen_key.as_str())
62+
.expect("entry in lockfile should appear as a key in reverse lookup");
63+
all_keys.iter().copied().collect::<Vec<_>>()
64+
}
65+
None => {
66+
// If there isn't a seen key, then there won't be any merged entries so we can
67+
// just add the key as is
68+
vec![key.as_str()]
69+
}
70+
};
4071
// Keys must be sorted before they get wrapped
4172
keys.sort();
4273

@@ -111,6 +142,52 @@ impl fmt::Display for Entry {
111142
}
112143
}
113144

145+
#[allow(dead_code)]
146+
struct Pattern {
147+
name: String,
148+
range: String,
149+
has_version: bool,
150+
}
151+
152+
impl Pattern {
153+
// This is an exact port of JS code. It is intentionally keeps JS-isms to make
154+
// patching easier in the future https://github.com/yarnpkg/yarn/blob/3c3ef8278121c0598c61caf8023d9bb2af888152/src/util/normalize-pattern.js
155+
fn new(pattern: &str) -> Self {
156+
let mut name = pattern;
157+
let mut range = "latest".to_owned();
158+
let mut has_version = false;
159+
let mut is_scoped = false;
160+
if name.starts_with('@') {
161+
is_scoped = true;
162+
name = &name[1..];
163+
}
164+
165+
let mut parts: Vec<_> = name.split('@').collect();
166+
if parts.len() > 1 {
167+
name = parts.remove(0);
168+
range = parts.join("@");
169+
170+
if !range.is_empty() {
171+
has_version = true;
172+
} else {
173+
range = "*".to_owned();
174+
}
175+
}
176+
177+
let name = if is_scoped {
178+
format!("@{name}")
179+
} else {
180+
name.to_owned()
181+
};
182+
183+
Self {
184+
name,
185+
range,
186+
has_version,
187+
}
188+
}
189+
}
190+
114191
#[derive(Debug, Clone, Copy)]
115192
enum LeadingNewline {
116193
First,

0 commit comments

Comments
 (0)