Skip to content

feat(tarball): support remote https-tarball direct dependencies#12076

Merged
zkochan merged 3 commits into
mainfrom
fix/12053
May 30, 2026
Merged

feat(tarball): support remote https-tarball direct dependencies#12076
zkochan merged 3 commits into
mainfrom
fix/12053

Conversation

@zkochan

@zkochan zkochan commented May 29, 2026

Copy link
Copy Markdown
Member

What

Implements end-to-end support in pacquet for remote (non-registry) http(s)-tarball direct dependencies — e.g. "foo": "https://cdn.example.com/foo-1.0.0.tgz". Fixes #12053.

The bug

A direct dependency whose specifier is a non-registry tarball URL panicked while building the lockfile:

dep paths produced by the resolver always parse as PkgNameVerPeer: MissingSuffix
  at pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs

TarballResolver returned no name/version/integrity (those live in the tarball's package.json), so the dep path was just the URL (no @) and the importer-dep-path parse hit MissingSuffix. (A registry-host tarball URL is claimed by the npm resolver instead and was already fine.)

The fix

pnpm learns name/version/integrity in the package-requester after the fetch, but pacquet builds the lockfile before the install pass. So the TarballResolver now fetches the tarball during resolution: it downloads, computes the sha512 integrity, extracts to the store, and reads the bundled manifest, warming the shared MemCache (keyed by URL) so the install pass reuses the extraction without a second download (one download total).

name_ver stays None so the depPath is name@<url> — matching pnpm (see installing/deps-installer/test/lockfile.ts, "packages installed via tarball URL … are normalized"). The name_ver-dependent call sites fall back to the manifest (mirroring pnpm's getManifestFromResponse).

Changes (pacquet-only)

  • crates/tarball — new public FetchTarballForResolution entry point that downloads + computes integrity (refactored the integrity step of fetch_and_extract_once to verify-or-compute), extracts to the store, reads the manifest, queues the store-index row, and warms MemCache.
  • TarballResolver — fetches during resolution when given a TarballFetchContext; legacy HEAD-only shape when none (unit tests).
  • install_with_fresh_lockfile.rs — store handles opened before resolver construction; context wired in.
  • Manifest fallbacks where name_ver was required (install_package_from_registry, prefetch-key collector, and — scoped to remote http tarballs only — real_name in the lockfile builder, leaving file:/git deps untouched).
  • Flipped the gated regression test remote_tarball_integrity_survives_unrelated_install. Its original premise was inverted: the test registry binds to 127.0.0.1, so its .replace("localhost", "127.0.0.1") was a no-op and routed through the npm resolver — corrected so the tarball host is localhost (registry stays 127.0.0.1) and TarballResolver is actually exercised.

Verification

  • The un-gated regression test passes end to end (fresh install records integrity under the name@<url> key → unrelated add preserves it verbatim → --frozen-lockfile succeeds). Confirmed it fails when the integrity is dropped.
  • Full pacquet-cli (152) and pacquet-package-manager (370) suites pass.
  • cargo check / clippy -D warnings / fmt / taplo / cargo doc -D warnings / typos / dylint all clean.

Notes / out of scope

  • Issue item 3 (Integrity hash get's deleted from tarball installs, when subsequent packages are installed #12001/fix: preserve tarball dependency integrity in the lockfile #12040 integrity-carry-over-on-reuse) is moot here: pacquet re-resolves every dependency on every install (no lockfile-reuse fast path), so the tarball is re-fetched and its integrity recomputed each time — the unrelated-add + frozen steps pass on that basis. The carry-over guard becomes necessary only if/when a resolve-time reuse fast path lands.
  • Pre-existing, cross-cutting lockfile-emitter parity gaps surfaced (not introduced here; they affect all url/git/file keys): pacquet omits the version: field on URL-keyed packages: entries, and its YAML emitter quotes keys containing ://+:port (pnpm leaves them bare). Worth a follow-up.
  • Under --lockfile-only, a remote tarball direct dep now writes CAS files to the store (the lockfile needs the integrity, which requires the download). Minor; possible follow-up.
  • No changeset (pacquet-only, per repo convention).

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

Summary by CodeRabbit

  • New Features

    • Remote HTTPS tarball dependencies are fetched during resolution, compute integrity, and read bundled manifests.
  • Bug Fixes

    • Integrity values for direct tarball URL dependencies are preserved verbatim in the lockfile across re-resolves.
  • Improvements

    • Better frozen-lockfile install behavior and reuse of tarball fetch/cache across resolution and install for more reliable installs.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1169f6ce-c2f1-4b71-9b8e-576ee6cf3427

📥 Commits

Reviewing files that changed from the base of the PR and between d37d702 and ce5214c.

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

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/*.rs: Log emissions are part of matching pnpm — when porting a function that fires pnpm:<channel> events through globalLogger, logger.debug(...), or streamParser.write(...), mirror the call site, payload, and ordering so @pnpm/cli.default-reporter parses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing the brand into a plain String or &str in Rust
If upstream TypeScript always validates before construction of a branded string, validate in the Rust wrapper too via TryFrom<String> and/or FromStr and do not provide an infallible public constructor
If upstream TypeScript never validates a branded string, just brand for type-safety in Rust by exposing an infallible From<String> constructor
If upstream TypeScript occasionally constructs a branded string without validation, expose from_str_unchecked in Rust as an escape hatch alongside the validating constructor
Match upstream serde behavior for branded strings crossing JSON, YAML, or INI boundaries by using #[serde(try_from = "String")] for deserialization and #[serde(into = "String")] for serialization
Derive simple conversions for branded strings using #[derive(derive_more::From)] and #[derive(derive_more::Into)] instead of handwriting impl blocks; use manual impl only when conversion needs custom logic
Model TypeScript string literal unions (like 'auto' | 'always' | 'never') as Rust enums instead of newtype wrappers, since the set of valid values is closed
Treat TypeScript string template literal types (like `${string}@${string}`) the same as branded string types in Rust, using a newtype wrapper with validation
Follow the code style guide in CODE_STYLE_GUIDE.md — imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching, pipe-trait, error handling, test layout, and cloning of Arc and Rc
Choose owned vs. borrowed parameters to minimize copies; widen to t...

Files:

  • pacquet/crates/cli/tests/tarball_url_dependency.rs
pacquet/**/tests/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/tests/**/*.rs: Prefer real fixtures over dependency-injection seams — use tempfile::TempDir, the mocked registry, or integration tests spawning the actual binary for happy paths and most error paths; use the DI seam only for filesystem error kinds, deterministic time, shared process-global state, or external-service happy paths
Follow test-logging guidance — log before non-assert_eq! assertions, dbg! complex structures, skip logging for simple scalar assert_eq!
Tests must not be tolerant of missing build/runtime environment by silently returning early when a tool isn't found — let the test panic when a required tool is absent
Prefer #[cfg_attr(target_os = "windows", ignore = "...")] (or matching #[cfg(unix)] gates) over runtime probe-and-skip helpers for platform-locked tools
Port relevant pnpm tests to Rust tests whenever they translate when porting behavior from pnpm
Use snapshot tests with insta and carefully review diffs when intentional changes alter snapshots; accept with cargo insta review only after careful review
Tests that need the mocked registry should start pnpr through pacquet-testing-utils; cargo test / cargo nextest run should not require a separate just registry-mock launch step

Files:

  • pacquet/crates/cli/tests/tarball_url_dependency.rs
🧠 Learnings (3)
📚 Learning: 2026-05-20T19:40:55.051Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11774
File: pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs:0-0
Timestamp: 2026-05-20T19:40:55.051Z
Learning: In the pacquet Rust code, ensure the semver implementation uses the `node-semver` crate (not `nodejs-semver`). `node-semver`’s public API does not include a `satisfies_with_prerelease`-style method; prerelease-tolerant matching should be implemented inline by first calling `Range::satisfies`, and when it rejects a prerelease version, retry matching against a stripped `MAJOR.MINOR.PATCH` base of the prerelease version.

Applied to files:

  • pacquet/crates/cli/tests/tarball_url_dependency.rs
📚 Learning: 2026-05-22T00:08:44.646Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11837
File: pacquet/crates/resolving-npm-resolver/src/pick_package.rs:33-51
Timestamp: 2026-05-22T00:08:44.646Z
Learning: In the pnpm/pnpm repo’s pacquet Rust crates, do not flag Unicode ellipsis characters (U+2026, `…`) in Rust doc comments (`///` / `/** */`) as a lint violation. The pacquet crate’s `dylint.toml` only enables `perfectionist::derive_ordering`, and the Dylint `unicode-ellipsis` rule is not enabled for this project—so `…` in doc comments is an intentional, repo-consistent style.

Applied to files:

  • pacquet/crates/cli/tests/tarball_url_dependency.rs
📚 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/crates/cli/tests/tarball_url_dependency.rs
🔇 Additional comments (1)
pacquet/crates/cli/tests/tarball_url_dependency.rs (1)

45-58: LGTM!


📝 Walkthrough

Walkthrough

This PR implements resolution-time tarball fetching and integrity computation for HTTP/HTTPS tarball dependencies. The tarball fetch/extract pipeline supports verifying known integrity at install time or computing SHA-512 during resolution. A new TarballFetchContext wires store/index/cache/auth/retry into the resolver so resolution can fetch/extract tarballs and capture bundled manifests. Fresh-lockfile install reorders initialization to share store-index handles; name/version fallbacks derive missing metadata from fetched manifests.

Changes

Resolution-time tarball fetching and integrity preservation

Layer / File(s) Summary
Tarball fetch/extract dual-mode pipeline
pacquet/crates/tarball/src/lib.rs, pacquet/crates/tarball/src/tests.rs
fetch_and_extract_once and fetch_and_extract_with_retry accept an optional expected_integrity and return the computed/validated Integrity with CAS paths and index. Call sites (install-time and tests) updated to pass Some(&pkg_integrity) when known.
Resolution-time tarball fetching
pacquet/crates/tarball/src/lib.rs
New ResolvedTarball and FetchTarballForResolution fetch/extract tarballs during resolution with expected_integrity=None, compute integrity from bytes, read bundled package.json, derive name@version, queue store-index entries, and warm MemCache.
TarballResolver with fetch context
pacquet/crates/resolving-tarball-resolver/Cargo.toml, pacquet/crates/resolving-tarball-resolver/src/lib.rs, pacquet/crates/resolving-tarball-resolver/src/tarball_resolver.rs, pacquet/crates/resolving-tarball-resolver/src/tarball_resolver/tests.rs
Introduces TarballFetchContext (store_dir, store_index_writer, mem_cache, auth_headers, retry_opts). TarballResolver gains optional fetch_context; when present it downloads/extracts tarballs and populates ResolveResult.integrity/manifest, otherwise returns HEAD-only results. Cargo.toml dependencies expanded.
Fresh lockfile install pipeline wiring
pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
Initialization of StoreIndex, StoreIndexWriter, and verified_files_cache moved before resolver construction so shared handles can be passed into TarballFetchContext and PrefetchingResolver. TarballResolver is wired with the fetch context (store, writer, cache, auth, retry). Prefetch key generation falls back to manifest name/version when name_ver is absent; lockfile-only writer await handling improved.
Name/version fallback from manifests
pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs, pacquet/crates/package-manager/src/install_package_from_registry.rs
real_name now extracts package name from resolver-attached manifest for remote HTTP(S) tarballs when name_ver is missing; is_remote_http_tarball helper added. real_name_version helper prefers name_ver then falls back to manifest.name/manifest.version. Minor import reformatting.
End-to-end tarball URL dependency test
pacquet/crates/cli/tests/tarball_url_dependency.rs
Test verifies installation of remote HTTPS-tarball direct deps, reads computed integrity from generated lockfile, re-resolves with an unrelated dependency to trigger lockfile rewrite, asserts integrity is preserved, and runs a --frozen-lockfile install. package_integrity helper made tolerant of quoted/unquoted YAML keys. Removed known_failures path.

Sequence Diagram(s)

sequenceDiagram
  participant Installer
  participant Resolver
  participant TarballResolver
  participant FetchForResolution as FetchTarballForResolution
  participant TarballLib as tarball::fetch_and_extract_with_retry
  participant StoreIndex
  participant MemCache

  Installer->>Resolver: build resolver chain (with TarballFetchContext)
  Resolver->>TarballResolver: resolve(url)
  TarballResolver->>FetchForResolution: run(expected_integrity=None, auth, store handles)
  FetchForResolution->>TarballLib: fetch_and_extract_with_retry
  TarballLib-->>FetchForResolution: (Integrity, CAS paths, manifest)
  FetchForResolution->>StoreIndex: queue entry (name@version)
  FetchForResolution->>MemCache: warm cache (url -> cas paths)
  FetchForResolution-->>TarballResolver: ResolvedTarball(integrity, manifest)
  TarballResolver-->>Resolver: ResolveResult(integrity, manifest)
  Resolver-->>Installer: resolved dependency with integrity/manifest
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related issues

Possibly related PRs

  • pnpm/pnpm#12046: Overlaps on install_with_fresh_lockfile.rs resolver pipeline and lockfile-only gating.
  • pnpm/pnpm#11773: Introduced the initial HEAD-only tarball resolver that this PR extends to perform fetch+compute.
  • pnpm/pnpm#11856: Related prefetching and store-index wiring changes in the fresh-lockfile pipeline.

Poem

"I hopped along the network trail,
a tarball clutched within my tail.
I counted bytes and checked the sum,
then wrote the lockfile—safe and done. 🐇📦"

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding support for remote https-tarball direct dependencies to the tarball resolver. It accurately reflects the primary objective of the PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/12053

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 29, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.02      7.8±0.35ms   556.2 KB/sec    1.00      7.7±0.29ms   565.3 KB/sec

@codecov-commenter

codecov-commenter commented May 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.18182% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.19%. Comparing base (13b1b9a) to head (ce5214c).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...package-manager/src/install_with_fresh_lockfile.rs 83.33% 5 Missing ⚠️
...ckage-manager/src/install_package_from_registry.rs 72.72% 3 Missing ⚠️
...resolving-tarball-resolver/src/tarball_resolver.rs 95.83% 2 Missing ⚠️
...kage-manager/src/dependencies_graph_to_lockfile.rs 91.66% 1 Missing ⚠️
pacquet/crates/tarball/src/lib.rs 98.66% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12076      +/-   ##
==========================================
+ Coverage   87.12%   87.19%   +0.06%     
==========================================
  Files         243      243              
  Lines       26680    26798     +118     
==========================================
+ Hits        23246    23366     +120     
+ Misses       3434     3432       -2     

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

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

@github-actions

github-actions Bot commented May 29, 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.076 ± 0.121 1.994 2.411 1.03 ± 0.06
pacquet@main 2.015 ± 0.020 1.982 2.043 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.0762402314400004,
      "stddev": 0.12073024496585608,
      "median": 2.04945675714,
      "user": 2.6042301799999996,
      "system": 3.3302193599999996,
      "min": 1.99416201114,
      "max": 2.4105446721400003,
      "times": [
        2.0423394261400003,
        2.05848998114,
        2.0186868061400003,
        1.99906101314,
        1.99416201114,
        2.05657408814,
        2.4105446721400003,
        2.07034330514,
        2.0750705161400003,
        2.03713049514
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.01541480234,
      "stddev": 0.020070360409999873,
      "median": 2.0196317821400003,
      "user": 2.66518768,
      "system": 3.32857486,
      "min": 1.9819707611400001,
      "max": 2.04343035214,
      "times": [
        2.0010366211400004,
        2.0227502371400004,
        2.01651332714,
        1.9819707611400001,
        2.04343035214,
        2.02950844314,
        2.00434080514,
        2.0270574321400003,
        2.0361086251400002,
        1.99143141914
      ]
    }
  ]
}

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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 658.6 ± 34.3 632.1 748.8 1.00
pacquet@main 667.7 ± 20.8 644.3 716.6 1.01 ± 0.06
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.6586396517199999,
      "stddev": 0.03427436674910925,
      "median": 0.6463502214200001,
      "user": 0.3566351,
      "system": 1.32091662,
      "min": 0.63205356942,
      "max": 0.74879009342,
      "times": [
        0.74879009342,
        0.63960582742,
        0.64894263042,
        0.6437578124200001,
        0.64031168242,
        0.6566287364200001,
        0.6419367274200001,
        0.6793481464200001,
        0.63205356942,
        0.6550212914200001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.6676749488200001,
      "stddev": 0.02079440118679335,
      "median": 0.66305271692,
      "user": 0.3644737,
      "system": 1.33240012,
      "min": 0.6442812214200001,
      "max": 0.7165735054200001,
      "times": [
        0.7165735054200001,
        0.65939691542,
        0.6495215724200001,
        0.6755957854200001,
        0.68261240842,
        0.65747339542,
        0.65499733542,
        0.6442812214200001,
        0.6695888304200001,
        0.66670851842
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.350 ± 0.034 2.299 2.411 1.00
pacquet@main 2.378 ± 0.035 2.299 2.414 1.01 ± 0.02
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.34954648394,
      "stddev": 0.03430545871879044,
      "median": 2.3473935702400004,
      "user": 3.8219358799999994,
      "system": 3.13737552,
      "min": 2.29900723824,
      "max": 2.41067265524,
      "times": [
        2.3436725222400003,
        2.35623044724,
        2.3464191482400003,
        2.41067265524,
        2.34836799224,
        2.3670729922400002,
        2.30081704324,
        2.29900723824,
        2.38648594224,
        2.3367188582400003
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.37786590494,
      "stddev": 0.034877463866717376,
      "median": 2.38954264474,
      "user": 3.8093407800000003,
      "system": 3.16118672,
      "min": 2.2992680232400002,
      "max": 2.41449695924,
      "times": [
        2.34334780924,
        2.2992680232400002,
        2.41449695924,
        2.41022902524,
        2.36404311324,
        2.40109647924,
        2.3759144772400003,
        2.39115005124,
        2.3879352382400003,
        2.39117787324
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, hot cache + hot store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.537 ± 0.030 1.511 1.601 1.02 ± 0.02
pacquet@main 1.505 ± 0.019 1.476 1.535 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.53686065292,
      "stddev": 0.03003544845807274,
      "median": 1.53083083252,
      "user": 1.7323798,
      "system": 1.8583711400000003,
      "min": 1.51133117002,
      "max": 1.6014741940200001,
      "times": [
        1.57768380102,
        1.5350995630200002,
        1.5130792880200001,
        1.51853832702,
        1.53214674102,
        1.6014741940200001,
        1.5295149240199999,
        1.51133117002,
        1.51149335302,
        1.53824516802
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.5045975943200003,
      "stddev": 0.01944251852850025,
      "median": 1.5020156190200002,
      "user": 1.7128933999999998,
      "system": 1.83995754,
      "min": 1.47566352402,
      "max": 1.5345241730199999,
      "times": [
        1.50257432702,
        1.49071280102,
        1.5345241730199999,
        1.53178631102,
        1.47566352402,
        1.49118310902,
        1.5022976250200002,
        1.49162731302,
        1.50173361302,
        1.52387314702
      ]
    }
  ]
}

@github-actions

github-actions Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12076
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
2,349.55 ms
(-1.49%)Baseline: 2,385.11 ms
2,862.14 ms
(82.09%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,536.86 ms
(+1.34%)Baseline: 1,516.56 ms
1,819.87 ms
(84.45%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
2,076.24 ms
(-1.95%)Baseline: 2,117.57 ms
2,541.08 ms
(81.71%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
658.64 ms
(-2.76%)Baseline: 677.33 ms
812.79 ms
(81.03%)
🐰 View full continuous benchmarking report in Bencher

zkochan and others added 2 commits May 29, 2026 23:32
A direct dependency whose specifier is a remote (non-registry) http(s)
tarball URL (e.g. "foo": "https://cdn.example.com/foo-1.0.0.tgz")
panicked while building the lockfile: TarballResolver returned no
name/version/integrity (those live in the tarball's package.json), so
the dep path was just the URL and the importer-dep-path parse hit
MissingSuffix.

pnpm learns name/version/integrity in the package-requester after the
fetch; pacquet builds the lockfile before the install pass, so the
TarballResolver now fetches the tarball during resolution: it downloads,
computes the sha512 integrity, extracts to the store, and reads the
bundled manifest, warming the shared mem cache (keyed by URL) so the
install pass reuses the extraction without a second download. name_ver
stays None so the depPath is name@<url>, matching pnpm; the
name_ver-dependent call sites fall back to the manifest.

Flips the gated regression test (its premise was inverted: the test
registry binds to 127.0.0.1, so the tarball host must be localhost to
miss the registry prefix and reach TarballResolver).

Refs #12053.

Co-authored-by: Claude <noreply@anthropic.com>
Satisfy perfectionist's bare_issue_reference lint (upgraded to
0.0.0-rc.17 on main): expand bare `#12053` / `#12001` references in the
new comments + test to full URLs.
@zkochan zkochan marked this pull request as ready for review May 30, 2026 08:31
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Review Summary by Qodo

Support remote https-tarball direct dependencies with resolve-time fetch

✨ Enhancement 🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Implement end-to-end support for remote https-tarball direct dependencies
• TarballResolver now fetches tarball during resolution to compute integrity
• Extract name/version/integrity from tarball's package.json for lockfile
• Warm shared mem cache so install pass reuses extraction without re-downloading
• Fix regression test by correcting tarball host URL routing logic
Diagram
flowchart LR
  A["Remote tarball URL"] -->|TarballResolver| B["Fetch during resolution"]
  B -->|Download + extract| C["Compute sha512 integrity"]
  C -->|Read manifest| D["Extract name/version"]
  D -->|Warm mem cache| E["Lockfile builder"]
  E -->|Install pass| F["Reuse extraction"]

Loading

Grey Divider

File Changes

1. pacquet/crates/cli/tests/tarball_url_dependency.rs 🧪 Tests +50/-69

Flip regression test to exercise TarballResolver path

pacquet/crates/cli/tests/tarball_url_dependency.rs


2. pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs ✨ Enhancement +30/-4

Add manifest fallback for remote tarball name extraction

pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs


3. pacquet/crates/package-manager/src/install_package_from_registry.rs ✨ Enhancement +19/-4

Add manifest fallback for name/version resolution

pacquet/crates/package-manager/src/install_package_from_registry.rs


View more (7)
4. pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs ✨ Enhancement +70/-40

Wire TarballFetchContext into resolver before resolution

pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs


5. pacquet/crates/resolving-tarball-resolver/src/lib.rs ✨ Enhancement +11/-4

Export TarballFetchContext for resolve-time tarball fetch

pacquet/crates/resolving-tarball-resolver/src/lib.rs


6. pacquet/crates/resolving-tarball-resolver/src/tarball_resolver.rs ✨ Enhancement +88/-9

Fetch tarball during resolution when context provided

pacquet/crates/resolving-tarball-resolver/src/tarball_resolver.rs


7. pacquet/crates/resolving-tarball-resolver/src/tarball_resolver/tests.rs 🧪 Tests +1/-1

Update test resolver to include fetch_context field

pacquet/crates/resolving-tarball-resolver/src/tarball_resolver/tests.rs


8. pacquet/crates/tarball/src/lib.rs ✨ Enhancement +144/-25

Refactor integrity computation and add resolve-time fetch entry point

pacquet/crates/tarball/src/lib.rs


9. pacquet/crates/tarball/src/tests.rs 🧪 Tests +14/-14

Update tests for optional expected_integrity parameter

pacquet/crates/tarball/src/tests.rs


10. pacquet/crates/resolving-tarball-resolver/Cargo.toml Dependencies +6/-1

Add dependencies for resolve-time tarball fetch

pacquet/crates/resolving-tarball-resolver/Cargo.toml


Grey Divider

Qodo Logo

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pacquet/crates/resolving-tarball-resolver/src/tarball_resolver.rs (1)

89-94: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Attach auth to the HEAD preflight.

When fetch_context is present, the new resolve-time GET is authenticated, but this HEAD probe still isn't. Private tarball URLs will fail here before FetchTarballForResolution ever gets a chance to run, so the new direct-tarball flow remains broken for authenticated hosts.

Suggested fix
-        let client = self.http_client.acquire_for_url(&normalized_bare_specifier).await;
-        let response = client
-            .head(&normalized_bare_specifier)
-            .send()
+        let client = self.http_client.acquire_for_url(&normalized_bare_specifier).await;
+        let mut request = client.head(&normalized_bare_specifier);
+        if let Some(value) = self
+            .fetch_context
+            .as_ref()
+            .and_then(|ctx| ctx.auth_headers.for_url(&normalized_bare_specifier))
+        {
+            request = request.header("authorization", value);
+        }
+        let response = request
+            .send()
             .await
             .map_err(|err| Box::new(err) as ResolveError)?;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pacquet/crates/resolving-tarball-resolver/src/tarball_resolver.rs` around
lines 89 - 94, The HEAD preflight created with
self.http_client.acquire_for_url(&normalized_bare_specifier) is not using the
resolution fetch_context auth, so private tarballs fail before
FetchTarballForResolution; modify the HEAD probe in tarball_resolver.rs to
attach the same authentication used for the later GET when fetch_context is
present (e.g., pull auth headers or credentials from fetch_context and add them
to the client/request before calling
client.head(&normalized_bare_specifier).send()). Ensure you still map errors to
ResolveError as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pacquet/crates/cli/tests/tarball_url_dependency.rs`:
- Around line 41-49: The helper currently scans until "snapshots:" which can
cross into the next package section; update the stopping condition so the
iterator stops at the next sibling "packages:" entry as well. Specifically, in
the chain that starts with lockfile.lines().skip_while(|line|
!is_header(line)).take_while(...).find_map(...), change the take_while predicate
to also break when line.trim_start().starts_with("packages:") (e.g.
.take_while(|line| { let t = line.trim_start(); !t.starts_with("snapshots:") &&
!t.starts_with("packages:") })). This ensures the search for "integrity:" for
the package identified by is_header ends at the next packages: sibling.

In `@pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs`:
- Around line 509-527: The store-index writer join result from TarballResolver's
TarballFetchContext (store_index_writer) can yield Ok(Err(_)) which is currently
ignored on the --lockfile-only path; update the cleanup that awaits the
store_index_writer join (the code that currently only checks join failure) to
also match and handle the inner Err variant: when the join returns Ok(Err(e))
log or propagate that error (same warning/handling used by the materializing
path) so store-index flush failures are not silently dropped; locate
TarballResolver / TarballFetchContext and the cleanup awaiting
store_index_writer to implement this additional match arm.

---

Outside diff comments:
In `@pacquet/crates/resolving-tarball-resolver/src/tarball_resolver.rs`:
- Around line 89-94: The HEAD preflight created with
self.http_client.acquire_for_url(&normalized_bare_specifier) is not using the
resolution fetch_context auth, so private tarballs fail before
FetchTarballForResolution; modify the HEAD probe in tarball_resolver.rs to
attach the same authentication used for the later GET when fetch_context is
present (e.g., pull auth headers or credentials from fetch_context and add them
to the client/request before calling
client.head(&normalized_bare_specifier).send()). Ensure you still map errors to
ResolveError as before.
🪄 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: 36e263d6-7cab-4feb-b2c1-d26900fd35e9

📥 Commits

Reviewing files that changed from the base of the PR and between e2f801e and d37d702.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • pacquet/crates/cli/tests/tarball_url_dependency.rs
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs
  • pacquet/crates/package-manager/src/install_package_from_registry.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
  • pacquet/crates/resolving-tarball-resolver/Cargo.toml
  • pacquet/crates/resolving-tarball-resolver/src/lib.rs
  • pacquet/crates/resolving-tarball-resolver/src/tarball_resolver.rs
  • pacquet/crates/resolving-tarball-resolver/src/tarball_resolver/tests.rs
  • pacquet/crates/tarball/src/lib.rs
  • pacquet/crates/tarball/src/tests.rs
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
pacquet/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/*.rs: Log emissions are part of matching pnpm — when porting a function that fires pnpm:<channel> events through globalLogger, logger.debug(...), or streamParser.write(...), mirror the call site, payload, and ordering so @pnpm/cli.default-reporter parses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing the brand into a plain String or &str in Rust
If upstream TypeScript always validates before construction of a branded string, validate in the Rust wrapper too via TryFrom<String> and/or FromStr and do not provide an infallible public constructor
If upstream TypeScript never validates a branded string, just brand for type-safety in Rust by exposing an infallible From<String> constructor
If upstream TypeScript occasionally constructs a branded string without validation, expose from_str_unchecked in Rust as an escape hatch alongside the validating constructor
Match upstream serde behavior for branded strings crossing JSON, YAML, or INI boundaries by using #[serde(try_from = "String")] for deserialization and #[serde(into = "String")] for serialization
Derive simple conversions for branded strings using #[derive(derive_more::From)] and #[derive(derive_more::Into)] instead of handwriting impl blocks; use manual impl only when conversion needs custom logic
Model TypeScript string literal unions (like 'auto' | 'always' | 'never') as Rust enums instead of newtype wrappers, since the set of valid values is closed
Treat TypeScript string template literal types (like `${string}@${string}`) the same as branded string types in Rust, using a newtype wrapper with validation
Follow the code style guide in CODE_STYLE_GUIDE.md — imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching, pipe-trait, error handling, test layout, and cloning of Arc and Rc
Choose owned vs. borrowed parameters to minimize copies; widen to t...

Files:

  • pacquet/crates/resolving-tarball-resolver/src/tarball_resolver/tests.rs
  • pacquet/crates/package-manager/src/install_package_from_registry.rs
  • pacquet/crates/resolving-tarball-resolver/src/lib.rs
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs
  • pacquet/crates/resolving-tarball-resolver/src/tarball_resolver.rs
  • pacquet/crates/tarball/src/tests.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
  • pacquet/crates/cli/tests/tarball_url_dependency.rs
  • pacquet/crates/tarball/src/lib.rs
pacquet/**/Cargo.toml

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/Cargo.toml: Check whether the workspace already depends on something suitable in [workspace.dependencies] in the root Cargo.toml before adding a new dependency
Keep dependencies at the right level — add a new dependency to the specific crate that needs it, not to the workspace root or shared crate unless multiple crates depend on it

Files:

  • pacquet/crates/resolving-tarball-resolver/Cargo.toml
pacquet/**/tests/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/tests/**/*.rs: Prefer real fixtures over dependency-injection seams — use tempfile::TempDir, the mocked registry, or integration tests spawning the actual binary for happy paths and most error paths; use the DI seam only for filesystem error kinds, deterministic time, shared process-global state, or external-service happy paths
Follow test-logging guidance — log before non-assert_eq! assertions, dbg! complex structures, skip logging for simple scalar assert_eq!
Tests must not be tolerant of missing build/runtime environment by silently returning early when a tool isn't found — let the test panic when a required tool is absent
Prefer #[cfg_attr(target_os = "windows", ignore = "...")] (or matching #[cfg(unix)] gates) over runtime probe-and-skip helpers for platform-locked tools
Port relevant pnpm tests to Rust tests whenever they translate when porting behavior from pnpm
Use snapshot tests with insta and carefully review diffs when intentional changes alter snapshots; accept with cargo insta review only after careful review
Tests that need the mocked registry should start pnpr through pacquet-testing-utils; cargo test / cargo nextest run should not require a separate just registry-mock launch step

Files:

  • pacquet/crates/cli/tests/tarball_url_dependency.rs
🧠 Learnings (3)
📚 Learning: 2026-05-20T19:40:55.051Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11774
File: pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs:0-0
Timestamp: 2026-05-20T19:40:55.051Z
Learning: In the pacquet Rust code, ensure the semver implementation uses the `node-semver` crate (not `nodejs-semver`). `node-semver`’s public API does not include a `satisfies_with_prerelease`-style method; prerelease-tolerant matching should be implemented inline by first calling `Range::satisfies`, and when it rejects a prerelease version, retry matching against a stripped `MAJOR.MINOR.PATCH` base of the prerelease version.

Applied to files:

  • pacquet/crates/resolving-tarball-resolver/src/tarball_resolver/tests.rs
  • pacquet/crates/package-manager/src/install_package_from_registry.rs
  • pacquet/crates/resolving-tarball-resolver/src/lib.rs
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs
  • pacquet/crates/resolving-tarball-resolver/src/tarball_resolver.rs
  • pacquet/crates/tarball/src/tests.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
  • pacquet/crates/cli/tests/tarball_url_dependency.rs
  • pacquet/crates/tarball/src/lib.rs
📚 Learning: 2026-05-22T00:08:44.646Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11837
File: pacquet/crates/resolving-npm-resolver/src/pick_package.rs:33-51
Timestamp: 2026-05-22T00:08:44.646Z
Learning: In the pnpm/pnpm repo’s pacquet Rust crates, do not flag Unicode ellipsis characters (U+2026, `…`) in Rust doc comments (`///` / `/** */`) as a lint violation. The pacquet crate’s `dylint.toml` only enables `perfectionist::derive_ordering`, and the Dylint `unicode-ellipsis` rule is not enabled for this project—so `…` in doc comments is an intentional, repo-consistent style.

Applied to files:

  • pacquet/crates/resolving-tarball-resolver/src/tarball_resolver/tests.rs
  • pacquet/crates/package-manager/src/install_package_from_registry.rs
  • pacquet/crates/resolving-tarball-resolver/src/lib.rs
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs
  • pacquet/crates/resolving-tarball-resolver/src/tarball_resolver.rs
  • pacquet/crates/tarball/src/tests.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
  • pacquet/crates/cli/tests/tarball_url_dependency.rs
  • pacquet/crates/tarball/src/lib.rs
📚 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/crates/resolving-tarball-resolver/src/tarball_resolver/tests.rs
  • pacquet/crates/package-manager/src/install_package_from_registry.rs
  • pacquet/crates/resolving-tarball-resolver/src/lib.rs
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs
  • pacquet/crates/resolving-tarball-resolver/src/tarball_resolver.rs
  • pacquet/crates/tarball/src/tests.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
  • pacquet/crates/cli/tests/tarball_url_dependency.rs
  • pacquet/crates/tarball/src/lib.rs
🔇 Additional comments (4)
pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs (1)

17-20: LGTM!

Also applies to: 337-364

pacquet/crates/package-manager/src/install_package_from_registry.rs (1)

128-133: LGTM!

Also applies to: 224-239

pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs (1)

37-38: LGTM!

Also applies to: 454-483, 1325-1359

pacquet/crates/cli/tests/tarball_url_dependency.rs (1)

1-22: LGTM!

Also applies to: 33-40, 61-117

Comment thread pacquet/crates/cli/tests/tarball_url_dependency.rs
Comment thread pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
…ter-error/test handling

Address review feedback on #12076:
- Attach auth headers to the TarballResolver HEAD preflight so private
  tarball hosts aren't rejected before FetchTarballForResolution runs.
- Handle Ok(Err(_)) from the store-index writer task on the
  --lockfile-only path, mirroring the materializing path.
- Stop package_integrity at the next sibling lockfile entry so a tarball
  entry that lost its integrity can't borrow another package's.
@zkochan

zkochan commented May 30, 2026

Copy link
Copy Markdown
Member Author

Addressed the outside-diff finding on the HEAD preflight in ce5214c: tarball_resolver.rs now attaches the authorization header from fetch_context.auth_headers.for_url(...) to the HEAD probe, the same way the resolve-time GET in FetchTarballForResolution does — so a private tarball host is no longer rejected at the preflight before the download runs. (The unit-test path with no fetch_context keeps the unauthenticated HEAD-only shape.)

The two inline findings (test-helper sibling scan, lockfile-only writer-error handling) are addressed in the same commit; replies are on their threads.

Verified: cargo check / clippy -D warnings / fmt clean; tarball_url_dependency integration test and the 7 tarball_resolver unit tests pass.


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

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.

pacquet: support remote (non-registry) https-tarball direct dependencies (prerequisite for #12001 integrity parity)

2 participants