From 312167bcc3409f6c76356fe1f7ded246fd1aa045 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 21:34:26 +0000 Subject: [PATCH 1/4] Start fix for #437 Assisted-by: Claude Code:claude-opus-5-5 From a08a8cb5e62e735267f1f18b68c7a0cd7d33fc50 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 21:44:59 +0000 Subject: [PATCH 2/4] Find yarn 1.0.x global packages Yarn 1.0.0 to 1.0.2 has no `yarn global dir` command, so global scans (`scan -g`, `get -g`, `rollback -g`, `vex -g`) found none of the packages installed with `yarn global add` and still reported success. When `yarn global dir` gives no answer and `yarn --version` reports 1.0.x, the global folder yarn 1.0.x itself uses is crawled instead: ~/.config/yarn/global (/usr/local/share/.config/yarn/global for root), or %LOCALAPPDATA%\Yarn\config\global on Windows. Newer yarn releases and machines without yarn behave as before. Fixes #437 Assisted-by: Claude Code:claude-opus-5-5 --- .../src/crawlers/npm_crawler.rs | 36 +++++++- .../src/patch/redirect/yarnrc.rs | 20 +++++ .../src/patch/redirect/yarnrc_tests.rs | 66 +++++++++++++++ .../tests/crawler_npm_e2e.rs | 83 ++++++++++++++++++- 4 files changed, 200 insertions(+), 5 deletions(-) diff --git a/crates/socket-patch-core/src/crawlers/npm_crawler.rs b/crates/socket-patch-core/src/crawlers/npm_crawler.rs index 19cc35481..48b921d61 100644 --- a/crates/socket-patch-core/src/crawlers/npm_crawler.rs +++ b/crates/socket-patch-core/src/crawlers/npm_crawler.rs @@ -2343,12 +2343,44 @@ pub fn get_yarn_global_prefix() -> Option { /// Version of `get_yarn_global_prefix` that accepts an injected /// `CommandRunner`. See `get_npm_global_prefix_with`. pub fn get_yarn_global_prefix_with(runner: &dyn CommandRunner) -> Option { - parse_yarn_dir_output( + get_yarn_global_prefix_or( + runner, + crate::patch::redirect::yarnrc::yarn_1_0_default_global_folder_for_process, + ) +} + +/// [`get_yarn_global_prefix_with`] with the yarn 1.0.x default global +/// folder injected. Yarn 1.0.0 - 1.0.2 has no `yarn global dir` (it +/// exits 1 with "Invalid subcommand"), so when `yarn --version` reports +/// 1.0.x the folder yarn itself would use stands in (#437). +pub fn get_yarn_global_prefix_or( + runner: &dyn CommandRunner, + yarn_1_0_folder: impl FnOnce() -> Option, +) -> Option { + let reported = parse_yarn_dir_output( runner .run("yarn", &["global", "dir"]) .as_deref() .unwrap_or(""), - ) + ); + if reported.is_some() { + return reported; + } + let version = runner.run("yarn", &["--version"])?; + if !is_yarn_1_0(&version) { + return None; + } + let dir = yarn_1_0_folder()?; + Some(dir.join("node_modules").to_string_lossy().to_string()) +} + +/// Whether `yarn --version` stdout names a 1.0.x release. +fn is_yarn_1_0(version: &str) -> bool { + version + .trim() + .strip_prefix("1.0.") + .and_then(|patch| patch.split(['-', '+']).next()) + .is_some_and(|patch| !patch.is_empty() && patch.bytes().all(|b| b.is_ascii_digit())) } /// Pure parser for `yarn global dir` stdout. Returns `/node_modules` diff --git a/crates/socket-patch-core/src/patch/redirect/yarnrc.rs b/crates/socket-patch-core/src/patch/redirect/yarnrc.rs index e51474154..c7ba30129 100644 --- a/crates/socket-patch-core/src/patch/redirect/yarnrc.rs +++ b/crates/socket-patch-core/src/patch/redirect/yarnrc.rs @@ -567,6 +567,26 @@ pub fn configured_yarn_global_folders_for_process() -> Vec { ) } +/// The global folder yarn 1.0.x uses by default, which it can't report: +/// `yarn global dir` only exists from yarn 1.1.0 (#437). This is yarn +/// 1.0.2's `GLOBAL_MODULE_DIRECTORY`, `/global`, where +/// the config directory is `%LOCALAPPDATA%\Yarn\config` on Windows (when +/// `LOCALAPPDATA` is set) and `/.config/yarn` otherwise. +pub fn yarn_1_0_default_global_folder(env: &NpmConfigEnv, root_user: bool) -> Option { + if env.windows { + if let Some(local) = env.var("LOCALAPPDATA") { + return Some(Path::new(local).join("Yarn").join("config").join("global")); + } + } + let user_home = RcLayout::new(env, root_user).user_home?; + Some(user_home.join(".config").join("yarn").join("global")) +} + +/// [`yarn_1_0_default_global_folder`] for this process. +pub fn yarn_1_0_default_global_folder_for_process() -> Option { + yarn_1_0_default_global_folder(&NpmConfigEnv::from_process(), process_is_root_user()) +} + /// Whether this process runs as uid 0 (yarn's `getuid() === 0`). pub fn process_is_root_user() -> bool { #[cfg(unix)] diff --git a/crates/socket-patch-core/src/patch/redirect/yarnrc_tests.rs b/crates/socket-patch-core/src/patch/redirect/yarnrc_tests.rs index 6e975243d..2bb9e1d0f 100644 --- a/crates/socket-patch-core/src/patch/redirect/yarnrc_tests.rs +++ b/crates/socket-patch-core/src/patch/redirect/yarnrc_tests.rs @@ -610,3 +610,69 @@ fn global_folder_values_read_like_yarn() { paths(&["/opt/nq"]) ); } + +/// #437: yarn 1.0.x can't report its global folder, so it is derived the +/// way yarn 1.0.2's `constants.js` does: `/.config/yarn/global` +/// (`userHome` is `/usr/local/share` for a root user that isn't under +/// fakeroot), or `%LOCALAPPDATA%\Yarn\config\global` on Windows. +#[test] +fn yarn_1_0_default_global_folder_follows_yarn_1_0_2() { + let vars = |pairs: &[(&str, &str)]| -> Vec<(String, String)> { + pairs + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect() + }; + let unix = NpmConfigEnv::from_parts(vars(&[("HOME", "/home/u")]), None, false); + assert_eq!( + yarn_1_0_default_global_folder(&unix, false), + Some(PathBuf::from("/home/u/.config/yarn/global")) + ); + assert_eq!( + yarn_1_0_default_global_folder(&unix, true), + Some(PathBuf::from("/usr/local/share/.config/yarn/global")) + ); + let fakeroot = NpmConfigEnv::from_parts( + vars(&[("HOME", "/home/u"), ("FAKEROOTKEY", "1")]), + None, + false, + ); + assert_eq!( + yarn_1_0_default_global_folder(&fakeroot, true), + Some(PathBuf::from("/home/u/.config/yarn/global")) + ); + assert_eq!( + yarn_1_0_default_global_folder(&NpmConfigEnv::from_parts(Vec::new(), None, false), false), + None, + "no home, no folder" + ); + + let windows = NpmConfigEnv::from_parts( + vars(&[ + ("USERPROFILE", r"C:\Users\u"), + ("LOCALAPPDATA", r"C:\Users\u\AppData\Local"), + ]), + None, + true, + ); + assert_eq!( + yarn_1_0_default_global_folder(&windows, false), + Some( + Path::new(r"C:\Users\u\AppData\Local") + .join("Yarn") + .join("config") + .join("global") + ) + ); + // Without LOCALAPPDATA yarn 1.0.x falls back to the home layout. + let windows = NpmConfigEnv::from_parts(vars(&[("USERPROFILE", r"C:\Users\u")]), None, true); + assert_eq!( + yarn_1_0_default_global_folder(&windows, false), + Some( + Path::new(r"C:\Users\u") + .join(".config") + .join("yarn") + .join("global") + ) + ); +} diff --git a/crates/socket-patch-core/tests/crawler_npm_e2e.rs b/crates/socket-patch-core/tests/crawler_npm_e2e.rs index 880c016a1..d78c26c8e 100644 --- a/crates/socket-patch-core/tests/crawler_npm_e2e.rs +++ b/crates/socket-patch-core/tests/crawler_npm_e2e.rs @@ -10,9 +10,9 @@ use socket_patch_core::crawlers::npm_crawler::{ build_npm_purl, bun_global_dir_from_env, find_store_peer_variant_copies, get_bun_global_prefix, get_bun_global_prefix_with, get_npm_global_prefix, get_npm_global_prefix_with, get_pnpm_global_prefix, get_pnpm_global_prefix_with, get_yarn_global_prefix, - get_yarn_global_prefix_with, parse_bun_ls_global_output, parse_npm_root_output, - parse_package_name, parse_pnpm_root_output, parse_yarn_dir_output, read_package_json, - resolve_bun_global_prefix_with, + get_yarn_global_prefix_or, get_yarn_global_prefix_with, parse_bun_ls_global_output, + parse_npm_root_output, parse_package_name, parse_pnpm_root_output, parse_yarn_dir_output, + read_package_json, resolve_bun_global_prefix_with, }; use socket_patch_core::crawlers::types::CrawlerOptions; use socket_patch_core::crawlers::NpmCrawler; @@ -352,6 +352,83 @@ fn get_yarn_global_prefix_with_mock_runner_success() { ); } +/// #437: yarn 1.0.0 - 1.0.2 has no `yarn global dir` (it exits 1), so +/// the folder yarn 1.0.x itself uses stands in for the answer it can't +/// give. Real process environment: HOME points at a scratch home and +/// FAKEROOTKEY keeps a root test runner on the plain-home layout. +#[cfg(unix)] +#[test] +#[serial_test::serial] +fn get_yarn_global_prefix_with_falls_back_for_yarn_1_0() { + let home = tempfile::tempdir().unwrap(); + let saved: Vec<_> = ["HOME", "FAKEROOTKEY"] + .iter() + .map(|k| (*k, std::env::var_os(k))) + .collect(); + std::env::set_var("HOME", home.path()); + std::env::set_var("FAKEROOTKEY", "1"); + let runner = common::MockCommandRunner::new() + .with_response("yarn", &["global", "dir"], None) + .with_response("yarn", &["--version"], Some("1.0.2\n")); + let got = get_yarn_global_prefix_with(&runner); + for (k, v) in saved { + match v { + Some(v) => std::env::set_var(k, v), + None => std::env::remove_var(k), + } + } + assert_eq!( + got.map(PathBuf::from), + Some(home.path().join(".config/yarn/global/node_modules")) + ); +} + +/// The fallback is for yarn 1.0.x only: a newer yarn whose +/// `yarn global dir` failed, or no yarn at all, still finds nothing. +#[test] +#[serial_test::parallel] +fn get_yarn_global_prefix_or_falls_back_only_for_yarn_1_0() { + let folder = || Some(PathBuf::from("/h/.config/yarn/global")); + let want = Path::new("/h/.config/yarn/global") + .join("node_modules") + .to_string_lossy() + .to_string(); + for version in ["1.0.0", "1.0.1\n", "1.0.2", "1.0.2-rc.1"] { + let runner = common::MockCommandRunner::new() + .with_response("yarn", &["global", "dir"], None) + .with_response("yarn", &["--version"], Some(version)); + assert_eq!( + get_yarn_global_prefix_or(&runner, folder).as_deref(), + Some(want.as_str()), + "{version:?}" + ); + } + for version in [ + Some("1.1.0"), + Some("1.22.22"), + Some("1.0"), + Some("4.5.0"), + None, + ] { + let runner = common::MockCommandRunner::new() + .with_response("yarn", &["global", "dir"], None) + .with_response("yarn", &["--version"], version); + assert_eq!( + get_yarn_global_prefix_or(&runner, folder), + None, + "{version:?}" + ); + } + // A yarn that answers `yarn global dir` is taken at its word. + let runner = common::MockCommandRunner::new() + .with_response("yarn", &["global", "dir"], Some("/other/global\n")) + .with_response("yarn", &["--version"], Some("1.0.2")); + assert_eq!( + get_yarn_global_prefix_or(&runner, folder).map(PathBuf::from), + Some(Path::new("/other/global").join("node_modules")) + ); +} + #[test] #[serial_test::parallel] fn get_pnpm_global_prefix_with_mock_runner_success() { From 7cacf1e2874fa88c6908b7d707b1d5200af2ab0f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 22:03:06 +0000 Subject: [PATCH 3/4] Prefer a configured yarn 1.0.x global folder Yarn 1.0.x also reads `--global-folder` from .yarnrc, so a configured global folder that exists is crawled before the default one. The `yarn --version` probe now runs only when one of those folders exists, so a Yarn Berry machine (no `yarn global` at all) doesn't pay for a second yarn process on every global scan. Assisted-by: Claude Code:claude-opus-5-5 --- .../src/crawlers/npm_crawler.rs | 25 +++--- .../src/patch/redirect/yarnrc.rs | 13 ++- .../tests/crawler_npm_e2e.rs | 85 +++++++++++++------ 3 files changed, 84 insertions(+), 39 deletions(-) diff --git a/crates/socket-patch-core/src/crawlers/npm_crawler.rs b/crates/socket-patch-core/src/crawlers/npm_crawler.rs index 48b921d61..2d6acefd5 100644 --- a/crates/socket-patch-core/src/crawlers/npm_crawler.rs +++ b/crates/socket-patch-core/src/crawlers/npm_crawler.rs @@ -2345,17 +2345,20 @@ pub fn get_yarn_global_prefix() -> Option { pub fn get_yarn_global_prefix_with(runner: &dyn CommandRunner) -> Option { get_yarn_global_prefix_or( runner, - crate::patch::redirect::yarnrc::yarn_1_0_default_global_folder_for_process, + crate::patch::redirect::yarnrc::yarn_1_0_global_folders_for_process, ) } -/// [`get_yarn_global_prefix_with`] with the yarn 1.0.x default global -/// folder injected. Yarn 1.0.0 - 1.0.2 has no `yarn global dir` (it -/// exits 1 with "Invalid subcommand"), so when `yarn --version` reports -/// 1.0.x the folder yarn itself would use stands in (#437). +/// [`get_yarn_global_prefix_with`] with the yarn 1.0.x global folder +/// candidates injected. Yarn 1.0.0 - 1.0.2 has no `yarn global dir` (it +/// exits 1 with "Invalid subcommand"), so the first candidate that holds +/// a `node_modules` stands in for its answer when `yarn --version` +/// reports 1.0.x (#437). The version is asked only once such a folder +/// exists, so a yarn without the subcommand for another reason (Berry +/// has no `global`) costs no second spawn. pub fn get_yarn_global_prefix_or( runner: &dyn CommandRunner, - yarn_1_0_folder: impl FnOnce() -> Option, + yarn_1_0_folders: impl FnOnce() -> Vec, ) -> Option { let reported = parse_yarn_dir_output( runner @@ -2366,12 +2369,12 @@ pub fn get_yarn_global_prefix_or( if reported.is_some() { return reported; } + let node_modules = yarn_1_0_folders() + .into_iter() + .map(|dir| dir.join("node_modules")) + .find(|dir| is_dir_sync(dir))?; let version = runner.run("yarn", &["--version"])?; - if !is_yarn_1_0(&version) { - return None; - } - let dir = yarn_1_0_folder()?; - Some(dir.join("node_modules").to_string_lossy().to_string()) + is_yarn_1_0(&version).then(|| node_modules.to_string_lossy().to_string()) } /// Whether `yarn --version` stdout names a 1.0.x release. diff --git a/crates/socket-patch-core/src/patch/redirect/yarnrc.rs b/crates/socket-patch-core/src/patch/redirect/yarnrc.rs index c7ba30129..dbd4bb029 100644 --- a/crates/socket-patch-core/src/patch/redirect/yarnrc.rs +++ b/crates/socket-patch-core/src/patch/redirect/yarnrc.rs @@ -582,9 +582,16 @@ pub fn yarn_1_0_default_global_folder(env: &NpmConfigEnv, root_user: bool) -> Op Some(user_home.join(".config").join("yarn").join("global")) } -/// [`yarn_1_0_default_global_folder`] for this process. -pub fn yarn_1_0_default_global_folder_for_process() -> Option { - yarn_1_0_default_global_folder(&NpmConfigEnv::from_process(), process_is_root_user()) +/// Where yarn 1.0.x run by this process keeps its global packages, most +/// likely first: any folder configured as yarn's global folder (yarn +/// 1.0.x reads `--global-folder` from `.yarnrc`), then its default. +pub fn yarn_1_0_global_folders_for_process() -> Vec { + let mut folders = configured_yarn_global_folders_for_process(); + folders.extend(yarn_1_0_default_global_folder( + &NpmConfigEnv::from_process(), + process_is_root_user(), + )); + folders } /// Whether this process runs as uid 0 (yarn's `getuid() === 0`). diff --git a/crates/socket-patch-core/tests/crawler_npm_e2e.rs b/crates/socket-patch-core/tests/crawler_npm_e2e.rs index d78c26c8e..634cd2b2a 100644 --- a/crates/socket-patch-core/tests/crawler_npm_e2e.rs +++ b/crates/socket-patch-core/tests/crawler_npm_e2e.rs @@ -352,6 +352,35 @@ fn get_yarn_global_prefix_with_mock_runner_success() { ); } +/// Sets environment variables for a test and puts the old values back on +/// drop, a panic included. +struct EnvGuard(Vec<(&'static str, Option)>); + +impl EnvGuard { + fn set(vars: &[(&'static str, &Path)]) -> Self { + let saved = vars + .iter() + .map(|(k, v)| { + let old = std::env::var_os(k); + std::env::set_var(k, v); + (*k, old) + }) + .collect(); + EnvGuard(saved) + } +} + +impl Drop for EnvGuard { + fn drop(&mut self) { + for (k, v) in self.0.drain(..) { + match v { + Some(v) => std::env::set_var(k, v), + None => std::env::remove_var(k), + } + } + } +} + /// #437: yarn 1.0.0 - 1.0.2 has no `yarn global dir` (it exits 1), so /// the folder yarn 1.0.x itself uses stands in for the answer it can't /// give. Real process environment: HOME points at a scratch home and @@ -361,45 +390,38 @@ fn get_yarn_global_prefix_with_mock_runner_success() { #[serial_test::serial] fn get_yarn_global_prefix_with_falls_back_for_yarn_1_0() { let home = tempfile::tempdir().unwrap(); - let saved: Vec<_> = ["HOME", "FAKEROOTKEY"] - .iter() - .map(|k| (*k, std::env::var_os(k))) - .collect(); - std::env::set_var("HOME", home.path()); - std::env::set_var("FAKEROOTKEY", "1"); + let global = home.path().join(".config/yarn/global/node_modules"); + std::fs::create_dir_all(global.join("left-pad")).unwrap(); + let _env = EnvGuard::set(&[("HOME", home.path()), ("FAKEROOTKEY", Path::new("1"))]); let runner = common::MockCommandRunner::new() .with_response("yarn", &["global", "dir"], None) .with_response("yarn", &["--version"], Some("1.0.2\n")); - let got = get_yarn_global_prefix_with(&runner); - for (k, v) in saved { - match v { - Some(v) => std::env::set_var(k, v), - None => std::env::remove_var(k), - } - } assert_eq!( - got.map(PathBuf::from), - Some(home.path().join(".config/yarn/global/node_modules")) + get_yarn_global_prefix_with(&runner).map(PathBuf::from), + Some(global) ); } /// The fallback is for yarn 1.0.x only: a newer yarn whose -/// `yarn global dir` failed, or no yarn at all, still finds nothing. +/// `yarn global dir` failed, or no yarn at all, still finds nothing. The +/// first candidate folder that exists wins, and none existing finds +/// nothing without asking yarn for its version. #[test] #[serial_test::parallel] fn get_yarn_global_prefix_or_falls_back_only_for_yarn_1_0() { - let folder = || Some(PathBuf::from("/h/.config/yarn/global")); - let want = Path::new("/h/.config/yarn/global") - .join("node_modules") - .to_string_lossy() - .to_string(); + let tmp = tempfile::tempdir().unwrap(); + let configured = tmp.path().join("configured"); + let default = tmp.path().join("default"); + std::fs::create_dir_all(default.join("node_modules")).unwrap(); + let folders = || vec![configured.clone(), default.clone()]; + let want = default.join("node_modules"); for version in ["1.0.0", "1.0.1\n", "1.0.2", "1.0.2-rc.1"] { let runner = common::MockCommandRunner::new() .with_response("yarn", &["global", "dir"], None) .with_response("yarn", &["--version"], Some(version)); assert_eq!( - get_yarn_global_prefix_or(&runner, folder).as_deref(), - Some(want.as_str()), + get_yarn_global_prefix_or(&runner, folders).map(PathBuf::from), + Some(want.clone()), "{version:?}" ); } @@ -414,17 +436,30 @@ fn get_yarn_global_prefix_or_falls_back_only_for_yarn_1_0() { .with_response("yarn", &["global", "dir"], None) .with_response("yarn", &["--version"], version); assert_eq!( - get_yarn_global_prefix_or(&runner, folder), + get_yarn_global_prefix_or(&runner, folders), None, "{version:?}" ); } + // A configured global folder that exists comes before the default. + std::fs::create_dir_all(configured.join("node_modules")).unwrap(); + let runner = common::MockCommandRunner::new() + .with_response("yarn", &["global", "dir"], None) + .with_response("yarn", &["--version"], Some("1.0.2")); + assert_eq!( + get_yarn_global_prefix_or(&runner, folders).map(PathBuf::from), + Some(configured.join("node_modules")) + ); + // No candidate folder: no version probe is needed (the mock has no + // answer for it either way). + let runner = common::MockCommandRunner::new().with_response("yarn", &["global", "dir"], None); + assert_eq!(get_yarn_global_prefix_or(&runner, Vec::new), None); // A yarn that answers `yarn global dir` is taken at its word. let runner = common::MockCommandRunner::new() .with_response("yarn", &["global", "dir"], Some("/other/global\n")) .with_response("yarn", &["--version"], Some("1.0.2")); assert_eq!( - get_yarn_global_prefix_or(&runner, folder).map(PathBuf::from), + get_yarn_global_prefix_or(&runner, folders).map(PathBuf::from), Some(Path::new("/other/global").join("node_modules")) ); } From cefc03a3a57ff5f432bab94cf03ce73c349410c2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 22:23:10 +0000 Subject: [PATCH 4/4] Read yarn 1.0.x config from the probe dir The yarn 1.0.x fallback read global-folder settings from the current project, so a project .yarnrc naming an existing folder could pass a project directory off as the machine-wide yarn install. It now reads yarn's config from the same neutral directory the `yarn global dir` probe runs in. Assisted-by: Claude Code:claude-opus-5-5 --- .../src/patch/redirect/yarnrc.rs | 24 ++++++++++++------- .../tests/crawler_npm_e2e.rs | 14 ++++++++++- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/crates/socket-patch-core/src/patch/redirect/yarnrc.rs b/crates/socket-patch-core/src/patch/redirect/yarnrc.rs index dbd4bb029..5e40f0f17 100644 --- a/crates/socket-patch-core/src/patch/redirect/yarnrc.rs +++ b/crates/socket-patch-core/src/patch/redirect/yarnrc.rs @@ -582,15 +582,23 @@ pub fn yarn_1_0_default_global_folder(env: &NpmConfigEnv, root_user: bool) -> Op Some(user_home.join(".config").join("yarn").join("global")) } -/// Where yarn 1.0.x run by this process keeps its global packages, most -/// likely first: any folder configured as yarn's global folder (yarn -/// 1.0.x reads `--global-folder` from `.yarnrc`), then its default. +/// Where yarn 1.0.x keeps this user's global packages, most likely +/// first: any folder configured as yarn's global folder (yarn 1.0.x reads +/// `--global-folder` from `.yarnrc`), then its default. The config is read +/// as yarn run from the neutral probe dir sees it, like the `yarn global +/// dir` probe it stands in for, so a project's `.yarnrc` can't answer +/// where the machine-wide install lives. pub fn yarn_1_0_global_folders_for_process() -> Vec { - let mut folders = configured_yarn_global_folders_for_process(); - folders.extend(yarn_1_0_default_global_folder( - &NpmConfigEnv::from_process(), - process_is_root_user(), - )); + let env = NpmConfigEnv::from_process(); + let root_user = process_is_root_user(); + let mut folders = crate::utils::process::neutral_probe_dir() + .map(|dir| { + configured_yarn_global_folders(&env, root_user, &dir, |path| { + crate::utils::fs::read_regular_to_string_sync(path).ok() + }) + }) + .unwrap_or_default(); + folders.extend(yarn_1_0_default_global_folder(&env, root_user)); folders } diff --git a/crates/socket-patch-core/tests/crawler_npm_e2e.rs b/crates/socket-patch-core/tests/crawler_npm_e2e.rs index 634cd2b2a..39079287c 100644 --- a/crates/socket-patch-core/tests/crawler_npm_e2e.rs +++ b/crates/socket-patch-core/tests/crawler_npm_e2e.rs @@ -398,8 +398,20 @@ fn get_yarn_global_prefix_with_falls_back_for_yarn_1_0() { .with_response("yarn", &["--version"], Some("1.0.2\n")); assert_eq!( get_yarn_global_prefix_with(&runner).map(PathBuf::from), - Some(global) + Some(global.clone()) ); + + // A project's `.yarnrc` in the cwd names a global folder that exists: + // it can't stand in for the machine-wide answer, which yarn run from + // the neutral probe dir (HOME) never sees. + let project = tempfile::tempdir().unwrap(); + std::fs::create_dir_all(project.path().join("g/node_modules")).unwrap(); + std::fs::write(project.path().join(".yarnrc"), "--global-folder \"./g\"\n").unwrap(); + let cwd = std::env::current_dir().unwrap(); + std::env::set_current_dir(project.path()).unwrap(); + let got = get_yarn_global_prefix_with(&runner); + std::env::set_current_dir(cwd).unwrap(); + assert_eq!(got.map(PathBuf::from), Some(global)); } /// The fallback is for yarn 1.0.x only: a newer yarn whose