Skip to content

Commit f68b18b

Browse files
Fix yarnrc compressionLevel comment refusal (#370) (#508)
* Start fix for #370 Assisted-by: Claude Code:claude-opus-5-5 * Read yarnrc compressionLevel past YAML comments A .yarnrc.yml line like `compressionLevel: 0 # keep yarn default` is the yarn default (cacheKey 10c0), but socket-patch read the comment as part of the value and refused the project in both vendored and hosted mode. The reader now parses the value as a YAML scalar: a quoted value ends at its closing quote and a plain value ends before a whitespace-separated `#`. Anything not positively `0` still refuses. Fixes #370 Assisted-by: Claude Code:claude-opus-5-5 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d63ae5f commit f68b18b

3 files changed

Lines changed: 89 additions & 6 deletions

File tree

‎crates/socket-patch-cli/tests/e2e_vendor_yarn_berry_build.rs‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,20 @@ fn copy_dir_recursive(src: &Path, dst: &Path) {
265265

266266
#[tokio::test(flavor = "multi_thread")]
267267
async fn yarn_berry_vendor_fresh_checkout_immutable_check_cache_and_revert() {
268-
run_berry_capstone(VendorDriver::VendorCli).await;
268+
run_berry_capstone(VendorDriver::VendorCli, "").await;
269+
}
270+
271+
/// #370: `compressionLevel: 0` with a trailing YAML comment is the default
272+
/// to yarn (cacheKey `10c0`), so vendor must not refuse it as a
273+
/// checksum-changing level. The whole capstone runs against that
274+
/// `.yarnrc.yml`, fresh-checkout `--immutable` proof included.
275+
#[tokio::test(flavor = "multi_thread")]
276+
async fn yarn_berry_vendor_commented_default_compression_level() {
277+
run_berry_capstone(
278+
VendorDriver::VendorCli,
279+
"compressionLevel: 0 # keep yarn default\n",
280+
)
281+
.await;
269282
}
270283

271284
/// get-driven twin (v3.6): `get <uuid> --mode vendored` consumes the SAME
@@ -277,10 +290,11 @@ async fn yarn_berry_vendor_fresh_checkout_immutable_check_cache_and_revert() {
277290
/// the `vendor` front door's contract (the capstone above).
278291
#[tokio::test(flavor = "multi_thread")]
279292
async fn berry_get_uuid_vendored_fresh_checkout_immutable() {
280-
run_berry_capstone(VendorDriver::GetUuid).await;
293+
run_berry_capstone(VendorDriver::GetUuid, "").await;
281294
}
282295

283-
async fn run_berry_capstone(driver: VendorDriver) {
296+
/// `yarnrc_extra` is appended to the capstone's `.yarnrc.yml`.
297+
async fn run_berry_capstone(driver: VendorDriver, yarnrc_extra: &str) {
284298
if !has_corepack_pm(yarn_berry()) {
285299
skip!(
286300
"SKIP e2e_vendor_yarn_berry_build ({driver:?}): `corepack {}` unavailable \
@@ -304,7 +318,7 @@ async fn run_berry_capstone(driver: VendorDriver) {
304318
// (the only checksum recipe vendor reproduces offline — spike B4).
305319
std::fs::write(
306320
proj.join(".yarnrc.yml"),
307-
"nodeLinker: node-modules\nenableGlobalCache: false\n",
321+
format!("nodeLinker: node-modules\nenableGlobalCache: false\n{yarnrc_extra}"),
308322
)
309323
.unwrap();
310324

‎crates/socket-patch-core/src/patch/redirect/mod.rs‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14450,6 +14450,15 @@ packages:
1445014450
Err("redirect_yarn_berry_cache_unsupported".to_string())
1445114451
);
1445214452
assert_eq!(code(&crlf, Some("compressionLevel: 0\n")), Ok(()));
14453+
// #370: a trailing YAML comment is not part of the value.
14454+
assert_eq!(
14455+
code(&crlf, Some("compressionLevel: 0 # keep yarn default\n")),
14456+
Ok(())
14457+
);
14458+
assert_eq!(
14459+
code(&crlf, Some("compressionLevel: mixed # smaller cache\n")),
14460+
Err("redirect_yarn_berry_cache_unsupported".to_string())
14461+
);
1445314462
}
1445414463

1445514464
/// The whole-file gates read the NORMALIZED lock: a CRLF lock at an

‎crates/socket-patch-core/src/vendor/yarn_berry_lock.rs‎

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1348,11 +1348,25 @@ fn root_workspace_name(blocks: &[LockBlock]) -> Option<String> {
13481348
/// skipped the way yarn's YAML parser skips it — otherwise a knob on the
13491349
/// first line of a BOM'd file would read as unset (the offline-reproducible
13501350
/// default) while yarn applies it and every install fails YN0018.
1351+
///
1352+
/// The value is read as a YAML scalar: a quoted value ends at its closing
1353+
/// quote, and a plain value ends before a whitespace-separated `#` comment
1354+
/// (`compressionLevel: 0 # keep yarn default` is `0`, #370). A `#` with no
1355+
/// whitespace before it stays part of a plain value, as in YAML.
13511356
pub(crate) fn yarnrc_compression_level(rc: &str) -> Option<&str> {
13521357
let rc = rc.strip_prefix('\u{feff}').unwrap_or(rc);
13531358
rc.lines().find_map(|line| {
1354-
let rest = line.strip_prefix("compressionLevel:")?;
1355-
Some(rest.trim().trim_matches(['\'', '"']))
1359+
let rest = line.strip_prefix("compressionLevel:")?.trim();
1360+
if let Some(quote) = rest.chars().next().filter(|c| matches!(c, '\'' | '"')) {
1361+
if let Some(end) = rest[1..].find(quote) {
1362+
return Some(&rest[1..1 + end]);
1363+
}
1364+
}
1365+
let value = rest
1366+
.char_indices()
1367+
.find(|&(i, c)| c == '#' && rest[..i].ends_with([' ', '\t']))
1368+
.map_or(rest, |(i, _)| &rest[..i]);
1369+
Some(value.trim_end().trim_matches(['\'', '"']))
13561370
})
13571371
}
13581372

@@ -1858,6 +1872,21 @@ __metadata:
18581872
assert!(result.success, "{:?}", result.error);
18591873
}
18601874

1875+
/// #370: `compressionLevel: 0` with a trailing YAML comment is the
1876+
/// default yarn reads as `0`, so vendoring proceeds rather than refusing.
1877+
#[tokio::test]
1878+
async fn commented_default_compression_level_vendors() {
1879+
let fx = fixture().await;
1880+
tokio::fs::write(
1881+
fx.root().join(YARNRC),
1882+
"nodeLinker: node-modules\ncompressionLevel: 0 # keep yarn default\n",
1883+
)
1884+
.await
1885+
.unwrap();
1886+
let (result, _, _) = expect_done(fx.vendor(false).await);
1887+
assert!(result.success, "{:?}", result.error);
1888+
}
1889+
18611890
#[tokio::test]
18621891
async fn user_resolutions_entry_is_refused_never_overwritten() {
18631892
let pkg = B3_BEFORE_PKG.replace(
@@ -3963,6 +3992,37 @@ __metadata:
39633992
);
39643993
}
39653994

3995+
/// A trailing YAML comment is not part of the scalar (#370): yarn reads
3996+
/// `compressionLevel: 0 # keep yarn default` as `0`, quoted or not.
3997+
#[test]
3998+
fn yarnrc_compression_level_drops_a_trailing_comment() {
3999+
for (rc, level) in [
4000+
("compressionLevel: 0 # keep yarn default\n", "0"),
4001+
("compressionLevel: 0\t# tab-separated\r\n", "0"),
4002+
("compressionLevel: 0 #\n", "0"),
4003+
("compressionLevel: \"0\" # quoted\n", "0"),
4004+
("compressionLevel: '0'# quoted, no gap\n", "0"),
4005+
("compressionLevel: mixed # not the default\n", "mixed"),
4006+
("compressionLevel: 9 #\r\n", "9"),
4007+
] {
4008+
assert_eq!(yarnrc_compression_level(rc), Some(level), "{rc:?}");
4009+
}
4010+
}
4011+
4012+
/// A `#` with no whitespace before it is part of a plain scalar in YAML,
4013+
/// so `0#x` is not the default and must still refuse (fail closed).
4014+
#[test]
4015+
fn yarnrc_compression_level_keeps_an_unseparated_hash() {
4016+
assert_eq!(
4017+
yarnrc_compression_level("compressionLevel: 0#x\n"),
4018+
Some("0#x")
4019+
);
4020+
assert_eq!(
4021+
yarnrc_compression_level("compressionLevel: \"0 # in quotes\"\n"),
4022+
Some("0 # in quotes")
4023+
);
4024+
}
4025+
39664026
/// yarn 4.0.x spells `10c0` checksums bare, 4.1+ prefixed: a written
39674027
/// entry follows the lock (an `--immutable` install rejects a respelled
39684028
/// checksum with YN0028). A lock with no checksum keeps the prefix.

0 commit comments

Comments
 (0)