Skip to content

refactor(pacquet): replace callback-DI with capability traits#11730

Merged
KSXGitHub merged 12 commits into
mainfrom
claude/refactor-dependency-injection-nMUVz
May 19, 2026
Merged

refactor(pacquet): replace callback-DI with capability traits#11730
KSXGitHub merged 12 commits into
mainfrom
claude/refactor-dependency-injection-nMUVz

Conversation

@KSXGitHub

@KSXGitHub KSXGitHub commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

PR #11708 introduced the proper trait-based DI seam (Sys generic + Host provider), but a few functions still emulated the JS/TS dependency-injection pattern by taking accessor closures (FnOnce/FnMut). Convert them to the trait shape so the seam is uniform across pacquet.

Changes

  • crates/config/src/api.rs — add EnvVarOs, GetHomeDir, and GetCurrentDir capability traits alongside the existing EnvVar; all implemented for Host.
  • default_store_dir — drop the HomeDir/CurrentDir FnOnce closures. Signature collapses to default_store_dir<Sys: EnvVar + GetHomeDir + GetCurrentDir>(); the SmartDefault expression collapses to default_store_dir::<Host>().
  • Config::current — three changes:
    • CurrentDir closure → direct start_dir: &Path parameter. Production was always passing the canonicalized --dir, never the host cwd, so it was a parameter dressed as a DI seam.
    • HomeDir closure → Sys: GetHomeDir.
    • Inline std::env::var_os("NPM_CONFIG_WORKSPACE_DIR")Sys::var_os(...), gated on Sys: EnvVarOs. Three tests that held EnvGuard and ran unsafe { env::set_var(...) } to drive the env-var override now use per-test EnvVarOs fakes instead — no more process-state mutation, no unsafe.
  • crates/workspace/src/api.rs (new) — pacquet-workspace's own EnvVarOs trait and Host provider, per the "each crate declares its own provider" convention.
  • find_workspace_dir_from_env_withFnMut(&str) -> Option<OsString> closure → Sys: EnvVarOs.
  • CODE_STYLE_GUIDE.md — "Reading process state" updated to discourage callback-shape DI and cite the new default_store_dir<Sys: EnvVar + GetHomeDir + GetCurrentDir>() signature as the canonical shape.

The remaining FnOnce/FnMut closures in the codebase (retry_on_fd_pressure, StoreCommand::run's config factory, State::init's LoadLockfile, Config::current's default) are legitimate lazy/conditional patterns rather than callback-DI and stay as-is.

Test plan

  • cargo test -p pacquet-config (155 tests pass)
  • cargo test -p pacquet-workspace (26 tests pass)
  • cargo clippy -p pacquet-config -p pacquet-workspace -p pacquet-cli --tests --locked -- --deny warnings
  • cargo fmt --check

Written by an agent (Claude Code, claude-opus-4-7).

https://claude.ai/code/session_01Btd6rp2MgfXb3ArHwSJpqA

Summary by CodeRabbit

  • Documentation

    • Updated code style guide with revised dependency-injection patterns for configuration and environment access.
  • Refactor

    • Simplified configuration loading through improved capability-based design.
    • Refactored environment variable and directory resolution to use a trait-based dependency-injection approach.
  • Tests

    • Updated test suite to align with refactored configuration patterns.

Review Change Stack

PR #11708 introduced the proper trait-based DI seam (`Sys` generic +
`Host` provider), but a handful of functions still emulated the JS/TS
pattern by taking accessor closures (`FnOnce`/`FnMut`) — "poor man DI"
that the seam was meant to retire. Convert them to the trait shape.

Changes:

- `crates/config/src/api.rs`: add `EnvVarOs`, `GetHomeDir`, and
  `GetCurrentDir` capability traits alongside the existing `EnvVar`,
  all implemented for `Host`.
- `default_store_dir`: replace `HomeDir`/`CurrentDir` `FnOnce` closures
  with `Sys: EnvVar + GetHomeDir + GetCurrentDir`. The SmartDefault
  expression collapses from `default_store_dir::<Host, _, _, _>(
  home::home_dir, env::current_dir)` to `default_store_dir::<Host>()`.
- `Config::current`: drop the `CurrentDir` closure in favour of a
  direct `start_dir: &Path` parameter (production always passed the
  canonicalized `--dir`, never the host cwd, so it was a parameter
  dressed as a DI seam); thread the home-dir lookup through
  `Sys: GetHomeDir`; route the inline `std::env::var_os(
  "NPM_CONFIG_WORKSPACE_DIR")` lookups through `Sys: EnvVarOs`. The
  three tests that previously held `EnvGuard` and called `unsafe {
  env::set_var(...) }` to drive the env-var override now use per-test
  `EnvVarOs` fakes instead.
- `crates/workspace/src/api.rs` (new): pacquet-workspace's own
  `EnvVarOs` trait and `Host` provider, following the per-crate
  provider convention.
- `find_workspace_dir_from_env_with`: replace the `FnMut(&str) ->
  Option<OsString>` closure with `Sys: EnvVarOs`.

Test updates use per-test unit-struct fakes (or shared
`HostNoHome` / `HostUnreachableHome` helpers in `pacquet-config`'s
test module) per the convention in CODE_STYLE_GUIDE.md's
"Dependency injection for tests" section.

CODE_STYLE_GUIDE.md's "Reading process state" section is updated to
mark the closure shape as discouraged and cite the new
`default_store_dir<Sys: EnvVar + GetHomeDir + GetCurrentDir>()`
signature as the canonical example.

The remaining `FnOnce`/`FnMut` closures in the codebase
(`retry_on_fd_pressure`, `StoreCommand::run`'s `config` factory,
`State::init`'s `LoadLockfile`, `Config::current`'s `default`) are
legitimate lazy/conditional patterns rather than callback-DI and
stay as-is.

https://claude.ai/code/session_01Btd6rp2MgfXb3ArHwSJpqA
@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 703fdcba-abc1-4987-9ab5-850e89099b99

📥 Commits

Reviewing files that changed from the base of the PR and between 205ab50 and 6a29343.

