Skip to content

Commit

Permalink
fix(prune): fix Yarn1 entries getting merged erroneously (#9627)
Browse files Browse the repository at this point in the history
### 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.
  • Loading branch information
chris-olszewski authored Jan 2, 2025
1 parent c8d4946 commit 02ac381
Show file tree
Hide file tree
Showing 3 changed files with 187 additions and 13 deletions.
95 changes: 95 additions & 0 deletions crates/turborepo-lockfiles/fixtures/gh_8849.lock
Original file line number Diff line number Diff line change
@@ -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:[email protected]":
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"

[email protected]:
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"

[email protected]:
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==

[email protected]:
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==

[email protected]:
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==

[email protected]:
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==

[email protected]:
version "2.0.9"
resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-2.0.9.tgz#d52835302e722cc7de670b90aca55ce2b3516879"
integrity sha512-ZVo0apxUvaRq4Vm1qhsfqKKhtRgReYlBVf9MQvVU1O9AoyydEQvLDO1ryqpXDZWpcHoFxHAQc9msjAMtE5K2lA==

[email protected]:
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==
2 changes: 2 additions & 0 deletions crates/turborepo-lockfiles/src/yarn1/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
103 changes: 90 additions & 13 deletions crates/turborepo-lockfiles/src/yarn1/ser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand All @@ -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::<Vec<_>>();
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::<Vec<_>>()
}
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();

Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 02ac381

Please sign in to comment.