feat(registry): implement pnpm-registry server and adopt it in pacquet's test mock#11898
Conversation
Adds the registry/ directory as a sibling top-level Rust project to pacquet/, hosting a pnpm-compatible npm registry server (verdaccio analogue) written in Rust. Currently a single placeholder crate pnpm-registry under registry/crates/pnpm-registry/, wired into the root Cargo workspace via the registry/crates/* glob. pnpm-registry, pnpm-registry-cli, and pnpm-registry-server are reserved on crates.io to prevent name squatting; only pnpm-registry lives in this repo.
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a new Rust crate Changespnpm-registry: Full-featured npm-compatible registry server
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Micro-Benchmark ResultsLinux |
Integrated-Benchmark Report (Linux)Scenario: Isolated linker: fresh restore, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 2.0719660001199998,
"stddev": 0.052360435791117654,
"median": 2.05770013802,
"user": 2.6671975999999997,
"system": 3.4929088399999997,
"min": 2.00368176552,
"max": 2.16482178452,
"times": [
2.0383348425200003,
2.02822783952,
2.06451652252,
2.05088375352,
2.06610218552,
2.04419655952,
2.00368176552,
2.16482178452,
2.11475343652,
2.1441413115200003
]
},
{
"command": "pacquet@main",
"mean": 2.02068917952,
"stddev": 0.06580179740971927,
"median": 2.00112112502,
"user": 2.7195522,
"system": 3.4629835399999997,
"min": 1.9475041955199999,
"max": 2.18556385752,
"times": [
1.98288780952,
1.98833249952,
2.0039558355200002,
1.98385791652,
1.9475041955199999,
2.01670018052,
2.18556385752,
2.0431110295200003,
1.99828641452,
2.05669205652
]
},
{
"command": "pnpm",
"mean": 4.62142910522,
"stddev": 0.06275028011521339,
"median": 4.62060150452,
"user": 8.0812595,
"system": 4.039469240000001,
"min": 4.506656771519999,
"max": 4.75326843852,
"times": [
4.6508683265199995,
4.75326843852,
4.506656771519999,
4.5774053945199995,
4.629353452519999,
4.61324739452,
4.6539253905199995,
4.6011857665199996,
4.627955614519999,
4.60042450252
]
}
]
}Scenario: Isolated linker: fresh restore, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 0.64658576022,
"stddev": 0.015169635463587409,
"median": 0.64268186232,
"user": 0.36310014,
"system": 1.4325313999999998,
"min": 0.63597789382,
"max": 0.68762423382,
"times": [
0.68762423382,
0.6466263948200001,
0.64231756382,
0.63597789382,
0.63620190382,
0.6373903588200001,
0.64304616082,
0.64071806282,
0.65052170282,
0.64543332682
]
},
{
"command": "pacquet@main",
"mean": 0.64360887652,
"stddev": 0.01519484401233153,
"median": 0.63917505582,
"user": 0.36070124000000003,
"system": 1.4388782,
"min": 0.62731663682,
"max": 0.68192553382,
"times": [
0.68192553382,
0.64684731282,
0.63369629082,
0.62731663682,
0.64564797782,
0.63816140982,
0.63319179282,
0.63908449282,
0.63926561882,
0.65095169882
]
},
{
"command": "pnpm",
"mean": 2.6428437381200003,
"stddev": 0.054161558559668234,
"median": 2.6385535303200003,
"user": 3.2198431399999996,
"system": 2.1768289999999997,
"min": 2.57072887782,
"max": 2.76302478382,
"times": [
2.66039223582,
2.65575655082,
2.57072887782,
2.67683126882,
2.63759035482,
2.62319539882,
2.62468578582,
2.57671541882,
2.76302478382,
2.63951670582
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 4.46079136094,
"stddev": 0.06058344001890242,
"median": 4.45299544274,
"user": 6.226288299999999,
"system": 3.47415558,
"min": 4.3805544872399995,
"max": 4.55310885824,
"times": [
4.42454223824,
4.42869573824,
4.52743358624,
4.4772951472399996,
4.52011312524,
4.55310885824,
4.48725328624,
4.38947621524,
4.3805544872399995,
4.41944092724
]
},
{
"command": "pacquet@main",
"mean": 4.51446738694,
"stddev": 0.10804016761029132,
"median": 4.488685845239999,
"user": 6.3102228,
"system": 3.49624778,
"min": 4.41695513824,
"max": 4.76824240724,
"times": [
4.4750829112399995,
4.50508506124,
4.46701745724,
4.62770025624,
4.52135034524,
4.50228877924,
4.4225235302399994,
4.4384279832399995,
4.76824240724,
4.41695513824
]
},
{
"command": "pnpm",
"mean": 6.106545974039999,
"stddev": 0.04269163139871392,
"median": 6.111024415739999,
"user": 10.439794299999999,
"system": 4.31590018,
"min": 6.02551687124,
"max": 6.16174542824,
"times": [
6.06655397724,
6.13630598324,
6.11155630824,
6.16068002224,
6.11049252324,
6.0812122002399995,
6.12428656924,
6.16174542824,
6.08710985724,
6.02551687124
]
}
]
}Scenario: Isolated linker: fresh install, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 3.70207631244,
"stddev": 0.1130171519320921,
"median": 3.69768980774,
"user": 4.01928062,
"system": 2.14688874,
"min": 3.55874616874,
"max": 3.95514486574,
"times": [
3.7905718457399997,
3.59422420174,
3.7015600377399998,
3.71764204474,
3.72496766074,
3.55874616874,
3.95514486574,
3.6042116737399996,
3.67987504774,
3.69381957774
]
},
{
"command": "pacquet@main",
"mean": 3.65654937394,
"stddev": 0.07967558564123971,
"median": 3.6245914887399997,
"user": 4.0024573199999995,
"system": 2.14416754,
"min": 3.57243483274,
"max": 3.80174377474,
"times": [
3.80174377474,
3.7300452377399997,
3.57243483274,
3.6149189327399998,
3.6342640447399996,
3.60075125374,
3.71363370474,
3.57834569974,
3.59320142274,
3.7261548357399996
]
},
{
"command": "pnpm",
"mean": 4.277146074639999,
"stddev": 0.041865835825971386,
"median": 4.27359119224,
"user": 5.47857922,
"system": 2.4391188400000003,
"min": 4.21227320874,
"max": 4.325532566740001,
"times": [
4.323982468740001,
4.21227320874,
4.314198016740001,
4.27795199174,
4.26024679074,
4.21285843274,
4.26570269574,
4.325532566740001,
4.30948418174,
4.26923039274
]
}
]
} |
|
| Branch | pr/11898 |
| Testbed | pacquet |
Click to view all benchmark results
| Benchmark | Latency | Benchmark Result milliseconds (ms) (Result Δ%) | Upper Boundary milliseconds (ms) (Limit %) |
|---|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 4,460.79 ms(-10.42%)Baseline: 4,979.55 ms | 5,975.46 ms (74.65%) |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 3,702.08 ms(-5.30%)Baseline: 3,909.10 ms | 4,690.92 ms (78.92%) |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 2,071.97 ms(-13.26%)Baseline: 2,388.78 ms | 2,866.53 ms (72.28%) |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 646.59 ms(-1.77%)Baseline: 658.21 ms | 789.85 ms (81.86%) |
Turns the pnpm-registry crate from a placeholder into a working verdaccio-shaped proxy server. Exposes two unauthenticated GET endpoints — `/:name` (and `/:scope/:name`) for packuments and `/:name/-/:filename` (and the scoped variant) for tarballs — that fetch from an upstream npm registry and cache responses on disk. Packuments have a configurable TTL; tarballs are cached indefinitely. `dist.tarball` URLs in packuments are rewritten to flow back through this server so cached tarballs get served from the local cache. Built on axum + tower-http, added to the workspace dependencies.
…ay statuses Replaces the bespoke `reqwest::Client` builder in `Upstream` with `pacquet_network::ThrottledClient::new_for_installs()`, so the registry inherits pnpm's tuned defaults — `User-Agent: pnpm`, HTTP/1.1, hickory DNS, pool and timeout settings, the concurrency semaphore, and (later) per-registry TLS routing — instead of rolling its own and missing every one of them. Also splits the gateway error response by failure mode rather than falling back to a blanket `502` on every upstream failure: * `is_timeout()` → `504 Gateway Timeout` * `is_connect()` → `503 Service Unavailable` * anything else / upstream 5xx → `502 Bad Gateway` Mirrors verdaccio's role in the proxy: the npm client already has the configured `fetch-retries` policy, so the registry's job is to surface accurate gateway status so the client's retry loop fires on truly transient failures and gives up on permanent ones. No proxy-side retry — see verdaccio v4.0.3 / #1331 for the same reasoning.
Replaces the buffer-then-serve tarball path with a streaming one:
* Cache hit: open the cached file and stream it as the response
body (chunked reads, no full-buffer).
* Cache miss: tee upstream chunks into a temp file owned by a
spawned task, forward each chunk to the client via a bounded
mpsc channel, and atomically promote the temp file to the final
cache path on stream completion. Upstream error or client
disconnect drops the temp file.
Channel capacity (16 chunks) provides natural backpressure: when
the client lags, the upstream-read loop stalls on `send`, so we
don't unboundedly buffer.
Also fixes a tmp-path race: `write_atomic` used to derive
`path.with_extension("tmp")`, so two concurrent writes to the
same cache path collided. Tmp filenames now embed a per-process
atomic counter and the pid; rename stays atomic on POSIX since
src and dst share a directory.
Packuments remain buffered — small JSON, and the URL rewrite
needs the whole document anyway.
Adds 10 tests that close the gaps listed in the previous coverage review: * `timeout_error_maps_to_gateway_timeout` (unit test in error.rs) — spins a TCP listener that holds connections open, fires reqwest with a sub-second timeout, asserts the resulting `is_timeout()` error maps to 504. Constructed inside the crate since `RegistryError` is `#[non_exhaustive]`. * `scoped_tarball_is_proxied` — scoped path (`/@scope/name/-/...`). * `packument_is_refetched_after_ttl_expires` — TTL set to 50 ms, asserts the upstream is hit twice across the boundary. * `stale_packument_is_served_when_upstream_fails` — warms the cache against a live upstream, then spins up a second router pointed at a dead port and confirms the on-disk packument is served. * `invalid_package_name_returns_bad_request` — `.hidden` rejected at the HTTP layer with 400. * `concurrent_tarball_fetches_settle_to_one_cache_file` — two parallel `oneshot` requests for the same uncached tarball, both succeed with matching bytes, exactly one final file remains in the cache dir with no `.tmp.*` survivors (validates the unique-tmp suffix change from the streaming commit). * `cache_tmp_open_failure_falls_back_to_uncached_stream` — points `cache_dir` at a regular file so `create_dir_all` fails, asserts the response still streams correctly. * `malformed_upstream_json_maps_to_bad_gateway` — upstream returns HTML instead of JSON, registry returns 502. * `upstream_stream_error_clears_cache` — hand-rolled TCP listener speaks HTTP/1.1, advertises Content-Length 1 MiB, sends 100 bytes, drops the socket. Body read errors and no cache files remain. * `client_disconnect_mid_stream_clears_cache` — drops the response before consuming the 8 MiB body (large enough that the tee channel can't buffer the whole thing), confirms no cache files remain after the tee task observes the dropped receiver. All 26 tests pass; the four timing-sensitive ones (TTL, concurrent, disconnect, mid-stream error) are stable across five sequential reruns.
CI surfaced three failure modes the local `cargo test` + `cargo
clippy` run didn't:
* `taplo format --check` reflowed the wide `axum` features array
into one-per-line.
* `rustdoc -D warnings` flagged a redundant explicit link target
in `lib.rs` (`[`router`](server::router)` → `[`router`]` since
`router` is re-exported at the crate root).
* The nightly Perfectionist dylint pass flagged the rest:
- single-letter generics on `Result<T, E>` and `FetchOutcome<T>`,
renamed to `Result<Value, Error>` and `FetchOutcome<Payload>`;
- single-letter function param `s` on `is_safe_segment`, renamed
to `segment`;
- single-letter closure params `|e|` over `read_dir` entries,
renamed to `|entry|`;
- `tokio::join!(app.clone().oneshot(...), ...)` bound to `let`s
before the macro call (perfectionist requires impure macro args
to be evaluated exactly once at the call site);
- trailing comma added to two multi-line `assert!` invocations.
Verified locally with `cargo dylint --all`, `cargo doc -D
warnings`, `taplo format --check`, and `cargo test`.
…de mock launcher
Three coupled changes that move pacquet's test registry off
Node + verdaccio and onto pnpm-registry itself:
* **pnpm-registry adopts the verdaccio storage layout.** The cache
now uses `<root>/<pkg>/package.json` + `<root>/<pkg>/<basename>.tgz`
(flat, no `-/` subdir) — the same shape `@pnpm/registry-mock`
publishes in its `registry/storage-cache/`. So a populated
verdaccio storage can be served directly with no conversion step.
* **pnpm-registry gains static-serve mode.** `Config.upstream` is
now `Option<String>`; a `None` upstream puts the server in
`--static` mode where cache misses become 404 and the storage
directory is treated as the authoritative source. The packument
rewriter is also generalised — it no longer needs to know what
prefix the source URL had (verdaccio's
`/{scope}/{name}/-/{scope}/{name}-ver.tgz` quirk vs. npm's
canonical form), it just extracts the basename and rebuilds.
`--static` and `--storage` land as CLI flags.
* **pacquet's mock launcher spawns pnpm-registry, not node.**
`pacquet/tasks/registry-mock` now builds the registry command via
`assert_cmd::cargo::cargo_bin("pnpm-registry")` against
`node_modules/@pnpm/registry-mock/registry/storage-cache`. The
`prepare` step is dropped (storage-cache ships ready to serve),
the node `launch.mjs` wrapper and `kill_verdaccio` process-tree
walker are gone, replaced by a one-process `kill_process_by_pid`.
`@pnpm/registry-mock` stays as a devDep purely for its shipped
storage fixtures.
Four new integration tests in `pnpm-registry` exercise static mode
end-to-end against the registry-mock storage (packument rewrite,
tarball stream, 404s for unknown package/tarball). 30 tests total
pass; dylint, clippy, and taplo are clean.
…_EXE
`assert_cmd::cargo::cargo_bin("pnpm-registry")` aborts at runtime
when called from a different crate than the binary's owner:
`CARGO_BIN_EXE_<name>` is only injected by cargo for integration
tests of the binary's defining crate, never for `cargo run` of a
sibling crate or for downstream tests. CI surfaced this as:
`CARGO_BIN_EXE_pnpm-registry` is unset
at the `just registry-mock launch` step.
Replace the call with a small `pnpm_registry_binary()` resolver
that honors `CARGO_BIN_EXE_pnpm-registry` if present, then falls
back to `$CARGO_TARGET_DIR/<profile>/pnpm-registry` (defaulting to
`<workspace_root>/target/<profile>/`) with the profile picked via
`cfg!(debug_assertions)`.
Also wire the `registry-mock` and `test` recipes in `justfile` to
`cargo build --bin=pnpm-registry` first — cargo doesn't implicitly
build the binary of a sibling crate that a downstream test happens
to spawn.
* `pacquet/tasks/registry-mock/src/pnpm_registry_command.rs` referenced `port_to_url` with an explicit intra-doc path that was both `rustdoc::redundant_explicit_links` (the label already resolves the same way) *and* `rustdoc::private_intra_doc_links` (`port_to_url` lives in a private module, so the link only worked under `--document-private-items`). Drop the link syntax — keep the inline-code `port_to_url` reference, no target. * The pacquet CI workflow already covers pnpm-registry tests: `Lint and Test` runs `just test`, which builds the `pnpm-registry` binary and runs all 30 pnpm-registry tests via workspace-wide nextest. The gap is purely in path filters — registry-only PRs didn't trigger CI at all. Add `registry/**` to both `pacquet-ci.yml` and `pacquet-codecov.yml`, and add `registry` to the typos check. Annotate the workflow with a comment explaining the intentional cross-stack coverage so future readers don't split it.
…dpoint
Pacquet's `add` command fetches `/<pkg>/<tag>` to resolve a
dist-tag to a single-version manifest (the same shape as one entry
of `packument.versions`). Our server only exposed the full
packument and tarball endpoints, so pacquet hit a 404, reqwest
tried to parse it as JSON, and the install failed with
`EOF while parsing a value`. CI surfaced this on the
`should_install_all_dependencies` / `should_add_to_package_json` /
`should_add_dev_dependency` / `should_add_peer_dependency` tests.
Implementation:
* `extract_version_manifest()` resolves the version-or-tag against
`dist-tags` (falling back to literal-version lookup), pulls the
matching `versions[v]` subobject, and rewrites its `dist.tarball`
to point at this server. Reuses `rewrite_tarball_urls`, which
now handles both packument shape (walk `versions[*].dist`) and
single-version manifest shape (rewrite top-level `dist`) in one
pass.
* `load_packument_bytes()` factored out of `serve_packument` so the
version-manifest handler shares the same cache-then-upstream
flow (with stale fallback on upstream failure).
* The router now uses unified 2- and 3-segment handlers that
dispatch on the `@` prefix and the literal-`-` middle segment.
axum's matchit can't disambiguate `/{scope}/{name}` from
`/{name}/{version}` at the router level, so the handler does it.
Adds 3 unit tests for `extract_version_manifest` and 3 integration
tests against `@pnpm/registry-mock` storage (dist-tag, literal
version, unknown version → 404). Brings the total to 36 tests.
…serve Verdaccio in @pnpm/registry-mock is configured with `'**': proxy: npmjs` (see registry/config.yaml) — the storage holds the scoped fixtures (@foo, @pnpm.e2e, ...) and anything else is fetched from registry.npmjs.org on demand. Pacquet's install tests rely on this: `should_install_all_dependencies` pulls in `is-positive`, `lifecycle_scripts_run_in_dependency_order` needs `json-append`, etc. — none of which are in registry-mock's storage. Switching the launcher to `--static` mode 404'd those requests, breaking those tests. Restore the verdaccio-equivalent behavior by spawning `pnpm-registry` in proxy mode against `https://registry.npmjs.org`, with the storage-cache as the cache dir. Set `--packument-ttl-secs 31536000` (one year) so the fixture packuments stay authoritative: their on-disk mtime is whenever the npm tarball was built (days/weeks/months old), so any sane TTL would mark them stale and the refetch would 404 against npm where the fixtures don't exist.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #11898 +/- ##
==========================================
+ Coverage 87.84% 87.88% +0.03%
==========================================
Files 206 214 +8
Lines 24525 25236 +711
==========================================
+ Hits 21545 22178 +633
- Misses 2980 3058 +78 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
…exe relink
Windows CI failed on `just test` after `just registry-mock launch`:
error: failed to remove file `target\debug\pnpm-registry.exe`
Caused by: Access is denied. (os error 5)
The flow: `just registry-mock launch` builds and spawns
`pnpm-registry.exe` (Windows now file-locks the running binary),
then `just test` runs `cargo nextest run` which wants to re-link
the same .exe with a slightly different fingerprint than what
plain `cargo build` produced — and Windows refuses.
The fix is to pre-build at *exactly* the invocation
`cargo nextest run` will reuse, so the test phase finds
everything already built and re-links nothing:
* Drop the `cargo build --bin=pnpm-registry` from the `test`
recipe — it had different fingerprint semantics from nextest's
own internal build and bought us nothing.
* Replace the `registry-mock` recipe's `cargo build --bin=pnpm-registry`
with `cargo nextest run --no-run`. Workspace-wide is required:
a `-p pnpm-registry`-scoped pre-build still triggered a re-link
during workspace-wide test runs (feature unification differs).
Verified locally: after the fix, `cargo clean -p pnpm-registry &&
just registry-mock launch && just test` logs `Compiling
pnpm-registry` exactly once (in launch), never in test, and all
2043 pacquet tests pass.
…the mock
`just integrated-benchmark` auto-spawned the mock via
`AutoMockInstance::load_or_init()`, which calls our
`pnpm_registry_binary()` resolver — and that aborted with:
pnpm-registry binary not found at "target/debug/pnpm-registry"
— run `cargo build -p pnpm-registry` before invoking the mock
The benchmark workflow only builds `integrated-benchmark` and the
pacquet revisions under test, never `pnpm-registry`. Add the
pre-build to the recipe so any invocation (CI or local) has the
binary in place. `cargo build --bin=pnpm-registry` (not the
nextest variant the `registry-mock` recipe uses) is fine here:
the benchmark doesn't run cargo tests against pnpm-registry, so
there's no fingerprint to match.
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@registry/AGENTS.md`:
- Around line 36-47: The fenced code block in AGENTS.md containing the registry
crate tree is missing a language tag causing markdownlint MD040; update that
block by adding a language identifier (e.g., "text") after the opening backticks
in the block that starts with "registry/" so the fence becomes ```text and keep
the contents unchanged.
🪄 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: 61977120-0d8f-4757-8718-df7b7c8bb4bc
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (29)
.github/workflows/pacquet-ci.yml.github/workflows/pacquet-codecov.ymlCargo.tomljustfilepacquet/tasks/registry-mock/Cargo.tomlpacquet/tasks/registry-mock/launch.mjspacquet/tasks/registry-mock/src/dirs.rspacquet/tasks/registry-mock/src/kill_verdaccio.rspacquet/tasks/registry-mock/src/lib.rspacquet/tasks/registry-mock/src/mock_instance.rspacquet/tasks/registry-mock/src/node_registry_mock.rspacquet/tasks/registry-mock/src/pnpm_registry_command.rspacquet/tasks/registry-mock/src/process_kill.rspacquet/tasks/registry-mock/src/registry_anchor.rspacquet/tasks/registry-mock/src/registry_info.rsregistry/AGENTS.mdregistry/crates/pnpm-registry/Cargo.tomlregistry/crates/pnpm-registry/README.mdregistry/crates/pnpm-registry/src/cache.rsregistry/crates/pnpm-registry/src/config.rsregistry/crates/pnpm-registry/src/error.rsregistry/crates/pnpm-registry/src/lib.rsregistry/crates/pnpm-registry/src/main.rsregistry/crates/pnpm-registry/src/package_name.rsregistry/crates/pnpm-registry/src/server.rsregistry/crates/pnpm-registry/src/streaming.rsregistry/crates/pnpm-registry/src/upstream.rsregistry/crates/pnpm-registry/tests/registry_mock.rsregistry/crates/pnpm-registry/tests/server.rs
💤 Files with no reviewable changes (3)
- pacquet/tasks/registry-mock/launch.mjs
- pacquet/tasks/registry-mock/src/kill_verdaccio.rs
- pacquet/tasks/registry-mock/src/node_registry_mock.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). (2)
- GitHub Check: ubuntu-latest / Node.js 24 / Test
- GitHub Check: Run benchmark on ubuntu-latest
🧰 Additional context used
📓 Path-based instructions (3)
registry/crates/*/Cargo.toml
📄 CodeRabbit inference engine (registry/AGENTS.md)
Name registry-only crates under
registry/crates/<short-name>/with the package namepnpm-registry-<short-name>inCargo.toml; use thepnpm-registry-prefix exclusively for registry-only crates
Files:
registry/crates/pnpm-registry/Cargo.toml
pacquet/**/*.rs
📄 CodeRabbit inference engine (pacquet/AGENTS.md)
pacquet/**/*.rs: When porting a function that firespnpm:<channel>events throughglobalLogger,logger.debug(), orstreamParser.write(), mirror the call site, payload, and ordering so the reporter parses pacquet's NDJSON the same way it parses pnpm's.
Declare a newtype wrapper for branded string types. Do not collapse the brand into a plainStringor&str.
If upstream always validates before construction, validate in pacquet's wrapper too. The wrapper must construct only viaTryFrom<String>and/orFromStr. Do not provide an infallible public constructor.
If upstream never validates, just brand for type-safety. Expose an infallibleFrom<String>(andFrom<&str>when convenient).
If upstream occasionally constructs without validation, exposefrom_str_uncheckedas an escape hatch alongside the validating constructor.
Match upstream serde behavior for branded types that cross JSON, YAML, or INI boundaries. Use#[serde(try_from = "String")]for deserialization and#[serde(into = "String")]for serialization.
Use#[derive(derive_more::From)]and#[derive(derive_more::Into)]for mechanical conversion impls. Fall back to manualimplonly when conversion needs custom logic.
String-literal unions should becomeenums, not newtype wrappers. Model closed sets of valid string values as enums.
Template literal types should be treated as branded strings with validation discipline from rules 2-5.
Choose owned vs. borrowed parameters to minimize copies. Widen to the most encompassing type (&Pathover&PathBuf,&strover&String) when it doesn't force extra copies.
PreferArc::clone(&x)/Rc::clone(&x)overx.clone()for reference-counted types, so the cost is visible at the call site.
Follow Rust API Guidelines for naming conventions.
Do not use star imports inside module bodies. Writeuse super::{Foo, bar}instead ofuse super::*;. Two forms stay allowed: external-crate preludes likeuse rayon::prelude::*;and root-of-module re-...
Files:
pacquet/tasks/registry-mock/src/process_kill.rspacquet/tasks/registry-mock/src/dirs.rspacquet/tasks/registry-mock/src/pnpm_registry_command.rspacquet/tasks/registry-mock/src/lib.rspacquet/tasks/registry-mock/src/registry_anchor.rspacquet/tasks/registry-mock/src/mock_instance.rspacquet/tasks/registry-mock/src/registry_info.rs
registry/**/*.rs
📄 CodeRabbit inference engine (registry/AGENTS.md)
Follow the pacquet code-style guide (
../pacquet/CODE_STYLE_GUIDE.md) for Rust-level conventions including imports, naming, ownership, and error handlingFollow the pacquet contributing guide (
../pacquet/CONTRIBUTING.md) for Rust test layout and testing practices
Files:
registry/crates/pnpm-registry/src/error.rsregistry/crates/pnpm-registry/src/streaming.rsregistry/crates/pnpm-registry/src/config.rsregistry/crates/pnpm-registry/src/lib.rsregistry/crates/pnpm-registry/src/main.rsregistry/crates/pnpm-registry/src/upstream.rsregistry/crates/pnpm-registry/tests/registry_mock.rsregistry/crates/pnpm-registry/src/package_name.rsregistry/crates/pnpm-registry/tests/server.rsregistry/crates/pnpm-registry/src/cache.rsregistry/crates/pnpm-registry/src/server.rs
🧠 Learnings (3)
📓 Common learnings
Learnt from: CR
Repo: pnpm/pnpm
Timestamp: 2026-05-24T14:18:56.968Z
Learning: Read the monorepo-wide conventions documented in `../AGENTS.md` first, which covers GitHub PR workflow, signing agent-authored content, conventional commit messages, code-reuse philosophy, and never ignoring test failures
Learnt from: CR
Repo: pnpm/pnpm
Timestamp: 2026-05-24T14:18:56.968Z
Learning: Prefer existing `pacquet-*` crates over writing new code; check whether `pacquet-*` already solves non-trivial problems before implementing new functionality
Learnt from: CR
Repo: pnpm/pnpm
Timestamp: 2026-05-24T14:18:56.968Z
Learning: Declare new third-party dependencies in `[workspace.dependencies]` at the root workspace before using them; adding a new third-party crate requires an explicit human request
Learnt from: CR
Repo: pnpm/pnpm
Timestamp: 2026-05-24T14:18:56.968Z
Learning: Use Conventional Commits with `registry` as the scope in commit messages (e.g., `feat(registry): ...`, `fix(registry): ...`)
Learnt from: CR
Repo: pnpm/pnpm
Timestamp: 2026-05-24T14:18:56.968Z
Learning: Run `just check` (cargo check --locked --workspace --all-targets), `just test` (cargo nextest run), `just lint` (cargo clippy --workspace --all-targets -- --deny warnings), and `just fmt` (cargo fmt + taplo format) before declaring work done
📚 Learning: 2026-05-20T23:07:58.444Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11784
File: pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs:120-133
Timestamp: 2026-05-20T23:07:58.444Z
Learning: When reviewing code in this pacquet Rust port, follow the upstream pnpm compatibility rule: only match pnpm’s behavior exactly. Do not propose review changes that intentionally deviate from pnpm’s documented/observed behavior, even if pnpm appears buggy. If you identify a real bug in pnpm behavior, the review should prioritize fixing it upstream in pnpm first, and avoid implementing a pnpm-behavior workaround here unless the same fix has already landed upstream.
Applied to files:
pacquet/tasks/registry-mock/src/process_kill.rspacquet/tasks/registry-mock/src/dirs.rspacquet/tasks/registry-mock/src/pnpm_registry_command.rspacquet/tasks/registry-mock/src/lib.rspacquet/tasks/registry-mock/src/registry_anchor.rspacquet/tasks/registry-mock/src/mock_instance.rspacquet/tasks/registry-mock/src/registry_info.rs
📚 Learning: 2026-05-14T17:38:34.573Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11640
File: pacquet/AGENTS.md:1-6
Timestamp: 2026-05-14T17:38:34.573Z
Learning: In this repository, files named `AGENTS.md` are agent-guide instructions for AI coding tools (e.g., Claude Code/Codex/Cursor) following the agentsmd.net convention, not documentation of software agents or an agentic framework. When reviewing `**/AGENTS.md`, do not apply review rules intended for agentic-framework documentation (e.g., documenting agent capabilities, inputs/outputs, or integration points); instead, treat these files as tooling/assistive guidance for contributors.
Applied to files:
registry/AGENTS.md
🪛 LanguageTool
registry/AGENTS.md
[grammar] ~20-~20: Ensure spelling is correct
Context: ...ock stays unified. ## Relationship to pacquet - **pacquet/`** is a port of the pnpm CLI. Its cardin...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
🪛 markdownlint-cli2 (0.22.1)
registry/AGENTS.md
[warning] 36-36: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🔇 Additional comments (42)
Cargo.toml (1)
3-3: LGTM!Also applies to: 69-70, 74-80, 128-129, 132-132
justfile (1)
57-57: LGTM!Also applies to: 87-93, 95-95, 98-101, 103-103
.github/workflows/pacquet-ci.yml (1)
3-9: LGTM!Also applies to: 19-19, 38-38, 152-152
.github/workflows/pacquet-codecov.yml (1)
14-14: LGTM!Also applies to: 20-20
registry/crates/pnpm-registry/src/streaming.rs (4)
1-24: LGTM!
25-59: LGTM!
61-74: LGTM!
76-114: LGTM!registry/crates/pnpm-registry/tests/server.rs (15)
1-26: LGTM!
27-72: LGTM!
74-93: LGTM!
95-128: LGTM!
130-146: LGTM!
148-189: LGTM!
191-202: LGTM!
204-220: LGTM!
222-233: LGTM!
235-257: LGTM!
259-288: LGTM!
290-326: LGTM!
328-384: LGTM!
386-434: LGTM!
436-548: LGTM!pacquet/tasks/registry-mock/Cargo.toml (1)
4-4: LGTM!pacquet/tasks/registry-mock/src/dirs.rs (1)
40-55: LGTM!pacquet/tasks/registry-mock/src/lib.rs (1)
4-4: LGTM!Also applies to: 6-6, 13-13
pacquet/tasks/registry-mock/src/mock_instance.rs (1)
2-3: LGTM!Also applies to: 16-16, 24-29, 82-85, 90-90, 103-103
pacquet/tasks/registry-mock/src/pnpm_registry_command.rs (1)
1-76: LGTM!pacquet/tasks/registry-mock/src/process_kill.rs (1)
1-20: LGTM!pacquet/tasks/registry-mock/src/registry_anchor.rs (1)
1-1: LGTM!Also applies to: 47-49
pacquet/tasks/registry-mock/src/registry_info.rs (1)
1-1: LGTM!Also applies to: 80-82
registry/crates/pnpm-registry/tests/registry_mock.rs (1)
1-181: LGTM!registry/crates/pnpm-registry/Cargo.toml (1)
1-44: LGTM!registry/crates/pnpm-registry/README.md (1)
1-6: LGTM!registry/crates/pnpm-registry/src/lib.rs (1)
1-21: LGTM!registry/crates/pnpm-registry/src/main.rs (1)
1-67: LGTM!registry/crates/pnpm-registry/src/package_name.rs (1)
1-110: LGTM!registry/crates/pnpm-registry/src/config.rs (1)
1-60: LGTM!registry/crates/pnpm-registry/src/error.rs (1)
1-115: LGTM!registry/crates/pnpm-registry/src/upstream.rs (1)
30-80: LGTM!Also applies to: 91-143, 144-246
registry/crates/pnpm-registry/src/server.rs (1)
38-59: LGTM!Also applies to: 66-199, 211-289
registry/crates/pnpm-registry/src/cache.rs (1)
52-61: ⚡ Quick winWindows
renamereplacement workaround likely unnecessary
The specific claim thattokio::fs::rename/std::fs::renamecan fail on Windows just because the destination exists doesn’t match Rust’s Windows behavior:renameusesMOVEFILE_REPLACE_EXISTING, so overwriting an existing destination should not yield anAlreadyExists-style failure. The proposedAlreadyExists→remove_filefallback is therefore unlikely to help; if you’re seeing Windows failures, they’re more likely due to other error modes (e.g., sharing/lock/open-handle conditions), which should be handled based on the actualio::ErrorKindobserved.> Likely an incorrect or invalid review comment.
… test cleanup Three findings from a second AI reviewer: * **Stale doc comments** referenced `--static` mode in `pacquet/tasks/registry-mock/src/dirs.rs:42` and `mock_instance.rs:84`, but we switched the launcher to proxy mode (with `npmjs.org` upstream + 1-year TTL) in 1ab9a9e so off-storage packages like `is-positive` fall through to npm. Update both comments to point at `pnpm_registry_command` where the rationale is laid out. * **Mid-stream cache-write failures used to kill the client.** If `write.file.write_all` errored mid-body (disk full, FS error, etc.), `run_tee` would send the error to the client and abort — even though upstream was healthy. Align with how `serve_tarball` handles `open_tarball_tmp` failures ("streaming without cache"): on a write error, abandon the temp file and *continue* forwarding upstream bytes to the client. The cache is now best-effort throughout. `cache_write: Option<TarballWrite>` is the state switch. * **Test cleanup sleeps were timing-fragile.** `upstream_stream_error_clears_cache` and `client_disconnect_mid_stream_clears_cache` slept 100/200ms before asserting no `.tgz` survivors, with `if dir.exists()` papering over the timing dependency. Replaced with a `await_no_tgz` helper that polls for the abandon signal up to a 1-second budget — tests fail fast on success (return on first iteration) and have headroom on slow Windows CI.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@registry/crates/pnpm-registry/tests/server.rs`:
- Around line 525-538: The await_no_tgz function only looks for .tgz files;
update its directory scan (the closure that computes still_present in
await_no_tgz) to also detect orphaned temp files (e.g., filenames containing
".tmp." or matching the .tmp.* pattern) so the loop waits until neither .tgz nor
.tmp.* files exist; keep the same structure (dir.read_dir().map(|iter| { ...
}).unwrap_or(false)) and extend the any(...) predicate to check
entry.file_name() for either ends_with(".tgz") or the tmp-pattern.
🪄 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: b8ac3d42-48ea-4683-aee9-bf17a5fbe2cb
📒 Files selected for processing (4)
pacquet/tasks/registry-mock/src/dirs.rspacquet/tasks/registry-mock/src/mock_instance.rsregistry/crates/pnpm-registry/src/streaming.rsregistry/crates/pnpm-registry/tests/server.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: Lint and Test (windows-latest)
- GitHub Check: Lint and Test (ubuntu-latest)
- GitHub Check: Lint and Test (macos-latest)
- GitHub Check: Doc
- GitHub Check: Code Coverage
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (2)
pacquet/**/*.rs
📄 CodeRabbit inference engine (pacquet/AGENTS.md)
pacquet/**/*.rs: When porting a function that firespnpm:<channel>events throughglobalLogger,logger.debug(), orstreamParser.write(), mirror the call site, payload, and ordering so the reporter parses pacquet's NDJSON the same way it parses pnpm's.
Declare a newtype wrapper for branded string types. Do not collapse the brand into a plainStringor&str.
If upstream always validates before construction, validate in pacquet's wrapper too. The wrapper must construct only viaTryFrom<String>and/orFromStr. Do not provide an infallible public constructor.
If upstream never validates, just brand for type-safety. Expose an infallibleFrom<String>(andFrom<&str>when convenient).
If upstream occasionally constructs without validation, exposefrom_str_uncheckedas an escape hatch alongside the validating constructor.
Match upstream serde behavior for branded types that cross JSON, YAML, or INI boundaries. Use#[serde(try_from = "String")]for deserialization and#[serde(into = "String")]for serialization.
Use#[derive(derive_more::From)]and#[derive(derive_more::Into)]for mechanical conversion impls. Fall back to manualimplonly when conversion needs custom logic.
String-literal unions should becomeenums, not newtype wrappers. Model closed sets of valid string values as enums.
Template literal types should be treated as branded strings with validation discipline from rules 2-5.
Choose owned vs. borrowed parameters to minimize copies. Widen to the most encompassing type (&Pathover&PathBuf,&strover&String) when it doesn't force extra copies.
PreferArc::clone(&x)/Rc::clone(&x)overx.clone()for reference-counted types, so the cost is visible at the call site.
Follow Rust API Guidelines for naming conventions.
Do not use star imports inside module bodies. Writeuse super::{Foo, bar}instead ofuse super::*;. Two forms stay allowed: external-crate preludes likeuse rayon::prelude::*;and root-of-module re-...
Files:
pacquet/tasks/registry-mock/src/mock_instance.rspacquet/tasks/registry-mock/src/dirs.rs
registry/**/*.rs
📄 CodeRabbit inference engine (registry/AGENTS.md)
Follow the pacquet code-style guide and contributing guide for Rust-level conventions including imports, naming, ownership, error handling, and test layout
Files:
registry/crates/pnpm-registry/src/streaming.rsregistry/crates/pnpm-registry/tests/server.rs
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
Repo: pnpm/pnpm
Timestamp: 2026-05-24T14:35:15.488Z
Learning: Prefer existing `pacquet-*` crates over writing new code; check whether `pacquet-*` already solves the problem before implementing anything non-trivial
Learnt from: CR
Repo: pnpm/pnpm
Timestamp: 2026-05-24T14:35:15.488Z
Learning: Dependencies already declared in `[workspace.dependencies]` may be used by any crate; adding a new third-party crate requires an explicit human request
Learnt from: CR
Repo: pnpm/pnpm
Timestamp: 2026-05-24T14:35:15.488Z
Learning: Use Conventional Commits with `registry` as the scope (`feat(registry): ...`, `fix(registry): ...`)
Learnt from: CR
Repo: pnpm/pnpm
Timestamp: 2026-05-24T14:35:15.488Z
Learning: Run `just check`, `just test`, `just lint`, and `just fmt` to verify work before completion
Learnt from: CR
Repo: pnpm/pnpm
Timestamp: 2026-05-24T14:35:15.488Z
Learning: The registry is a new server implementation and not a port of pnpm CLI; behavior is designed rather than ported, so the 'match upstream pnpm' discipline does not apply
Learnt from: CR
Repo: pnpm/pnpm
Timestamp: 2026-05-24T14:35:15.488Z
Learning: The registry must be compatible with the npm registry protocol that pnpm, npm, yarn, and other clients speak
📚 Learning: 2026-05-20T23:07:58.444Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11784
File: pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs:120-133
Timestamp: 2026-05-20T23:07:58.444Z
Learning: When reviewing code in this pacquet Rust port, follow the upstream pnpm compatibility rule: only match pnpm’s behavior exactly. Do not propose review changes that intentionally deviate from pnpm’s documented/observed behavior, even if pnpm appears buggy. If you identify a real bug in pnpm behavior, the review should prioritize fixing it upstream in pnpm first, and avoid implementing a pnpm-behavior workaround here unless the same fix has already landed upstream.
Applied to files:
pacquet/tasks/registry-mock/src/mock_instance.rspacquet/tasks/registry-mock/src/dirs.rs
…r benchmark parity Switching pacquet's mock launcher from Node + Verdaccio to \`pnpm-registry\` regressed the \`isolated-linker.fresh-restore.cold-cache.cold-store\` benchmark by +50.48% (3.59s vs 2.86s baseline, threshold alert fired). Other three scenarios stayed within noise. Root cause: the benchmark's fixture lockfile pulls ~2706 packages, of which ~2300+ are unscoped npm packages (acorn, ajv, accepts, …). Registry-mock's storage-cache only ships 8 scoped fixture roots, so every unscoped fetch falls through to npmjs. Verdaccio kept those packages warm across CI runs via the \`integrated-benchmark-verdaccio-…\` cache step on \`~/.local/share/verdaccio/storage\`. My setup pointed \`pnpm-registry --storage\` at \`node_modules/@pnpm/registry-mock/registry/storage-cache\` — which CI doesn't cache *and* which \`pnpm install\` wipes — so every benchmark run started fully cold. Fix: * New \`runtime_storage()\` helper returns a stable path (\`$HOME/.cache/pnpm-registry/storage\` by default, or \`PNPM_REGISTRY_STORAGE\` if set). The launcher points \`pnpm-registry --storage\` there instead of at \`node_modules\`, so the cache survives a fresh \`pnpm install\` and isn't installed inside an unrelated npm package. * New \`seed_storage::seed_runtime_storage()\` mirrors \`@pnpm/registry-mock\`'s shipped \`storage-cache/\` into the runtime path on every launch — idempotent (existing files left alone), hardlinks where possible (zero disk overhead) and falls back to \`fs::copy\` when hardlinking isn't supported. * \`pacquet-integrated-benchmark.yml\` cache step replaces the stale \`~/.local/share/verdaccio/storage\` with \`~/.cache/pnpm-registry/storage\`. Key prefix renamed to \`integrated-benchmark-pnpm-registry-\` to start fresh — the old cache would be useless anyway since the layout differs. Verified locally that the launcher seeds 491 fixture files (the 8 scoped roots plus previously-proxied unscoped packages from prior runs) into the runtime path and \`pnpm-registry\` serves them.
…nt doc link
CI surfaced two issues my local stable-clippy didn't catch:
* `clippy::collapsible_if` on Rust 1.95 flagged the two nested
`if let Some(write) = ... { if let Err(err) = ... { ... } }` in
`streaming.rs::run_tee` introduced in 10fcb85. The fix is the
let-chain syntax (stabilized in Rust 1.88) — both `let` patterns
on one `if`, joined by `&&`. Same semantics, cleaner shape.
* `rustdoc::redundant_explicit_links` on
`seed_storage.rs`'s module doc — `[`runtime_storage`](crate::runtime_storage)`
and `[`registry_mock_storage`](crate::registry_mock_storage)`
resolve through the label alone since both are crate-level
exports. Drop the explicit targets.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/tasks/registry-mock/src/seed_storage.rs`:
- Around line 30-52: The loop currently swallows WalkDir errors via
filter_map(Result::ok) and also does a TOCTOU exists() check before calling
link_or_copy; change the traversal to propagate errors (e.g., for entry in
WalkDir::new(src) { let entry = entry?; ... }) so seed_runtime_storage returns
an error if WalkDir yields one, remove the dest_path.exists() pre-check (rely on
atomic link/copy), and make link_or_copy idempotent by treating AlreadyExists as
success: attempt fs::hard_link(src,dest) and if Err(e) and e.kind() ==
io::ErrorKind::AlreadyExists return Ok(()), otherwise fall back to
fs::copy(src,dest) and treat its AlreadyExists as Ok(()), propagating other
errors; keep create_dir_all(parent)? before calling link_or_copy.
🪄 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: 5287da77-1d01-4c9e-a8f1-0b0d3941c21b
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (6)
.github/workflows/pacquet-integrated-benchmark.ymlpacquet/tasks/registry-mock/Cargo.tomlpacquet/tasks/registry-mock/src/dirs.rspacquet/tasks/registry-mock/src/lib.rspacquet/tasks/registry-mock/src/pnpm_registry_command.rspacquet/tasks/registry-mock/src/seed_storage.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). (6)
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Lint and Test (macos-latest)
- GitHub Check: Lint and Test (windows-latest)
- GitHub Check: Code Coverage
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (1)
pacquet/**/*.rs
📄 CodeRabbit inference engine (pacquet/AGENTS.md)
pacquet/**/*.rs: When porting a function that firespnpm:<channel>events throughglobalLogger,logger.debug(), orstreamParser.write(), mirror the call site, payload, and ordering so the reporter parses pacquet's NDJSON the same way it parses pnpm's.
Declare a newtype wrapper for branded string types. Do not collapse the brand into a plainStringor&str.
If upstream always validates before construction, validate in pacquet's wrapper too. The wrapper must construct only viaTryFrom<String>and/orFromStr. Do not provide an infallible public constructor.
If upstream never validates, just brand for type-safety. Expose an infallibleFrom<String>(andFrom<&str>when convenient).
If upstream occasionally constructs without validation, exposefrom_str_uncheckedas an escape hatch alongside the validating constructor.
Match upstream serde behavior for branded types that cross JSON, YAML, or INI boundaries. Use#[serde(try_from = "String")]for deserialization and#[serde(into = "String")]for serialization.
Use#[derive(derive_more::From)]and#[derive(derive_more::Into)]for mechanical conversion impls. Fall back to manualimplonly when conversion needs custom logic.
String-literal unions should becomeenums, not newtype wrappers. Model closed sets of valid string values as enums.
Template literal types should be treated as branded strings with validation discipline from rules 2-5.
Choose owned vs. borrowed parameters to minimize copies. Widen to the most encompassing type (&Pathover&PathBuf,&strover&String) when it doesn't force extra copies.
PreferArc::clone(&x)/Rc::clone(&x)overx.clone()for reference-counted types, so the cost is visible at the call site.
Follow Rust API Guidelines for naming conventions.
Do not use star imports inside module bodies. Writeuse super::{Foo, bar}instead ofuse super::*;. Two forms stay allowed: external-crate preludes likeuse rayon::prelude::*;and root-of-module re-...
Files:
pacquet/tasks/registry-mock/src/seed_storage.rspacquet/tasks/registry-mock/src/dirs.rspacquet/tasks/registry-mock/src/pnpm_registry_command.rspacquet/tasks/registry-mock/src/lib.rs
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
Repo: pnpm/pnpm
Timestamp: 2026-05-24T14:46:00.798Z
Learning: Prefer existing `pacquet-*` crates over writing new code; check whether `pacquet-*` already solves it before implementing anything non-trivial
Learnt from: CR
Repo: pnpm/pnpm
Timestamp: 2026-05-24T14:46:00.798Z
Learning: Use Conventional Commits with `registry` as the scope for commit messages (e.g., `feat(registry): ...`, `fix(registry): ...`)
Learnt from: CR
Repo: pnpm/pnpm
Timestamp: 2026-05-24T14:46:00.798Z
Learning: Run `just check`, `just test`, `just lint`, and `just fmt` before declaring work done in the registry
Learnt from: CR
Repo: pnpm/pnpm
Timestamp: 2026-05-24T14:46:00.798Z
Learning: Never ignore test failures; ensure all tests pass before completion
📚 Learning: 2026-05-20T23:07:58.444Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11784
File: pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs:120-133
Timestamp: 2026-05-20T23:07:58.444Z
Learning: When reviewing code in this pacquet Rust port, follow the upstream pnpm compatibility rule: only match pnpm’s behavior exactly. Do not propose review changes that intentionally deviate from pnpm’s documented/observed behavior, even if pnpm appears buggy. If you identify a real bug in pnpm behavior, the review should prioritize fixing it upstream in pnpm first, and avoid implementing a pnpm-behavior workaround here unless the same fix has already landed upstream.
Applied to files:
pacquet/tasks/registry-mock/src/seed_storage.rspacquet/tasks/registry-mock/src/dirs.rspacquet/tasks/registry-mock/src/pnpm_registry_command.rspacquet/tasks/registry-mock/src/lib.rs
🔇 Additional comments (6)
pacquet/tasks/registry-mock/src/seed_storage.rs (1)
1-29: LGTM!Also applies to: 53-54
.github/workflows/pacquet-integrated-benchmark.yml (1)
90-105: LGTM!pacquet/tasks/registry-mock/Cargo.toml (1)
26-33: LGTM!pacquet/tasks/registry-mock/src/dirs.rs (1)
41-43: LGTM!Also applies to: 56-83
pacquet/tasks/registry-mock/src/pnpm_registry_command.rs (1)
1-3: LGTM!Also applies to: 66-74, 77-77
pacquet/tasks/registry-mock/src/lib.rs (1)
9-9: LGTM!
…oncurrent runs Two findings from coderabbit on f1791ed / 934c1d5: * `await_no_tgz` only matched `.tgz` files, so the `upstream_stream_error_clears_cache` and `client_disconnect_mid_stream_clears_cache` tests would still pass if `write.abandon()` left a `.tmp.<pid>.<counter>` orphan behind. That was the whole point of those tests. Extend the poll predicate to match `.tmp.` substrings too. * `seed_runtime_storage` silently dropped `WalkDir` traversal errors via `filter_map(Result::ok)` (partial mirror returns `Ok(seeded)` — hard to debug) and had a TOCTOU on `dest_path.exists()` (two pacquet processes racing past `GuardFile` on a shared CI worker could both see "absent", both attempt the hard link, one trips `AlreadyExists`). Fix: propagate WalkDir errors via `io::Error::other`, drop the pre-existence check, and treat `AlreadyExists` from `hard_link`/`copy` as success directly inside `link_or_copy` via a new `LinkOutcome` enum (so we can still count *newly* seeded files for the launcher log line).
…mark 934c1d5 fixed the cache *persistence* — the runtime storage path is now under `~/.cache/pnpm-registry/storage` and the workflow caches it across runs. But the first run with a new cache key (and every run after the benchmark lockfile hash changes) still pays a full ~2.3k-packument fetch from npmjs, which trips the +25% threshold on `isolated-linker.fresh-restore.cold-cache.cold-store`. Verdaccio didn't have this problem because the old `integrated-benchmark-verdaccio-…` cache had been written by hundreds of past runs. Add a one-shot `Pre-warm pnpm-registry cache` step that runs between `Precompile benchmark revisions` and the first benchmark scenario. It launches the mock, reads its dynamic port from the prepared-info JSON, and runs `pnpm fetch` against the benchmark lockfile — which downloads every package into the pnpm store *without* linking them (no `node_modules` pollution) and, via the mock, fills `~/.cache/pnpm-registry/storage`. The actual benchmark spawns a fresh mock later and finds everything already on disk. Cost on a cold cache: ~1 minute of npm downloads. On a warm cache, `pnpm fetch` verifies in seconds. Either way the benchmark scenarios that follow run against a warm registry, matching verdaccio's historical setup.
…en cargo-nextest is absent The integrated-benchmark workflow doesn't install cargo-nextest (it never runs \`just test\`), but my pre-warm step (af3d2e4) calls \`just registry-mock launch\`, whose recipe unconditionally invoked \`cargo nextest run --no-run\`. CI failed with: error: no such command: \`nextest\` Make the recipe a bash conditional: use nextest's pre-build when it's installed (preserves the Windows .exe-lock fix for the test workflows that follow up with \`cargo nextest run\`); fall back to plain \`cargo build --bin=pnpm-registry\` otherwise (good enough for workflows that only spawn the mock and don't run cargo tests against it).
Pacquet (and pnpm/npm/yarn) send
\`Accept: application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*\`
for install metadata. Verdaccio honors it via
\`getPackageByOptions\` → \`convertAbbreviatedManifest\`, stripping
the packument down to only the fields needed for resolution
(name, version, dist, deps, peerDeps, etc.) and tagging the
response with the install-v1 MIME type. The full packument
includes per-version \`_npmUser\`, \`_nodeVersion\`, README
metadata, contributors lists, and so on — none of which a client
needs to pick a version and download a tarball.
pnpm-registry was ignoring \`Accept\` entirely and serving the
full document on every request. For \`lodash\`:
full: 247 184 bytes
abbreviated: 77 023 bytes (3.2× smaller)
The benchmark fixture has ~2.3k unscoped packages and pacquet
fetches each per scenario, so the ratio compounds into a real
+50% wall-clock regression on
\`isolated-linker.fresh-restore.cold-cache.cold-store\` —
identical for pacquet@HEAD and pacquet@main, which is what
clinched it as a server-side issue.
Mirror verdaccio's behavior:
* New \`upstream::abbreviate_packument\` rebuilds the JSON tree
with the install-v1 field set (top-level and per-version),
matching verdaccio's exact list from
\`packages/store/src/storage.ts:convertAbbreviatedManifest\`.
* \`wants_abbreviated(&HeaderMap)\` checks for the install-v1 MIME
in \`Accept\` (substring match — clients always send it as the
top-priority option).
* \`packument_response\` either returns the full document with
\`Content-Type: application/json\` or the trimmed one with
\`Content-Type: application/vnd.npm.install-v1+json\`.
* Version-manifest endpoint stays full-only (matches verdaccio).
Two new integration tests against \`@pnpm/registry-mock\` storage
cover both shapes — abbreviated drops \`_attachments\`,
\`_uplinks\`, per-version \`_nodeVersion\` etc.; full keeps them.
…ecipe when cargo-nextest is absent" This reverts commit cbc03c7.
…ed benchmark" This reverts commit af3d2e4.
…iated form
Real npm packuments store the last-modified timestamp at
\`time.modified\`, not at the top level. My abbreviated form was
copying \`modified\` verbatim — which always missed for npm-sourced
packuments because the source object doesn't have a top-level
\`modified\`. Verdaccio synthesizes both fields explicitly:
modified: manifest.time.modified,
readmeFilename: '',
Pacquet's resolver reads \`meta.modified\` in
\`pick_package_from_meta.rs\` (version-pick heuristics) and
\`pick_package.rs\` (freshness check); omitting it pushed the
resolver onto a slower fallback path.
Local head-to-head with the abbreviated form fixed:
pnpm-registry: 8.162 s ± 0.205 s (8 runs)
verdaccio: 8.284 s ± 0.469 s (8 runs)
Same hardware, same pacquet binary, same lockfile and fixtures.
pnpm-registry is 1.01× faster than verdaccio — within noise. Per-
request server perf is at parity. The remaining +50% gap in CI's
\`isolated-linker.fresh-restore.cold-cache.cold-store\` benchmark
must be artifact (incomplete CI storage coverage, threshold drift,
or runner specifics) rather than a real per-request regression.
Two extra assertions in the abbreviated-form integration test
cover both fields.
Re-introduces a pre-warm step (reverted in 2bcc9fd) now that local benchmarks proved per-request server perf is at parity with verdaccio. The remaining +50% gap on \`isolated-linker.fresh-restore.cold-cache.cold-store\` comes from the CI cache: verdaccio's \`~/.local/share/verdaccio/storage\` had been filled organically over *months* of benchmark runs, so by the time anyone looked it covered every package the lockfile needs. The pnpm-registry cache is fresh and likely missing entries — hyperfine's single warmup pass isn't enough to fill in fresh-install packuments that fresh-restore doesn't touch. The previous pre-warm used \`pnpm fetch\`, which only downloads tarballs (packuments stay missing because the lockfile already pins their resolution). Use \`pacquet install\` against the same fixture lockfile instead — it issues the exact same \`Accept: install-v1\` packument lookups + version-manifest fetches + tarball downloads the timed runs will issue, so the storage ends up populated with precisely the set the benchmark requests. \`PNPM_CONFIG_MINIMUM_RELEASE_AGE=0\` disables pacquet's release-age gate that would otherwise reject the fixture lockfile. Restores the \`cargo-nextest\`-or-\`cargo build\` fallback in the \`registry-mock\` recipe (also reverted in 98ea954) so the benchmark workflow can call \`just registry-mock launch\` without \`cargo-nextest\` installed.
…nstall\`" This reverts commit f748a41.
…DELAY The CI integrated benchmark was comparing **debug-mode Rust** to **JIT-optimized V8** — the recipe used \`cargo build --bin=pnpm-registry\` (no \`--release\`), so the mock server ran unoptimized while verdaccio always runs through Node's JIT. This gave pnpm-registry a multi-hundred-millisecond per-iteration handicap that the integrated benchmark surfaced as a +22% regression on \`isolated-linker.fresh-restore.cold-cache.cold-store\`, affecting both pacquet@HEAD and pacquet@main equally (because pacquet's code is unchanged — only the mock changed). My earlier local head-to-head (pnpm-registry 8.16s vs verdaccio 8.28s, equal) didn't catch this because I built pnpm-registry with \`cargo build -p pnpm-registry --bin pnpm-registry --release\` explicitly. CI's recipe didn't. Two coupled changes: * \`justfile\` \`integrated-benchmark\` recipe now pre-builds pnpm-registry with \`--release\`. * The launcher's binary resolver now **prefers \`target/release/pnpm-registry\` when it exists**, falling back to \`target/debug\` for local dev. The test workflows still get the debug binary (they don't trigger the release build); the benchmark workflow gets the release one. No env var or per-workflow flag needed — having a release artifact override a debug one is always the right choice. Also add \`TCP_NODELAY\` on each accepted socket. Node's HTTP server enables it by default; hyper 1.x leaves it off. With Nagle on, Linux's epoll-driven write coalescing adds tens of µs of per-response delay while waiting for follow-up bytes — invisible on macOS's kqueue but accumulates across the install fan-out. Implemented via a \`NodelayTcpListener\` wrapper around the axum \`Listener\` trait.
Summary
Creates a working pnpm-compatible npm registry server (verdaccio analogue, in Rust) — and replaces
@pnpm/registry-mock's Node + Verdaccio launcher in pacquet's test setup with the new binary, against@pnpm/registry-mock's shipped storage.What
pnpm-registrydoesGET /<pkg>— packument (/{name}and/{scope}/{name})GET /<pkg>/<version-or-tag>— single-version manifest, resolvesdist-tagsand rewritesdist.tarballto point at this serverGET /<pkg>/-/<tarball>— tarball, streamedhttps://registry.npmjs.org), caches to disk--static) — serves the storage directory verbatim, 404s on cache miss<root>/<pkg>/package.json+ flat tarballs) — drop-in compatible with the storage@pnpm/registry-mockpublishespacquet_network::ThrottledClient::new_for_installs(), inheriting pnpm's tuned defaults (User-Agent: pnpm, HTTP/1.1, hickory DNS, connection-pool tuning, concurrency semaphore)is_timeout()→ 504,is_connect()→ 503, everything else (incl. upstream 5xx) → 502. No proxy-side retry (the pnpm client already hasfetch-retries; stacking retries would only multiply latency on real failures).What changed in pacquet
pacquet/tasks/registry-mocknow spawnspnpm-registryagainstnode_modules/@pnpm/registry-mock/registry/storage-cache(proxy mode withnpmjs.orgupstream and a 1-year packument TTL — matching@pnpm/registry-mock's'**': proxy: npmjsverdaccio config). No more Node, no more Verdaccio, no morelaunch.mjs, no more process-tree walk to kill child verdaccios.@pnpm/registry-mockstays as a devDep — only for the storage data it ships, not the launcher.Tests
@pnpm/registry-mockstorage in static mode + 17 mockito-based proxy/cache/streaming): packument rewrite, version-manifest resolution, tarball streaming (large body, cache finalize, mid-stream upstream error, client disconnect mid-stream, concurrent fetches → one cache file), gateway status mapping (504/503/502), stale-cache fallback on upstream failure, TTL refresh, invalid-package-name 400, scoped vs unscoped routing.pnpm-registry-backed mock.CI
pacquet-ci.ymlandpacquet-codecov.ymlpath filters now includeregistry/**(so registry-only PRs trigger the workspace CI); typos checker coversregistrytoo. The workflow name stays "Pacquet CI" but a header comment explains the intentional cross-stack scope.just registry-mock launchpre-builds withcargo nextest run --no-run(workspace-wide) so its fingerprint matches whatjust testwill later need — without this, Windows MSVC fails withos error 5trying to re-link the runningpnpm-registry.exe.Crates.io name reservations (from the original scaffold commit)
pnpm-registry— published from this repopnpm-registry-cli/pnpm-registry-server— placeholder stubs, name reservation onlyTest plan
cargo nextest run -p pnpm-registry— 36 tests passcargo nextest run(workspace, with mock launched) — 2043 tests pass locallycargo clippy --workspace --all-targets -- --deny warningscleancargo dylint --all -- --all-targets --workspaceclean (perfectionist)RUSTDOCFLAGS=-D warnings cargo doc --no-deps --workspace --document-private-itemscleantaplo format --checkcleanpnpm-registryrunning against registry-mock storage serves both fixture packuments (rewriting verdaccio-form tarball URLs to npm-spec form) and proxied npm packages (e.g.is-positive)Written by an agent (Claude Code, claude-opus-4-7).
Summary by CodeRabbit
New Features
Tests
Documentation
Chores / Refactor