Skip to content

feat(registry): implement pnpm-registry server and adopt it in pacquet's test mock#11898

Merged
zkochan merged 27 commits into
mainfrom
pnpm-server
May 24, 2026
Merged

feat(registry): implement pnpm-registry server and adopt it in pacquet's test mock#11898
zkochan merged 27 commits into
mainfrom
pnpm-server

Conversation

@zkochan

@zkochan zkochan commented May 24, 2026

Copy link
Copy Markdown
Member

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-registry does

  • HTTP server (axum + tower-http) with the three endpoints pnpm/npm clients need:
    • GET /<pkg> — packument (/{name} and /{scope}/{name})
    • GET /<pkg>/<version-or-tag> — single-version manifest, resolves dist-tags and rewrites dist.tarball to point at this server
    • GET /<pkg>/-/<tarball> — tarball, streamed
  • Two modes:
    • Proxy — fetches missing packuments/tarballs from a configurable upstream (defaults to https://registry.npmjs.org), caches to disk
    • Static (--static) — serves the storage directory verbatim, 404s on cache miss
  • Verdaccio-shaped on-disk storage (<root>/<pkg>/package.json + flat tarballs) — drop-in compatible with the storage @pnpm/registry-mock publishes
  • Tarball streaming — cache hits stream off disk; cache misses tee upstream chunks into a temp file via an mpsc channel and forward them to the client at the same time, atomically renaming on success and abandoning on upstream error or client disconnect
  • Tuned HTTP client — wraps pacquet_network::ThrottledClient::new_for_installs(), inheriting pnpm's tuned defaults (User-Agent: pnpm, HTTP/1.1, hickory DNS, connection-pool tuning, concurrency semaphore)
  • Gateway-style status mappingis_timeout() → 504, is_connect() → 503, everything else (incl. upstream 5xx) → 502. No proxy-side retry (the pnpm client already has fetch-retries; stacking retries would only multiply latency on real failures).

What changed in pacquet

  • pacquet/tasks/registry-mock now spawns pnpm-registry against node_modules/@pnpm/registry-mock/registry/storage-cache (proxy mode with npmjs.org upstream and a 1-year packument TTL — matching @pnpm/registry-mock's '**': proxy: npmjs verdaccio config). No more Node, no more Verdaccio, no more launch.mjs, no more process-tree walk to kill child verdaccios.
  • @pnpm/registry-mock stays as a devDep — only for the storage data it ships, not the launcher.

Tests

  • 36 pnpm-registry tests (12 unit + 7 against @pnpm/registry-mock storage 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.
  • Full pacquet test suite (2043 tests) runs green against pnpm-registry-backed mock.

CI

  • pacquet-ci.yml and pacquet-codecov.yml path filters now include registry/** (so registry-only PRs trigger the workspace CI); typos checker covers registry too. The workflow name stays "Pacquet CI" but a header comment explains the intentional cross-stack scope.
  • just registry-mock launch pre-builds with cargo nextest run --no-run (workspace-wide) so its fingerprint matches what just test will later need — without this, Windows MSVC fails with os error 5 trying to re-link the running pnpm-registry.exe.

Crates.io name reservations (from the original scaffold commit)

Test plan

  • cargo nextest run -p pnpm-registry — 36 tests pass
  • cargo nextest run (workspace, with mock launched) — 2043 tests pass locally
  • cargo clippy --workspace --all-targets -- --deny warnings clean
  • cargo dylint --all -- --all-targets --workspace clean (perfectionist)
  • RUSTDOCFLAGS=-D warnings cargo doc --no-deps --workspace --document-private-items clean
  • taplo format --check clean
  • Manual smoke: pnpm-registry running 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

    • Adds a Rust pnpm-compatible registry server (proxy/static modes) with on-disk cache, tarball streaming, and rewritten tarball URLs; introduces a workspace crate for it.
  • Tests

    • Adds extensive unit and integration tests covering caching, proxying, streaming, concurrency, and error mappings.
  • Documentation

    • Adds registry-specific developer guidance.
  • Chores / Refactor

    • CI/workflows and workspace updated to include the registry; test tooling switched to run the new registry binary and local test recipes improved.

Review Change Stack

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.
@coderabbitai

coderabbitai Bot commented May 24, 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

Adds a new Rust crate pnpm-registry (library + binary) implementing a pnpm-compatible registry (static and proxy modes with disk cache), integrates it into the workspace and CI, migrates pacquet’s registry-mock to run the new binary, and provides extensive unit and integration tests.

Changes

pnpm-registry: Full-featured npm-compatible registry server

Layer / File(s) Summary
Workspace, CI, and build recipes
Cargo.toml, .github/workflows/*, justfile
Workspace members include registry/crates/*; shared dependencies updated (axum, tower, tower-http, expanded tokio features). CI/codecov path filters and typos checks include registry/**. justfile recipes prebuild/test pnpm-registry.
Registry developer guidance
registry/AGENTS.md
Adds registry-specific contributor guidance: layout, crate naming, dependency policy, and workflow commands.
pnpm-registry crate foundation
registry/crates/pnpm-registry/{Cargo.toml,README.md,src/lib.rs,src/main.rs}
New crate manifest and README; library re-exports Config, RegistryError, Result, router, serve; binary CLI parses args and runs server in static or proxy mode.
Types and configuration
registry/crates/pnpm-registry/src/{package_name.rs,config.rs,error.rs}
PackageName validates package/tarball naming; Config provides proxy/static constructors and public_url derivation; RegistryError maps error kinds to HTTP status codes and includes tests.
On-disk cache layer
registry/crates/pnpm-registry/src/cache.rs
Cache implements verdaccio-style layout, TTL-aware packument reads, atomic packument writes, tarball open and per-request temp file handling via TarballWrite.
Streaming with cache write-through
registry/crates/pnpm-registry/src/streaming.rs
stream_file() reads cached files in chunks; tee_to_cache() and run_tee() stream upstream responses to client while writing to a temp cache file, handling errors, disconnects, and finalize/abandon semantics.
Upstream fetch and rewriting
registry/crates/pnpm-registry/src/upstream.rs
Upstream fetches packuments/tarballs with 404 signaling via FetchOutcome; rewrite_tarball_urls and extract_version_manifest rewrite tarball URLs to the registry's public_url.
HTTP server and routing
registry/crates/pnpm-registry/src/server.rs
Axum router and handlers for packuments, version manifests, and tarballs; integrates cache and upstream logic, JSON rewriting, streaming responses, and maps RegistryError to HTTP responses.
Integration tests: static serve
registry/crates/pnpm-registry/tests/registry_mock.rs
E2E tests for static mode using @pnpm/registry-mock storage: packuments, tarballs, version manifests, and 404 cases.
Integration tests: proxy and resilience
registry/crates/pnpm-registry/tests/server.rs
Comprehensive tests covering proxy caching, TTL/refetch, stale fallback, streaming finalization, error mapping, concurrency, truncated upstream, client disconnects, and cache tmp failure handling.
Migrate pacquet registry-mock
pacquet/tasks/registry-mock/*
Remove Node launch wrapper and node-based mock; add registry_mock_storage(), pnpm_registry_command(), process_kill::kill_process_by_pid(), and seed_runtime_storage(); simplify mock instance spawn and shutdown to use the pnpm-registry binary and PID-based kills; update crate metadata and justfile recipes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • KSXGitHub

Poem

🐰 I tunneled through crates and CI tonight,
Wove caching, streaming, and URLs just right,
Spawned a registry binary, tests in tow,
Pacquet's mock now runs the Rusty show,
Hooray — crates, docs, and CI shine bright!

🚥 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 PR title clearly summarizes the main change: implementing a Rust-based pnpm-registry server and integrating it into pacquet's test mock, which aligns with the extensive changes across workflow files, Cargo.toml, registry crates, and pacquet/tasks/registry-mock.
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 pnpm-server

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.

@github-actions

github-actions Bot commented May 24, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.01      5.4±0.09ms   800.7 KB/sec    1.00      5.4±0.08ms   809.6 KB/sec

@github-actions

github-actions Bot commented May 24, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Scenario: Isolated linker: fresh restore, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.072 ± 0.052 2.004 2.165 1.03 ± 0.04
pacquet@main 2.021 ± 0.066 1.948 2.186 1.00
pnpm 4.621 ± 0.063 4.507 4.753 2.29 ± 0.08
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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 646.6 ± 15.2 636.0 687.6 1.00 ± 0.03
pacquet@main 643.6 ± 15.2 627.3 681.9 1.00
pnpm 2642.8 ± 54.2 2570.7 2763.0 4.11 ± 0.13
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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.461 ± 0.061 4.381 4.553 1.00
pacquet@main 4.514 ± 0.108 4.417 4.768 1.01 ± 0.03
pnpm 6.107 ± 0.043 6.026 6.162 1.37 ± 0.02
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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 3.702 ± 0.113 3.559 3.955 1.01 ± 0.04
pacquet@main 3.657 ± 0.080 3.572 3.802 1.00
pnpm 4.277 ± 0.042 4.212 4.326 1.17 ± 0.03
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
      ]
    }
  ]
}

@github-actions

github-actions Bot commented May 24, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/11898
Testbedpacquet
Click to view all benchmark results
BenchmarkLatencyBenchmark 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%)
🐰 View full continuous benchmarking report in Bencher

zkochan added 10 commits May 24, 2026 13:14
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-commenter

codecov-commenter commented May 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.22653% with 79 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.88%. Comparing base (74a219e) to head (1bc8038).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
registry/crates/pnpm-registry/src/server.rs 81.15% 39 Missing ⚠️
registry/crates/pnpm-registry/src/main.rs 0.00% 17 Missing ⚠️
registry/crates/pnpm-registry/src/streaming.rs 84.21% 9 Missing ⚠️
registry/crates/pnpm-registry/src/cache.rs 94.00% 6 Missing ⚠️
registry/crates/pnpm-registry/src/upstream.rs 97.67% 4 Missing ⚠️
registry/crates/pnpm-registry/src/error.rs 93.10% 2 Missing ⚠️
registry/crates/pnpm-registry/src/package_name.rs 97.10% 2 Missing ⚠️
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.
📢 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.

…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.
@zkochan zkochan changed the title feat(registry): scaffold pnpm-registry Rust project feat(registry): implement pnpm-registry server and adopt it in pacquet's test mock May 24, 2026
…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.
@zkochan zkochan marked this pull request as ready for review May 24, 2026 14:18
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between a456dc7 and 1a34650.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (29)
  • .github/workflows/pacquet-ci.yml
  • .github/workflows/pacquet-codecov.yml
  • Cargo.toml
  • justfile
  • pacquet/tasks/registry-mock/Cargo.toml
  • pacquet/tasks/registry-mock/launch.mjs
  • pacquet/tasks/registry-mock/src/dirs.rs
  • pacquet/tasks/registry-mock/src/kill_verdaccio.rs
  • pacquet/tasks/registry-mock/src/lib.rs
  • pacquet/tasks/registry-mock/src/mock_instance.rs
  • pacquet/tasks/registry-mock/src/node_registry_mock.rs
  • pacquet/tasks/registry-mock/src/pnpm_registry_command.rs
  • pacquet/tasks/registry-mock/src/process_kill.rs
  • pacquet/tasks/registry-mock/src/registry_anchor.rs
  • pacquet/tasks/registry-mock/src/registry_info.rs
  • registry/AGENTS.md
  • registry/crates/pnpm-registry/Cargo.toml
  • registry/crates/pnpm-registry/README.md
  • registry/crates/pnpm-registry/src/cache.rs
  • registry/crates/pnpm-registry/src/config.rs
  • registry/crates/pnpm-registry/src/error.rs
  • registry/crates/pnpm-registry/src/lib.rs
  • registry/crates/pnpm-registry/src/main.rs
  • registry/crates/pnpm-registry/src/package_name.rs
  • registry/crates/pnpm-registry/src/server.rs
  • registry/crates/pnpm-registry/src/streaming.rs
  • registry/crates/pnpm-registry/src/upstream.rs
  • registry/crates/pnpm-registry/tests/registry_mock.rs
  • registry/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 name pnpm-registry-<short-name> in Cargo.toml; use the pnpm-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 fires pnpm:<channel> events through globalLogger, logger.debug(), or streamParser.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 plain String or &str.
If upstream always validates before construction, validate in pacquet's wrapper too. The wrapper must construct only via TryFrom<String> and/or FromStr. Do not provide an infallible public constructor.
If upstream never validates, just brand for type-safety. Expose an infallible From<String> (and From<&str> when convenient).
If upstream occasionally constructs without validation, expose from_str_unchecked as 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 manual impl only when conversion needs custom logic.
String-literal unions should become enums, 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 (&Path over &PathBuf, &str over &String) when it doesn't force extra copies.
Prefer Arc::clone(&x) / Rc::clone(&x) over x.clone() for reference-counted types, so the cost is visible at the call site.
Follow Rust API Guidelines for naming conventions.
Do not use star imports inside module bodies. Write use super::{Foo, bar} instead of use super::*;. Two forms stay allowed: external-crate preludes like use rayon::prelude::*; and root-of-module re-...

Files:

  • pacquet/tasks/registry-mock/src/process_kill.rs
  • pacquet/tasks/registry-mock/src/dirs.rs
  • pacquet/tasks/registry-mock/src/pnpm_registry_command.rs
  • pacquet/tasks/registry-mock/src/lib.rs
  • pacquet/tasks/registry-mock/src/registry_anchor.rs
  • pacquet/tasks/registry-mock/src/mock_instance.rs
  • pacquet/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 handling

Follow the pacquet contributing guide (../pacquet/CONTRIBUTING.md) for Rust test layout and testing practices

Files:

  • registry/crates/pnpm-registry/src/error.rs
  • registry/crates/pnpm-registry/src/streaming.rs
  • registry/crates/pnpm-registry/src/config.rs
  • registry/crates/pnpm-registry/src/lib.rs
  • registry/crates/pnpm-registry/src/main.rs
  • registry/crates/pnpm-registry/src/upstream.rs
  • registry/crates/pnpm-registry/tests/registry_mock.rs
  • registry/crates/pnpm-registry/src/package_name.rs
  • registry/crates/pnpm-registry/tests/server.rs
  • registry/crates/pnpm-registry/src/cache.rs
  • registry/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.rs
  • pacquet/tasks/registry-mock/src/dirs.rs
  • pacquet/tasks/registry-mock/src/pnpm_registry_command.rs
  • pacquet/tasks/registry-mock/src/lib.rs
  • pacquet/tasks/registry-mock/src/registry_anchor.rs
  • pacquet/tasks/registry-mock/src/mock_instance.rs
  • pacquet/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 win

Windows rename replacement workaround likely unnecessary
The specific claim that tokio::fs::rename/std::fs::rename can fail on Windows just because the destination exists doesn’t match Rust’s Windows behavior: rename uses MOVEFILE_REPLACE_EXISTING, so overwriting an existing destination should not yield an AlreadyExists-style failure. The proposed AlreadyExistsremove_file fallback 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 actual io::ErrorKind observed.

			> Likely an incorrect or invalid review comment.

Comment thread registry/AGENTS.md Outdated
… 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between 4a3dc4b and 10fcb85.

📒 Files selected for processing (4)
  • pacquet/tasks/registry-mock/src/dirs.rs
  • pacquet/tasks/registry-mock/src/mock_instance.rs
  • registry/crates/pnpm-registry/src/streaming.rs
  • registry/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 fires pnpm:<channel> events through globalLogger, logger.debug(), or streamParser.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 plain String or &str.
If upstream always validates before construction, validate in pacquet's wrapper too. The wrapper must construct only via TryFrom<String> and/or FromStr. Do not provide an infallible public constructor.
If upstream never validates, just brand for type-safety. Expose an infallible From<String> (and From<&str> when convenient).
If upstream occasionally constructs without validation, expose from_str_unchecked as 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 manual impl only when conversion needs custom logic.
String-literal unions should become enums, 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 (&Path over &PathBuf, &str over &String) when it doesn't force extra copies.
Prefer Arc::clone(&x) / Rc::clone(&x) over x.clone() for reference-counted types, so the cost is visible at the call site.
Follow Rust API Guidelines for naming conventions.
Do not use star imports inside module bodies. Write use super::{Foo, bar} instead of use super::*;. Two forms stay allowed: external-crate preludes like use rayon::prelude::*; and root-of-module re-...

Files:

  • pacquet/tasks/registry-mock/src/mock_instance.rs
  • pacquet/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.rs
  • registry/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.rs
  • pacquet/tasks/registry-mock/src/dirs.rs

Comment thread registry/crates/pnpm-registry/tests/server.rs Outdated
zkochan added 2 commits May 24, 2026 16:45
…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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between 10fcb85 and 934c1d5.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • .github/workflows/pacquet-integrated-benchmark.yml
  • pacquet/tasks/registry-mock/Cargo.toml
  • pacquet/tasks/registry-mock/src/dirs.rs
  • pacquet/tasks/registry-mock/src/lib.rs
  • pacquet/tasks/registry-mock/src/pnpm_registry_command.rs
  • pacquet/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 fires pnpm:<channel> events through globalLogger, logger.debug(), or streamParser.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 plain String or &str.
If upstream always validates before construction, validate in pacquet's wrapper too. The wrapper must construct only via TryFrom<String> and/or FromStr. Do not provide an infallible public constructor.
If upstream never validates, just brand for type-safety. Expose an infallible From<String> (and From<&str> when convenient).
If upstream occasionally constructs without validation, expose from_str_unchecked as 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 manual impl only when conversion needs custom logic.
String-literal unions should become enums, 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 (&Path over &PathBuf, &str over &String) when it doesn't force extra copies.
Prefer Arc::clone(&x) / Rc::clone(&x) over x.clone() for reference-counted types, so the cost is visible at the call site.
Follow Rust API Guidelines for naming conventions.
Do not use star imports inside module bodies. Write use super::{Foo, bar} instead of use super::*;. Two forms stay allowed: external-crate preludes like use rayon::prelude::*; and root-of-module re-...

Files:

  • pacquet/tasks/registry-mock/src/seed_storage.rs
  • pacquet/tasks/registry-mock/src/dirs.rs
  • pacquet/tasks/registry-mock/src/pnpm_registry_command.rs
  • pacquet/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.rs
  • pacquet/tasks/registry-mock/src/dirs.rs
  • pacquet/tasks/registry-mock/src/pnpm_registry_command.rs
  • pacquet/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!

Comment thread pacquet/tasks/registry-mock/src/seed_storage.rs Outdated
…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).
zkochan added 9 commits May 24, 2026 17:20
…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.
…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.
…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.
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