Skip to content

feat(pacquet): add pnpmfile hooks support#12044

Merged
zkochan merged 23 commits into
pnpm:mainfrom
kairosci:feat/pnpmfile-hooks
Jun 1, 2026
Merged

feat(pacquet): add pnpmfile hooks support#12044
zkochan merged 23 commits into
pnpm:mainfrom
kairosci:feat/pnpmfile-hooks

Conversation

@kairosci

@kairosci kairosci commented May 28, 2026

Copy link
Copy Markdown
Contributor

Implements Tier 4 pnpmfile hooks for pacquet (#11633, point 4.1): pacquet now discovers and runs a project .pnpmfile during dependency management, matching pnpm.

What it does

  • Discovery — looks for .pnpmfile.mjs then .pnpmfile.cjs (dotted names only, .mjs preferred), matching pnpm's requireHooks. Only actual files are accepted (is_file()).
  • readPackage — wired into resolution. Mirrors pnpm's requirePnpmfile contract: the four dependency fields are defaulted to {} before the hook runs, and the returned manifest is validated (must be a non-null object whose dependency fields, when present, are objects rather than arrays). A throwing/syntactically-invalid pnpmfile, a missing require, or a hook that returns nothing aborts the install (PNPMFILE_FAIL) instead of being silently ignored.
  • afterAllResolved — wired into the lockfile write. The resolved lockfile is passed to the hook and its return value is what gets written to pnpm-lock.yaml. The round-trip goes through serde_json::Value (the workspace already enables preserve_order) so hook-added keys the typed Lockfile cannot represent survive to disk; the round-trip only runs when a hook is present, so unmodified installs write byte-identical lockfiles. A throwing hook aborts the install.
  • preResolution — wired. Receives the resolution context (wanted/current lockfile, existsCurrentLockfile, existsNonEmptyWantedLockfile, lockfile dir, store dir, registries) over stdin.
  • filterLog — implemented in the bridge but not yet routed through the reporter (pacquet's reporter is a stateless synchronous emitter); deferred, see follow-ups.

How hooks run

Hooks are served by a long-lived Node.js worker, spawned lazily once per pnpmfile. Requests and responses are newline-delimited JSON over the worker's stdin/stdout, multiplexed by a monotonic request id so the concurrent readPackage calls the resolver makes (it resolves dependencies in parallel) share one process. This removes the per-package node startup cost on the resolution hot path and avoids interpolating payloads into a node -e argument (no E2BIG risk for large lockfiles). Each context.log(...) a hook emits is forwarded back to the call's log callback. preResolution keeps a one-shot node invocation since it runs once per install and needs an info/warn logger.

Tests

  • Unit (hooks crate): readPackage validation (returns nothing / non-object / array dependency fields), manifest-field normalization, syntax-error and missing-module failures, worker request-id multiplexing under concurrency, and context.log forwarding.
  • Integration (package-manager): a readPackage hook pins a transitive dependency version; a hook that returns nothing aborts the install; a pnpmfile syntax error aborts the install; an afterAllResolved hook's mutation is written to pnpm-lock.yaml; a throwing afterAllResolved aborts the install.

Scope

The remaining pnpmfile-hook surface pnpm has but pacquet does not yet implement — wiring filterLog and the pnpm:hook log channel into the reporter, the --pnpmfile / --global-pnpmfile / --ignore-pnpmfile flags, pnpmfile checksum invalidation, updateConfig, and finders/resolvers/fetchers — is tracked in #12118.

Changeset: pacquet minor.

@kairosci kairosci requested a review from zkochan as a code owner May 28, 2026 22:42
Copilot AI review requested due to automatic review settings May 28, 2026 22:42
@coderabbitai

coderabbitai Bot commented May 28, 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 pacquet-hooks crate that discovers and executes .pnpmfile.{cjs,mjs} hooks via a Node bridge, exposes the PnpmfileHooks async trait (readPackage, afterAllResolved, preResolution, filterLog), provides finder/loading helpers and tests, and threads a workspace-scoped pnpmfile_hook through install and resolution.

Changes

Pnpmfile hooks feature

Layer / File(s) Summary
Changeset & workspace dependency wiring
.changeset/add-pnpmfile-hooks.md, Cargo.toml, pacquet/crates/hooks/Cargo.toml, pacquet/crates/package-manager/Cargo.toml, .github/workflows/pacquet-ci.yml
Adds a changeset declaring a minor bump; registers pacquet-hooks in workspace and crate dependencies; adds CI step to install Node.js for tests.
Hooks crate manifest and public API
pacquet/crates/hooks/Cargo.toml, pacquet/crates/hooks/src/lib.rs
Creates pacquet-hooks crate and defines public API: HookContext, PreResolutionHookContext/logger, ReadPackageResult, PnpmfileHooks trait, and NoopHooks.
pnpmfile discovery and loader
pacquet/crates/hooks/src/finder.rs
Implements find_pnpmfile and load_pnpmfile to locate .pnpmfile.mjs/.pnpmfile.cjs and return a NodeJsHooks-backed Arc<dyn PnpmfileHooks>.
Node.js runtime bridge
pacquet/crates/hooks/src/node_runtime.rs
Implements NodeJsHooks, HookError, call_node helper, and PnpmfileHooks methods; provides JSON marshalling, .mjs/.cjs module loading, timeout handling, stdout normalization, and preResolution side-effect invocation.
Fixtures and Node-based tests
pacquet/crates/hooks/tests/__fixtures__/*, pacquet/crates/hooks/tests/node_hooks.rs
Adds CommonJS/.mjs fixture modules and Tokio integration tests validating discovery preference, readPackage mutations, filterLog behavior, and preResolution invocation.
Dependency resolver integration
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
Adds optional pnpmfile_hook to resolver options/context, threads it through WorkspaceTreeCtx/TreeCtx, and calls pnpmfile_hook.read_package during per-dependency manifest mutation.
Importer & workspace resolve wiring
pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs, pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
Extends ResolveImporterOptions with catalog_server and pnpmfile_hook, seeds shared WorkspaceTreeCtx with both hooks, updates Debug output and test defaults.
Install flow & package-manager integration
pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
InstallWithFreshLockfile loads workspace pnpmfile via finder::load_pnpmfile, runs pre_resolution with computed contexts, passes the hook into WorkspaceResolveOptions, and avoids per-importer application.
Resolver test wiring updates
pacquet/crates/resolving-deps-resolver/src/tests.rs
Updates many resolver tests to include pnpmfile_hook: None in ResolveDependencyTreeOptions initializers across suites.

Sequence Diagram

sequenceDiagram
  participant Installer as InstallWithFreshLockfile
  participant Finder as finder::load_pnpmfile
  participant Resolver as resolve_workspace / resolve_dependency_tree
  participant NodeHook as NodeJsHooks
  Installer->>Finder: load_pnpmfile(lockfile_dir)
  Finder-->>Installer: Option<Arc<PnpmfileHooks>>
  Installer->>Resolver: resolve_workspace(..., pnpmfile_hook)
  Resolver->>NodeHook: pnpmfile_hook.read_package(pkg, HookContext)
  NodeHook-->>Resolver: Option<JSON manifest>
  Resolver->>Resolver: apply updated manifest if Some(...)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related issues

Possibly related PRs

  • pnpm/pnpm#11791: Touches resolver pipeline and similar context fields.
  • pnpm/pnpm#12046: Modifies install pipeline at the same install_with_fresh_lockfile integration point.
  • pnpm/pnpm#11905: Overlaps with fresh-lockfile/InstallWithFreshLockfile flow changes.

Suggested reviewers

  • zkochan
  • KSXGitHub

"🐰 I hopped through code to find a hook,
Node whispered JSON, I gave it a look.
Manifests may change with a tiny tap,
Rust waits politely for Node's reply clap,
A carrot-sized feature beneath my book."

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(pacquet): add pnpmfile hooks support' accurately and clearly describes the main change: implementing support for pnpmfile hooks.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Review Summary by Qodo

Add pnpmfile hooks support for package manifest modification

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Implements pnpmfile hooks support for readPackage, afterAllResolved, preResolution, and
  filterLog
• Creates new pacquet-hooks crate with trait-based hook interface and Node.js runtime execution
• Integrates pnpmfile hooks into dependency resolution pipeline via readPackage hook
• Adds comprehensive tests for pnpmfile discovery and hook execution
Diagram
flowchart LR
  A["pnpmfile finder"] -->|discovers| B[".pnpmfile.cjs/mjs"]
  B -->|loads| C["NodeJsHooks runtime"]
  C -->|executes| D["readPackage hook"]
  D -->|modifies| E["package manifest"]
  E -->|feeds into| F["dependency resolution"]

Loading

Grey Divider

File Changes

1. pacquet/crates/hooks/src/lib.rs ✨ Enhancement +50/-0

Define PnpmfileHooks trait and NoopHooks implementation

pacquet/crates/hooks/src/lib.rs


2. pacquet/crates/hooks/src/finder.rs ✨ Enhancement +26/-0

Implement pnpmfile discovery and loading logic

pacquet/crates/hooks/src/finder.rs


3. pacquet/crates/hooks/src/node_runtime.rs ✨ Enhancement +69/-0

Execute pnpmfile hooks via Node.js subprocess

pacquet/crates/hooks/src/node_runtime.rs


View more (14)
4. pacquet/crates/hooks/Cargo.toml ⚙️ Configuration changes +13/-0

Define new pacquet-hooks crate dependencies

pacquet/crates/hooks/Cargo.toml


5. pacquet/crates/hooks/tests/node_hooks.rs 🧪 Tests +146/-0

Test pnpmfile discovery and hook execution

pacquet/crates/hooks/tests/node_hooks.rs


6. pacquet/crates/hooks/tests/__fixtures__/readPackageHook.cjs 🧪 Tests +10/-0

Test fixture for readPackage hook

pacquet/crates/hooks/tests/fixtures/readPackageHook.cjs


7. pacquet/crates/hooks/tests/__fixtures__/filterLogHook.cjs 🧪 Tests +7/-0

Test fixture for filterLog hook

pacquet/crates/hooks/tests/fixtures/filterLogHook.cjs


8. pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs ✨ Enhancement +5/-0

Load and pass pnpmfile hooks to resolver

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


9. pacquet/crates/package-manager/Cargo.toml Dependencies +1/-0

Add pacquet-hooks dependency

pacquet/crates/package-manager/Cargo.toml


10. pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs ✨ Enhancement +42/-25

Apply readPackage hook during manifest resolution

pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs


11. pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs ✨ Enhancement +21/-9

Thread pnpmfile hook through resolver options

pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs


12. pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs ✨ Enhancement +10/-1

Pass pnpmfile hook to workspace resolution

pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs


13. pacquet/crates/resolving-deps-resolver/src/tests.rs 🧪 Tests +31/-0

Update tests with pnpmfile_hook field

pacquet/crates/resolving-deps-resolver/src/tests.rs


14. pacquet/crates/resolving-deps-resolver/Cargo.toml Dependencies +1/-0

Add pacquet-hooks dependency

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


15. Cargo.toml Dependencies +1/-0

Register pacquet-hooks workspace crate

Cargo.toml


16. .changeset/add-pnpmfile-hooks.md 📝 Documentation +5/-0

Document pnpmfile hooks feature addition

.changeset/add-pnpmfile-hooks.md


17. pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs Additional files +2/-0

...

pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs


Grey Divider

Qodo Logo

@zkochan

zkochan commented May 28, 2026

Copy link
Copy Markdown
Member

I am not sure about how pnpmfiles will be supported #11685

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

🧹 Nitpick comments (1)
pacquet/crates/hooks/Cargo.toml (1)

9-9: ⚡ Quick win

Move tokio to dev-dependencies (avoid full in runtime dependencies)

tokio in pacquet/crates/hooks/Cargo.toml (line 9) is only used in tests via #[tokio::test] in pacquet/crates/hooks/tests/node_hooks.rs (no non-test tokio::... usage found). Move it to [dev-dependencies] and trim features to only what the test macro/runtime needs.

🤖 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/hooks/Cargo.toml` at line 9, The tokio dependency is only used
in tests (see #[tokio::test] in tests/node_hooks.rs), so remove the tokio entry
from the regular [dependencies] block and add it under [dev-dependencies]; also
drop the heavy "full" feature and use the minimal features needed for the test
runtime (e.g., features = ["rt-multi-thread", "macros"] with workspace = true)
so tokio is only a test-time dependency for the node_hooks tests.
🤖 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/hooks/src/finder.rs`:
- Around line 16-17: The check for pnpmfile candidates uses path.exists(), which
can match directories or symlinks and lead to non-file paths being returned;
update the condition in the finder function to use path.is_file()
(Path::is_file()) instead so only actual files are accepted before returning
Some(path), leaving the rest of the logic unchanged.

In `@pacquet/crates/hooks/src/node_runtime.rs`:
- Around line 14-23: The current construction of the `wrapper` string in
node_runtime.rs interpolates `self.file.to_string_lossy()` (and other dynamic
inputs) directly into the single-quoted `require('{}')` JS literal, which can
break for paths containing quotes/backslashes or enable injection; change the
`wrapper` to a fixed `node -e` script that reads the module path (and
func/payload) from `process.argv` or environment and calls `require(fileArg)` at
runtime instead of embedding `self.file` into the string, then pass `self.file`,
`func`, and `payload` as separate argv/env entries when spawning Node so the
`wrapper` variable no longer contains any unescaped dynamic file path.
- Around line 16-18: The generated Node wrapper unsafe interpolates
self.file.to_string_lossy() into require('...') and still uses require() for
.mjs, which risks JS injection and breaks async ESM; instead generate a wrapper
that passes the hook path via process.argv (or JSON-encodes it) rather than raw
string interpolation and uses JSON.stringify/escape when embedding any string;
for .mjs produce an async wrapper that uses dynamic import(path) inside an async
IIFE (await import(path)) and then calls (hooks.hooks &&
hooks.hooks['hookName'])?.(args) and console.log(JSON.stringify(res)); update
the code that emits "require('{}')" and the call to hooks.hooks['{}'] in
node_runtime.rs to use process.argv[2] (or a safely JSON-encoded literal) and to
branch to import() for files ending with .mjs so top-level await/async ESM works
correctly.

In `@pacquet/crates/hooks/tests/node_hooks.rs`:
- Around line 41-146: Add a new async test mirroring
test_node_js_hooks_read_package but using an ESM pnpmfile: create a TempDir and
write a ".pnpmfile.mjs" file that exports hooks with ESM syntax (e.g. `export
const hooks = { readPackage };` and `function readPackage(pkg) { if (pkg.name
=== 'foo') pkg.dependencies = { bar: '100.0.0' }; return pkg; }`), instantiate
pacquet_hooks::node_runtime::NodeJsHooks { file: pnpmfile_path }, call
hooks.read_package(manifest, HookContext { log: Arc::new(|_| {}) }).await, and
assert the returned manifest has dependencies.bar == "100.0.0"; this ensures
NodeJsHooks::read_package works for .mjs loader paths.

---

Nitpick comments:
In `@pacquet/crates/hooks/Cargo.toml`:
- Line 9: The tokio dependency is only used in tests (see #[tokio::test] in
tests/node_hooks.rs), so remove the tokio entry from the regular [dependencies]
block and add it under [dev-dependencies]; also drop the heavy "full" feature
and use the minimal features needed for the test runtime (e.g., features =
["rt-multi-thread", "macros"] with workspace = true) so tokio is only a
test-time dependency for the node_hooks tests.
🪄 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: c70a62d8-4b0d-4f4e-877f-af2d711d948b

📥 Commits

Reviewing files that changed from the base of the PR and between a9011ad and 969eb25.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (17)
  • .changeset/add-pnpmfile-hooks.md
  • Cargo.toml
  • pacquet/crates/hooks/Cargo.toml
  • pacquet/crates/hooks/src/finder.rs
  • pacquet/crates/hooks/src/lib.rs
  • pacquet/crates/hooks/src/node_runtime.rs
  • pacquet/crates/hooks/tests/__fixtures__/filterLogHook.cjs
  • pacquet/crates/hooks/tests/__fixtures__/readPackageHook.cjs
  • pacquet/crates/hooks/tests/node_hooks.rs
  • pacquet/crates/package-manager/Cargo.toml
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
  • pacquet/crates/resolving-deps-resolver/Cargo.toml
  • pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.rs
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
pacquet/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/*.rs: Warnings are errors (--deny warnings in lint). Do not silence them with #[allow(...)] unless there is a specific, justified reason.
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: https://rust-lang.github.io/api-guidelines/naming.html
No star imports inside module bodies. Write use super::{Foo, bar} instead of use super::*;, and the same for any other glob whose target is a module you control. Two forms stay allowed: external-crate preludes such as use rayon::prelude::*; and root-of-module re-exports such as pub use submodule::*; in a lib.rs.
Doc comments (///, //!) document the contract: preconditions, postconditions, panics, the reason the function exists. They are not a re-narration of the body.
Do not restate at call sites what the callee's doc comment already says. If /// on the function says 'no-op when …', the caller should not repeat that. Update the doc once; let every call site benefit.
Tests are documentation. Do not duplicate them in prose. If a behavioral scenario, edge case, failure mode, or worked example is already captured by a test, do not also narrate it in the doc comment on the implementation. The same applies in reverse: a test's own doc comment should not re-explain what the asserts already say, only the why if it is not obvious.
Use // SAFETY:, // TODO:, and similar prefixes to signal hidden invariants or known follow-ups that a reader cannot recover from the code alone.
When editing existing code, do not break a method chain (including pipe-trait .pipe(...) chains) into intermediate let bindings unless you can justify the rewrite. Valid justifications include a chain that fails to compile after your...

Files:

  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/hooks/src/finder.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs
  • pacquet/crates/hooks/src/lib.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
  • pacquet/crates/hooks/tests/node_hooks.rs
  • pacquet/crates/hooks/src/node_runtime.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.rs
pacquet/**/{tests,test}/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/{tests,test}/*.rs: Tests must not be tolerant of a missing build / runtime environment by silently returning early when a tool isn't found. If the test needs a tool, just call into it and let the existing .unwrap() / .expect(...) panic when the tool is absent.
Prefer platform-specific gates like #[cfg_attr(target_os = "windows", ignore = "...")] or #[cfg(unix)] over runtime probe-and-skip helpers for platform-locked tools, because the gate is visible to cargo test and shows up in the test report.
Follow the test-logging guidance in the style guide — log before non-assert_eq! assertions, dbg! complex structures, skip logging for simple scalar assert_eq!.

Files:

  • pacquet/crates/hooks/tests/node_hooks.rs
🧠 Learnings (6)
📚 Learning: 2026-05-26T21:01:06.666Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11966
File: .changeset/require-tarball-integrity.md:6-6
Timestamp: 2026-05-26T21:01:06.666Z
Learning: In pnpm lockfile-related release notes/docs (especially changeset markdown), preserve URL hostnames exactly as they appear in pnpm-lock.yaml tarball resolution entries—keep hosts like `codeload.github.com`, `bitbucket.org`, and `gitlab.com` in lowercase. Do not “correct” them to title-case/preserve brand capitalization (e.g., LanguageTool rules like `GITHUB` capitalization) because these are literal URL fragments, not platform brand names.

Applied to files:

  • .changeset/add-pnpmfile-hooks.md
📚 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-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/hooks/src/finder.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs
  • pacquet/crates/hooks/src/lib.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
  • pacquet/crates/hooks/tests/node_hooks.rs
  • pacquet/crates/hooks/src/node_runtime.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.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-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/hooks/src/finder.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs
  • pacquet/crates/hooks/src/lib.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
  • pacquet/crates/hooks/tests/node_hooks.rs
  • pacquet/crates/hooks/src/node_runtime.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.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-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/hooks/src/finder.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs
  • pacquet/crates/hooks/src/lib.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
  • pacquet/crates/hooks/tests/node_hooks.rs
  • pacquet/crates/hooks/src/node_runtime.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.rs
📚 Learning: 2026-05-24T21:10:50.292Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11915
File: pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs:553-617
Timestamp: 2026-05-24T21:10:50.292Z
Learning: When implementing dependency “revisit”/optional-folding in pacquet, mirror pnpm’s `resolveDependencies.ts` behavior: on revisit, update the `optional` flag only for the directly visited package, and do not automatically change `optional` for transitive descendants. pnpm corrects stale optional flags later via `copyDependencySubGraph` (a BFS in `lockfile/pruner/src/index.ts`/`copyDependencySubGraph`). Until pacquet has an equivalent pruner/copy-subgraph step, be aware that the raw `node.optional` can flow through to the lockfile/virtual store via `dependencies_graph_to_lockfile.rs` → `create_virtual_store.rs` → `installability.rs`, so review changes that affect optional flag propagation for this mismatch.

Applied to files:

  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.rs
📚 Learning: 2026-05-24T21:10:50.292Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11915
File: pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs:553-617
Timestamp: 2026-05-24T21:10:50.292Z
Learning: In pacquet’s Rust resolver layer, the behavior of `ResolvedPackage.optional` “AND-folding”/revisit updates is intentionally aligned with pnpm: when a package is directly revisited, only that package’s `optional` flag is updated; transitive descendants are not re-walked/rescored in this stage. pnpm later corrects any stale optional flags downstream via its pruner/BFS logic (`copyDependencySubGraph` + `nonOptional` reachability stamping in the lockfile pruner). Since pacquet does not yet have that pruner equivalent, review should NOT flag the downstream/stale optional propagation as a bug in pacquet PRs; treat the resolver-layer optional propagation gap as expected parity until the pruning/stamping step is ported.

Applied to files:

  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.rs
🔇 Additional comments (12)
pacquet/crates/hooks/src/lib.rs (1)

1-50: LGTM!

pacquet/crates/hooks/tests/__fixtures__/readPackageHook.cjs (1)

1-10: LGTM!

pacquet/crates/hooks/tests/__fixtures__/filterLogHook.cjs (1)

1-7: LGTM!

pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs (1)

13-13: LGTM!

Also applies to: 52-53, 62-63, 180-181, 305-306, 321-322, 352-355, 462-467, 619-637

pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs (1)

129-129: LGTM!

Also applies to: 136-139, 159-162, 199-207, 240-247

pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs (1)

66-70: LGTM!

Also applies to: 109-115

pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs (1)

99-101: LGTM!

pacquet/crates/package-manager/Cargo.toml (1)

34-34: LGTM!

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

18-18: LGTM!

Also applies to: 597-597, 605-605, 653-653, 655-655

pacquet/crates/resolving-deps-resolver/Cargo.toml (1)

18-18: LGTM!

pacquet/crates/resolving-deps-resolver/src/tests.rs (1)

120-120: LGTM!

Also applies to: 189-189, 274-274, 315-315, 359-359, 438-438, 480-480, 523-523, 568-568, 609-609, 661-661, 711-711, 764-764, 888-888, 967-967, 1016-1016, 1113-1113, 1208-1208, 1315-1315, 1384-1384, 1494-1494, 1602-1602, 1712-1712, 1801-1801, 1849-1849, 1883-1883, 1933-1933, 2003-2003, 2058-2058, 2123-2123, 2176-2176

Cargo.toml (1)

49-49: LGTM!

Comment thread pacquet/crates/hooks/src/finder.rs Outdated
Comment thread pacquet/crates/hooks/src/node_runtime.rs Outdated
Comment thread pacquet/crates/hooks/src/node_runtime.rs Outdated
Comment thread pacquet/crates/hooks/tests/node_hooks.rs Outdated

@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

🤖 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/hooks/src/node_runtime.rs`:
- Around line 25-31: NodeJsHooks::call_node currently uses blocking
std::process::Command::output() which can block Tokio worker threads; change
call_node into an async function that spawns an asynchronous process using
tokio::process::Command and await its output with a tokio::time::timeout
(configurable duration) so hung hooks error out instead of blocking; update all
callers mentioned (e.g., read_package and other #[async_trait] trait methods
used by resolve_dependency_tree::resolve_node) to call .await on the new async
call_node and propagate timeout/errors appropriately; consider exposing the
timeout value as a parameter or config and ensure stderr/stdout are converted to
String on timeout/error paths.
- Around line 47-71: The NodeJsHooks methods are swallowing call_node errors
(using .ok()/.unwrap_or) and ignoring HookContext; capture the Result from
self.call_node in read_package, after_all_resolved, pre_resolution, and
filter_log, and on Err(e) call ctx.log.error (or the appropriate HookContext
logging method) with a clear message including the hook name and the error, then
fall back to the previous return value; for read_package use ctx.log and return
None on error, for after_all_resolved/pre_resolution log and return None, and
for filter_log log and return true as before — if you want installs to fail like
pnpm consider changing these signatures to return Result<> and propagate the
error instead of falling back.
🪄 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: bea01777-76bd-46fa-a192-f1da4be37483

📥 Commits

Reviewing files that changed from the base of the PR and between 969eb25 and ed3cfd1.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (17)
  • .changeset/add-pnpmfile-hooks.md
  • Cargo.toml
  • pacquet/crates/hooks/Cargo.toml
  • pacquet/crates/hooks/src/finder.rs
  • pacquet/crates/hooks/src/lib.rs
  • pacquet/crates/hooks/src/node_runtime.rs
  • pacquet/crates/hooks/tests/__fixtures__/filterLogHook.cjs
  • pacquet/crates/hooks/tests/__fixtures__/readPackageHook.cjs
  • pacquet/crates/hooks/tests/node_hooks.rs
  • pacquet/crates/package-manager/Cargo.toml
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
  • pacquet/crates/resolving-deps-resolver/Cargo.toml
  • pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.rs
✅ Files skipped from review due to trivial changes (3)
  • .changeset/add-pnpmfile-hooks.md
  • Cargo.toml
  • pacquet/crates/hooks/Cargo.toml
🚧 Files skipped from review as they are similar to previous changes (9)
  • pacquet/crates/hooks/tests/fixtures/filterLogHook.cjs
  • pacquet/crates/hooks/src/finder.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs
  • pacquet/crates/package-manager/Cargo.toml
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
  • pacquet/crates/hooks/tests/node_hooks.rs
  • pacquet/crates/hooks/src/lib.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.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). (7)
  • GitHub Check: ubuntu-latest / Node.js 24 / Test
  • 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: Code Coverage
  • GitHub Check: Run benchmark on ubuntu-latest
🧰 Additional context used
📓 Path-based instructions (1)
pacquet/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/*.rs: Warnings are errors (--deny warnings in lint). Do not silence them with #[allow(...)] unless there is a specific, justified reason.
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: https://rust-lang.github.io/api-guidelines/naming.html
No star imports inside module bodies. Write use super::{Foo, bar} instead of use super::*;, and the same for any other glob whose target is a module you control. Two forms stay allowed: external-crate preludes such as use rayon::prelude::*; and root-of-module re-exports such as pub use submodule::*; in a lib.rs.
Doc comments (///, //!) document the contract: preconditions, postconditions, panics, the reason the function exists. They are not a re-narration of the body.
Do not restate at call sites what the callee's doc comment already says. If /// on the function says 'no-op when …', the caller should not repeat that. Update the doc once; let every call site benefit.
Tests are documentation. Do not duplicate them in prose. If a behavioral scenario, edge case, failure mode, or worked example is already captured by a test, do not also narrate it in the doc comment on the implementation. The same applies in reverse: a test's own doc comment should not re-explain what the asserts already say, only the why if it is not obvious.
Use // SAFETY:, // TODO:, and similar prefixes to signal hidden invariants or known follow-ups that a reader cannot recover from the code alone.
When editing existing code, do not break a method chain (including pipe-trait .pipe(...) chains) into intermediate let bindings unless you can justify the rewrite. Valid justifications include a chain that fails to compile after your...

Files:

  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/hooks/src/node_runtime.rs
🧠 Learnings (5)
📚 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-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/hooks/src/node_runtime.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-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/hooks/src/node_runtime.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-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/hooks/src/node_runtime.rs
📚 Learning: 2026-05-24T21:10:50.292Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11915
File: pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs:553-617
Timestamp: 2026-05-24T21:10:50.292Z
Learning: When implementing dependency “revisit”/optional-folding in pacquet, mirror pnpm’s `resolveDependencies.ts` behavior: on revisit, update the `optional` flag only for the directly visited package, and do not automatically change `optional` for transitive descendants. pnpm corrects stale optional flags later via `copyDependencySubGraph` (a BFS in `lockfile/pruner/src/index.ts`/`copyDependencySubGraph`). Until pacquet has an equivalent pruner/copy-subgraph step, be aware that the raw `node.optional` can flow through to the lockfile/virtual store via `dependencies_graph_to_lockfile.rs` → `create_virtual_store.rs` → `installability.rs`, so review changes that affect optional flag propagation for this mismatch.

Applied to files:

  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
📚 Learning: 2026-05-24T21:10:50.292Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11915
File: pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs:553-617
Timestamp: 2026-05-24T21:10:50.292Z
Learning: In pacquet’s Rust resolver layer, the behavior of `ResolvedPackage.optional` “AND-folding”/revisit updates is intentionally aligned with pnpm: when a package is directly revisited, only that package’s `optional` flag is updated; transitive descendants are not re-walked/rescored in this stage. pnpm later corrects any stale optional flags downstream via its pruner/BFS logic (`copyDependencySubGraph` + `nonOptional` reachability stamping in the lockfile pruner). Since pacquet does not yet have that pruner equivalent, review should NOT flag the downstream/stale optional propagation as a bug in pacquet PRs; treat the resolver-layer optional propagation gap as expected parity until the pruning/stamping step is ported.

Applied to files:

  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
🔇 Additional comments (4)
pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs (1)

136-138: LGTM!

Also applies to: 159-161, 199-208, 242-247

pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs (1)

66-69: LGTM!

Also applies to: 109-115

pacquet/crates/resolving-deps-resolver/Cargo.toml (1)

18-18: LGTM!

pacquet/crates/hooks/src/node_runtime.rs (1)

14-23: Unsafe JS interpolation of self.file and require() on .mjs hooks.

self.file.to_string_lossy() is embedded into a single-quoted JS literal (require('{}')), so paths containing ', backslashes, or other special characters produce broken JS or enable code injection. Pass the path via process.argv/env (or JSON.stringify it) instead of raw string interpolation. Separately, require() will fail for .pnpmfile.mjs hooks that rely on async ESM (top-level await); prefer dynamic import() for .mjs.

Comment thread pacquet/crates/hooks/src/node_runtime.rs Outdated
Comment thread pacquet/crates/hooks/src/node_runtime.rs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

@codecov-commenter

codecov-commenter commented May 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.65744% with 27 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.40%. Comparing base (ae6e077) to head (6990a10).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
pacquet/crates/hooks/src/lib.rs 0.00% 8 Missing ⚠️
pacquet/crates/hooks/src/node_runtime.rs 90.47% 8 Missing ⚠️
pacquet/crates/hooks/src/worker.rs 90.00% 8 Missing ⚠️
...es/resolving-deps-resolver/src/resolve_importer.rs 66.66% 2 Missing ⚠️
...lving-deps-resolver/src/resolve_dependency_tree.rs 95.65% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12044      +/-   ##
==========================================
+ Coverage   87.34%   87.40%   +0.05%     
==========================================
  Files         255      260       +5     
  Lines       28703    29347     +644     
==========================================
+ Hits        25072    25652     +580     
- Misses       3631     3695      +64     

☔ 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 28, 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.052 ± 0.031 1.995 2.102 1.01 ± 0.02
pacquet@main 2.039 ± 0.035 1.979 2.082 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.05215488258,
      "stddev": 0.030803851839023223,
      "median": 2.0593559935799997,
      "user": 2.59145464,
      "system": 3.3509695600000002,
      "min": 1.9948375250800001,
      "max": 2.10223386908,
      "times": [
        2.0678187870799998,
        2.0364871660799997,
        2.06040434508,
        2.0690367780799996,
        2.02262041508,
        2.03279234108,
        1.9948375250800001,
        2.10223386908,
        2.07700995708,
        2.05830764208
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.0391500709799995,
      "stddev": 0.034510775269862806,
      "median": 2.04285212108,
      "user": 2.64039584,
      "system": 3.3707286599999997,
      "min": 1.97878239208,
      "max": 2.08225380908,
      "times": [
        2.08225380908,
        2.03236821908,
        2.05124877608,
        2.0100345850799997,
        1.97878239208,
        1.99958024008,
        2.03445546608,
        2.0753007700799997,
        2.05770368908,
        2.06977276308
      ]
    }
  ]
}

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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 650.4 ± 15.5 629.3 688.4 1.00
pacquet@main 710.3 ± 47.5 648.7 799.2 1.09 ± 0.08
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.65038852572,
      "stddev": 0.015461715260718592,
      "median": 0.64806880552,
      "user": 0.35475871999999997,
      "system": 1.3342111199999998,
      "min": 0.62926649202,
      "max": 0.68837923802,
      "times": [
        0.68837923802,
        0.65727400702,
        0.65419806502,
        0.64969583402,
        0.64963561102,
        0.64650200002,
        0.64516244402,
        0.64435233902,
        0.63941922702,
        0.62926649202
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.7103098370200001,
      "stddev": 0.04745633529321599,
      "median": 0.7043471945199999,
      "user": 0.36994192000000004,
      "system": 1.3285393199999997,
      "min": 0.64866043702,
      "max": 0.79916245802,
      "times": [
        0.79916245802,
        0.67397988402,
        0.75522206902,
        0.71316708602,
        0.65164302002,
        0.73644706102,
        0.73522736902,
        0.69552730302,
        0.64866043702,
        0.69406168302
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.290 ± 0.021 2.267 2.329 1.00
pacquet@main 2.323 ± 0.029 2.272 2.375 1.01 ± 0.02
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.28971615116,
      "stddev": 0.021233044189596498,
      "median": 2.27971043726,
      "user": 3.7397467200000003,
      "system": 3.11472576,
      "min": 2.26694589476,
      "max": 2.32852518776,
      "times": [
        2.30935088576,
        2.27173441276,
        2.27390489476,
        2.26694589476,
        2.27812294676,
        2.29928550176,
        2.31324011776,
        2.28129792776,
        2.27475374176,
        2.32852518776
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.32339245786,
      "stddev": 0.028903926919204296,
      "median": 2.32538907726,
      "user": 3.7587490199999998,
      "system": 3.17947216,
      "min": 2.27186920876,
      "max": 2.37466470676,
      "times": [
        2.28316516576,
        2.32349109476,
        2.3446302597599997,
        2.3215126757599998,
        2.32990499776,
        2.33390831476,
        2.32448016376,
        2.37466470676,
        2.27186920876,
        2.32629799076
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.469 ± 0.017 1.441 1.495 1.00
pacquet@main 1.500 ± 0.103 1.450 1.788 1.02 ± 0.07
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.46892133656,
      "stddev": 0.0168255320521366,
      "median": 1.46852695676,
      "user": 1.6695813999999998,
      "system": 1.8346263999999999,
      "min": 1.44090346576,
      "max": 1.49452149176,
      "times": [
        1.49452149176,
        1.4671705407600002,
        1.46916207976,
        1.44090346576,
        1.4891157217600002,
        1.47655602976,
        1.46789183376,
        1.4521143007600001,
        1.4529204177600001,
        1.4788574837600001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.5001952348599998,
      "stddev": 0.10340991654557849,
      "median": 1.45906583426,
      "user": 1.7059956,
      "system": 1.8316168999999995,
      "min": 1.44983353176,
      "max": 1.78839553676,
      "times": [
        1.45588404376,
        1.4584151487600001,
        1.78839553676,
        1.49950535276,
        1.4597165197600002,
        1.45010921376,
        1.4511653237600002,
        1.44983353176,
        1.4818051787600002,
        1.50712249876
      ]
    }
  ]
}

@github-actions

github-actions Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12044
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,289.72 ms
(-2.83%)Baseline: 2,356.31 ms
2,827.57 ms
(80.98%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,468.92 ms
(-3.52%)Baseline: 1,522.47 ms
1,826.97 ms
(80.40%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
2,052.15 ms
(-1.69%)Baseline: 2,087.46 ms
2,504.96 ms
(81.92%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
650.39 ms
(-2.68%)Baseline: 668.32 ms
801.99 ms
(81.10%)
🐰 View full continuous benchmarking report in Bencher

@kairosci

Copy link
Copy Markdown
Contributor Author

Using a Node.js subprocess to run .pnpmfile.mjs/.pnpmfile.cjs is only a temporary solution. Although the ideal goal for a Rust project is a WASM plugin system (pnpmfile.wasm), the current approach ensures immediate compatibility with the original pnpm ecosystem, exactly replicating its hooks and JavaScript interface. This technical bridge allows for support of both ESM and CommonJS right from the start, while keeping the internal architecture ready to transition to WASM in the future without disrupting the public APIs of the features.

Copilot AI review requested due to automatic review settings May 28, 2026 23:21
@kairosci kairosci force-pushed the feat/pnpmfile-hooks branch from 7baf1a3 to 483b238 Compare May 28, 2026 23:21

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

🤖 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/hooks/src/node_runtime.rs`:
- Around line 58-61: The timeout currently drops the future returned by
Command::new("node").arg("-e").arg(&wrapper).output() which can leave a hung
node child running; update the Command invocation in the timeout call (the
expression that builds the child before calling output()) to call
.kill_on_drop(true) on the Command so the child process is terminated when the
future is dropped (i.e., modify the code around
Command::new("node").arg("-e").arg(&wrapper).output() inside the timeout in
node_runtime.rs).
- Around line 38-58: The current implementation interpolates the full JSON
payload into the wrapper string and passes it via node -e, risking E2BIG; change
NodeJsHooks::call_node so the wrapper no longer embeds {payload} but instead
reads the payload from stdin (or an env/temp file), and invoke
Command::new("node") with stdin piped and write the serialized payload into the
child process before awaiting output; keep existing logic that switches between
the async .mjs import wrapper and the require wrapper (the wrapper variable),
and preserve the HOOK_TIMEOUT usage around the Command::output() call while
replacing .arg("-e").arg(&wrapper).output() with a piped-stdin invocation that
feeds the payload to node.
- Around line 38-56: The wrapper sent by NodeJsHooks::call_node calls the hook
without awaiting it, causing Promise results to be JSON-serialized too early;
modify the wrapper generation so the .mjs branch awaits the hook invocation
(e.g., use "await (hooks.hooks && hooks.hooks['{func}'])?.({payload})" inside
the async IIFE) and change the CJS branch to also use an async IIFE (instead of
top-level require+call) so you can await the hook there as well before
console.log(JSON.stringify(res)); update the code that builds `wrapper` (the
file_path.ends_with(".mjs") conditional) accordingly so both branches await the
hook result.
🪄 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: 8c8cde1e-f794-4474-b27a-49ebd4261256

📥 Commits

Reviewing files that changed from the base of the PR and between ed3cfd1 and 7baf1a3.

📒 Files selected for processing (5)
  • pacquet/crates/hooks/Cargo.toml
  • pacquet/crates/hooks/src/finder.rs
  • pacquet/crates/hooks/src/lib.rs
  • pacquet/crates/hooks/src/node_runtime.rs
  • pacquet/crates/hooks/tests/node_hooks.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • pacquet/crates/hooks/src/finder.rs
  • pacquet/crates/hooks/src/lib.rs
  • pacquet/crates/hooks/tests/node_hooks.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). (5)
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Code Coverage
🧰 Additional context used
📓 Path-based instructions (1)
pacquet/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/*.rs: Warnings are errors (--deny warnings in lint). Do not silence them with #[allow(...)] unless there is a specific, justified reason.
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: https://rust-lang.github.io/api-guidelines/naming.html
No star imports inside module bodies. Write use super::{Foo, bar} instead of use super::*;, and the same for any other glob whose target is a module you control. Two forms stay allowed: external-crate preludes such as use rayon::prelude::*; and root-of-module re-exports such as pub use submodule::*; in a lib.rs.
Doc comments (///, //!) document the contract: preconditions, postconditions, panics, the reason the function exists. They are not a re-narration of the body.
Do not restate at call sites what the callee's doc comment already says. If /// on the function says 'no-op when …', the caller should not repeat that. Update the doc once; let every call site benefit.
Tests are documentation. Do not duplicate them in prose. If a behavioral scenario, edge case, failure mode, or worked example is already captured by a test, do not also narrate it in the doc comment on the implementation. The same applies in reverse: a test's own doc comment should not re-explain what the asserts already say, only the why if it is not obvious.
Use // SAFETY:, // TODO:, and similar prefixes to signal hidden invariants or known follow-ups that a reader cannot recover from the code alone.
When editing existing code, do not break a method chain (including pipe-trait .pipe(...) chains) into intermediate let bindings unless you can justify the rewrite. Valid justifications include a chain that fails to compile after your...

Files:

  • pacquet/crates/hooks/src/node_runtime.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/hooks/src/node_runtime.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/hooks/src/node_runtime.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/hooks/src/node_runtime.rs
🔇 Additional comments (6)
pacquet/crates/hooks/src/node_runtime.rs (4)

83-96: Divergence from pnpm: readPackage failure should abort, not continue.

Logging via ctx.log is a good improvement over silently swallowing. However, in pnpm a throw from hooks.readPackage aborts the install, whereas here the error is logged and resolution continues with the original manifest. This is the same divergence raised previously. Based on learnings, follow the upstream pnpm compatibility rule: match pnpm's behavior exactly (a failing readPackage should fail the install rather than fall back).


16-28: LGTM!


63-78: LGTM!


98-116: 🏗️ Heavy lift

Map null/undefined hook outputs to None for after_all_resolved and pre_resolution

NodeJsHooks::read_package maps Ok(Value::Null) (from JS null/undefined) to None, but after_all_resolved and pre_resolution currently wrap it as Some(Value::Null), which can cause null to be treated as a real value instead of “hook returned nothing”.

🛠️ Proposed fix
         match self.call_node("afterAllResolved", lockfile).await {
+            Ok(v) if v.is_null() => None,
             Ok(v) => Some(v),
             Err(e) => {
                 (ctx.log)(format!("pnpmfile hook afterAllResolved failed: {}", e));
                 None
             }
         }
         match self.call_node("preResolution", Value::Null).await {
+            Ok(v) if v.is_null() => None,
             Ok(v) => Some(v),
             Err(e) => {
                 (ctx.log)(format!("pnpmfile hook preResolution failed: {}", e));
                 None
             }
         }
pacquet/crates/hooks/Cargo.toml (2)

9-9: LGTM!


10-10: ⚡ Quick win

Ensure the workspace-level derive_more enables the display/full feature

pacquet/crates/hooks/Cargo.toml declares derive_more = { workspace = true }, and many other crates do the same—so the #[derive(Display)] usage depends entirely on how derive_more is configured in the workspace dependency (e.g., features = ["display"] or ["full"]). The gathered results don’t include the workspace-level derive_more features, so that declaration needs inspection to guarantee this crate compiles.

Comment thread pacquet/crates/hooks/src/node_runtime.rs Outdated
Comment thread pacquet/crates/hooks/src/node_runtime.rs Outdated
Comment thread pacquet/crates/hooks/src/node_runtime.rs Outdated

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

♻️ Duplicate comments (3)
pacquet/crates/hooks/src/node_runtime.rs (3)

38-56: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Hook result is never awaited — async pnpmfile hooks return a Promise that gets serialized as {}.

pnpm's readPackage/afterAllResolved/etc. may be async. In both branches the hook is invoked without await, so an async hook's Promise reaches JSON.stringify(res)"{}". For readPackage that means the manifest is silently replaced with an empty object (data corruption), not a no-op. The CJS branch also needs an async IIFE since top-level await is invalid under node -e CommonJS.

🐛 Proposed fix (await in both branches; wrap CJS in async IIFE)
         let wrapper = if file_path.ends_with(".mjs") {
             format!(
                 r#"(async () => {{
   const hooks = await import({file_path_escaped});
-  const res = (hooks.hooks && hooks.hooks['{func}'])?.({payload});
+  const res = await (hooks.hooks && hooks.hooks['{func}'])?.({payload});
   console.log(JSON.stringify(res));
 }})();
 "#,
                 file_path_escaped = file_path_escaped
             )
         } else {
             format!(
-                r#"const hooks = require({file_path_escaped});
-const res = (hooks.hooks && hooks.hooks['{func}'])?.({payload});
-console.log(JSON.stringify(res));
+                r#"(async () => {{
+  const hooks = require({file_path_escaped});
+  const res = await (hooks.hooks && hooks.hooks['{func}'])?.({payload});
+  console.log(JSON.stringify(res));
+}})();
 "#,
                 file_path_escaped = file_path_escaped
             )
         };

Separately, the hook is invoked with only {payload}; pnpm's readPackage(pkg, context) passes a second context argument (with a log function). Hooks relying on context would throw here. Please confirm the intended JS contract.

What arguments does pnpm pass to the readPackage hook in .pnpmfile.cjs, and does the context include a log function?
🤖 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/hooks/src/node_runtime.rs` around lines 38 - 56, The generated
JS never awaits the hook result and the CJS branch runs top-level code, so async
hooks serialize as {} and context is missing; update the template generation for
variable wrapper so both branches invoke the hook inside an async IIFE and await
the call (e.g. replace (hooks.hooks && hooks.hooks['{func}'])?.({payload}) with
await (hooks.hooks && hooks.hooks['{func}'])?.({payload}, {context}) ), and for
the CJS branch wrap require(...) usage in an async IIFE like the ESM branch so
console.log(JSON.stringify(res)) receives the resolved value; keep the same
identifiers (wrapper, file_path.ends_with(".mjs"), hooks.hooks['{func}'],
console.log(JSON.stringify(res))) when making the changes.

58-61: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Set kill_on_drop(true) so a hung node child is terminated on timeout.

When the timeout fires the output() future is dropped, but Tokio does not kill the spawned process on Child drop by default, leaving an orphaned node process.

🔪 Proposed fix
-        let output = timeout(HOOK_TIMEOUT, Command::new("node").arg("-e").arg(&wrapper).output())
-            .await
-            .map_err(|_| HookError::Timeout(func.to_string()))?
-            .map_err(|e| HookError::ExecutionFailed(e.to_string()))?;
+        let output = timeout(
+            HOOK_TIMEOUT,
+            Command::new("node").arg("-e").arg(&wrapper).kill_on_drop(true).output(),
+        )
+        .await
+        .map_err(|_| HookError::Timeout(func.to_string()))?
+        .map_err(|e| HookError::ExecutionFailed(e.to_string()))?;
🤖 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/hooks/src/node_runtime.rs` around lines 58 - 61, The spawned
node process must be configured to be killed when its Child is dropped; update
the Command chain used in the timeout call (the Command::new("node")... sequence
used to produce .output()) to call .kill_on_drop(true) before .output() so that
when the timeout future is dropped it will terminate the child instead of
leaving an orphaned node process (keep the existing error mapping to
HookError::Timeout and HookError::ExecutionFailed).

58-77: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

The node -e argv + raw-stdout IPC is fragile: large payloads can hit E2BIG, and any hook stdout output corrupts JSON parsing.

Two problems with the current channel:

  • after_all_resolved forwards the full lockfile, which gets JSON-serialized into the wrapper and passed as a single node -e argv string; large lockfiles can exceed the OS arg limit and fail with E2BIG.
  • The entire stdout is parsed as JSON (Line 68-77). If the hook (or anything it imports) writes to stdout (console.log, banners, etc.), parsing fails and the hook is silently treated as a no-op.

Prefer a fixed Node script that reads the payload from stdin and emits the result on a dedicated channel (e.g., a unique marker or fd 3), then extract only that on the Rust side.

🛠️ Sketch: marker-delimited result (still fixes stdout pollution; combine with stdin for E2BIG)
-console.log(JSON.stringify(res));
+process.stdout.write("\u0000PNPMFILE\u0000" + JSON.stringify(res) + "\u0000PNPMFILE\u0000");
let stdout = String::from_utf8_lossy(&output.stdout);
let json = stdout
    .split("\u{0}PNPMFILE\u{0}")
    .nth(1)
    .map(str::trim)
    .ok_or_else(|| HookError::HookFailed(func.to_string(), "missing hook result".into()))?;
🤖 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/hooks/src/node_runtime.rs` around lines 58 - 77, The current
invocation in node_runtime.rs that builds
Command::new("node").arg("-e").arg(&wrapper).output() is fragile (E2BIG) and
parses raw stdout as JSON; change it to feed the JSON payload into the child
process via stdin (so nothing is passed as a large argv), run a fixed Node
script (either a bundled small wrapper or -e that reads stdin) which emits the
hook result wrapped in a unique marker (e.g.
"\u{0}PNPMFILE\u{0}RESULT\u{0}...RESULT\u{0}"), and then in the Rust code (where
output.stdout is inspected) extract only the marker-delimited JSON (fail with
HookError::HookFailed(func.to_string(), "missing hook result") if missing)
before calling serde_json::from_str; keep existing timeout handling
(HOOK_TIMEOUT), status/stderr checks (HookError::HookFailed), and error mappings
(HookError::Timeout, HookError::ExecutionFailed) while replacing the
.arg(&wrapper) argv-payload usage and raw stdout parse with stdin-based payload
and marker-delimited extraction.
🤖 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.

Duplicate comments:
In `@pacquet/crates/hooks/src/node_runtime.rs`:
- Around line 38-56: The generated JS never awaits the hook result and the CJS
branch runs top-level code, so async hooks serialize as {} and context is
missing; update the template generation for variable wrapper so both branches
invoke the hook inside an async IIFE and await the call (e.g. replace
(hooks.hooks && hooks.hooks['{func}'])?.({payload}) with await (hooks.hooks &&
hooks.hooks['{func}'])?.({payload}, {context}) ), and for the CJS branch wrap
require(...) usage in an async IIFE like the ESM branch so
console.log(JSON.stringify(res)) receives the resolved value; keep the same
identifiers (wrapper, file_path.ends_with(".mjs"), hooks.hooks['{func}'],
console.log(JSON.stringify(res))) when making the changes.
- Around line 58-61: The spawned node process must be configured to be killed
when its Child is dropped; update the Command chain used in the timeout call
(the Command::new("node")... sequence used to produce .output()) to call
.kill_on_drop(true) before .output() so that when the timeout future is dropped
it will terminate the child instead of leaving an orphaned node process (keep
the existing error mapping to HookError::Timeout and
HookError::ExecutionFailed).
- Around line 58-77: The current invocation in node_runtime.rs that builds
Command::new("node").arg("-e").arg(&wrapper).output() is fragile (E2BIG) and
parses raw stdout as JSON; change it to feed the JSON payload into the child
process via stdin (so nothing is passed as a large argv), run a fixed Node
script (either a bundled small wrapper or -e that reads stdin) which emits the
hook result wrapped in a unique marker (e.g.
"\u{0}PNPMFILE\u{0}RESULT\u{0}...RESULT\u{0}"), and then in the Rust code (where
output.stdout is inspected) extract only the marker-delimited JSON (fail with
HookError::HookFailed(func.to_string(), "missing hook result") if missing)
before calling serde_json::from_str; keep existing timeout handling
(HOOK_TIMEOUT), status/stderr checks (HookError::HookFailed), and error mappings
(HookError::Timeout, HookError::ExecutionFailed) while replacing the
.arg(&wrapper) argv-payload usage and raw stdout parse with stdin-based payload
and marker-delimited extraction.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d4397d9b-c471-4a28-8c27-615766c987af

📥 Commits

Reviewing files that changed from the base of the PR and between 7baf1a3 and 483b238.

📒 Files selected for processing (4)
  • pacquet/crates/hooks/Cargo.toml
  • pacquet/crates/hooks/src/finder.rs
  • pacquet/crates/hooks/src/node_runtime.rs
  • pacquet/crates/hooks/tests/node_hooks.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • pacquet/crates/hooks/tests/node_hooks.rs
  • pacquet/crates/hooks/src/finder.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). (3)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Analyze (javascript)
🧰 Additional context used
📓 Path-based instructions (1)
pacquet/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/*.rs: Warnings are errors (--deny warnings in lint). Do not silence them with #[allow(...)] unless there is a specific, justified reason.
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: https://rust-lang.github.io/api-guidelines/naming.html
No star imports inside module bodies. Write use super::{Foo, bar} instead of use super::*;, and the same for any other glob whose target is a module you control. Two forms stay allowed: external-crate preludes such as use rayon::prelude::*; and root-of-module re-exports such as pub use submodule::*; in a lib.rs.
Doc comments (///, //!) document the contract: preconditions, postconditions, panics, the reason the function exists. They are not a re-narration of the body.
Do not restate at call sites what the callee's doc comment already says. If /// on the function says 'no-op when …', the caller should not repeat that. Update the doc once; let every call site benefit.
Tests are documentation. Do not duplicate them in prose. If a behavioral scenario, edge case, failure mode, or worked example is already captured by a test, do not also narrate it in the doc comment on the implementation. The same applies in reverse: a test's own doc comment should not re-explain what the asserts already say, only the why if it is not obvious.
Use // SAFETY:, // TODO:, and similar prefixes to signal hidden invariants or known follow-ups that a reader cannot recover from the code alone.
When editing existing code, do not break a method chain (including pipe-trait .pipe(...) chains) into intermediate let bindings unless you can justify the rewrite. Valid justifications include a chain that fails to compile after your...

Files:

  • pacquet/crates/hooks/src/node_runtime.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/hooks/src/node_runtime.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/hooks/src/node_runtime.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/hooks/src/node_runtime.rs
🔇 Additional comments (2)
pacquet/crates/hooks/Cargo.toml (1)

1-15: LGTM!

pacquet/crates/hooks/src/node_runtime.rs (1)

81-127: LGTM!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

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

♻️ Duplicate comments (3)
pacquet/crates/hooks/src/node_runtime.rs (3)

58-61: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add kill_on_drop(true) to prevent orphaned Node processes on timeout.

When the timeout fires, the output() future is dropped but Tokio does not kill the spawned process by default. The Node child can continue running indefinitely.

🔪 Proposed fix
-        let output = timeout(HOOK_TIMEOUT, Command::new("node").arg("-e").arg(&wrapper).output())
+        let output = timeout(
+            HOOK_TIMEOUT,
+            Command::new("node")
+                .arg("-e")
+                .arg(&wrapper)
+                .kill_on_drop(true)
+                .output(),
+        )
             .await
             .map_err(|_| HookError::Timeout(func.to_string()))?
             .map_err(|e| HookError::ExecutionFailed(e.to_string()))?;
🤖 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/hooks/src/node_runtime.rs` around lines 58 - 61, The Node
child can become orphaned because the future returned by Command::output() is
dropped on timeout; modify the Command used in the timeout call to set
kill_on_drop(true) before calling output() so the child is killed when the
future is dropped. Locate the expression using timeout(HOOK_TIMEOUT,
Command::new("node").arg("-e").arg(&wrapper).output()) and call
kill_on_drop(true) on the tokio::process::Command (the same chain that builds
the Node invocation) so the Timeout branch that maps to
HookError::Timeout(func.to_string()) won’t leave a running Node process.

38-56: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Async pnpmfile hooks are not awaited before serialization.

Both .mjs and .cjs wrappers call the hook synchronously (const res = ...) without await. If the hook is async (valid per pnpm docs), res is a Promise that serializes to {} or is parsed as null, silently ignoring the hook's actual return value.

🤖 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/hooks/src/node_runtime.rs` around lines 38 - 56, The wrapper
currently captures hook results synchronously so async hooks get serialized as
unresolved Promises; update both branches so the call to hooks.hooks['{func}']
is awaited (use await (hooks.hooks && hooks.hooks['{func}'])?.({payload})),
ensure the surrounding code is inside an async context (the .mjs branch already
uses an async IIFE but the .cjs branch should be converted to an async IIFE like
the .mjs one) and keep JSON.stringify on the awaited result; adjust the code
that builds wrapper (the file_path.ends_with(".mjs") conditional and the wrapper
variable) accordingly.

129-140: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

pre_resolution always uses require(), breaking .mjs pnpmfiles.

The wrapper hardcodes require({file_path_escaped}) but the call_node method (lines 38-56) branches on .mjs to use import(). If the user's pnpmfile is pnpmfile.mjs, this hook will fail with an ES module error.

🐛 Proposed fix: branch on `.mjs` like `call_node` does
+        let wrapper = if file_path.ends_with(".mjs") {
+            format!(
+                r#"(async () => {{
+  const hooks = await import({file_path_escaped});
+  const ctx = {ctx_payload};
+  const logger = {{
+    info: (m) => {{ console.log(JSON.stringify({{"level":"info","message":m}})); }},
+    warn: (m) => {{ console.log(JSON.stringify({{"level":"warn","message":m}})); }}
+  }};
+  await hooks.hooks?.['preResolution']?.(ctx, logger);
+}})();
+"#,
+                file_path_escaped = file_path_escaped,
+                ctx_payload = ctx_payload
+            )
+        } else {
-        let wrapper = format!(
+            format!(
             r#"const hooks = require({file_path_escaped});
 const ctx = {ctx_payload};
 const logger = {{
   info: (m) => {{ console.log(JSON.stringify({{"level":"info","message":m}})); }},
   warn: (m) => {{ console.log(JSON.stringify({{"level":"warn","message":m}})); }}
 }};
 hooks.hooks && hooks.hooks['preResolution']?.(ctx, logger);
 "#,
-            file_path_escaped = file_path_escaped,
-            ctx_payload = ctx_payload
-        );
+                file_path_escaped = file_path_escaped,
+                ctx_payload = ctx_payload
+            )
+        };
🤖 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/hooks/src/node_runtime.rs` around lines 129 - 140, The wrapper
for invoking preResolution always uses require(...) which breaks ES module
pnpmfiles; update the wrapper generation in node_runtime.rs to branch on the
file extension like call_node does (check for ".mjs") and use dynamic
import(...) for ES modules and require(...) for CommonJS, then call
hooks.hooks['preResolution']?.(ctx, logger) (or the equivalent invocation) with
the same ctx_payload and logger wiring so both .mjs and .js pnpmfiles are
supported.
🤖 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/hooks/src/node_runtime.rs`:
- Around line 142-148: The timeout call returns Result<Result<Output,
io::Error>, Elapsed>, so update the handling around
HOOK_TIMEOUT/Command::new("node")/wrapper to unwrap both layers: match the outer
timeout result and then the inner Result<Output, io::Error> (e.g.,
Ok(Ok(output)) case) before accessing output.status or output.stderr; handle
Ok(Err(e)) to log the spawn/IO error and Err(_) to handle timeout, and in the
Ok(Ok(output)) branch keep the existing non-panic logging of
String::from_utf8_lossy(&output.stderr) when !output.status.success().

In `@pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs`:
- Around line 598-630: The code block that constructs and calls the
pnpmfile_hook (pnpmfile_hook, PreResolutionHookContext, hook.pre_resolution) is
misformatted and failing cargo fmt; run rustfmt (cargo fmt) and reformat this
block so it matches project style (fix indentation, spacing, and line breaks
around the wanted_lockfile/current_lockfile/registries/context construction and
the hook.pre_resolution call), then re-run cargo fmt to ensure no remaining
differences before committing.
- Around line 610-621: The field exists_current_lockfile is using
wanted_lockfile.is_some() which is wrong; change it to reflect the actual
current lockfile presence (use the variable that holds the loaded virtual-store
lockfile, e.g. current_lockfile or current_lockfile_json.is_some() as
appropriate) so that
pacquet_hooks::PreResolutionHookContext.exists_current_lockfile correctly
reports the current lockfile rather than the wanted_lockfile; update the
assignment where the context is constructed (the exists_current_lockfile field)
to check the correct variable.

---

Duplicate comments:
In `@pacquet/crates/hooks/src/node_runtime.rs`:
- Around line 58-61: The Node child can become orphaned because the future
returned by Command::output() is dropped on timeout; modify the Command used in
the timeout call to set kill_on_drop(true) before calling output() so the child
is killed when the future is dropped. Locate the expression using
timeout(HOOK_TIMEOUT, Command::new("node").arg("-e").arg(&wrapper).output()) and
call kill_on_drop(true) on the tokio::process::Command (the same chain that
builds the Node invocation) so the Timeout branch that maps to
HookError::Timeout(func.to_string()) won’t leave a running Node process.
- Around line 38-56: The wrapper currently captures hook results synchronously
so async hooks get serialized as unresolved Promises; update both branches so
the call to hooks.hooks['{func}'] is awaited (use await (hooks.hooks &&
hooks.hooks['{func}'])?.({payload})), ensure the surrounding code is inside an
async context (the .mjs branch already uses an async IIFE but the .cjs branch
should be converted to an async IIFE like the .mjs one) and keep JSON.stringify
on the awaited result; adjust the code that builds wrapper (the
file_path.ends_with(".mjs") conditional and the wrapper variable) accordingly.
- Around line 129-140: The wrapper for invoking preResolution always uses
require(...) which breaks ES module pnpmfiles; update the wrapper generation in
node_runtime.rs to branch on the file extension like call_node does (check for
".mjs") and use dynamic import(...) for ES modules and require(...) for
CommonJS, then call hooks.hooks['preResolution']?.(ctx, logger) (or the
equivalent invocation) with the same ctx_payload and logger wiring so both .mjs
and .js pnpmfiles are supported.
🪄 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: 093719fb-ce06-4298-9aad-3ac98b788134

📥 Commits

Reviewing files that changed from the base of the PR and between 483b238 and 6e67d37.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • pacquet/crates/hooks/src/lib.rs
  • pacquet/crates/hooks/src/node_runtime.rs
  • pacquet/crates/hooks/tests/node_hooks.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • pacquet/crates/hooks/tests/node_hooks.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: windows-latest / Node.js 22.13.0 / Test
🧰 Additional context used
📓 Path-based instructions (1)
pacquet/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/*.rs: Warnings are errors (--deny warnings in lint). Do not silence them with #[allow(...)] unless there is a specific, justified reason.
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: https://rust-lang.github.io/api-guidelines/naming.html
No star imports inside module bodies. Write use super::{Foo, bar} instead of use super::*;, and the same for any other glob whose target is a module you control. Two forms stay allowed: external-crate preludes such as use rayon::prelude::*; and root-of-module re-exports such as pub use submodule::*; in a lib.rs.
Doc comments (///, //!) document the contract: preconditions, postconditions, panics, the reason the function exists. They are not a re-narration of the body.
Do not restate at call sites what the callee's doc comment already says. If /// on the function says 'no-op when …', the caller should not repeat that. Update the doc once; let every call site benefit.
Tests are documentation. Do not duplicate them in prose. If a behavioral scenario, edge case, failure mode, or worked example is already captured by a test, do not also narrate it in the doc comment on the implementation. The same applies in reverse: a test's own doc comment should not re-explain what the asserts already say, only the why if it is not obvious.
Use // SAFETY:, // TODO:, and similar prefixes to signal hidden invariants or known follow-ups that a reader cannot recover from the code alone.
When editing existing code, do not break a method chain (including pipe-trait .pipe(...) chains) into intermediate let bindings unless you can justify the rewrite. Valid justifications include a chain that fails to compile after your...

Files:

  • pacquet/crates/hooks/src/lib.rs
  • pacquet/crates/hooks/src/node_runtime.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.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/hooks/src/lib.rs
  • pacquet/crates/hooks/src/node_runtime.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.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/hooks/src/lib.rs
  • pacquet/crates/hooks/src/node_runtime.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.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/hooks/src/lib.rs
  • pacquet/crates/hooks/src/node_runtime.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
🪛 GitHub Actions: Pacquet CI / 0_Format.txt
pacquet/crates/hooks/src/node_runtime.rs

[error] 105-105: cargo fmt --all -- --check failed: formatting differences detected (async fn pre_resolution parameter formatting).


[error] 115-115: cargo fmt --all -- --check failed: formatting differences detected (whitespace/blank line change).


[error] 139-139: cargo fmt --all -- -- --check failed: formatting differences detected (timeout(...) and Command::new(...) call formatting/line breaks).


[error] 158-158: cargo fmt --all -- --check failed: formatting differences detected (trailing whitespace/blank line).

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

[error] 665-665: cargo fmt --all -- --check failed: formatting differences detected (whitespace/blank line change).


[error] 672-672: cargo fmt --all -- --check failed: formatting differences detected (current_lockfile_json expression line breaks/indentation).


[error] 698-698: cargo fmt --all -- --check failed: formatting differences detected (whitespace/blank line change).

🪛 GitHub Actions: Pacquet CI / 1_Lint and Test (ubuntu-latest).txt
pacquet/crates/hooks/src/lib.rs

[error] 1-1: Command failed: cargo clippy --locked --workspace --all-targets -- -D warnings (exit code 101). could not compile pacquet-hooks (lib) due to 2 previous errors.

pacquet/crates/hooks/src/node_runtime.rs

[error] 143-143: clippy (via cargo) failed to compile: error[E0609]: no field status on type std::result::Result<std::process::Output, std::io::Error>. Suggestion: use output.unwrap().status.success().


[error] 144-144: clippy (via cargo) failed to compile: error[E0609]: no field stderr on type std::result::Result<std::process::Output, std::io::Error>. Suggestion: use output.unwrap().stderr.


[error] 1-1: Command failed: cargo clippy --locked --workspace --all-targets -- -D warnings (exit code 101). could not compile pacquet-hooks (lib) due to 2 previous errors.

🪛 GitHub Actions: Pacquet CI / 2_Lint and Test (macos-latest).txt
pacquet/crates/hooks/src/node_runtime.rs

[error] 143-143: cargo clippy (with -D warnings) failed to compile: error[E0609] no field status on type std::result::Result<std::process::Output, std::io::Error>.


[error] 144-144: cargo clippy (with -D warnings) failed to compile: error[E0609] no field stderr on type std::result::Result<std::process::Output, std::io::Error>.


[error] 143-144: Build failed for crate pacquet-hooks due to 2 previous compilation errors.

🪛 GitHub Actions: Pacquet CI / 7_Doc.txt
pacquet/crates/hooks/src/lib.rs

[error] 1-1: cargo doc --no-deps --workspace --document-private-items: could not compile pacquet-hooks due to 2 previous errors (exit code 101).

pacquet/crates/hooks/src/node_runtime.rs

[error] 143-143: cargo doc (RUSTDOCFLAGS: -D warnings) failed to compile pacquet-hooks: error[E0609]: no field status on type Result<Output, std::io::Error>. Suggested fix: use output.unwrap().status.success().


[error] 144-144: cargo doc failed to compile pacquet-hooks: error[E0609]: no field stderr on type Result<Output, std::io::Error>. Suggested fix: use output.unwrap().stderr.


[error] 1-1: cargo doc --no-deps --workspace --document-private-items: could not compile pacquet-hooks due to 2 previous errors (exit code 101).

🪛 GitHub Actions: Pacquet CI / Doc
pacquet/crates/hooks/src/node_runtime.rs

[error] 143-143: Rust compile error E0609: no field status on type Result<Output, std::io::Error>. Suggested fix: use output.unwrap().status.success().


[error] 144-144: Rust compile error E0609: no field stderr on type Result<Output, std::io::Error>. Suggested fix: use output.unwrap().stderr.

🪛 GitHub Actions: Pacquet CI / Dylint
pacquet/crates/hooks/src/node_runtime.rs

[error] 143-143: Rust compilation error E0609: no field status on type std::result::Result<std::process::Output, std::io::Error>. Help: use output.unwrap().status.success().


[error] 144-144: Rust compilation error E0609: no field stderr on type std::result::Result<std::process::Output, std::io::Error>. Help: use output.unwrap().stderr.

🪛 GitHub Actions: Pacquet CI / Format
pacquet/crates/hooks/src/node_runtime.rs

[error] 105-115: rustfmt check failed: async fn pre_resolution signature formatting (line breaks/indentation) differs.


[error] 115-139: rustfmt check failed: whitespace/newline differences detected around ctx_json / file_path handling.


[error] 139-158: rustfmt check failed: timeout(Command::new(...).output()).await formatting differs (line breaks).

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

[error] 665-672: rustfmt check failed: whitespace/newline differences detected before calling pre-resolution hook.


[error] 672-698: rustfmt check failed: current_lockfile_json assignment formatting differs (line breaks/indentation).


[error] 698-699: rustfmt check failed: whitespace/newline differences detected after hook call / before workspace opts.

🪛 GitHub Actions: Pacquet CI / Lint and Test (macos-latest)
pacquet/crates/hooks/src/lib.rs

[error] 1-1: Command failed: cargo clippy --locked --workspace --all-targets -- -D warnings. Compilation failed for crate pacquet-hooks due to 2 previous errors (exit code 101).

pacquet/crates/hooks/src/node_runtime.rs

[error] 143-143: clippy (rustc) failed: E0609 no field status on type std::result::Result<std::process::Output, std::io::Error>. Use output.unwrap().status.success() (or handle the Result).


[error] 144-144: clippy (rustc) failed: E0609 no field stderr on type std::result::Result<std::process::Output, std::io::Error>. Use output.unwrap().stderr (or handle the Result).


[error] 1-1: Command failed: cargo clippy --locked --workspace --all-targets -- -D warnings. Compilation failed for crate pacquet-hooks due to 2 previous errors (exit code 101).

🪛 GitHub Actions: Pacquet CI / Lint and Test (ubuntu-latest)
pacquet/crates/hooks/src/node_runtime.rs

[error] 143-143: Rust compile error (E0609): no field status on type std::result::Result<std::process::Output, std::io::Error>. Command/step: cargo clippy --locked --workspace --all-targets -- -D warnings.


[error] 144-144: Rust compile error (E0609): no field stderr on type std::result::Result<std::process::Output, std::io::Error>. Command/step: cargo clippy --locked --workspace --all-targets -- -D warnings.

🪛 GitHub Actions: Pacquet Code Coverage / Code Coverage
pacquet/crates/hooks/src/lib.rs

[error] 1-1: Build failed: could not compile pacquet-hooks due to 2 previous errors.

pacquet/crates/hooks/src/node_runtime.rs

[error] 143-143: Rust compilation failed: E0609 no field status on type Result<Output, std::io::Error>. Suggestion: use output.unwrap().status.success().


[error] 144-144: Rust compilation failed: E0609 no field stderr on type Result<Output, std::io::Error>. Suggestion: use output.unwrap().stderr.


[error] 1-1: Build failed: could not compile pacquet-hooks due to 2 previous errors.

🔇 Additional comments (1)
pacquet/crates/hooks/src/lib.rs (1)

16-31: LGTM!

Also applies to: 42-43, 60-62

Comment thread pacquet/crates/hooks/src/node_runtime.rs Outdated
Comment thread pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs Outdated
Comment thread pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
Copilot AI review requested due to automatic review settings May 29, 2026 09:38
@kairosci kairosci force-pushed the feat/pnpmfile-hooks branch from c2c6ae4 to ede2317 Compare May 29, 2026 09:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

@kairosci kairosci force-pushed the feat/pnpmfile-hooks branch from ede2317 to a46b8f9 Compare May 29, 2026 09:49
Copilot AI review requested due to automatic review settings May 29, 2026 09:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

@kairosci kairosci force-pushed the feat/pnpmfile-hooks branch from 3b70f48 to e3414c0 Compare May 29, 2026 10:16
kairosci and others added 20 commits June 2, 2026 00:40
pnpm's requireHooks recognizes only .pnpmfile.mjs and .pnpmfile.cjs
(preferring .mjs); the non-dotted pnpmfile.cjs/pnpmfile.mjs candidates
were behavior pnpm does not have.
`await hooks.hooks && hooks.hooks[func]?.(...)` parses as
`(await hooks.hooks) && (...)`, so an async preResolution hook was
never awaited. Group the operands so the hook call itself is awaited,
matching the call_node path.
Every other pacquet crate is 0.0.1; pacquet-hooks was the lone 0.1.0.
The rebase carried over cargo update artifacts (chacha20, system-config,
etc.); regenerate from main so the lockfile only adds pacquet-hooks.
pnpm/setup already installs the standalone pnpm, which can provision
Node itself; drop the redundant actions/setup-node step in favor of
`pnpm runtime set node 22 --global`.
…pnpm

Mirror pnpm's requirePnpmfile contract for the readPackage hook: default the
four dependency fields to {} before the hook runs, validate that the hook
returns a manifest object whose dependency fields are objects (not arrays),
and propagate a HookError (PNPMFILE_FAIL) so a throwing/syntactically-invalid
pnpmfile or a hook that returns nothing aborts the install instead of being
silently swallowed.

Ports the pnpm readPackage tests: validation/normalization unit tests in the
hooks crate, and install-level tests (pin a transitive dep, fail when the hook
returns nothing, fail on a pnpmfile syntax error).
Run the afterAllResolved pnpmfile hook on the resolved lockfile before writing
pnpm-lock.yaml, matching pnpm. The lockfile is round-tripped through
serde_json::Value so hook-added keys the typed Lockfile cannot represent
survive to disk; serde_json's preserve_order keeps the output byte-identical to
the typed write when no hook is present (the round-trip only runs when an
afterAllResolved hook exists). A throwing hook aborts the install.

Ports the pnpm afterAllResolved tests (hook mutates the written lockfile; a
throwing hook fails the install).
…eneric name)

Add the trailing comma the multi-line readPackage format! invocation needs and
rename the single-letter generic on save_value_to_path.
Replace the fresh node -e process per hook call with one persistent Node worker
per pnpmfile, addressed by a newline-delimited JSON protocol multiplexed by
request id so concurrent readPackage calls (resolution runs in parallel) share
one process. This removes the per-package node startup cost on the resolution
hot path and forwards each context.log(...) back to the call's HookContext log
callback, the basis for the pnpm:hook reporter channel.

readPackage/afterAllResolved/filterLog now go through the worker; preResolution
keeps its one-shot invocation (it runs once per install and needs an info/warn
logger). Adds concurrency and log-forwarding tests.
@zkochan zkochan force-pushed the feat/pnpmfile-hooks branch from e9fd840 to a589b47 Compare June 1, 2026 22:46
@zkochan

zkochan commented Jun 1, 2026

Copy link
Copy Markdown
Member

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

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

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

🧹 Nitpick comments (2)
.github/workflows/pacquet-ci.yml (2)

85-85: ⚡ Quick win

Consider parameterizing the Node.js version.

The Node.js version "22" is hardcoded. Consider using a variable or matrix parameter to make version updates easier in the future. For example:

env:
  NODE_VERSION: 22

Then reference it as:

run: pnpm runtime set node ${{ env.NODE_VERSION }} --global

This makes the version easier to update and more visible at the workflow level.

🤖 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 @.github/workflows/pacquet-ci.yml at line 85, The workflow hardcodes the
Node.js version in the step that runs "pnpm runtime set node 22 --global";
change this to use a parameter by adding an env variable (e.g., NODE_VERSION) at
the workflow or job level and reference that variable in the run command
(replace the literal "22" with the env variable like ${{ env.NODE_VERSION }}),
so updates to Node version are made in one place and optionally expose it for
use in a matrix.

84-86: pnpm runtime setup step is using correct syntax across the CI OS matrix

  • pnpm runtime set node 22 --global matches pnpm’s documented syntax for installing/activating a Node runtime globally (--global == -g), and pnpm runtime is intended to work on Windows/macOS/Linux.
  • Optional: pin the pnpm/setup pnpm CLI version (to ensure pnpm v11+ runtime support) and/or parameterize the Node version instead of hardcoding 22.
🤖 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 @.github/workflows/pacquet-ci.yml around lines 84 - 86, Update the CI
workflow step that currently runs "pnpm runtime set node 22 --global" to ensure
cross-OS compatibility by using the documented pnpm runtime syntax (keep the
--global/-g flag) and make the Node version configurable (replace hardcoded "22"
with a parameter/variable like NODE_VERSION); also consider pinning the
pnpm/setup action to a minimum version that supports runtime (e.g., v11+) so the
runtime command is supported across the matrix.
🤖 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.

Nitpick comments:
In @.github/workflows/pacquet-ci.yml:
- Line 85: The workflow hardcodes the Node.js version in the step that runs
"pnpm runtime set node 22 --global"; change this to use a parameter by adding an
env variable (e.g., NODE_VERSION) at the workflow or job level and reference
that variable in the run command (replace the literal "22" with the env variable
like ${{ env.NODE_VERSION }}), so updates to Node version are made in one place
and optionally expose it for use in a matrix.
- Around line 84-86: Update the CI workflow step that currently runs "pnpm
runtime set node 22 --global" to ensure cross-OS compatibility by using the
documented pnpm runtime syntax (keep the --global/-g flag) and make the Node
version configurable (replace hardcoded "22" with a parameter/variable like
NODE_VERSION); also consider pinning the pnpm/setup action to a minimum version
that supports runtime (e.g., v11+) so the runtime command is supported across
the matrix.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9631bddc-a4cd-40cc-a9e7-cd97579955c9

📥 Commits

Reviewing files that changed from the base of the PR and between 483b238 and a589b47.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • .changeset/add-pnpmfile-hooks.md
  • .github/workflows/pacquet-ci.yml
  • Cargo.toml
✅ Files skipped from review due to trivial changes (1)
  • .changeset/add-pnpmfile-hooks.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • Cargo.toml
📜 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: Lint and Test (windows-latest)
  • GitHub Check: Run benchmark on ubuntu-latest
🔇 Additional comments (2)
.github/workflows/pacquet-ci.yml (2)

4-7: LGTM!

Also applies to: 19-19, 38-38


151-151: LGTM!

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.

4 participants