Skip to content

feat(package-manager): run project lifecycle scripts during install#12051

Merged
zkochan merged 1 commit into
mainfrom
feat/pacquet-project-lifecycle-scripts
May 29, 2026
Merged

feat(package-manager): run project lifecycle scripts during install#12051
zkochan merged 1 commit into
mainfrom
feat/pacquet-project-lifecycle-scripts

Conversation

@zkochan

@zkochan zkochan commented May 29, 2026

Copy link
Copy Markdown
Member

What

Ports pnpm's project (workspace/root) lifecycle scripts that run during pnpm installpreinstall, install, postinstall, preprepare, prepare, postprepare, in that order — to pacquet. These are the project's own scripts, distinct from the dependency build scripts pacquet already runs in BuildModules.

Mirrors pnpm's runLifecycleHooksConcurrently(['preinstall', 'install', 'postinstall', 'preprepare', 'prepare', 'postprepare'], ...) call near the end of the install in pkg-manager/core and pkg-manager/headless.

How

  • executor — extracted the per-stage loop out of run_postinstall_hooks into a shared run_lifecycle_stages (same binding.gyp → node-gyp rebuild fallback and npx only-allow pnpm skip). Added run_project_lifecycle_scripts + the PROJECT_LIFECYCLE_STAGES constant.
  • executor — honor SelectedShell::windows_verbatim_args: on the Windows cmd /d /s /c path the script body is now appended with raw_arg, so embedded quoting (e.g. node -e "...") reaches the child intact, matching Node's windowsVerbatimArguments. This was previously a no-op (let _ = shell.windows_verbatim_args;), which mangled quoted scripts under cmd.exe — and is why the first test revision failed on Windows.
  • package-manager — new run_projects_lifecycle_scripts helper, fanned out over every importer (root + workspace projects), wired into Install::run after the dependency graph is materialized, bins are linked, and .modules.yaml / the current lockfile are written, before the closing pnpm:summary. Runs on both the frozen and fresh paths. New InstallError::ProjectLifecycleScript variant — a project script failure always fails the install (unlike optional-dep build failures, which BuildModules swallows).
  • package-manager — gated on Install::is_full_install: pacquet add is a partial install (pnpm's mutation: 'installSome'), so the project's own scripts must not run — mirroring pnpm's mutation === 'install' filter at the runLifecycleHooksConcurrently call site.

Tests

Ported the project-script tests from pnpm's pkg-manager/core/test/install/lifecycleScripts.ts and pnpm/test/install/lifecycleScripts.ts:

  • stage ordering (proven by scrambling JSON key order + a postpare typo that must not run)
  • re-run on --frozen-lockfile
  • failure propagation (exit 1)
  • runs when the project name differs from its directory
  • INIT_CWD is the lockfile directory
  • add does not run project scripts (the named-install gate)

Validated the Windows-only raw_arg path compiles + lints clean against x86_64-pc-windows-gnu locally.

Notes / follow-ups

  • No ignoreScripts gate. pacquet hasn't implemented ignoreScripts anywhere (it hardcodes ignore_scripts: false across the dep-build path), so project scripts run unconditionally — matching pnpm's default. Adding the flag is a separate, cross-cutting change.
  • Root-first, sequential. pnpm orders importers by buildIndex (workspace topological order) and re-links bins between groups; pacquet doesn't compute a per-importer build index yet, so projects run root-first and sequentially. Documented as a follow-up; the common single-project case is unaffected.
  • pnpm:devPreinstall (the root-only hook that runs before resolution) is a separate mechanism and out of scope here.

Pacquet-only change, so no changeset.


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

Summary by CodeRabbit

Release Notes

  • New Features

    • Workspace project lifecycle scripts (preinstall, install, postinstall, preprepare, prepare, postprepare) are now executed during pacquet install.
    • pacquet add explicitly does not execute project lifecycle scripts, maintaining partial install behavior.
  • Tests

    • Added comprehensive test coverage for workspace lifecycle script execution, including stage ordering, failure handling, and environment variable configuration.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR implements workspace project-level lifecycle script execution for pacquet install by introducing an is_full_install flag to distinguish full installs from partial installs (like pacquet add), refactoring the executor to support arbitrary stage sequences, and wiring conditional execution into the install workflow.

Changes

Project Lifecycle Script Execution During Install

Layer / File(s) Summary
Lifecycle Stage Definitions and Executor Exports
pacquet/crates/executor/src/lifecycle.rs, pacquet/crates/executor/src/lib.rs
New PROJECT_LIFECYCLE_STAGES constant defines the ordered six-stage sequence (preinstall/install/postinstall/preprepare/prepare/postprepare) for workspace projects, alongside internal DEPENDENCY_LIFECYCLE_STAGES. Executor module re-exports updated to surface the new constant.
Executor Runner Implementation and Helpers
pacquet/crates/executor/src/lifecycle.rs
Adds run_project_lifecycle_scripts wrapper that invokes the refactored run_lifecycle_stages with the project stage list. run_lifecycle_stages now iterates arbitrary stage sequences, snapshots parent env once per package, conditionally selects each stage's script, and returns whether any script executed. run_lifecycle_hook command construction updated to append script via new push_script_arg helper; push_script_arg uses raw_arg on Windows (when verbatim args enabled) or arg elsewhere.
Install Struct Extension and Error Handling
pacquet/crates/package-manager/src/install.rs, pacquet/crates/package-manager/src/install/tests.rs
Adds is_full_install: bool field to Install struct and ProjectLifecycleScript(LifecycleScriptError) error variant. Imports executor lifecycle types. All test struct initializations updated to include is_full_install: true.
Install::run Execution and Project Script Integration
pacquet/crates/package-manager/src/install.rs
Destructures is_full_install from self in Install::run. After lockfile writes, conditionally invokes new run_projects_lifecycle_scripts helper when is_full_install is true. Helper iterates workspace projects, derives per-project paths and dependency context, maps scripts_prepend_node_path config, calls executor's run_project_lifecycle_scripts, and converts failures into InstallError::ProjectLifecycleScript.
Command Entry Points: Install vs Add
pacquet/crates/cli/src/cli_args/install.rs, pacquet/crates/package-manager/src/add.rs
CLI install command sets is_full_install: true with clarifying comment. Add command sets is_full_install: false to skip project lifecycle execution (matching pnpm "installSome" behavior).
Integration Tests for Project Lifecycle Scripts
pacquet/crates/cli/tests/lifecycle_scripts.rs
New project_scripts test module with helpers to generate Node scripts appending stages to order.txt. Tests verify: expected stage execution order, stage rerun on --frozen-lockfile, failure propagation from non-zero script exit, execution with mismatched package.json name, INIT_CWD environment variable canonicalization to workspace directory, and that pacquet add does not execute project scripts.

Sequence Diagram(s)

sequenceDiagram
  participant RunProjectLifecycleScripts
  participant RunLifecycleStages
  participant SnapshotEnv as Snapshot Env
  participant RunLifecycleHook
  participant PushScriptArg
  
  RunProjectLifecycleScripts->>RunLifecycleStages: invoke with PROJECT_LIFECYCLE_STAGES
  RunLifecycleStages->>SnapshotEnv: snapshot parent process env once
  loop for each stage (preinstall...postprepare)
    RunLifecycleStages->>RunLifecycleStages: select script from manifest
    RunLifecycleStages->>RunLifecycleHook: invoke with stage script
    RunLifecycleHook->>PushScriptArg: append script to command
    PushScriptArg-->>RunLifecycleHook: raw_arg (Windows) or arg (other)
    RunLifecycleHook->>RunLifecycleHook: set current_dir, clear inherited env
    RunLifecycleHook->>RunLifecycleHook: apply snapshot env, pipe stdout/stderr
  end
  RunLifecycleStages-->>RunProjectLifecycleScripts: return ran_any
Loading
flowchart TD
  A["Install::run"] --> B["Write modules.yaml + lockfile"]
  B --> C{is_full_install?}
  C -->|true| D["run_projects_lifecycle_scripts"]
  C -->|false| E["Skip project scripts"]
  D --> F["Iterate workspace projects"]
  F --> G["Derive per-project paths and config"]
  G --> H["Call run_project_lifecycle_scripts"]
  H --> I{Success?}
  I -->|Error| J["InstallError::ProjectLifecycleScript"]
  I -->|OK| K["Continue to workspace-state"]
  J --> L["Report error"]
  E --> K
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • pnpm/pacquet#397: Directly implements the lifecycle subsystem work (PROJECT_LIFECYCLE_STAGES, run_project_lifecycle_scripts, environment snapshot behavior, push_script_arg cross-platform handling, scripts_prepend_node_path mapping, and is_full_install gating) that the issue requests.
  • pnpm/pnpm#11870: Related through shared lifecycle execution and install wiring changes; adds project lifecycle stage support and is_full_install flag though does not target the fresh-lockfile dependency rebuild path.

Possibly related PRs

  • pnpm/pnpm#11904: Both modify Install::run in package-manager/src/install.rs; the frozen-lockfile no-op early-return in #11904 directly affects the new full-install gating and project lifecycle script execution added in this PR.
  • pnpm/pnpm#11665: Both modify Install::run and InstallError around the same "after modules.yaml + lockfile" execution phase; this PR adds project lifecycle script execution (gated by is_full_install), while #11665 adds .pnpm-workspace-state-v1.json writer and its error handling.

Poem

🐰 A workspace breathes with scripts so keen,
preinstall, prepare, and stages clean—
Full installs run the project's dance,
While partial installs give it no chance! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding project lifecycle scripts execution during the install command.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/pacquet-project-lifecycle-scripts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.02      7.6±0.33ms   571.4 KB/sec    1.00      7.5±0.36ms   581.3 KB/sec

@codecov-commenter

codecov-commenter commented May 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.73418% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 88.97%. Comparing base (a593082) to head (7ccab3a).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
pacquet/crates/executor/src/lifecycle.rs 96.55% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12051      +/-   ##
==========================================
+ Coverage   88.81%   88.97%   +0.16%     
==========================================
  Files         234      235       +1     
  Lines       30594    30989     +395     
==========================================
+ Hits        27172    27573     +401     
+ Misses       3422     3416       -6     

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

@zkochan zkochan force-pushed the feat/pacquet-project-lifecycle-scripts branch from ab6f05e to 3f77c8c Compare May 29, 2026 00:42
Port pnpm's project (workspace/root) lifecycle scripts that run during
`pnpm install` — preinstall, install, postinstall, preprepare, prepare,
postprepare — distinct from the dependency build scripts already run in
`BuildModules`.

- executor: extract the per-stage loop into a shared
  `run_lifecycle_stages`, keeping the `binding.gyp -> node-gyp rebuild`
  fallback and the `npx only-allow pnpm` skip identical for both paths.
  Add `run_project_lifecycle_scripts` + `PROJECT_LIFECYCLE_STAGES`,
  mirroring the `runLifecycleHooksConcurrently` call sites in
  pkg-manager/core and pkg-manager/headless.
- executor: honor `SelectedShell::windows_verbatim_args` — on the
  Windows `cmd /d /s /c` path the script body is now appended with
  `raw_arg` so embedded quoting (e.g. `node -e "..."`) reaches the child
  intact, matching Node's `windowsVerbatimArguments`. Previously a
  no-op, which mangled quoted scripts under cmd.exe.
- package-manager: run each project's scripts in `Install::run` after
  the dependency graph is materialized, bins are linked, and
  `.modules.yaml` / the current lockfile are written, before the closing
  `pnpm:summary`. Runs on both the frozen and fresh paths. A project
  script failure always fails the install via the new
  `InstallError::ProjectLifecycleScript` variant (unlike optional
  dependency build failures).
- gate on `Install::is_full_install`: `pacquet add` is a partial install
  (pnpm's `mutation: 'installSome'`), so the project's own scripts must
  not run — mirroring pnpm's `mutation === 'install'` filter.
- tests: stage ordering, re-run on --frozen-lockfile, failure
  propagation, name-differs-from-directory, INIT_CWD, and the
  `add`-skips-project-scripts gate.

Projects run root-first and sequentially; pnpm's buildIndex ordering and
child_concurrency fan-out are a follow-up once pacquet computes a
per-importer build index. No `ignoreScripts` gate yet — pacquet hardcodes
`ignore_scripts: false` across the dep-build path, so this matches pnpm's
default of running them.

Ref: https://github.com/pnpm/pnpm/blob/80037699fb/pkg-manager/core/src/install/index.ts#L1517-L1530
@zkochan zkochan force-pushed the feat/pacquet-project-lifecycle-scripts branch from 3f77c8c to 7ccab3a Compare May 29, 2026 00:51
@zkochan zkochan marked this pull request as ready for review May 29, 2026 00:53
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Review Summary by Qodo

Run project lifecycle scripts during install

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Port pnpm's project lifecycle scripts (preinstall, install, postinstall, preprepare, prepare,
  postprepare) to run during pacquet install
• Extract shared lifecycle stage loop into run_lifecycle_stages for both dependency and project
  scripts
• Honor Windows cmd /d /s /c verbatim arguments via raw_arg for proper quote handling in scripts
• Gate project scripts on is_full_install flag so pacquet add (partial install) skips them
• Add comprehensive tests covering stage ordering, frozen-lockfile re-runs, failure propagation, and
  add command exclusion
Diagram
flowchart LR
  Install["Install::run"]
  DepGraph["Dependency graph materialized"]
  BinsLinked["Bins linked"]
  LockfileWritten["Lockfile written"]
  ProjectScripts["run_projects_lifecycle_scripts"]
  Summary["pnpm:summary"]
  
  Install --> DepGraph
  DepGraph --> BinsLinked
  BinsLinked --> LockfileWritten
  LockfileWritten --> ProjectScripts
  ProjectScripts --> Summary

Loading

Grey Divider

File Changes

1. pacquet/crates/cli/src/cli_args/install.rs ⚙️ Configuration changes +4/-0

Gate project scripts on full install flag

• Add is_full_install: true field to InstallArgs for pacquet install command
• Document that pacquet add sets this to false to skip project lifecycle scripts

pacquet/crates/cli/src/cli_args/install.rs


2. pacquet/crates/executor/src/lib.rs ✨ Enhancement +2/-1

Export project lifecycle script functions

• Export new PROJECT_LIFECYCLE_STAGES constant
• Export new run_project_lifecycle_scripts function
• Update exports to include refactored lifecycle functions

pacquet/crates/executor/src/lib.rs


3. pacquet/crates/executor/src/lifecycle.rs ✨ Enhancement +99/-32

Refactor lifecycle stages and add project scripts support

• Add PROJECT_LIFECYCLE_STAGES constant with six stages in execution order
• Add DEPENDENCY_LIFECYCLE_STAGES constant for three-stage dependency builds
• Extract per-stage loop into shared run_lifecycle_stages function
• Add new run_project_lifecycle_scripts function for workspace projects
• Implement push_script_arg helper with platform-specific handling for Windows raw_arg
• Honor windows_verbatim_args to preserve embedded quotes in scripts on Windows

pacquet/crates/executor/src/lifecycle.rs


View more (4)
4. pacquet/crates/package-manager/src/add.rs ⚙️ Configuration changes +5/-0

Mark add command as partial install

• Set is_full_install: false for pacquet add command
• Document that partial installs skip project lifecycle scripts

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


5. pacquet/crates/package-manager/src/install.rs ✨ Enhancement +98/-0

Integrate project lifecycle scripts into install flow

• Add is_full_install field to Install struct with documentation
• Add new InstallError::ProjectLifecycleScript variant for script failures
• Import executor functions for running project lifecycle scripts
• Add run_projects_lifecycle_scripts helper function that iterates all workspace projects
• Call run_projects_lifecycle_scripts after lockfile is written but before pnpm:summary
• Gate project scripts execution on is_full_install flag

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


6. pacquet/crates/package-manager/src/install/tests.rs 🧪 Tests +61/-0

Update tests with is_full_install field

• Add is_full_install: true to all existing test Install struct initializations
• Ensure test coverage reflects the new required field across 50+ test cases

pacquet/crates/package-manager/src/install/tests.rs


7. pacquet/crates/cli/tests/lifecycle_scripts.rs 🧪 Tests +215/-0

Add project lifecycle script tests

• Add new project_scripts test module with six comprehensive tests
• Test stage ordering with scrambled JSON keys and typo detection
• Test re-execution on --frozen-lockfile flag
• Test failure propagation with non-zero exit codes
• Test script execution when project name differs from directory
• Test INIT_CWD environment variable is set to lockfile directory
• Test that pacquet add does not run project lifecycle scripts

pacquet/crates/cli/tests/lifecycle_scripts.rs


Grey Divider

Qodo Logo

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
pacquet/crates/package-manager/src/install.rs (1)

1398-1398: 💤 Low value

Consider removing the unused _manifest binding.

The loop destructures _manifest but never uses it since run_project_lifecycle_scripts reads the manifest from disk via safe_read_package_json_from_dir. This is intentional (the executor re-reads to get the latest), but the binding could be simplified.

♻️ Optional: simplify loop binding
-    for (project_dir, _manifest) in project_manifests {
+    for (project_dir, _) in project_manifests {
🤖 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/package-manager/src/install.rs` at line 1398, The loop
currently binds an unused `_manifest`; simplify the iteration to only take the
project directory so we don't create an unused binding. Replace the `for
(project_dir, _manifest) in project_manifests` loop with an iterator that yields
just the directory (e.g. `for project_dir in
project_manifests.into_iter().map(|(d, _)| d)` or an equivalent pattern) where
you call `run_project_lifecycle_scripts` (which re-reads via
`safe_read_package_json_from_dir`), so the manifest is not bound unnecessarily.
🤖 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 `@pacquet/crates/package-manager/src/install.rs`:
- Line 1398: The loop currently binds an unused `_manifest`; simplify the
iteration to only take the project directory so we don't create an unused
binding. Replace the `for (project_dir, _manifest) in project_manifests` loop
with an iterator that yields just the directory (e.g. `for project_dir in
project_manifests.into_iter().map(|(d, _)| d)` or an equivalent pattern) where
you call `run_project_lifecycle_scripts` (which re-reads via
`safe_read_package_json_from_dir`), so the manifest is not bound unnecessarily.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 751dca87-75cb-4ccf-9f5a-aa336a6df536

📥 Commits

Reviewing files that changed from the base of the PR and between efcc336 and 7ccab3a.

📒 Files selected for processing (7)
  • pacquet/crates/cli/src/cli_args/install.rs
  • pacquet/crates/cli/tests/lifecycle_scripts.rs
  • pacquet/crates/executor/src/lib.rs
  • pacquet/crates/executor/src/lifecycle.rs
  • pacquet/crates/package-manager/src/add.rs
  • pacquet/crates/package-manager/src/install.rs
  • pacquet/crates/package-manager/src/install/tests.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: Lint and Test (windows-latest)
🧰 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/package-manager/src/add.rs
  • pacquet/crates/cli/src/cli_args/install.rs
  • pacquet/crates/executor/src/lib.rs
  • pacquet/crates/package-manager/src/install.rs
  • pacquet/crates/cli/tests/lifecycle_scripts.rs
  • pacquet/crates/package-manager/src/install/tests.rs
  • pacquet/crates/executor/src/lifecycle.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/cli/tests/lifecycle_scripts.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/package-manager/src/add.rs
  • pacquet/crates/cli/src/cli_args/install.rs
  • pacquet/crates/executor/src/lib.rs
  • pacquet/crates/package-manager/src/install.rs
  • pacquet/crates/cli/tests/lifecycle_scripts.rs
  • pacquet/crates/package-manager/src/install/tests.rs
  • pacquet/crates/executor/src/lifecycle.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/package-manager/src/add.rs
  • pacquet/crates/cli/src/cli_args/install.rs
  • pacquet/crates/executor/src/lib.rs
  • pacquet/crates/package-manager/src/install.rs
  • pacquet/crates/cli/tests/lifecycle_scripts.rs
  • pacquet/crates/package-manager/src/install/tests.rs
  • pacquet/crates/executor/src/lifecycle.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/package-manager/src/add.rs
  • pacquet/crates/cli/src/cli_args/install.rs
  • pacquet/crates/executor/src/lib.rs
  • pacquet/crates/package-manager/src/install.rs
  • pacquet/crates/cli/tests/lifecycle_scripts.rs
  • pacquet/crates/package-manager/src/install/tests.rs
  • pacquet/crates/executor/src/lifecycle.rs
🔇 Additional comments (16)
pacquet/crates/executor/src/lifecycle.rs (5)

126-138: LGTM!


146-167: LGTM!


169-226: LGTM!


336-350: LGTM!


420-444: LGTM!

pacquet/crates/executor/src/lib.rs (1)

7-10: LGTM!

pacquet/crates/package-manager/src/install.rs (6)

16-19: LGTM!


118-126: LGTM!


198-204: LGTM!


340-340: LGTM!


994-1012: LGTM!


1383-1421: LGTM!

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

67-67: LGTM!

Also applies to: 137-137, 182-182, 256-256, 323-323, 407-407, 476-476, 565-565, 711-711, 813-813, 1015-1015, 1089-1089, 1186-1186, 1317-1317, 1416-1416, 1523-1523, 1574-1574, 1667-1667, 1762-1762, 1827-1827, 1893-1893, 1985-1985, 2093-2093, 2155-2155, 2244-2244, 2352-2352, 2467-2467, 2548-2548, 2749-2749, 2874-2874, 2975-2975, 3082-3082, 3174-3174, 3269-3269, 3361-3361, 3450-3450, 3545-3545, 3638-3638, 3737-3737, 3838-3838, 3925-3925, 4010-4010, 4081-4081, 4148-4148, 4214-4214, 4283-4283, 4368-4368, 4431-4431, 4517-4517, 4569-4569, 4641-4641, 4714-4714, 4781-4781, 5034-5034, 5216-5216, 5365-5365, 5515-5515, 5594-5594, 5646-5646, 5754-5754, 5850-5850

pacquet/crates/cli/src/cli_args/install.rs (1)

280-283: LGTM!

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

110-114: LGTM!

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

580-793: LGTM!

@github-actions

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.098 ± 0.097 1.994 2.252 1.04 ± 0.06
pacquet@main 2.026 ± 0.063 1.943 2.157 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.0983057266000005,
      "stddev": 0.09652177789976578,
      "median": 2.0499449882,
      "user": 2.7419418,
      "system": 3.27160102,
      "min": 1.9940293197,
      "max": 2.2521031567,
      "times": [
        2.2234507957,
        2.2521031567,
        2.0203626827,
        2.0453586037,
        2.0985377557,
        1.9940293197,
        2.0423082557,
        2.2215141807,
        2.0545313727,
        2.0308611427
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.0258037307000003,
      "stddev": 0.06298618053473456,
      "median": 2.0335259342,
      "user": 2.7078083,
      "system": 3.2379216199999994,
      "min": 1.9428540797,
      "max": 2.1565431587,
      "times": [
        2.0418141387,
        2.0252377297,
        1.9428540797,
        2.0036673427,
        1.9651890047,
        2.0526626157,
        2.1565431587,
        2.0498469087,
        1.9575798297,
        2.0626424987
      ]
    }
  ]
}

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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 650.5 ± 10.8 637.3 673.9 1.00
pacquet@main 659.8 ± 23.2 635.7 707.4 1.01 ± 0.04
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.65048105594,
      "stddev": 0.010763996733014198,
      "median": 0.65142690964,
      "user": 0.35763282,
      "system": 1.3370633,
      "min": 0.63726184464,
      "max": 0.67393915464,
      "times": [
        0.67393915464,
        0.65815192264,
        0.64068482364,
        0.64375063264,
        0.65238173764,
        0.63726184464,
        0.65401038164,
        0.6533407316400001,
        0.64081724864,
        0.65047208164
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.6598206840400002,
      "stddev": 0.02322356772977944,
      "median": 0.65153121814,
      "user": 0.36864502,
      "system": 1.3390514,
      "min": 0.63573361164,
      "max": 0.70737181464,
      "times": [
        0.69162593564,
        0.64309993864,
        0.64259004664,
        0.63573361164,
        0.64964264364,
        0.66788357664,
        0.64526071864,
        0.65341979264,
        0.70737181464,
        0.66157876164
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.355 ± 0.041 2.310 2.423 1.00
pacquet@main 2.377 ± 0.024 2.350 2.420 1.01 ± 0.02
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.35500055922,
      "stddev": 0.04115793225680974,
      "median": 2.3365192409200004,
      "user": 3.89092532,
      "system": 3.0823975,
      "min": 2.31037956342,
      "max": 2.42324052842,
      "times": [
        2.3755100864200003,
        2.41704887142,
        2.32953357942,
        2.3346952454200003,
        2.42324052842,
        2.33834323642,
        2.3320097274200005,
        2.31037956342,
        2.37812675842,
        2.31111799542
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.3771176153200004,
      "stddev": 0.024241813673767716,
      "median": 2.37122235942,
      "user": 3.87401062,
      "system": 3.0931998,
      "min": 2.35015941842,
      "max": 2.4200955154200003,
      "times": [
        2.35015941842,
        2.3763757254200004,
        2.3878097414200004,
        2.3693614104200003,
        2.4158563334200003,
        2.35808859742,
        2.4200955154200003,
        2.37308330842,
        2.3667613324200003,
        2.3535847704200004
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.545 ± 0.014 1.518 1.569 1.00
pacquet@main 1.564 ± 0.034 1.496 1.606 1.01 ± 0.02
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.5450942014000002,
      "stddev": 0.013910831416498055,
      "median": 1.5457404268000001,
      "user": 1.7042258799999999,
      "system": 1.8831305999999999,
      "min": 1.5176910143,
      "max": 1.5687480743,
      "times": [
        1.5176910143,
        1.5554832543000001,
        1.5555177823,
        1.5445010753000001,
        1.5474383843000001,
        1.5469797783,
        1.5330130232999999,
        1.5687480743,
        1.5441073393,
        1.5374622883
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.5644166244,
      "stddev": 0.03415191217078343,
      "median": 1.5671777983,
      "user": 1.7328850800000002,
      "system": 1.8752138999999999,
      "min": 1.4961591363,
      "max": 1.6055297232999999,
      "times": [
        1.4961591363,
        1.5977169373,
        1.6009267853,
        1.5614422043,
        1.5326781043,
        1.6055297232999999,
        1.5409553043,
        1.5636903653,
        1.5706652313,
        1.5744024523
      ]
    }
  ]
}

@github-actions

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12051
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,355.00 ms
(+0.10%)Baseline: 2,352.69 ms
2,823.23 ms
(83.42%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,545.09 ms
(+5.19%)Baseline: 1,468.80 ms
1,762.56 ms
(87.66%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
2,098.31 ms
(-0.05%)Baseline: 2,099.30 ms
2,519.16 ms
(83.29%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
650.48 ms
(-4.79%)Baseline: 683.20 ms
819.84 ms
(79.34%)
🐰 View full continuous benchmarking report in Bencher

@zkochan zkochan merged commit 64ce69a into main May 29, 2026
28 checks passed
@zkochan zkochan deleted the feat/pacquet-project-lifecycle-scripts branch May 29, 2026 09:38
zkochan added a commit that referenced this pull request May 29, 2026
…12051)

Port pnpm's project (workspace/root) lifecycle scripts that run during
`pnpm install` — preinstall, install, postinstall, preprepare, prepare,
postprepare — distinct from the dependency build scripts already run in
`BuildModules`.

- executor: extract the per-stage loop into a shared
  `run_lifecycle_stages`, keeping the `binding.gyp -> node-gyp rebuild`
  fallback and the `npx only-allow pnpm` skip identical for both paths.
  Add `run_project_lifecycle_scripts` + `PROJECT_LIFECYCLE_STAGES`,
  mirroring the `runLifecycleHooksConcurrently` call sites in
  pkg-manager/core and pkg-manager/headless.
- executor: honor `SelectedShell::windows_verbatim_args` — on the
  Windows `cmd /d /s /c` path the script body is now appended with
  `raw_arg` so embedded quoting (e.g. `node -e "..."`) reaches the child
  intact, matching Node's `windowsVerbatimArguments`. Previously a
  no-op, which mangled quoted scripts under cmd.exe.
- package-manager: run each project's scripts in `Install::run` after
  the dependency graph is materialized, bins are linked, and
  `.modules.yaml` / the current lockfile are written, before the closing
  `pnpm:summary`. Runs on both the frozen and fresh paths. A project
  script failure always fails the install via the new
  `InstallError::ProjectLifecycleScript` variant (unlike optional
  dependency build failures).
- gate on `Install::is_full_install`: `pacquet add` is a partial install
  (pnpm's `mutation: 'installSome'`), so the project's own scripts must
  not run — mirroring pnpm's `mutation === 'install'` filter.
- tests: stage ordering, re-run on --frozen-lockfile, failure
  propagation, name-differs-from-directory, INIT_CWD, and the
  `add`-skips-project-scripts gate.

Projects run root-first and sequentially; pnpm's buildIndex ordering and
child_concurrency fan-out are a follow-up once pacquet computes a
per-importer build index. No `ignoreScripts` gate yet — pacquet hardcodes
`ignore_scripts: false` across the dep-build path, so this matches pnpm's
default of running them.

Ref: https://github.com/pnpm/pnpm/blob/80037699fb/pkg-manager/core/src/install/index.ts#L1517-L1530
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.

2 participants