📒 Files selected for processing (3)
  • pacquet/crates/config/src/defaults.rs
  • pacquet/crates/config/src/defaults/tests.rs
  • pacquet/crates/config/src/lib.rs
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: ubuntu-latest / Node.js 24 / Test
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Lint and Test (windows-latest)
🧰 Additional context used
📓 Path-based instructions (1)
pacquet/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/*.rs: Log emissions must mirror pnpm's pnpm:<channel> events through globalLogger / logger.debug(...) / streamParser.write(...) calls with matching payload and ordering for @pnpm/cli.default-reporter compatibility
Prefer real fixtures (tempfile::TempDir, mocked registry, integration tests) over dependency-injection seams for testing, reserving DI only for branches real fixtures cannot cover (filesystem error kinds, deterministic time, process-global state, external-service paths)
Declare a newtype wrapper (struct) for branded string types instead of collapsing the brand into plain String or &str
If upstream pnpm always validates before construction of a branded string type, validate too by constructing only via TryFrom and/or FromStr
If upstream pnpm never validates a branded string type, brand only for type-safety by exposing an infallible From and From<&str> constructor
If upstream pnpm occasionally constructs a branded string type without validation, expose from_str_unchecked constructor alongside the validating constructor
Match upstream pnpm serde behavior for branded string types by using #[serde(try_from = "String")] for deserialization and #[serde(into = "String")] for serialization
Use #[derive(derive_more::From)] and #[derive(derive_more::Into)] for mechanical branded string type conversions instead of handwriting impl blocks
Model upstream string literal unions or string literal types as Rust enums instead of newtype wrappers
Treat string template literal types (e.g., ${string}@${string}) as branded string types with the same validation discipline as other branded strings
Choose owned vs. borrowed parameters to minimize copies; widen to the most encompassing type (&Path over &PathBuf, &str over &String) when it doesn't force extra copies
Prefer Arc::clone(&x) / Rc::clone(&x) over x.clone() for reference-counted types to make the cost visible at the call site
Follow test-logging guidance in the style guide: log before...

Files:

  • pacquet/crates/config/src/defaults.rs
  • pacquet/crates/config/src/defaults/tests.rs
  • pacquet/crates/config/src/lib.rs
🔇 Additional comments (6)
pacquet/crates/config/src/defaults.rs (1)

1-1: LGTM!

Also applies to: 62-75, 87-87, 91-91, 121-129, 133-133

pacquet/crates/config/src/defaults/tests.rs (1)

6-6: LGTM!

Also applies to: 9-9, 23-29, 40-50, 69-79, 84-89, 95-112, 126-129, 137-143, 156-168

pacquet/crates/config/src/lib.rs (4)

9-20: LGTM!


208-209: LGTM!

Also applies to: 661-662


896-1049: LGTM!


1067-1690: LGTM!


📝 Walkthrough

Walkthrough

Refactor environment-variable and directory-lookup seams to capability traits (EnvVarOs, GetHomeDir, GetCurrentDir) with Host providers. Rewire default_store_dir, Config::current, workspace root-finding, and tests to use trait-based Sys injection instead of closures or direct std::env calls.

Changes

Trait-Based Dependency Injection for Environment and Path Lookups

Layer / File(s) Summary
Capability traits and Host implementations
pacquet/crates/config/src/api.rs, pacquet/crates/workspace/src/api.rs, pacquet/crates/workspace/src/lib.rs
Add three new public traits (EnvVarOs, GetHomeDir, GetCurrentDir) to config API; implement all three in Host provider. Workspace API introduces EnvVarOs and Host; re-exports both at crate root. Updated module docs explain unified DI pattern.
Default directory functions refactor
pacquet/crates/config/src/defaults.rs
Change default_store_dir and default_cache_dir from closure-injected APIs to trait-based: default_store_dir<Sys>() where Sys: EnvVar + GetHomeDir + GetCurrentDir; default_cache_dir<Sys>() where Sys: EnvVar + GetHomeDir. Functions now call Sys::home_dir().expect(...) and Sys::current_dir().expect(...) instead of accepting closures.
Default directory function tests
pacquet/crates/config/src/defaults/tests.rs
Migrate all tests to inject GetHomeDir and GetCurrentDir via local env structs (returning fixed paths or unreachable!). Simplify calls to default_store_dir::<EnvType>() and default_cache_dir::<EnvType>(); remove prior closure-based test helpers.
Workspace root-finder refactor
pacquet/crates/workspace/src/root_finder.rs
Refactor find_workspace_dir_from_env_with to be generic over Sys: EnvVarOs; call Sys::var_os for both canonical NPM_CONFIG_WORKSPACE_DIR and lowercase npm_config_workspace_dir. Add preallocated lowercase constant. find_workspace_dir_from_env now invokes the generic helper with production Host.
Workspace root-finder tests
pacquet/crates/workspace/src/root_finder/tests.rs
Update three env-var override tests to define local EnvVarOs structs controlling return values and call find_workspace_dir_from_env_with::<...>() with struct types; remove closure-based setup.
Config public API and re-exports
pacquet/crates/config/src/lib.rs
Re-export capability traits (EnvVarOs, GetCurrentDir, GetHomeDir) alongside EnvVar and Host. Add fs to std imports. Change Config.store_dir and Config.cache_dir SmartDefaults to call default_store_dir::<Host>() and default_cache_dir::<Host>() directly.
Config::current signature and logic
pacquet/crates/config/src/lib.rs
Refactor Config::current from current<Sys, Error, CurrentDir, HomeDir, Default>(current_dir, home_dir, default) to current<Sys, Default>(start_dir: &Path, default) with Sys: EnvVar + EnvVarOs + GetHomeDir. Re-anchor default modules_dir and virtual_store_dir to start_dir. Read .npmrc from start_dir first, then home via Sys::home_dir(). Route workspace-dir overrides through Sys::var_os for both env-var spellings. Fall back to WorkspaceSettings::find_and_load(start_dir) if no env override.
Config test harness and fake implementations
pacquet/crates/config/src/lib.rs
Introduce test fake types (HostNoHome, HostUnreachableHome) implementing new capability traits; use OnceLock-backed fakes to control .npmrc and workspace-dir behavior in tests without mutating process environment (except proxy env-var smoke test).
Config test cases update
pacquet/crates/config/src/lib.rs
Update ~20 test cases across store-dir defaults, .npmrc loading, workspace discovery, global virtual store, and workspace-dir overrides to call Config::current(start_dir, default) with trait-based fakes; adjust assertions for new SmartDefault behavior.
CLI Config call-site
pacquet/crates/cli/src/cli_args.rs
Update CliArgs::run's config loader to call Config::current::<Host, _>(&dir, Default::default) and then leak the config and wrap errors.
Code style guide update
pacquet/CODE_STYLE_GUIDE.md
Revise "Reading process state" recommendations: focus parameter-passing on &Path and env-var values as &str/Option<String>; generalize DI-seam guidance to multiple capability traits (EnvVar, EnvVarOs, GetHomeDir, GetCurrentDir); cite Config::current and workspace env-var lookup as examples; update SmartDefault guidance to route default-field computation through the DI seam.

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related issues

  • pnpm/pacquet#343: This PR directly implements the dependency-injection pattern (EnvVar/EnvVarOs/GetHomeDir/GetCurrentDir traits and refactored default_store_dir, Config::current, workspace lookups, and tests) that the issue requests.
  • pnpm/pacquet#339: This PR extends and implements the DI pattern and env-var capability traits proposed in the issue.
  • pnpm/pnpm#11731: This PR modifies the Config::current API shape (signature and trait requirements) to which the issue is closely tied.

Possibly related PRs

  • pnpm/pnpm#11718: Refactors pacquet/crates/config around default_store_dir signature, trait wiring, and its SmartDefault integration—directly related to this PR's defaults refactor.
  • pnpm/pnpm#11708: Introduces the Host/Sys capability seam conventions that this PR extends with EnvVarOs/GetHomeDir/GetCurrentDir and integrates throughout Config::current and CLI call sites.

Suggested reviewers

  • zkochan

Poem

🐰 A rabbit hops through traits anew,
EnvVarOs, GetHomeDir too—
No more closures, just Sys to thread,
Through Config::current, tests spread,
DI seams now clean and true! 🎯

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately describes the main refactoring work: replacing callback-based dependency injection (FnOnce/FnMut closures) with trait-based capability injection across the pacquet codebase.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/refactor-dependency-injection-nMUVz

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

claude added 2 commits May 18, 2026 19:47
The "poor man DI" / "JS/TS pattern" framing carried context from the
discussion that produced the refactor, not the style guide itself.
Restate the convention in its own terms: a closure that only defers
a side-effecting read is a capability dressed as a parameter, and the
right shape for a capability is a trait on `Sys`.

https://claude.ai/code/session_01Btd6rp2MgfXb3ArHwSJpqA
The "use a direct value parameter, not a FnOnce closure" addition was
defending against a specific anti-pattern from the refactor that
produced this section. The positive guidance (trait-based DI on Sys
for side effects) already covers what readers need; the negative
phrasing is too absolute, and the lazy-evaluation carve-out only
exists to soften it.

https://claude.ai/code/session_01Btd6rp2MgfXb3ArHwSJpqA
@codecov-commenter

codecov-commenter commented May 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.84507% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.85%. Comparing base (cddf522) to head (6a29343).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
pacquet/crates/config/src/lib.rs 91.30% 10 Missing ⚠️
pacquet/crates/config/src/api.rs 66.66% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #11730      +/-   ##
==========================================
- Coverage   89.88%   89.85%   -0.03%     
==========================================
  Files         144      145       +1     
  Lines       16416    16424       +8     
==========================================
+ Hits        14755    14758       +3     
- Misses       1661     1666       +5     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.03      7.8±0.47ms   558.1 KB/sec    1.00      7.6±0.44ms   573.1 KB/sec

claude added 3 commits May 18, 2026 19:56
The "begrudging use behind pnpm/pacquet#343" phrasing reads as though
#343 introduced the direct env reads in default_store_dir /
default_modules_dir / default_virtual_store_dir. It did not — those
helpers have read process state since they existed; #343 tracks
retiring the env-mutating *tests* for them. The historical reference
doesn't carry any rule the reader needs, so drop it and let the
positive guidance stand on its own.

https://claude.ai/code/session_01Btd6rp2MgfXb3ArHwSJpqA
The `GetCurrentDir` doc comment linked to
`crate::defaults::default_store_dir`, but `defaults` is a private
module and the helper isn't re-exported, so rustdoc denies the
intra-doc link under `-D rustdoc::private-intra-doc-links` (the Doc
CI job runs with `-D warnings`). Point the example at
`Config::store_dir` instead — that's the public SmartDefault site
where the helper plugs in, which is the contract the trait's
production consumer cares about.

https://claude.ai/code/session_01Btd6rp2MgfXb3ArHwSJpqA
Pulls in dfd8fbf "chore: update pnpm-lock.yaml" (#11559), which bumps
`brace-expansion` from 5.0.5 to 5.0.6 — the upgrade the Audit
dependencies CI job is gating on. Also pulls in e01d2bc
"fix(pacquet/fs): fall back to absolute target across Windows drives"
(#11721), which doesn't touch any file this branch edits.

https://claude.ai/code/session_01Btd6rp2MgfXb3ArHwSJpqA
@github-actions

github-actions Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Scenario: Frozen Lockfile

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.414 ± 0.143 2.300 2.791 1.01 ± 0.07
pacquet@main 2.392 ± 0.077 2.299 2.506 1.00
pnpm 4.806 ± 0.123 4.671 5.024 2.01 ± 0.08
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.41430144168,
      "stddev": 0.14288989810496697,
      "median": 2.38894992398,
      "user": 2.7615984399999998,
      "system": 3.6343512999999996,
      "min": 2.29962797948,
      "max": 2.79062900148,
      "times": [
        2.39067405048,
        2.3872257974799997,
        2.79062900148,
        2.47478823148,
        2.40911149848,
        2.30185134248,
        2.36640876148,
        2.40295211248,
        2.31974564148,
        2.29962797948
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.3915785409800003,
      "stddev": 0.0768956008413182,
      "median": 2.37774740348,
      "user": 2.73857914,
      "system": 3.6439927999999995,
      "min": 2.29899098748,
      "max": 2.50605577448,
      "times": [
        2.43875745248,
        2.29899098748,
        2.50605577448,
        2.44397716548,
        2.29995147848,
        2.34746241948,
        2.37974556548,
        2.37574924148,
        2.32607310448,
        2.49902222048
      ]
    },
    {
      "command": "pnpm",
      "mean": 4.80582281618,
      "stddev": 0.12271078781517894,
      "median": 4.77339460748,
      "user": 8.29218234,
      "system": 4.1286124,
      "min": 4.671062178480001,
      "max": 5.02368115548,
      "times": [
        4.90037850948,
        4.73775214548,
        4.685754349480001,
        4.671062178480001,
        4.676704095480001,
        4.800104512480001,
        4.88800164648,
        4.7466847024800005,
        4.92810486648,
        5.02368115548
      ]
    }
  ]
}

Scenario: Frozen Lockfile (Hot Cache)

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 722.2 ± 10.2 712.6 748.3 1.00
pacquet@main 734.5 ± 35.1 688.1 798.7 1.02 ± 0.05
pnpm 2493.1 ± 122.3 2339.3 2729.6 3.45 ± 0.18
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.7221656387200002,
      "stddev": 0.01021492335387014,
      "median": 0.7203392209200001,
      "user": 0.4179009799999999,
      "system": 1.61878332,
      "min": 0.7126087474200001,
      "max": 0.7483014054200001,
      "times": [
        0.7483014054200001,
        0.7178336404200001,
        0.7159410674200001,
        0.7126087474200001,
        0.72373485542,
        0.7265018254200001,
        0.7132864874200001,
        0.7202033474200001,
        0.7227699164200001,
        0.7204750944200001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.7344691443200001,
      "stddev": 0.03506663505004002,
      "median": 0.7376287889200002,
      "user": 0.40161868000000006,
      "system": 1.61115362,
      "min": 0.6880551984200001,
      "max": 0.7986971064200001,
      "times": [
        0.7986971064200001,
        0.7285552384200001,
        0.7507327394200001,
        0.7630821164200001,
        0.7467023394200001,
        0.6962455304200001,
        0.7150897104200001,
        0.7572089874200001,
        0.7003224764200001,
        0.6880551984200001
      ]
    },
    {
      "command": "pnpm",
      "mean": 2.4931143183200004,
      "stddev": 0.12228393235799982,
      "median": 2.45142915942,
      "user": 3.0772481799999993,
      "system": 2.22363662,
      "min": 2.33931451842,
      "max": 2.7296457744200002,
      "times": [
        2.5543186064200003,
        2.44677034142,
        2.5596382444200003,
        2.7296457744200002,
        2.4208150184200004,
        2.45608797742,
        2.3640183184200003,
        2.4337400374200002,
        2.62679434642,
        2.33931451842
      ]
    }
  ]
}

@KSXGitHub KSXGitHub marked this pull request as ready for review May 19, 2026 00:02
@KSXGitHub KSXGitHub requested a review from zkochan as a code owner May 19, 2026 00:02
Copilot AI review requested due to automatic review settings May 19, 2026 00:02
@coderabbitai coderabbitai Bot added the area: config dependencies Changes related to configDependencies. label May 19, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR standardizes pacquet’s Rust DI seam by replacing callback-shaped “DI” (passing FnOnce/FnMut accessors) with capability traits on the Sys generic + a production Host provider. It also removes process-env mutation from several tests by routing env lookups through trait fakes.

Changes:

  • Introduces capability traits (EnvVarOs, GetHomeDir, GetCurrentDir) and implements them for Host, then refactors default_store_dir and Config::current to use these traits (and a direct start_dir: &Path parameter).
  • Adds pacquet-workspace’s crate-local EnvVarOs + Host provider and refactors workspace root-finder env lookup to use the trait seam.
  • Updates docs to discourage callback-shaped DI and reflect the canonical “capability traits + Host turbofish” pattern.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
pacquet/crates/workspace/src/root_finder/tests.rs Updates tests to use per-test EnvVarOs fakes instead of closures.
pacquet/crates/workspace/src/root_finder.rs Replaces env accessor closure with Sys: EnvVarOs capability trait.
pacquet/crates/workspace/src/lib.rs Adds api module and re-exports EnvVarOs/Host.
pacquet/crates/workspace/src/api.rs New workspace-local capability trait + Host provider for env var reads.
pacquet/crates/config/src/lib.rs Refactors Config::current signature and store-dir default to capability traits; updates tests accordingly.
pacquet/crates/config/src/defaults/tests.rs Updates default-store-dir tests to implement GetHomeDir/GetCurrentDir on fakes.
pacquet/crates/config/src/defaults.rs Refactors default_store_dir to Sys: EnvVar + GetHomeDir + GetCurrentDir (no accessor closures).
pacquet/crates/config/src/api.rs Adds capability traits (EnvVarOs, GetHomeDir, GetCurrentDir) and implements them for Host.
pacquet/crates/cli/src/cli_args.rs Updates production call site to Config::current::<Host, _>(&dir, ...).
pacquet/CODE_STYLE_GUIDE.md Updates guidance to discourage callback-DI and document the trait-based seam.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pacquet/crates/config/src/api.rs Outdated
Comment on lines 7 to 11
@@ -9,16 +9,15 @@
//! declares, with any per-test scenario data stored in a `static`
//! inside the test fn.
//!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
pacquet/crates/workspace/src/root_finder/tests.rs (1)

121-121: ⚡ Quick win

Use an allocation-free lowercase key comparison in the fake env provider.

This currently allocates a String on each call. Compare against a borrowed lowercase key constant/literal instead.

♻️ Proposed fix
-            (name == WORKSPACE_DIR_ENV_VAR.to_lowercase())
+            (name == "npm_config_workspace_dir")
                 .then(|| OsString::from("/lowercase/root"))

As per coding guidelines, "Choose owned vs. borrowed parameters to minimize copies; widen to the most encompassing type (&Path over &PathBuf, &str over &String) when it doesn't force extra copies".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pacquet/crates/workspace/src/root_finder/tests.rs` at line 121, The fake env
provider currently does an allocation by calling
WORKSPACE_DIR_ENV_VAR.to_lowercase() for each comparison (the expression name ==
WORKSPACE_DIR_ENV_VAR.to_lowercase()); change this to an allocation-free
comparison by either comparing against a pre-lowercased borrowed constant (e.g.
a &'static str like WORKSPACE_DIR_ENV_VAR_LOWER) or use
name.eq_ignore_ascii_case(WORKSPACE_DIR_ENV_VAR) /
name.eq_ignore_ascii_case(...) so no temporary String is created; update the
comparison in the fake env provider where WORKSPACE_DIR_ENV_VAR is referenced to
use one of these allocation-free approaches.
pacquet/crates/workspace/src/root_finder.rs (1)

124-124: ⚡ Quick win

Avoid per-call lowercase allocation for env-var fallback.

WORKSPACE_DIR_ENV_VAR.to_lowercase() allocates on every call. Prefer a lowercase &'static str constant and pass it directly.

♻️ Proposed fix
 pub(crate) const WORKSPACE_DIR_ENV_VAR: &str = "NPM_CONFIG_WORKSPACE_DIR";
+pub(crate) const WORKSPACE_DIR_ENV_VAR_LOWER: &str = "npm_config_workspace_dir";
@@
     Sys::var_os(WORKSPACE_DIR_ENV_VAR)
-        .or_else(|| Sys::var_os(&WORKSPACE_DIR_ENV_VAR.to_lowercase()))
+        .or_else(|| Sys::var_os(WORKSPACE_DIR_ENV_VAR_LOWER))
         .filter(|value| !value.is_empty())
         .map(PathBuf::from)

As per coding guidelines, "Choose owned vs. borrowed parameters to minimize copies; widen to the most encompassing type (&Path over &PathBuf, &str over &String) when it doesn't force extra copies".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pacquet/crates/workspace/src/root_finder.rs` at line 124, The call uses
WORKSPACE_DIR_ENV_VAR.to_lowercase() which allocates on every invocation;
introduce a static &'static str constant (e.g. WORKSPACE_DIR_ENV_VAR_LOWERCASE)
holding the precomputed lowercase name and replace the to_lowercase() call in
the Sys::var_os fallback with that constant so no per-call allocation occurs;
update any references in root_finder.rs that currently call to_lowercase() on
WORKSPACE_DIR_ENV_VAR to use the new constant.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pacquet/crates/config/src/lib.rs`:
- Around line 972-975: The test fakes currently delegate environment access to
the real host via Host::var_os (e.g., in the EnvVarOs impl for HostNoHome),
which makes tests flaky when ambient vars like NPM_CONFIG_WORKSPACE_DIR are set;
update these fake Sys/EnvVarOs implementations (including HostNoHome and the
other similar impls referenced around the other ranges) to return None
unconditionally (unless a specific test overrides behavior), so they do not read
real process env; locate the impl blocks named EnvVarOs for HostNoHome (and the
analogous fake types at the other cited locations) and replace the Host::var_os
call with a direct None return.

---

Nitpick comments:
In `@pacquet/crates/workspace/src/root_finder.rs`:
- Line 124: The call uses WORKSPACE_DIR_ENV_VAR.to_lowercase() which allocates
on every invocation; introduce a static &'static str constant (e.g.
WORKSPACE_DIR_ENV_VAR_LOWERCASE) holding the precomputed lowercase name and
replace the to_lowercase() call in the Sys::var_os fallback with that constant
so no per-call allocation occurs; update any references in root_finder.rs that
currently call to_lowercase() on WORKSPACE_DIR_ENV_VAR to use the new constant.

In `@pacquet/crates/workspace/src/root_finder/tests.rs`:
- Line 121: The fake env provider currently does an allocation by calling
WORKSPACE_DIR_ENV_VAR.to_lowercase() for each comparison (the expression name ==
WORKSPACE_DIR_ENV_VAR.to_lowercase()); change this to an allocation-free
comparison by either comparing against a pre-lowercased borrowed constant (e.g.
a &'static str like WORKSPACE_DIR_ENV_VAR_LOWER) or use
name.eq_ignore_ascii_case(WORKSPACE_DIR_ENV_VAR) /
name.eq_ignore_ascii_case(...) so no temporary String is created; update the
comparison in the fake env provider where WORKSPACE_DIR_ENV_VAR is referenced to
use one of these allocation-free approaches.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a9d37ea4-81c3-48b4-9925-3c810a8374f4

📥 Commits

Reviewing files that changed from the base of the PR and between dfd8fbf and 1e5cc5a.

📒 Files selected for processing (10)
  • pacquet/CODE_STYLE_GUIDE.md
  • pacquet/crates/cli/src/cli_args.rs
  • pacquet/crates/config/src/api.rs
  • pacquet/crates/config/src/defaults.rs
  • pacquet/crates/config/src/defaults/tests.rs
  • pacquet/crates/config/src/lib.rs
  • pacquet/crates/workspace/src/api.rs
  • pacquet/crates/workspace/src/lib.rs
  • pacquet/crates/workspace/src/root_finder.rs
  • pacquet/crates/workspace/src/root_finder/tests.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Agent
🧰 Additional context used
📓 Path-based instructions (1)
pacquet/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/*.rs: Log emissions must mirror pnpm's pnpm:<channel> events through globalLogger / logger.debug(...) / streamParser.write(...) calls with matching payload and ordering for @pnpm/cli.default-reporter compatibility
Prefer real fixtures (tempfile::TempDir, mocked registry, integration tests) over dependency-injection seams for testing, reserving DI only for branches real fixtures cannot cover (filesystem error kinds, deterministic time, process-global state, external-service paths)
Declare a newtype wrapper (struct) for branded string types instead of collapsing the brand into plain String or &str
If upstream pnpm always validates before construction of a branded string type, validate too by constructing only via TryFrom and/or FromStr
If upstream pnpm never validates a branded string type, brand only for type-safety by exposing an infallible From and From<&str> constructor
If upstream pnpm occasionally constructs a branded string type without validation, expose from_str_unchecked constructor alongside the validating constructor
Match upstream pnpm serde behavior for branded string types by using #[serde(try_from = "String")] for deserialization and #[serde(into = "String")] for serialization
Use #[derive(derive_more::From)] and #[derive(derive_more::Into)] for mechanical branded string type conversions instead of handwriting impl blocks
Model upstream string literal unions or string literal types as Rust enums instead of newtype wrappers
Treat string template literal types (e.g., ${string}@${string}) as branded string types with the same validation discipline as other branded strings
Choose owned vs. borrowed parameters to minimize copies; widen to the most encompassing type (&Path over &PathBuf, &str over &String) when it doesn't force extra copies
Prefer Arc::clone(&x) / Rc::clone(&x) over x.clone() for reference-counted types to make the cost visible at the call site
Follow test-logging guidance in the style guide: log before...

Files:

  • pacquet/crates/workspace/src/lib.rs
  • pacquet/crates/workspace/src/root_finder/tests.rs
  • pacquet/crates/config/src/defaults.rs
  • pacquet/crates/workspace/src/api.rs
  • pacquet/crates/cli/src/cli_args.rs
  • pacquet/crates/workspace/src/root_finder.rs
  • pacquet/crates/config/src/api.rs
  • pacquet/crates/config/src/defaults/tests.rs
  • pacquet/crates/config/src/lib.rs
🔇 Additional comments (10)
pacquet/crates/config/src/api.rs (1)

12-18: LGTM!

Also applies to: 36-76, 96-112

pacquet/crates/workspace/src/api.rs (1)

1-36: LGTM!

pacquet/crates/workspace/src/lib.rs (1)

20-27: LGTM!

pacquet/crates/config/src/defaults.rs (1)

1-1: LGTM!

Also applies to: 62-75, 87-92

pacquet/crates/config/src/defaults/tests.rs (1)

5-8: LGTM!

Also applies to: 22-28, 39-50, 68-79, 83-87, 94-110

pacquet/crates/workspace/src/root_finder.rs (1)

24-27: LGTM!

Also applies to: 105-123, 125-127

pacquet/crates/workspace/src/root_finder/tests.rs (1)

5-120: LGTM!

Also applies to: 122-129

pacquet/crates/config/src/lib.rs (1)

774-927: LGTM!

pacquet/crates/cli/src/cli_args.rs (1)

102-106: LGTM!

pacquet/CODE_STYLE_GUIDE.md (1)

511-514: LGTM!

Comment thread pacquet/crates/config/src/lib.rs
claude added 2 commits May 19, 2026 00:07
The module-level doc on the Host provider showed
`Config::current::<Host>(...)` (one generic), but the function now
takes two generic parameters (`Sys, Default`). The other example
lower in the same file already uses the two-param form
(`Config::current::<Host, _>(&dir, Default::default)`); align the
module-level mention with it.

Caught by Copilot review on #11730.

https://claude.ai/code/session_01Btd6rp2MgfXb3ArHwSJpqA
The `HostNoHome` / `HostUnreachableHome` test fakes (and the two
per-test `HostWithHome` fakes) delegated `EnvVarOs::var_os` to
`Host::var_os`, which means a developer with an ambient
`NPM_CONFIG_WORKSPACE_DIR` set in their shell could see those tests
get steered down `Config::current`'s env-var workspace-dir branch
unexpectedly. Tests that actually exercise that branch declare their
own `EnvVarOs` fakes (`HostWithEnvWorkspaceDir`,
`HostWithEmptyEnvWorkspaceDir`); the rest should return `None`
unconditionally so the env-var branch is dead by design.

`EnvVar::var` deliberately keeps the `Host` delegation because
`proxy_env_fallback_applies_through_current` mutates `HTTPS_PROXY` and
relies on the cascade reading it back.

Also pre-allocates the lowercase spelling of `NPM_CONFIG_WORKSPACE_DIR`
as `WORKSPACE_DIR_ENV_VAR_LOWER` and uses it directly in both the
production fallback and the `lowercase_env_var_is_honored_as_fallback`
test, dropping the per-call `.to_lowercase()` allocation. Negligible
perf-wise — `find_workspace_dir_from_env` runs once per pacquet
invocation — but the constant is clearer at the call site.

Caught by CodeRabbit review on #11730.

https://claude.ai/code/session_01Btd6rp2MgfXb3ArHwSJpqA
Copilot AI review requested due to automatic review settings May 19, 2026 00:11
coderabbitai[bot]
coderabbitai Bot previously requested changes May 19, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
pacquet/crates/config/src/lib.rs (1)

1476-1494: 💤 Low value

HostNoEnvNoHome duplicates HostNoHome — prefer the module-level fake.

The HostNoEnvNoHome struct is functionally identical to HostNoHome (lines 966–986): both delegate EnvVar to Host, return None for EnvVarOs, and return None for GetHomeDir. The test can use HostNoHome directly.

Suggested simplification
     #[test]
     pub fn single_project_anchors_modules_at_cwd() {
-        struct HostNoEnvNoHome;
-        impl EnvVar for HostNoEnvNoHome {
-            fn var(name: &str) -> Option<String> {
-                Host::var(name)
-            }
-        }
-        impl EnvVarOs for HostNoEnvNoHome {
-            fn var_os(_: &str) -> Option<OsString> {
-                None
-            }
-        }
-        impl GetHomeDir for HostNoEnvNoHome {
-            fn home_dir() -> Option<PathBuf> {
-                None
-            }
-        }
         let tmp = tempdir().unwrap();
         let config =
-            Config::current::<HostNoEnvNoHome, _>(tmp.path(), Config::new).expect("config loads");
+            Config::current::<HostNoHome, _>(tmp.path(), Config::new).expect("config loads");
         assert_eq!(config.modules_dir, tmp.path().join("node_modules"));
         assert_eq!(config.virtual_store_dir, tmp.path().join("node_modules/.pnpm"));
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pacquet/crates/config/src/lib.rs` around lines 1476 - 1494, The new test
introduces a redundant fake HostNoEnvNoHome that duplicates the existing
module-level HostNoHome; replace HostNoEnvNoHome with the existing HostNoHome in
the call to Config::current (and remove the added struct impls), i.e. use
Config::current::<HostNoHome, _>(tmp.path(), Config::new) so the test reuses the
shared fake implementations (EnvVar, EnvVarOs, GetHomeDir) instead of
duplicating HostNoEnvNoHome.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pacquet/crates/workspace/src/root_finder.rs`:
- Around line 127-129: The current chain calls
Sys::var_os(WORKSPACE_DIR_ENV_VAR).or_else(...).filter(...), so an empty
uppercase value (Some("")) prevents evaluating the lowercase fallback; change
the logic to treat empty as unset by applying the emptiness filter before the
fallback: call Sys::var_os(WORKSPACE_DIR_ENV_VAR).filter(|v|
!v.is_empty()).or_else(|| Sys::var_os(WORKSPACE_DIR_ENV_VAR_LOWER).filter(|v|
!v.is_empty())). Update the code that uses Sys::var_os, WORKSPACE_DIR_ENV_VAR
and WORKSPACE_DIR_ENV_VAR_LOWER accordingly and add a test covering the case
where the uppercase env var is set to an empty string and the lowercase var is
valid.

---

Nitpick comments:
In `@pacquet/crates/config/src/lib.rs`:
- Around line 1476-1494: The new test introduces a redundant fake
HostNoEnvNoHome that duplicates the existing module-level HostNoHome; replace
HostNoEnvNoHome with the existing HostNoHome in the call to Config::current (and
remove the added struct impls), i.e. use Config::current::<HostNoHome,
_>(tmp.path(), Config::new) so the test reuses the shared fake implementations
(EnvVar, EnvVarOs, GetHomeDir) instead of duplicating HostNoEnvNoHome.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0bcb36d2-a62b-4a6e-959d-fe678a17b68e

📥 Commits

Reviewing files that changed from the base of the PR and between 6eb0d70 and a11902f.

📒 Files selected for processing (3)
  • pacquet/crates/config/src/lib.rs
  • pacquet/crates/workspace/src/root_finder.rs
  • pacquet/crates/workspace/src/root_finder/tests.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • pacquet/crates/workspace/src/root_finder/tests.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Agent
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Code Coverage
  • GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (1)
pacquet/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/*.rs: Log emissions must mirror pnpm's pnpm:<channel> events through globalLogger / logger.debug(...) / streamParser.write(...) calls with matching payload and ordering for @pnpm/cli.default-reporter compatibility
Prefer real fixtures (tempfile::TempDir, mocked registry, integration tests) over dependency-injection seams for testing, reserving DI only for branches real fixtures cannot cover (filesystem error kinds, deterministic time, process-global state, external-service paths)
Declare a newtype wrapper (struct) for branded string types instead of collapsing the brand into plain String or &str
If upstream pnpm always validates before construction of a branded string type, validate too by constructing only via TryFrom and/or FromStr
If upstream pnpm never validates a branded string type, brand only for type-safety by exposing an infallible From and From<&str> constructor
If upstream pnpm occasionally constructs a branded string type without validation, expose from_str_unchecked constructor alongside the validating constructor
Match upstream pnpm serde behavior for branded string types by using #[serde(try_from = "String")] for deserialization and #[serde(into = "String")] for serialization
Use #[derive(derive_more::From)] and #[derive(derive_more::Into)] for mechanical branded string type conversions instead of handwriting impl blocks
Model upstream string literal unions or string literal types as Rust enums instead of newtype wrappers
Treat string template literal types (e.g., ${string}@${string}) as branded string types with the same validation discipline as other branded strings
Choose owned vs. borrowed parameters to minimize copies; widen to the most encompassing type (&Path over &PathBuf, &str over &String) when it doesn't force extra copies
Prefer Arc::clone(&x) / Rc::clone(&x) over x.clone() for reference-counted types to make the cost visible at the call site
Follow test-logging guidance in the style guide: log before...

Files:

  • pacquet/crates/workspace/src/root_finder.rs
  • pacquet/crates/config/src/lib.rs
🔇 Additional comments (13)
pacquet/crates/config/src/lib.rs (12)

8-8: LGTM!


187-188: LGTM!


774-780: LGTM!


784-796: LGTM!

Also applies to: 808-809, 853-856, 876-880


962-1008: LGTM!


1010-1029: LGTM!


1057-1105: LGTM!


1107-1162: LGTM!


1164-1195: LGTM!

Also applies to: 1225-1256


1271-1363: LGTM!

Also applies to: 1419-1431


1499-1548: LGTM!


1550-1584: LGTM!

pacquet/crates/workspace/src/root_finder.rs (1)

24-50: LGTM!

Also applies to: 109-126

Comment on lines +127 to 129
Sys::var_os(WORKSPACE_DIR_ENV_VAR)
.or_else(|| Sys::var_os(WORKSPACE_DIR_ENV_VAR_LOWER))
.filter(|value| !value.is_empty())

@coderabbitai coderabbitai Bot May 19, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# Find and examine the file
fd root_finder.rs --type f

Repository: pnpm/pnpm

Length of output: 97


🏁 Script executed:

# Read the specific file and lines around 127-129
cat -n pacquet/crates/workspace/src/root_finder.rs | head -140 | tail -30

Repository: pnpm/pnpm

Length of output: 1449


🏁 Script executed:

# Search for WORKSPACE_DIR_ENV_VAR definitions to understand the constants
rg "WORKSPACE_DIR_ENV_VAR" pacquet/crates/workspace/src/root_finder.rs -B 2 -A 2

Repository: pnpm/pnpm

Length of output: 708


🏁 Script executed:

# Check for any documentation or tests related to this behavior
rg "workspace.*dir\|WORKSPACE_DIR" pacquet/crates/workspace/src/ --type rs -i -l

Repository: pnpm/pnpm

Length of output: 80


🏁 Script executed:

# Search for tests or documentation related to workspace dir env logic
rg "workspace.*dir\|WORKSPACE_DIR" pacquet/crates/workspace/src/ -i --type-list

Repository: pnpm/pnpm

Length of output: 443


🏁 Script executed:

# Search for tests related to this function
rg "find_workspace_dir_from_env" pacquet/ -i -A 5 -B 2

Repository: pnpm/pnpm

Length of output: 8895


🏁 Script executed:

# Check for any test files for root_finder
fd root_finder pacquet/ --type f

Repository: pnpm/pnpm

Length of output: 97


🏁 Script executed:

# Look at the doc comment from the function to understand expected behavior
cat -n pacquet/crates/workspace/src/root_finder.rs | head -125 | tail -25

Repository: pnpm/pnpm

Length of output: 1348


🏁 Script executed:

# Read the test file to see what tests exist
cat -n pacquet/crates/workspace/src/root_finder/tests.rs

Repository: pnpm/pnpm

Length of output: 6090


🏁 Script executed:

# Search for the specific test case: uppercase empty, lowercase set
rg "empty.*lowercase\|lowercase.*empty" pacquet/crates/workspace/src/root_finder/tests.rs -A 5 -B 2

Repository: pnpm/pnpm

Length of output: 35


Empty uppercase env var blocks lowercase fallback.

If NPM_CONFIG_WORKSPACE_DIR is set to an empty string, .or_else() won't evaluate the lowercase fallback because the left side is Some("") (not None). The .filter() then drops the empty value, but the lowercase npm_config_workspace_dir is never tried. This contradicts the documented "empty means unset" behavior. A missing test case confirms this: there's no test covering uppercase=empty + lowercase=valid.

Proposed fix
-    Sys::var_os(WORKSPACE_DIR_ENV_VAR)
-        .or_else(|| Sys::var_os(WORKSPACE_DIR_ENV_VAR_LOWER))
-        .filter(|value| !value.is_empty())
+    Sys::var_os(WORKSPACE_DIR_ENV_VAR)
+        .filter(|value| !value.is_empty())
+        .or_else(|| {
+            Sys::var_os(WORKSPACE_DIR_ENV_VAR_LOWER)
+                .filter(|value| !value.is_empty())
+        })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Sys::var_os(WORKSPACE_DIR_ENV_VAR)
.or_else(|| Sys::var_os(WORKSPACE_DIR_ENV_VAR_LOWER))
.filter(|value| !value.is_empty())
Sys::var_os(WORKSPACE_DIR_ENV_VAR)
.filter(|value| !value.is_empty())
.or_else(|| {
Sys::var_os(WORKSPACE_DIR_ENV_VAR_LOWER)
.filter(|value| !value.is_empty())
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pacquet/crates/workspace/src/root_finder.rs` around lines 127 - 129, The
current chain calls Sys::var_os(WORKSPACE_DIR_ENV_VAR).or_else(...).filter(...),
so an empty uppercase value (Some("")) prevents evaluating the lowercase
fallback; change the logic to treat empty as unset by applying the emptiness
filter before the fallback: call Sys::var_os(WORKSPACE_DIR_ENV_VAR).filter(|v|
!v.is_empty()).or_else(|| Sys::var_os(WORKSPACE_DIR_ENV_VAR_LOWER).filter(|v|
!v.is_empty())). Update the code that uses Sys::var_os, WORKSPACE_DIR_ENV_VAR
and WORKSPACE_DIR_ENV_VAR_LOWER accordingly and add a test covering the case
where the uppercase env var is set to an empty string and the lowercase var is
valid.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skipping this — the proposed change would diverge from upstream pnpm.

Upstream's lookup at workspace/root-finder/src/index.ts is:

process.env['NPM_CONFIG_WORKSPACE_DIR'] ?? process.env['npm_config_workspace_dir']

JavaScript's ?? only falls through on null / undefined, not on "". So when NPM_CONFIG_WORKSPACE_DIR="" and npm_config_workspace_dir="/foo", upstream resolves to "" (the lowercase is never consulted), and the subsequent if (workspaceDir) truthy check then filters it out — net result: no env override, lowercase ignored.

The current Rust shape mirrors that exactly: or_else only fires on None, and the trailing .filter(|v| !v.is_empty()) is the equivalent of if (workspaceDir). Adding .filter() before .or_else() would make the lowercase value win in the uppercase=empty case, which is not what upstream does. Per pacquet's cardinal rule (any change must match how upstream pnpm implements the same feature), this stays as-is.

If a future upstream change starts honouring lowercase when uppercase is empty, pacquet should follow then.


Written by an agent (Claude Code, claude-opus-4-7).


Generated by Claude Code

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@KSXGitHub, thanks for the detailed explanation — you're right, and I apologize for the noise. The current chain (or_else + trailing .filter) faithfully mirrors the upstream JS ?? + if (workspaceDir) semantics, and my suggested reordering would have broken that parity by letting the lowercase value win when the uppercase one is explicitly set to "". I'll retract this concern.

🐇 (ear droop)


✏️ Learnings added
Learnt from: KSXGitHub
Repo: pnpm/pnpm PR: 11730
File: pacquet/crates/workspace/src/root_finder.rs:127-129
Timestamp: 2026-05-19T00:19:48.809Z
Learning: In `pacquet/crates/workspace/src/root_finder.rs`, the function `find_workspace_dir_from_env_with` uses `.or_else(|| Sys::var_os(WORKSPACE_DIR_ENV_VAR_LOWER)).filter(|v| !v.is_empty())` to mirror upstream pnpm's JS behavior: `process.env['NPM_CONFIG_WORKSPACE_DIR'] ?? process.env['npm_config_workspace_dir']` followed by `if (workspaceDir)`. JS `??` falls through only on null/undefined (not ""), so when the uppercase var is set to "" the lowercase is NOT consulted — the trailing `.filter` then discards the empty string. Applying `.filter` before `.or_else` would be wrong because it would let the lowercase var win when the uppercase is explicitly set to "" — contradicting upstream behavior.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Comment thread pacquet/crates/config/src/lib.rs Outdated
Comment on lines +1171 to +1173
// module-level constant — declare a `LazyLock` in the test
// body to give the static-initialised home_dir something to
// resolve to before the trait impl runs.
…ock comment

`HostNoEnvNoHome` (declared inside `single_project_anchors_modules_at_cwd`)
became functionally identical to the module-level `HostNoHome` once
`HostNoHome::var_os` was pinned to `None` in the previous commit:
both delegate `EnvVar` to `Host`, return `None` for `EnvVarOs`, and
return `None` for `GetHomeDir`. Drop the per-test duplicate and reuse
`HostNoHome`.

Also retargets `npmrc_in_home_folder_applies_registry`'s per-test
comment that called the home-dir holder a `LazyLock`; the code uses
`OnceLock` (set once at the top of the test body, read by the
associated-function `GetHomeDir::home_dir` impl).

Caught by CodeRabbit and Copilot review on #11730.

https://claude.ai/code/session_01Btd6rp2MgfXb3ArHwSJpqA
Copilot AI review requested due to automatic review settings May 19, 2026 00:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Comment thread pacquet/crates/config/src/lib.rs Outdated
}

/// Variant of [`HostNoHome`] that panics if [`GetHomeDir::home_dir`]
/// is consulted — documents the precondition that the test under
@KSXGitHub KSXGitHub marked this pull request as draft May 19, 2026 00:33
@KSXGitHub

Copy link
Copy Markdown
Contributor Author

PR has been converted into draft again because Claude Code is applying some bad advices from the AI reviewers that caused compiler errors.

…" doc

The phrase "the test under test" was confusing — `HostUnreachableHome`
documents a precondition of `Config::current` (the code under test),
not of the test itself. Reword accordingly.

Caught by Copilot review on #11730.

Also drops the prior clippy-fix commit (090dd1e) per the maintainer's
request — that commit's pre-existing-lint fixes are out of scope for
this PR, and a separate merge-with-main brings other lint surface (a
new `default_cache_dir` callback-DI helper) that needs to be addressed
together rather than piecemeal.

https://claude.ai/code/session_01Btd6rp2MgfXb3ArHwSJpqA
@KSXGitHub KSXGitHub force-pushed the claude/refactor-dependency-injection-nMUVz branch from 090dd1e to dde5162 Compare May 19, 2026 00:50
claude added 2 commits May 19, 2026 00:50
Pulls in:
- cddf522 feat(pacquet): port lockfile verification (minimumReleaseAge
  + trustPolicy) (#11729) — adds `default_cache_dir`, which was
  written against the pre-DI-refactor shape and uses the closure-based
  `HomeDir: FnOnce(...)` parameter plus a module-level `NoEnv` struct
  this branch's refactor retired. Followup commit ports it to the
  trait-based DI seam the PR introduces.
- c8d8fde feat(config-deps): support optionalDependencies with
  platform filtering (#11725) — does not touch files this branch
  edits.

https://claude.ai/code/session_01Btd6rp2MgfXb3ArHwSJpqA
…I seam

`default_cache_dir` was added on main (cddf522, #11729) in parallel
with this PR's DI refactor, using the pre-refactor closure-based
shape (`HomeDir: FnOnce() -> Option<PathBuf>` plus a module-level
`NoEnv` test fake). This branch's earlier commit `5d14def` retired
that module-level `NoEnv` in favour of per-test unit-struct fakes;
after merging main the resulting code referenced `NoEnv` at
`defaults/tests.rs:155` with no definition in scope (E0425).

Port `default_cache_dir` to match the same trait-based shape now used
by `default_store_dir`:

- Drop the `HomeDir: FnOnce(...)` parameter; bound `Sys: EnvVar +
  GetHomeDir` and call `Sys::home_dir()` instead.
- Collapse the `SmartDefault` expression on `Config::cache_dir` from
  `default_cache_dir::<Host, _>(home::home_dir)` to
  `default_cache_dir::<Host>()`.
- Rewrite the two `default_cache_dir` tests to implement [`EnvVar`]
  and [`GetHomeDir`] on per-test unit struct fakes (no module-level
  `NoEnv` reference).

https://claude.ai/code/session_01Btd6rp2MgfXb3ArHwSJpqA
@KSXGitHub KSXGitHub marked this pull request as ready for review May 19, 2026 00:55
Copilot AI review requested due to automatic review settings May 19, 2026 00:55
@KSXGitHub

Copy link
Copy Markdown
Contributor Author

@coderabbitai review.

@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown

@KSXGitHub Sure! I'll kick off a fresh review of the PR now.

(´・ω・`) Sorry to hear the earlier suggestions caused compiler errors — I'll take care to focus on correctness and avoid misleading advice this time.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: config dependencies Changes related to configDependencies.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants