Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions crates/socket-patch-cli/tests/e2e_vendor_yarn_berry_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1056,3 +1056,90 @@ fn run_revert_after_lock_change(change: LockChange) {
"the reverted project installs the registry bytes"
);
}

/// #783: a user's parent-scoped `resolutions` selector for the patched
/// package (`pkg-a/left-pad`) is an override vendoring must not stack its
/// own bare pin beside. Vendoring used to report success and leave a lock
/// every `yarn install --immutable` rejected (YN0028). It now refuses with
/// `vendor_override_conflict` and writes nothing, so the project still
/// installs immutably.
#[test]
fn issue_783_parent_scoped_resolution_refuses_vendoring() {
let pm = yarn_berry();
if !has_corepack_pm(pm) {
skip!("SKIP e2e_vendor_yarn_berry_build (#783): `corepack {pm}` unavailable");
return;
}
for selector in ["pkg-a/left-pad", "pkg-a/left-pad@^1.3.0"] {
let tmp = tempfile::tempdir().unwrap();
let proj = tmp.path().join("proj");
let global = tmp.path().join("yarn-global");
let env = [("YARN_GLOBAL_FOLDER", global.to_str().unwrap())];
std::fs::create_dir_all(proj.join("packages/pkg-a")).unwrap();
std::fs::write(
proj.join("package.json"),
format!(
r#"{{"name":"root","private":true,"workspaces":["packages/*"],"resolutions":{{"{selector}":"{DEP_VERSION}"}}}}"#
),
)
.unwrap();
std::fs::write(
proj.join("packages/pkg-a/package.json"),
format!(r#"{{"name":"pkg-a","version":"1.0.0","dependencies":{{"{DEP}":"^{DEP_VERSION}"}}}}"#),
)
.unwrap();
std::fs::write(
proj.join(".yarnrc.yml"),
"nodeLinker: node-modules\nenableGlobalCache: false\n",
)
.unwrap();
let out = corepack(&proj, pm, &["install"], &env);
if !out.status.success() {
skip!(
"SKIP e2e_vendor_yarn_berry_build (#783 {selector}): fixture install failed:\n{}",
yarn_berry_common::yarn_output(&out)
);
continue;
}
let pkg_before = std::fs::read(proj.join("package.json")).unwrap();
let lock_before = std::fs::read(proj.join("yarn.lock")).unwrap();

let orig = std::fs::read(proj.join("node_modules").join(DEP).join("index.js")).unwrap();
let patched: Vec<u8> = [MARKER.as_bytes(), orig.as_slice()].concat();
stage_patch(
&proj,
&format!("pkg:npm/{DEP}@{DEP_VERSION}"),
"package/index.js",
&orig,
&patched,
);
let cwd = proj.to_str().unwrap();
let (code, stdout, stderr) =
run_socket(&proj, &["vendor", "--json", "--offline", "--cwd", cwd]);
assert_eq!(
code, 1,
"{selector}: vendor must refuse.\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert!(
stdout.contains("vendor_override_conflict"),
"{selector}: refusal code.\nstdout:\n{stdout}"
);
assert_eq!(
std::fs::read(proj.join("package.json")).unwrap(),
pkg_before,
"{selector}: package.json untouched"
);
assert_eq!(
std::fs::read(proj.join("yarn.lock")).unwrap(),
lock_before,
"{selector}: yarn.lock untouched"
);

let out = corepack(&proj, pm, &["install", "--immutable"], &env);
assert!(
out.status.success(),
"{selector}: the project must still install immutably:\n{}",
yarn_berry_common::yarn_output(&out)
);
}
}
78 changes: 66 additions & 12 deletions crates/socket-patch-core/src/vendor/yarn_berry_lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,8 @@ fn root_workspace_gate(blocks: &[LockBlock]) -> Result<String, Box<VendorOutcome
/// exact version being vendored is TAKEN OVER (its value is rewritten to
/// our spec — the pin already forced this exact version, so semantics are
/// preserved — and recorded as the wiring `original` so revert restores
/// it): `Ok(Some(version))`. Anything else same-name still refuses.
/// it): `Ok(Some(version))`. Anything else overriding the same package,
/// parent-scoped selectors included, still refuses.
fn resolutions_gate(
pkg_obj: &serde_json::Map<String, Value>,
name: &str,
Expand All @@ -604,17 +605,20 @@ fn resolutions_gate(
)));
};
for (selector, value) in res_obj {
let sel_name = split_pattern(selector)
.map(|(n, _)| n)
.unwrap_or(selector.as_str());
if sel_name != name {
// The package the selector overrides, through any parent scope
// (`pkg-a/left-pad`, `**/left-pad`): yarn applies those before
// our bare pin, so they conflict just like a bare entry (#783).
if crate::formats::yarn::patterns::resolution_selector_target(selector) != Some(name) {
continue;
}
// Our own (possibly stale-uuid) entry is fine to overwrite; a
// user-authored override is never clobbered silently.
let ours = value
.as_str()
.is_some_and(|v| parse_vendor_path(v).is_some_and(|p| p.eco == "npm"));
// user-authored override is never clobbered silently. Vendoring
// only ever writes the bare `name` key, so a parent-scoped
// selector pointing into .socket/vendor is not ours.
let ours = selector == name
&& value
.as_str()
.is_some_and(|v| parse_vendor_path(v).is_some_and(|p| p.eco == "npm"));
if ours {
continue;
}
Expand All @@ -626,9 +630,9 @@ fn resolutions_gate(
"vendor_override_conflict",
format!(
"{PACKAGE_JSON} already has a resolutions entry for `{selector}` \
({value}); vendor will not overwrite a user-authored override (an \
exact-version pin `\"{name}\": \"{version}\"` is taken over \
automatically)"
({value}); vendor will not overwrite a user-authored override (only \
a bare exact-version pin `\"{name}\": \"{version}\"` is taken over \
automatically; parent-scoped selectors always refuse)"
),
)));
}
Expand Down Expand Up @@ -2302,6 +2306,56 @@ __metadata:
assert_eq!(tokio::fs::read(fx.pkg_path()).await.unwrap(), fx.pkg_bytes);
}

/// #783: a parent-scoped or glob selector that overrides the target
/// (`pkg-a/left-pad`, `pkg-a/left-pad@^1.3.0`, `<root>/left-pad`,
/// `@scope/parent/left-pad`, `**/left-pad`) is a user-authored override
/// too. Yarn applies it before our bare pin, so vendoring beside it
/// leaves a lock `--immutable` rejects: refuse and write nothing. A
/// selector naming the target only as a PARENT overrides another
/// package and still vendors.
#[tokio::test]
async fn parent_scoped_resolutions_selector_is_refused() {
for selector in [
"pkg-a/left-pad",
"pkg-a/left-pad@^1.3.0",
"vendor-spike/left-pad",
"@scope/parent/left-pad",
"**/left-pad",
] {
let pkg = B3_BEFORE_PKG.replace(
" }\n}",
&format!(" }},\n \"resolutions\": {{\n \"{selector}\": \"1.3.0\"\n }}\n}}"),
);
assert_ne!(pkg, B3_BEFORE_PKG, "the fixture edit must hit");
let fx = fixture_with(&pkg, B3_BEFORE_LOCK).await;
let detail = expect_refused(fx.vendor(false).await, "vendor_override_conflict");
assert!(detail.contains(selector), "{selector}: {detail}");
fx.assert_untouched().await;
}

// Vendoring only writes the bare key, so a parent-scoped selector
// pointing into .socket/vendor (hand-copied, or another uuid) is
// not ours to overwrite: it still refuses.
let pkg = B3_BEFORE_PKG.replace(
" }\n}",
&format!(
" }},\n \"resolutions\": {{\n \"pkg-a/left-pad\": \"file:./.socket/vendor/npm/{UUID}/left-pad-1.3.0.tgz\"\n }}\n}}"
),
);
let fx = fixture_with(&pkg, B3_BEFORE_LOCK).await;
expect_refused(fx.vendor(false).await, "vendor_override_conflict");
fx.assert_untouched().await;

let pkg = B3_BEFORE_PKG.replace(
" }\n}",
" },\n \"resolutions\": {\n \"left-pad/is-odd\": \"1.0.0\"\n }\n}",
);
let fx = fixture_with(&pkg, B3_BEFORE_LOCK).await;
let (result, entry, _) = expect_done(fx.vendor(false).await);
assert!(result.success, "{:?}", result.error);
assert!(entry.is_some());
}

/// A user-authored BARE-name pin to the exact version being vendored is
/// taken over: the value moves to our spec, the wiring records the pin
/// as `original`, and revert restores it (table kept). Range-keyed
Expand Down
Loading