Skip to content

fix(fs,package-manager): match upstream symlink-dir semantics in pacquet#11684

Merged
zkochan merged 7 commits into
mainfrom
symlink-dir
May 16, 2026
Merged

fix(fs,package-manager): match upstream symlink-dir semantics in pacquet#11684
zkochan merged 7 commits into
mainfrom
symlink-dir

Conversation

@zkochan

@zkochan zkochan commented May 16, 2026

Copy link
Copy Markdown
Member

Summary

Pacquet's pacquet_fs::symlink_dir was a thin wrapper over the
platform syscall and diverged from pnpm's
symlink-dir npm package (the
library @pnpm/fs.symlink-dependency uses) in three user-visible
ways. This PR closes those gaps and aligns pacquet's on-disk symlink
shape with what pnpm writes.

Per the cardinal rule in pacquet/AGENTS.md — "any change in pacquet
must match how the same feature is implemented in the TypeScript pnpm
CLI" — pacquet is the side moving toward pnpm here.

Divergences fixed

  1. Unix symlinks were absolute. Upstream writes the target as a
    path relative to the link's parent dir
    (path.relative(dirname(link), src)). Pacquet now does the same
    via pathdiff::diff_paths. The symlink_package.rs TODO calling
    this out is removed.

  2. Windows always created a junction. Upstream tries a true
    directory symlink first and only falls back to a junction on
    ERROR_PRIVILEGE_NOT_HELD (PermissionDenied). Pacquet now
    mirrors that probe and caches the winning branch process-wide in
    an AtomicU8, matching how the JS module rebinds
    createSymlinkAsync / createSymlinkSync after the first call.

  3. Re-installs left stale links in place. Upstream
    symlinkDependency uses symlinkDir's default { overwrite: true } — retarget stale symlinks, rename non-symlink occupants to
    .ignored_<basename>, mkdir -p missing parents. The pacquet
    primitive only exposed the bare syscall, and
    symlink_package.rs silently swallowed AlreadyExists. A new
    force_symlink_dir in pacquet_fs ports upstream's
    forceSymlink flow (including the macOS unlink fallback on the
    second-attempt path, per pnpm i get stuck forever on postinstall script #5909), and symlink_package
    now calls it.

Call sites that intentionally use { overwrite: false }
semantics (hoist::symlink_hoisted_dependencies,
register_project's heal path) stay on the bare symlink_dir
primitive.

Test plan

  • cargo test -p pacquet-fs — 21 passed (6 new tests covering
    relative encoding, idempotent re-symlink, stale retarget,
    occupant rename, missing-parent mkdir, and read-back).
  • cargo test -p pacquet-package-manager --lib — 268 passed.
    Three existing tests (install/tests.rs,
    create_symlink_layout/tests.rs,
    install_package_from_registry/tests.rs) were updated to
    compare against the new relative encoding (via pathdiff /
    canonicalize) instead of the prior absolute string.
  • cargo build --workspace — clean.
  • cargo clippy -p pacquet-fs -p pacquet-package-manager — no
    new warnings from this PR (the 6 pre-existing
    clippy::unnecessary_get_then_check /
    clippy::type_complexity warnings in unrelated test files are
    out of scope).
  • Windows CI — the symlink-first / junction-fallback branch is
    Linux-host-uncoverable; relying on CI to validate.

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

Summary by CodeRabbit

  • New Features

    • Symlink directories now store relative targets for portability and pnpm compatibility.
    • Idempotent symlink creation: reuses correct links, replaces stale ones, and moves blocking files aside.
    • Automatically creates missing parent directories when creating symlinks.
    • Improved Windows handling with cached privilege/probe fallbacks.
  • Bug Fixes

    • Stale or incorrect symlinks are reliably replaced during installs.
  • Tests

    • Added/updated tests validating relative-link behavior, idempotent semantics, and read/write parity.
  • Chores

    • Added pathdiff as a dev dependency for tests.

Review Change Stack

`pacquet_fs::symlink_dir` was a thin wrapper over the platform syscall
and diverged from pnpm's [`symlink-dir`](https://github.com/pnpm/symlink-dir)
npm package (v10.0.1) in three user-visible ways:

1. Unix symlinks stored the absolute target. Upstream writes the target
   as a path relative to the link's parent dir
   (`path.relative(dirname(link), src)`), so installs survive a project
   move and on-disk symlink contents match pnpm's byte-for-byte.
2. Windows always created junctions. Upstream tries a true directory
   symlink first and only falls back to a junction on
   `ERROR_PRIVILEGE_NOT_HELD`. Dev-Mode / Administrator users get a
   true symlink with pacquet now too. The choice is cached
   process-wide via `AtomicU8` so subsequent calls skip the EPERM
   probe, mirroring upstream's binding-rebind.
3. The library default `{ overwrite: true }` retargets stale links and
   moves non-symlink occupants to `.ignored_<basename>`. Pacquet only
   exposed the bare primitive, so `symlink_package` (the direct-dep
   linker, equivalent to pnpm's `symlinkDependency`) silently swallowed
   `AlreadyExists` — re-installs that retargeted a dependency left the
   stale link in place.

Adds `force_symlink_dir` and `ForceSymlinkOutcome` to `pacquet_fs`
porting upstream's `forceSymlink` flow: mkdir-on-`NotFound`, reuse on
matching target, unlink-and-retry on stale link, rename-to-`.ignored_*`
on non-symlink occupant (with the macOS unlink fallback on the second
attempt, per #5909). `symlink_package` calls it.

Existing call sites that intentionally use `{ overwrite: false }`
semantics (`hoist::symlink_hoisted_dependencies`, the
`register_project` heal path) stay on the bare `symlink_dir` primitive.

Tests:
- New tests in `pacquet/crates/fs/src/symlink_dir/tests.rs` cover the
  relative-target encoding, idempotent re-symlink, stale-link retarget,
  occupant rename, and missing-parent mkdir.
- Updated `create_symlink_layout/tests.rs`,
  `install_package_from_registry/tests.rs`, and `install/tests.rs` to
  compare against the relative encoding (via `pathdiff` /
  `canonicalize`) instead of the prior absolute string.

---
Written by an agent (Claude Code, claude-opus-4-7).
Copilot AI review requested due to automatic review settings May 16, 2026 09:33
@coderabbitai

coderabbitai Bot commented May 16, 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
📝 Walkthrough

Walkthrough

This PR makes directory symlinks store parent-relative targets on Unix, caches Windows junction vs symlink probing, adds an idempotent overwrite API force_symlink_dir (reuse, replace stale, relocate occupants), and updates package-manager and tests to validate the new behavior.

Changes

Relative symlinks and idempotent creation

Layer / File(s) Summary
Dependency setup
pacquet/crates/fs/Cargo.toml, pacquet/crates/package-manager/Cargo.toml
Add pathdiff workspace dependency to both fs and package-manager crates for relative path computation.
Core symlink_dir rework
pacquet/crates/fs/src/symlink_dir.rs
Rewrite symlink_dir to encode relative symlink paths on Unix (relative_target_for) and delegate to a platform-specific cached writer on Windows. Update read_symlink_dir documentation to reflect junction fallback behavior.
Idempotent force_symlink_dir API and helpers
pacquet/crates/fs/src/symlink_dir.rs
Add ForceSymlinkOutcome type and force_symlink_dir with recursive logic: reuse detection, stale-symlink replacement, parent creation, moving file/directory occupants to .ignored_* with pnpm-style warnings. Add helpers: existing_symlink_up_to_date, remove_occupant, rename_overwrite, and Windows junction-detection module with AtomicU8 caching.
Symlink_dir test suite
pacquet/crates/fs/src/symlink_dir/tests.rs
Add tests validating Unix relative target encoding, force_symlink_dir idempotency and retargeting, file occupant relocation, missing parent creation, and read_symlink_dir round-trip canonicalization.
Package manager symlink integration
pacquet/crates/package-manager/src/symlink_package.rs
Update symlink_package to call force_symlink_dir (overwrite-by-default semantics) and expand documentation to describe reuse vs replacement vs squatting behavior.
Package manager and consumer test updates
pacquet/crates/package-manager/src/create_symlink_layout/tests.rs, src/install/tests.rs, src/install_package_from_registry/tests.rs, pacquet/crates/cli/tests/add.rs, pacquet/crates/store-dir/src/project_registry.rs
Update tests to validate relative symlink contents using pathdiff::diff_paths and to canonicalize entry paths directly instead of using read_link, aligning assertions with relative-target storage.

🎯 3 (Moderate) | ⏱️ ~25 minutes

🐰 Symlinks now follow the path of kin,
Relative roots from parent within,
Idempotent with flair, stale links we replace,
File squatters get renamed, to .ignored_* space,
Tests hop in place to prove the case!

🚥 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 title accurately describes the main change: aligning pacquet's symlink behavior with pnpm's upstream implementation across fs and package-manager crates.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch symlink-dir

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.

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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@github-actions

github-actions Bot commented May 16, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.04      7.7±0.35ms   562.5 KB/sec    1.00      7.4±0.04ms   583.1 KB/sec

zkochan added 2 commits May 16, 2026 11:46
…ncoding

CI surfaced two more test sites that compared `fs::read_link` /
`dunce::canonicalize(read_link_output)` against the absolute target
path. Those resolved fine when pacquet wrote absolute symlinks but
fail now that the symlink contents are stored as paths relative to
the link's parent (matching upstream `symlink-dir`).

Canonicalize via the symlink path itself so the kernel resolves the
relative target, the same fix already applied to the
`package-manager` tests in the previous commit.

- `pacquet/crates/cli/tests/add.rs::should_symlink_correctly`
- `pacquet/crates/store-dir/src/project_registry.rs::register_creates_symlink_to_project_dir`

---
Written by an agent (Claude Code, claude-opus-4-7).
CI's `cargo dylint --all -- --all-targets --workspace` flagged two
multi-line macro invocations missing a trailing comma:

- `format!` in the mkdir-error wrapping branch (line 163).
- `matches!` over the rename-overwrite occupied-kind set (line 271).

Both surfaced under `-D warnings` via
`perfectionist::macro-trailing-comma`. Added the trailing commas;
local `cargo check -p pacquet-fs` is clean.

---
Written by an agent (Claude Code, claude-opus-4-7).
Copilot AI review requested due to automatic review settings May 16, 2026 09:49

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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@codecov-commenter

codecov-commenter commented May 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 61.36364% with 34 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.92%. Comparing base (f05845a) to head (eada30a).
⚠️ Report is 12 commits behind head on main.

Files with missing lines Patch % Lines
pacquet/crates/fs/src/symlink_dir.rs 63.75% 29 Missing ⚠️
...quet/crates/package-manager/src/symlink_package.rs 16.66% 5 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #11684      +/-   ##
==========================================
- Coverage   89.12%   88.92%   -0.21%     
==========================================
  Files         127      129       +2     
  Lines       14470    14666     +196     
==========================================
+ Hits        12897    13042     +145     
- Misses       1573     1624      +51     

☔ 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 16, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Scenario: Frozen Lockfile

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.485 ± 0.062 2.351 2.554 1.00
pacquet@main 2.541 ± 0.069 2.477 2.680 1.02 ± 0.04
pnpm 4.963 ± 0.059 4.841 5.053 2.00 ± 0.06
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.48463919444,
      "stddev": 0.061776005573105315,
      "median": 2.4939262837400005,
      "user": 2.6901946199999998,
      "system": 3.81208892,
      "min": 2.35142610224,
      "max": 2.55406450724,
      "times": [
        2.54176735924,
        2.55406450724,
        2.52463842724,
        2.35142610224,
        2.5217251432400003,
        2.4967426242400004,
        2.41668376424,
        2.49110994324,
        2.4580826572400003,
        2.4901514162400002
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.54103206154,
      "stddev": 0.06897087277471421,
      "median": 2.5193222302400002,
      "user": 2.64352842,
      "system": 3.8905692199999997,
      "min": 2.47696948024,
      "max": 2.6803493522400004,
      "times": [
        2.4958347612400003,
        2.6803493522400004,
        2.56878205424,
        2.62857830724,
        2.47696948024,
        2.50768984324,
        2.5596072102400003,
        2.47959714924,
        2.53095461724,
        2.48195784024
      ]
    },
    {
      "command": "pnpm",
      "mean": 4.96267035274,
      "stddev": 0.05906958402094379,
      "median": 4.96377433174,
      "user": 8.494304320000001,
      "system": 4.29851152,
      "min": 4.84093811524,
      "max": 5.0527600472400005,
      "times": [
        4.90864213724,
        5.0527600472400005,
        4.99516100324,
        4.9678596522400005,
        4.95968901124,
        4.94406685424,
        4.99945578024,
        5.01238727524,
        4.84093811524,
        4.94574365124
      ]
    }
  ]
}

Scenario: Frozen Lockfile (Hot Cache)

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 699.1 ± 50.0 666.1 839.0 1.00
pacquet@main 794.7 ± 88.2 709.3 978.0 1.14 ± 0.15
pnpm 2742.0 ± 88.0 2658.4 2935.8 3.92 ± 0.31
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.69906476008,
      "stddev": 0.05004767381943957,
      "median": 0.6856874772800001,
      "user": 0.38689724,
      "system": 1.57697986,
      "min": 0.6661487757800001,
      "max": 0.83901607578,
      "times": [
        0.83901607578,
        0.67910905478,
        0.6757591357800001,
        0.6985881907800001,
        0.6927374697800001,
        0.6833536667800001,
        0.6661487757800001,
        0.68802128778,
        0.6782053277800001,
        0.6897086157800001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.79473980198,
      "stddev": 0.08819724674546814,
      "median": 0.7532501362800001,
      "user": 0.39307654000000003,
      "system": 1.65113746,
      "min": 0.70927445178,
      "max": 0.9779977697800001,
      "times": [
        0.8893432507800001,
        0.70927445178,
        0.73717594478,
        0.7409030177800001,
        0.9779977697800001,
        0.7274115937800001,
        0.7655972547800001,
        0.7377112087800001,
        0.8677077347800001,
        0.79427579278
      ]
    },
    {
      "command": "pnpm",
      "mean": 2.7419538903800005,
      "stddev": 0.08795118575743076,
      "median": 2.71847687678,
      "user": 3.33364464,
      "system": 2.288340559999999,
      "min": 2.65839993778,
      "max": 2.93579695978,
      "times": [
        2.70548382178,
        2.67121579778,
        2.69426393078,
        2.75423133578,
        2.6633526037800004,
        2.8390229427800002,
        2.73146993178,
        2.76630164178,
        2.93579695978,
        2.65839993778
      ]
    }
  ]
}

