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
5 changes: 4 additions & 1 deletion crates/socket-patch-cli/CLI_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,10 @@ the model is **not uniform** today:
vlt.json `workspaces` — a glob, a list, or named groups of either — or vlt <= 0.0.0-12's
`vlt-workspaces.json`; vlt's declaration wins over the others and vlt never reads package.json
`workspaces`). One repo-root invocation discovers every member. A member that is itself a
workspace root is recursed into (bounded depth).
workspace root is recursed into (bounded depth). Vendored yarn classic wiring is root-relative (`file:./.socket/vendor/…`), which yarn 1
reads relative to the directory it runs in: a cold-cache yarn command run from a member
directory cannot fetch it, and the run warns `yarn_classic_workspace_member_install_risk`
(#691; see docs/ecosystems.md).
- **cwd-only (single project):** gem, pypi, composer. The crawler inspects only the project
rooted at `--cwd` (pypi first takes the env the project's manager records: PDM's `.pdm-python` interpreter, meaning its venv or, for a base interpreter, PEP 582 `__pypackages__/<X.Y>/lib`, and uv's `UV_PROJECT_ENVIRONMENT`. Otherwise it looks at `$VIRTUAL_ENV`, `<cwd>/.venv` / `venv`, then a Poetry project's out-of-tree virtualenv(s) under Poetry's `virtualenvs.path`; composer at the vendor tree); it does **not**
descend into sibling subprojects. A monorepo with several independent lockfiles in subdirectories
Expand Down
33 changes: 19 additions & 14 deletions crates/socket-patch-cli/src/commands/vendor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -865,28 +865,33 @@ fn format_revert_install_hint(cmd: &str) -> String {
)
}

/// Run-level advisory shared by the `vendor` command and the scan-driven
/// Run-level advisories shared by the `vendor` command and the scan-driven
/// vendor step: warn (once, at the envelope level — not per package) when
/// the project's classic `yarn.lock` carries vendored wiring that a stray
/// yarn 2+ install would silently drop. The probe is state-based (it reads
/// the on-disk lockfile), so callers invoke it unconditionally at
/// envelope-finalize time — unwired projects and fully-reverted runs stay
/// silent, and dry runs report the risk that already exists on disk.
/// yarn 2+ install would silently drop, or that installs run from a
/// workspace member directory cannot fetch (#691). The probes are
/// state-based (they read the on-disk lockfile), so callers invoke them
/// unconditionally at envelope-finalize time — unwired projects and
/// fully-reverted runs stay silent, and dry runs report the risk that
/// already exists on disk.
pub(crate) fn note_classic_migration_risk(
env: &mut Envelope,
project_root: &Path,
common: &GlobalArgs,
) {
let Some(w) = vendor::yarn_classic_berry_migration_risk(project_root) else {
return;
};
if !common.silent && !common.json {
eprintln!("Warning: {}", w.detail);
let warnings = [
vendor::yarn_classic_berry_migration_risk(project_root),
vendor::yarn_classic_workspace_member_risk(project_root),
];
for w in warnings.into_iter().flatten() {
if !common.silent && !common.json {
eprintln!("Warning: {}", w.detail);
}
env.warnings.push(RunWarning {
code: w.code.to_string(),
detail: w.detail,
});
}
env.warnings.push(RunWarning {
code: w.code.to_string(),
detail: w.detail,
});
}

/// The usage error for `vendor` under global scope, or `None` for a
Expand Down
46 changes: 46 additions & 0 deletions crates/socket-patch-cli/tests/covgap_commands_vendor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1194,6 +1194,52 @@ async fn vendor_state_write_failure_reports_failed_event() {
);
}

/// #691: yarn 1 resolves the vendored `file:./.socket/vendor/…` tarball
/// against the directory it runs in, so in a workspaces project every
/// cold-cache install run from a member directory fails once vendoring
/// wires one. The vendor run must say so (and say nothing for a project
/// without workspaces).
#[test]
fn yarn_classic_workspaces_vendor_warns_about_member_dir_installs() {
const CODE: &str = "yarn_classic_workspace_member_install_risk";
let yarn_lock = format!(
"# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.\n\
# yarn lockfile v1\n\n\n\
left-pad@^1.3.0:\n version \"1.3.0\"\n resolved \"{REG_RESOLVED}\"\n \
integrity {REG_INTEGRITY}\n"
);
for workspaces in [true, false] {
let fx = npm_fixture();
std::fs::remove_file(fx.lock_path()).unwrap();
let manifest = if workspaces {
std::fs::create_dir_all(fx.root().join("a")).unwrap();
std::fs::write(
fx.root().join("a/package.json"),
br#"{"name":"a","version":"1.0.0","dependencies":{"left-pad":"^1.3.0"}}"#,
)
.unwrap();
r#"{"name":"fixture","version":"1.0.0","private":true,"workspaces":["a"]}"#
} else {
r#"{"name":"fixture","version":"1.0.0","private":true,"dependencies":{"left-pad":"^1.3.0"}}"#
};
std::fs::write(fx.root().join("package.json"), manifest).unwrap();
std::fs::write(fx.root().join("yarn.lock"), &yarn_lock).unwrap();

let (code, env) = vendor_cli(fx.root(), &[]);
assert_eq!(code, 0, "{env:#}");
assert!(
std::fs::read_to_string(fx.root().join("yarn.lock"))
.unwrap()
.contains("resolved \"file:./.socket/vendor/"),
"{env:#}"
);
let warned = env["warnings"]
.as_array()
.is_some_and(|ws| ws.iter().any(|w| w["code"] == CODE));
assert_eq!(warned, workspaces, "workspaces={workspaces}: {env:#}");
}
}

/// #627: a symlinked lockfile (a lock shared with another checkout) is
/// refused like hosted mode refuses it — `redirect_symlinked_file_unsupported`,
/// exit 1 — instead of being renamed over: the link survives, its target
Expand Down
84 changes: 84 additions & 0 deletions crates/socket-patch-core/src/vendor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,50 @@ pub fn yarn_classic_berry_migration_risk(project_root: &Path) -> Option<VendorWa
))
}

/// Advisory probe (#691): does this yarn classic workspaces project carry
/// vendored wiring that installs from a workspace member directory cannot
/// fetch?
///
/// Vendored mode wires `resolved "file:./.socket/vendor/…"`, relative to
/// the workspace root that holds `yarn.lock`. Yarn 1 resolves a relative
/// `file:` tarball against the directory it runs in, so on a cold cache
/// every `yarn install` / `yarn add` run from a member directory (and
/// `yarn workspace <name> …`, which runs there) fails with "Tarball is not
/// in network and can not be located in cache", whether or not the member
/// depends on the patched package. No relative spelling works from both
/// (measured on yarn 1.7.0, 1.10.1 and 1.22.22). Returns the warning when
/// `yarn.lock` is classic with such a `file:./` resolution and the root
/// `package.json` declares workspaces. State-based, like
/// [`yarn_classic_berry_migration_risk`].
pub fn yarn_classic_workspace_member_risk(project_root: &Path) -> Option<VendorWarning> {
let lock = read_regular_to_string_sync(&project_root.join("yarn.lock")).ok()?;
if !lock.contains("# yarn lockfile v1") || !lock.contains("\"file:./.socket/vendor/") {
return None;
}
let manifest = read_regular_to_string_sync(&project_root.join("package.json")).ok()?;
let manifest: serde_json::Value =
serde_json::from_str(crate::formats::text::strip_bom(&manifest)).ok()?;
let workspaces = manifest.get("workspaces")?;
let globs = workspaces.as_array().or_else(|| {
workspaces
.get("packages")
.and_then(serde_json::Value::as_array)
})?;
if globs.is_empty() {
return None;
}
Some(VendorWarning::new(
"yarn_classic_workspace_member_install_risk",
"yarn.lock is yarn-classic (v1) in a workspaces project and its vendored entries \
resolve `file:./.socket/vendor/…` tarballs, which yarn 1 looks up relative to the \
directory it runs in: on a cold yarn cache, `yarn install`, `yarn add` or `yarn \
workspace <name> …` run from a workspace member directory fails (\"Tarball is not \
in network\"). Run `yarn install` from the workspace root (which also warms the \
cache for later member-directory commands), or patch this project with `--mode \
hosted`.",
))
}

