feat(package-manager): run project lifecycle scripts during install#12051
Conversation
📝 WalkthroughWalkthroughThis PR implements workspace project-level lifecycle script execution for ChangesProject Lifecycle Script Execution During Install
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
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Micro-Benchmark ResultsLinux |
Codecov Report❌ Patch coverage is
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. 🚀 New features to boost your workflow:
|
ab6f05e to
3f77c8c
Compare
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
3f77c8c to
7ccab3a
Compare
Review Summary by QodoRun project lifecycle scripts during install
WalkthroughsDescription• 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 Diagramflowchart 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
File Changes1. pacquet/crates/cli/src/cli_args/install.rs
|
There was a problem hiding this comment.
🧹 Nitpick comments (1)
pacquet/crates/package-manager/src/install.rs (1)
1398-1398: 💤 Low valueConsider removing the unused
_manifestbinding.The loop destructures
_manifestbut never uses it sincerun_project_lifecycle_scriptsreads the manifest from disk viasafe_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
📒 Files selected for processing (7)
pacquet/crates/cli/src/cli_args/install.rspacquet/crates/cli/tests/lifecycle_scripts.rspacquet/crates/executor/src/lib.rspacquet/crates/executor/src/lifecycle.rspacquet/crates/package-manager/src/add.rspacquet/crates/package-manager/src/install.rspacquet/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 warningsin 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 (&Pathover&PathBuf,&strover&String) when it doesn't force extra copies.
PreferArc::clone(&x)/Rc::clone(&x)overx.clone()for reference-counted types, so the cost is visible at the call site.
Follow Rust API Guidelines for naming: https://rust-lang.github.io/api-guidelines/naming.html
No star imports inside module bodies. Writeuse super::{Foo, bar}instead ofuse super::*;, and the same for any other glob whose target is a module you control. Two forms stay allowed: external-crate preludes such asuse rayon::prelude::*;and root-of-module re-exports such aspub use submodule::*;in alib.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 (includingpipe-trait.pipe(...)chains) into intermediateletbindings unless you can justify the rewrite. Valid justifications include a chain that fails to compile after your...
Files:
pacquet/crates/package-manager/src/add.rspacquet/crates/cli/src/cli_args/install.rspacquet/crates/executor/src/lib.rspacquet/crates/package-manager/src/install.rspacquet/crates/cli/tests/lifecycle_scripts.rspacquet/crates/package-manager/src/install/tests.rspacquet/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 tocargo testand 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 scalarassert_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.rspacquet/crates/cli/src/cli_args/install.rspacquet/crates/executor/src/lib.rspacquet/crates/package-manager/src/install.rspacquet/crates/cli/tests/lifecycle_scripts.rspacquet/crates/package-manager/src/install/tests.rspacquet/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.rspacquet/crates/cli/src/cli_args/install.rspacquet/crates/executor/src/lib.rspacquet/crates/package-manager/src/install.rspacquet/crates/cli/tests/lifecycle_scripts.rspacquet/crates/package-manager/src/install/tests.rspacquet/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.rspacquet/crates/cli/src/cli_args/install.rspacquet/crates/executor/src/lib.rspacquet/crates/package-manager/src/install.rspacquet/crates/cli/tests/lifecycle_scripts.rspacquet/crates/package-manager/src/install/tests.rspacquet/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!
Integrated-Benchmark Report (Linux)Scenario: Isolated linker: fresh restore, cold cache + cold store
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
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
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
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
]
}
]
} |
|
| Branch | pr/12051 |
| Testbed | pacquet |
Click to view all benchmark results
| Benchmark | Latency | Benchmark Result milliseconds (ms) (Result Δ%) | Upper Boundary milliseconds (ms) (Limit %) |
|---|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 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%) |
…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
What
Ports pnpm's project (workspace/root) lifecycle scripts that run during
pnpm install—preinstall,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 inBuildModules.Mirrors pnpm's
runLifecycleHooksConcurrently(['preinstall', 'install', 'postinstall', 'preprepare', 'prepare', 'postprepare'], ...)call near the end of the install inpkg-manager/coreandpkg-manager/headless.How
executor— extracted the per-stage loop out ofrun_postinstall_hooksinto a sharedrun_lifecycle_stages(samebinding.gyp → node-gyp rebuildfallback andnpx only-allow pnpmskip). Addedrun_project_lifecycle_scripts+ thePROJECT_LIFECYCLE_STAGESconstant.executor— honorSelectedShell::windows_verbatim_args: on the Windowscmd /d /s /cpath the script body is now appended withraw_arg, so embedded quoting (e.g.node -e "...") reaches the child intact, matching Node'swindowsVerbatimArguments. This was previously a no-op (let _ = shell.windows_verbatim_args;), which mangled quoted scripts undercmd.exe— and is why the first test revision failed on Windows.package-manager— newrun_projects_lifecycle_scriptshelper, fanned out over every importer (root + workspace projects), wired intoInstall::runafter the dependency graph is materialized, bins are linked, and.modules.yaml/ the current lockfile are written, before the closingpnpm:summary. Runs on both the frozen and fresh paths. NewInstallError::ProjectLifecycleScriptvariant — a project script failure always fails the install (unlike optional-dep build failures, whichBuildModulesswallows).package-manager— gated onInstall::is_full_install:pacquet addis a partial install (pnpm'smutation: 'installSome'), so the project's own scripts must not run — mirroring pnpm'smutation === 'install'filter at therunLifecycleHooksConcurrentlycall site.Tests
Ported the project-script tests from pnpm's
pkg-manager/core/test/install/lifecycleScripts.tsandpnpm/test/install/lifecycleScripts.ts:postparetypo that must not run)--frozen-lockfileexit 1)namediffers from its directoryINIT_CWDis the lockfile directoryadddoes not run project scripts (the named-install gate)Validated the Windows-only
raw_argpath compiles + lints clean againstx86_64-pc-windows-gnulocally.Notes / follow-ups
ignoreScriptsgate. pacquet hasn't implementedignoreScriptsanywhere (it hardcodesignore_scripts: falseacross the dep-build path), so project scripts run unconditionally — matching pnpm's default. Adding the flag is a separate, cross-cutting change.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
pacquet install.pacquet addexplicitly does not execute project lifecycle scripts, maintaining partial install behavior.Tests