zkochan added 2 commits May 16, 2026 12:06
`cargo fmt` collapsed five multi-line `assert!` calls in
`symlink_dir/tests.rs` to single lines but left the trailing comma
intact. Dylint's `perfectionist::macro-trailing-comma` rejects the
single-line-with-trailing-comma shape under `RUSTFLAGS=-D warnings`.

Removed the commas. `cargo test -p pacquet-fs` still passes.

---
Written by an agent (Claude Code, claude-opus-4-7).
The helper has always claimed to detect "symlink or junction" but
only called `junction::exists` on Windows, which checks the
`IO_REPARSE_TAG_MOUNT_POINT` reparse tag. With pacquet's symlink
writer now trying a true directory symlink first on Windows (and
only falling back to a junction on `PermissionDenied`), Windows
runners that have `SeCreateSymbolicLinkPrivilege` enabled —
including GitHub Actions `windows-latest`, which runs as
Administrator with Developer Mode on — get a true symlink at
`IO_REPARSE_TAG_SYMLINK`. The old helper returned `false` for those,
which fooled `should_install_dependencies` and
`install_resolves_env_var_in_npmrc_registry` into thinking the
install hadn't produced any symlink at all.

Combine `junction::exists` with `Path::is_symlink` so the helper
matches its name. No change on Unix.

---
Written by an agent (Claude Code, claude-opus-4-7).
Copilot AI review requested due to automatic review settings May 16, 2026 10:33

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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

zkochan added 2 commits May 16, 2026 12:45
Clippy's `needless_return` (denied under `-D warnings` on the Windows
`Clippy` step) flagged the trailing `return Ok(path.is_symlink())`
inside the `#[cfg(windows)]` block I added in the previous commit.
Fold the junction-or-symlink check into a single boolean expression
so each cfg branch ends with the result directly.

---
Written by an agent (Claude Code, claude-opus-4-7).
Upstream `symlink-dir`'s `resolveSrcOnTrueSymlink` (which feeds the
`fs.symlink(target, path, 'dir')` call) runs on both Unix and Windows
and always passes the path-relative-to-dirname target. The Windows
junction path takes the absolute target with a trailing backslash
instead, but the `junction` crate handles that wrapping internally.

Pacquet's Windows branch was calling
`std::os::windows::fs::symlink_dir(original, link)` with the absolute
target, which diverged from pnpm on hosts that have
`SeCreateSymbolicLinkPrivilege` (GitHub Actions Windows runners do —
they run as Administrator with Developer Mode on) and tripped the
two `create_symlink_layout` parity tests on Windows CI:

    Diff < left / right > :
    <"C:\\...\\.tmpdluV0r\\string-width@4.2.3\\node_modules\\string-width"
    >"..\\..\\string-width@4.2.3\\node_modules\\string-width"