/// Whether a root `package.json` text pins yarn classic through corepack's
/// `packageManager: yarn@1…`, which makes a stray yarn 2+ (berry) install
/// refuse instead of migrating a classic `yarn.lock` and dropping its
Expand Down Expand Up @@ -2061,6 +2105,46 @@ mod berry_migration_risk_tests {
assert!(probe_with_timeout(tmp.path(), &fifo).is_some());
}

/// #691: yarn 1 resolves a relative `file:` tarball in `resolved`
/// against the directory it runs in, not the one holding yarn.lock, so
/// in a workspaces project every cold-cache install run from a member
/// directory fails once vendoring wires one. Measured on yarn 1.7.0,
/// 1.10.1 and 1.22.22; no relative spelling installs from both.
#[test]
fn issue_691_wired_classic_workspaces_warn_about_member_dir_installs() {
for workspaces in [r#"["a","b"]"#, r#"{"packages":["packages/*"]}"#] {
let pkg = format!(r#"{{"name":"root","private":true,"workspaces":{workspaces}}}"#);
let tmp = project(Some(WIRED_V1), Some(&pkg));
let w = yarn_classic_workspace_member_risk(tmp.path()).expect("must warn");
assert_eq!(w.code, "yarn_classic_workspace_member_install_risk");
assert!(
w.detail.contains("member directory") && w.detail.contains("workspace root"),
"detail names the trap and the remedy: {}",
w.detail
);
}
// No workspaces, empty workspaces, an unwired or berry lock, or
// no manifest: nothing installs from a member directory through
// our wiring.
for (lock, pkg) in [
(Some(WIRED_V1), Some(r#"{"name":"x"}"#)),
(Some(WIRED_V1), Some(r#"{"name":"x","workspaces":[]}"#)),
(
Some(WIRED_V1),
Some(r#"{"name":"x","workspaces":{"packages":[]}}"#),
),
(Some(WIRED_V1), None),
(Some(UNWIRED_V1), Some(r#"{"name":"x","workspaces":["a"]}"#)),
(None, Some(r#"{"name":"x","workspaces":["a"]}"#)),
] {
let tmp = project(lock, pkg);
assert!(
yarn_classic_workspace_member_risk(tmp.path()).is_none(),
"{lock:?} {pkg:?}"
);
}
}

#[test]
fn malformed_or_missing_package_json_still_warns() {
// Fail toward warning: an unreadable pin must not silently vouch
Expand Down
10 changes: 10 additions & 0 deletions docs/ecosystems.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,16 @@ Each Legacy format has an upgrade path and an undo path. Both work in v5:
vendored wiring alike, so the packages install unpatched. A run that leaves such a pin
warns (`redirect_yarn_classic_berry_migration_risk` / `yarn_classic_berry_migration_risk`)
unless `package.json` pins yarn classic through `"packageManager": "yarn@1…"`.
- **yarn classic workspaces, vendored** — vendored mode wires
`resolved "file:./.socket/vendor/…"`, relative to the workspace root. Yarn 1 looks a
relative `file:` tarball up from the directory it runs in, so on a cold yarn cache
`yarn install`, `yarn add` or `yarn workspace <name> …` run from a member directory
fails ("Tarball is not in network and can not be located in cache"), whether or not the
member depends on the patched package (yarn 1.7.0, 1.10.1 and 1.22.22; no relative
spelling installs from both). Installs from the workspace root work, and warm the cache
for later member-directory commands. A vendored run in such a project warns
`yarn_classic_workspace_member_install_risk`; hosted mode, which pins an absolute URL,
is not affected.
- **yarn classic git dependencies** — yarn 1 fetches a git pattern (`git+https:`,
`git+ssh:`, `git:`, `ssh:`, a `….git` url, or a bare `https://github.com/<owner>/<repo>`)
with git, using the lock entry's `resolved` as the remote, so a rewritten `resolved`
Expand Down
Loading