Lift the existing Unix helper `relative_target_for` out of its
`cfg(any(unix, test))` gate so the Windows symlink branch can call
it too, and route both the cached `USE_SYMLINK` path and the
probe-and-cache path through a new `create_true_symlink` helper
that computes the relative form before the `CreateSymbolicLinkW`
syscall. The junction branch keeps the absolute `original`.

---
Written by an agent (Claude Code, claude-opus-4-7).
Copilot AI review requested due to automatic review settings May 16, 2026 11:37

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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@zkochan zkochan merged commit c178d13 into main May 16, 2026
28 of 29 checks passed
@zkochan zkochan deleted the symlink-dir branch May 16, 2026 11:57
github-actions Bot pushed a commit to Eyalm321/pnpm that referenced this pull request May 18, 2026
…uet (pnpm#11684)

* fix(fs,package-manager): match upstream symlink-dir semantics

`pacquet_fs::symlink_dir` was a thin wrapper over the platform syscall
and diverged from pnpm's [`symlink-dir`](https://github.com/pnpm/symlink-dir)
npm package (v10.0.1) in three user-visible ways:

1. Unix symlinks stored the absolute target. Upstream writes the target
   as a path relative to the link's parent dir
   (`path.relative(dirname(link), src)`), so installs survive a project
   move and on-disk symlink contents match pnpm's byte-for-byte.
2. Windows always created junctions. Upstream tries a true directory
   symlink first and only falls back to a junction on
   `ERROR_PRIVILEGE_NOT_HELD`. Dev-Mode / Administrator users get a
   true symlink with pacquet now too. The choice is cached
   process-wide via `AtomicU8` so subsequent calls skip the EPERM
   probe, mirroring upstream's binding-rebind.
3. The library default `{ overwrite: true }` retargets stale links and
   moves non-symlink occupants to `.ignored_<basename>`. Pacquet only
   exposed the bare primitive, so `symlink_package` (the direct-dep
   linker, equivalent to pnpm's `symlinkDependency`) silently swallowed
   `AlreadyExists` — re-installs that retargeted a dependency left the
   stale link in place.

Adds `force_symlink_dir` and `ForceSymlinkOutcome` to `pacquet_fs`
porting upstream's `forceSymlink` flow: mkdir-on-`NotFound`, reuse on
matching target, unlink-and-retry on stale link, rename-to-`.ignored_*`
on non-symlink occupant (with the macOS unlink fallback on the second
attempt, per pnpm#5909). `symlink_package` calls it.

Existing call sites that intentionally use `{ overwrite: false }`
semantics (`hoist::symlink_hoisted_dependencies`, the
`register_project` heal path) stay on the bare `symlink_dir` primitive.

Tests:
- New tests in `pacquet/crates/fs/src/symlink_dir/tests.rs` cover the
  relative-target encoding, idempotent re-symlink, stale-link retarget,
  occupant rename, and missing-parent mkdir.
- Updated `create_symlink_layout/tests.rs`,
  `install_package_from_registry/tests.rs`, and `install/tests.rs` to
  compare against the relative encoding (via `pathdiff` /
  `canonicalize`) instead of the prior absolute string.

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

* test(cli,store-dir): adapt remaining symlink assertions to relative encoding

CI surfaced two more test sites that compared `fs::read_link` /
`dunce::canonicalize(read_link_output)` against the absolute target
path. Those resolved fine when pacquet wrote absolute symlinks but
fail now that the symlink contents are stored as paths relative to
the link's parent (matching upstream `symlink-dir`).

Canonicalize via the symlink path itself so the kernel resolves the
relative target, the same fix already applied to the
`package-manager` tests in the previous commit.

- `pacquet/crates/cli/tests/add.rs::should_symlink_correctly`
- `pacquet/crates/store-dir/src/project_registry.rs::register_creates_symlink_to_project_dir`

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

* fix(fs): satisfy dylint perfectionist lints in symlink_dir

CI's `cargo dylint --all -- --all-targets --workspace` flagged two
multi-line macro invocations missing a trailing comma:

- `format!` in the mkdir-error wrapping branch (line 163).
- `matches!` over the rename-overwrite occupied-kind set (line 271).

Both surfaced under `-D warnings` via
`perfectionist::macro-trailing-comma`. Added the trailing commas;
local `cargo check -p pacquet-fs` is clean.

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

* fix(fs): drop trailing commas on collapsed single-line asserts

`cargo fmt` collapsed five multi-line `assert!` calls in
`symlink_dir/tests.rs` to single lines but left the trailing comma
intact. Dylint's `perfectionist::macro-trailing-comma` rejects the
single-line-with-trailing-comma shape under `RUSTFLAGS=-D warnings`.

Removed the commas. `cargo test -p pacquet-fs` still passes.

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

* test(testing-utils): recognize true symlinks in is_symlink_or_junction

The helper has always claimed to detect "symlink or junction" but
only called `junction::exists` on Windows, which checks the
`IO_REPARSE_TAG_MOUNT_POINT` reparse tag. With pacquet's symlink
writer now trying a true directory symlink first on Windows (and
only falling back to a junction on `PermissionDenied`), Windows
runners that have `SeCreateSymbolicLinkPrivilege` enabled —
including GitHub Actions `windows-latest`, which runs as
Administrator with Developer Mode on — get a true symlink at
`IO_REPARSE_TAG_SYMLINK`. The old helper returned `false` for those,
which fooled `should_install_dependencies` and
`install_resolves_env_var_in_npmrc_registry` into thinking the
install hadn't produced any symlink at all.

Combine `junction::exists` with `Path::is_symlink` so the helper
matches its name. No change on Unix.

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

* fix(testing-utils): drop needless return in is_symlink_or_junction

Clippy's `needless_return` (denied under `-D warnings` on the Windows
`Clippy` step) flagged the trailing `return Ok(path.is_symlink())`
inside the `#[cfg(windows)]` block I added in the previous commit.
Fold the junction-or-symlink check into a single boolean expression
so each cfg branch ends with the result directly.

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

* fix(fs): write relative target for Windows true symlinks too

Upstream `symlink-dir`'s `resolveSrcOnTrueSymlink` (which feeds the
`fs.symlink(target, path, 'dir')` call) runs on both Unix and Windows
and always passes the path-relative-to-dirname target. The Windows
junction path takes the absolute target with a trailing backslash
instead, but the `junction` crate handles that wrapping internally.

Pacquet's Windows branch was calling
`std::os::windows::fs::symlink_dir(original, link)` with the absolute
target, which diverged from pnpm on hosts that have
`SeCreateSymbolicLinkPrivilege` (GitHub Actions Windows runners do —
they run as Administrator with Developer Mode on) and tripped the
two `create_symlink_layout` parity tests on Windows CI:

    Diff < left / right > :
    <"C:\\...\\.tmpdluV0r\\string-width@4.2.3\\node_modules\\string-width"
    >"..\\..\\string-width@4.2.3\\node_modules\\string-width"

Lift the existing Unix helper `relative_target_for` out of its
`cfg(any(unix, test))` gate so the Windows symlink branch can call
it too, and route both the cached `USE_SYMLINK` path and the
probe-and-cache path through a new `create_true_symlink` helper
that computes the relative form before the `CreateSymbolicLinkW`
syscall. The junction branch keeps the absolute `original`.

---
Written by an agent (Claude Code, claude-opus-4-7).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants