Skip to content

fix(pacquet): run dependency build scripts on the fresh-lockfile path#12436

Merged
zkochan merged 11 commits into
mainfrom
fix/pacquet-fresh-path-build-phase
Jun 16, 2026
Merged

fix(pacquet): run dependency build scripts on the fresh-lockfile path#12436
zkochan merged 11 commits into
mainfrom
fix/pacquet-fresh-path-build-phase

Conversation

@zkochan

@zkochan zkochan commented Jun 15, 2026

Copy link
Copy Markdown
Member

Problem

pacquet add <pkg> (and any non-frozen pacquet install) took the fresh-lockfile path (InstallWithFreshLockfile::run), which never invoked BuildModules. As a result:

  • approved dependency lifecycle scripts (preinstall/install/postinstall) never ran, and
  • blocked build scripts were silently ignored — no warning, and the command exited zero.

This diverges from pnpm add, which runs the build phase, reports ignored builds, and (under the default strictDepBuilds) fails so the user approves the build. Reported for pacquet add esbuild, which finished with no warning about esbuild's blocked build script.

Fix — part 1: run the build phase on the fresh path

Extract the build tail shared by both install modes into run_build_phase (BuildModules + pnpm:ignored-scripts emit + post-build top-level bin link), plus BuildPhaseInputs / BuildPhaseError. The frozen path is refactored onto it (behavior-preserving — every variant stays #[diagnostic(transparent)], so error codes are unchanged); the fresh path now calls it too.

Fresh-path wiring: capture side_effects_maps_by_snapshot / requires_build_by_snapshot from CreateVirtualStore, surface hoisted_pkg_root_by_key and the public-hoist alias list, move importing_done + the store-index writer drain + the lockfile save to after the build, and defer the node --version engine probe so it overlaps CreateVirtualStore I/O (except under the global virtual store, whose layout needs it synchronously).

Fix — part 2: strictDepBuilds (default true)

pacquet had no strictDepBuilds setting, so it would only warn and exit zero. Implemented to match pnpm:

  • config: strict_dep_builds (default true), read from pnpm-workspace.yaml (strictDepBuilds) and the env overlay; moved into the asserted-defaults parity list.
  • package-manager: run_build_phase returns the ignored-build list, threaded out of both install paths; Install::run raises ERR_PNPM_IGNORED_BUILDS (non-zero exit) after the artifacts are written when strict — so the dependency is still added/materialized, and the user approves builds and reinstalls. The pnpm:ignored-scripts event is always emitted (empty list when nothing is ignored), mirroring pnpm.
  • default-reporter: suppress the "Ignored build scripts" warning box under strict mode (the error replaces it), mirroring pnpm's reportIgnoredBuilds gate (!strictDepBuilds); the CLI seeds the flag from config.

So pacquet add esbuild now exits non-zero with ERR_PNPM_IGNORED_BUILDS, like pnpm; strictDepBuilds: false restores the non-fatal warning + exit zero.

Tests

The dependency-build tests in lifecycle_scripts.rs already existed but were stubbed as known_failures that early-returned — they also placed allowBuilds in package.json#pnpm, which pnpm v11 and pacquet no longer read. Moved to pnpm-workspace.yaml and un-gated; all now run for real. Added pacquet add regression tests for both the strict-default failure and the non-strict warning. Tests that intentionally leave builds ignored set strictDepBuilds: false.

One known_failure remains, scoped to its actual cause: a warm --frozen-lockfile reinstall does not yet detect an allowBuilds policy change (the deps-status / workspace-state check short-circuits as up-to-date) — a separate workspace-state parity gap, not part of this fix.

Verification: cargo fmt --check, cargo clippy --all-targets, cargo doc -D warnings, cargo dylint, 489 pacquet-package-manager unit tests, and the full pacquet-cli integration suite (339) all pass.


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

Summary by CodeRabbit

  • New Features

    • Added strictDepBuilds configuration (default: enabled) to enforce pnpm-aligned behavior when dependency build scripts are ignored.
    • Install now consistently records and surfaces ignored dependency builds, failing with ERR_PNPM_IGNORED_BUILDS when unapproved.
    • strictDepBuilds can be set via PNPM_CONFIG_STRICT_DEP_BUILDS and pnpm-workspace.yaml.
  • Bug Fixes

    • Fixed install flow edge-cases so up-to-date and reinstall paths no longer incorrectly succeed when ignored builds are present.
  • Tests

    • Expanded lifecycle-script and registry/install coverage for ignored/blocked build script scenarios under both strict and non-strict modes.

`pacquet add <pkg>` (and any non-frozen `install`) took the fresh-lockfile
path, which never invoked `BuildModules`. So approved dependency lifecycle
scripts never ran, and — more visibly — blocked build scripts were silently
ignored with no "Ignored build scripts: …" warning, unlike `pnpm add`.

Extract the build tail shared by both install modes into `run_build_phase`
(`BuildModules` + `pnpm:ignored-scripts` emit + post-build top-level bin
link) plus `BuildPhaseInputs` / `BuildPhaseError`, and call it from the
frozen path (refactor) and the fresh path (new). On the fresh path: capture
`side_effects_maps_by_snapshot` / `requires_build_by_snapshot` from
`CreateVirtualStore`, surface `hoisted_pkg_root_by_key` and the public-hoist
alias list, move `importing_done` + the store-index writer drain + the
lockfile save to after the build, and defer the `node --version` engine probe
so it overlaps `CreateVirtualStore` I/O (except under the global virtual store,
whose layout needs it synchronously).

Enable the `lifecycle_scripts.rs` dependency-build tests that were stubbed as
`known_failures`. They placed `allowBuilds` in `package.json#pnpm`, which pnpm
v11 and pacquet no longer read — moved to `pnpm-workspace.yaml`. Add a
regression test that `pacquet add` reports the ignored build script. One
`known_failure` remains: a warm `--frozen-lockfile` reinstall does not yet
detect an `allowBuilds` policy change (a deps-status / workspace-state gap,
unrelated to this fix).
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 15, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (5) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0)

Grey Divider


Action required

1. Strict bypass via no-op ✓ Resolved 🐞 Bug ≡ Correctness
Description
The frozen up-to-date fast path returns Ok(()) without checking for previously-recorded ignored
builds under strict_dep_builds, so rerunning install after an ERR_PNPM_IGNORED_BUILDS failure
can exit 0 and undermine enforcement. Ignored builds are persisted into .modules.yaml and the
strict failure is only raised at the very end of the non-no-op install flow.
Code

pacquet/crates/package-manager/src/install.rs[R970-979]

&& let Some(current) = current_lockfile.as_ref()
&& wanted_lockfile == current
&& is_modules_yaml_consistent(&config.modules_dir, config, node_linker, included)
+            // An `allowBuilds` change that now permits a previously-ignored
+            // build must rebuild it, even though the lockfile and layout are
+            // unchanged. Mirrors pnpm's `runUnignoredDependencyBuilds`.
+            && !has_newly_allowed_ignored_builds(&config.modules_dir, config)
{
// Nothing to materialize means no fetch to overlap; verify
// eagerly before the up-to-date early return.
Evidence
The no-op short-circuit returns success before the strict ignored-builds gate, even though the
install persists ignored builds into .modules.yaml and later throws ERR_PNPM_IGNORED_BUILDS only
at the tail of the full install flow.

pacquet/crates/package-manager/src/install.rs[958-1019]
pacquet/crates/package-manager/src/install.rs[1254-1266]
pacquet/crates/package-manager/src/install.rs[1378-1386]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`Install::run` has a frozen no-op fast path that returns early when the lockfile and `.modules.yaml` match. Under `strictDepBuilds`, this path should still fail if there are any previously ignored dependency builds recorded in `.modules.yaml` (until the user approves builds), otherwise users can simply rerun install to avoid the non-zero exit.
### Issue Context
- Ignored builds are written into `.modules.yaml` via `build_modules_manifest(..., &ignored_builds)`.
- Strict mode currently raises `InstallError::IgnoredBuilds` only at the tail of the full install path.
### Fix Focus Areas
- pacquet/crates/package-manager/src/install.rs[958-1019]
- pacquet/crates/package-manager/src/install.rs[1254-1266]
- pacquet/crates/package-manager/src/install.rs[1378-1386]
### Implementation notes
- In the no-op short-circuit block, when `config.strict_dep_builds` is true:
- read `.modules.yaml` (you can reuse `read_modules_manifest::<Host>(&config.modules_dir)`),
- if `modules.ignored_builds` is `Some` and non-empty, convert to a sorted `Vec<String>` (matching the rest of the strict error behavior),
- return `Err(InstallError::IgnoredBuilds { package_names })`.
- Keep the existing `has_newly_allowed_ignored_builds` behavior intact (it handles the rebuild-needed case).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Stale strictDepBuilds in reporter 🐞 Bug ≡ Correctness
Description
pacquet_default_reporter::set_strict_dep_builds is called before run_update_config_hooks, but
those hooks can mutate cfg.strict_dep_builds; because the reporter caches the value in a
OnceLock, it cannot be updated later. This can suppress the “Ignored build scripts” warning when
the final config is non-strict (install exits 0) or show the warning even though the final config is
strict (install fails with ERR_PNPM_IGNORED_BUILDS).
Code

pacquet/crates/cli/src/cli_args.rs[325]

+                pacquet_default_reporter::set_strict_dep_builds(cfg.strict_dep_builds);
Evidence
The CLI seeds the reporter from the pre-hook config, but updateConfig hooks can change
strict_dep_builds afterward; the reporter cannot be updated because it stores the value in a
OnceLock. The install’s strictness gate uses the (possibly updated) config.strict_dep_builds, so
reporter output can diverge from actual install behavior (error vs warning).

pacquet/crates/cli/src/cli_args.rs[270-370]
pacquet/crates/cli/src/cli_args.rs[447-488]
pacquet/crates/cli/src/config_deps.rs[206-306]
pacquet/crates/config/src/workspace_yaml.rs[820-846]
pacquet/crates/default-reporter/src/lib.rs[62-73]
pacquet/crates/package-manager/src/install.rs[1373-1381]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The default reporter’s `strictDepBuilds` mode is currently seeded from `cfg.strict_dep_builds` before config-dependency `updateConfig` hooks run. Since hooks can modify `strict_dep_builds`, the reporter can become permanently out of sync (it uses `OnceLock`), causing incorrect/missing ignored-build messaging.
## Issue Context
- `run_update_config_hooks` applies a `WorkspaceSettings` delta back onto `Config`, including `strict_dep_builds`.
- The install behavior (error vs warning) is gated on `config.strict_dep_builds` at the end of `Install::run`.
- The default reporter suppresses the ignored-build warning box when it believes strict mode is enabled.
## Fix Focus Areas
- pacquet/crates/cli/src/cli_args.rs[314-488]
- pacquet/crates/cli/src/config_deps.rs[206-306]
- pacquet/crates/default-reporter/src/lib.rs[62-73]
## Recommended fix
1. Remove the early `pacquet_default_reporter::set_strict_dep_builds(cfg.strict_dep_builds);` call in the `CliCommand::Install` branch.
2. Add a call to `pacquet_default_reporter::set_strict_dep_builds(cfg.strict_dep_builds);` inside `InstallPipeline::run` **after** `config_deps::run_update_config_hooks::<Reporter>(...)` and **before** `State::init(...)` / the main install emits the `IgnoredScripts` event.
3. If keeping the “must be called before the first event” contract is important, change the reporter’s storage from `OnceLock<bool>` to an updatable primitive (e.g., `AtomicBool`) so you can seed early and then re-seed after hooks; otherwise update the comment to reflect the real requirement (“before IgnoredScripts render”).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. allowBuilds error swallowed 🐞 Bug ≡ Correctness
Description
unapproved_recorded_ignored_builds uses AllowBuildPolicy::from_config(config).ok()?, so invalid
allowBuilds specs are treated as “no unapproved ignored builds” on strict up-to-date fast paths.
This can hide ERR_PNPM_INVALID_VERSION_UNION/ERR_PNPM_NAME_PATTERN_IN_VERSION_UNION and may let
strict reruns short-circuit to success instead of enforcing ERR_PNPM_IGNORED_BUILDS.
Code

pacquet/crates/package-manager/src/install.rs[R1742-1748]

+fn unapproved_recorded_ignored_builds(modules: &Modules, config: &Config) -> Option<Vec<String>> {
+    let ignored = modules.ignored_builds.as_ref().filter(|set| !set.is_empty())?;
+    let policy = crate::AllowBuildPolicy::from_config(config).ok()?;
+    let mut names: Vec<String> = ignored
+        .iter()
+        .filter(|dep_path| policy.check(dep_path.as_str()).is_none())
+        .map(|dep_path| dep_path.as_str().to_string())
Evidence
The strict fast-path helper explicitly drops AllowBuildPolicy::from_config errors (.ok()?), even
though from_config is documented and tested to surface invalid allowBuilds keys as errors.

pacquet/crates/package-manager/src/install.rs[1742-1751]
pacquet/crates/package-manager/src/build_modules.rs[159-203]
pacquet/crates/package-manager/src/build_modules/tests.rs[207-228]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`unapproved_recorded_ignored_builds()` converts `AllowBuildPolicy::from_config()` errors into `None` (`.ok()?`). Callers interpret `None` as “no unapproved ignored builds”, which can let strict up-to-date fast paths return success even when config is invalid and/or ignored builds are recorded.
## Issue Context
`AllowBuildPolicy::from_config` is designed to *surface* invalid `allowBuilds` keys as typed `VersionPolicyError` (e.g. `ERR_PNPM_INVALID_VERSION_UNION`). The helper should not silently drop these errors in strict enforcement logic.
## Fix Focus Areas
- pacquet/crates/package-manager/src/install.rs[1742-1752]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Ignored-scripts event suppressed ✓ Resolved 🐞 Bug ◔ Observability
Description
run_build_phase suppresses emitting the pnpm:ignored-scripts event when strict_dep_builds is
true and the ignored list is non-empty, so structured/NDJSON consumers lose the ignored-build list
on the strict failure path. This contradicts the reporter contract that the event is emitted with
the names of skipped packages, and it makes strict failures less observable except via the rendered
error text.
Code

pacquet/crates/package-manager/src/install_frozen_lockfile.rs[R477-494]

+    // Emit the `pnpm:ignored-scripts` event so the reporter can render
+    // the "Ignored build scripts" warning box, mirroring pnpm's
+    // `reportIgnoredBuilds`. The empty-list event is always emitted
+    // (pnpm fires `ignoredScriptsLogger.debug({ packageNames: [] })`
+    // unconditionally at
+    // <https://github.com/pnpm/pnpm/blob/80037699fb/installing/deps-installer/src/install/index.ts#L526>),
+    // but a non-empty list is suppressed under `strictDepBuilds`: there
+    // the install fails with `ERR_PNPM_IGNORED_BUILDS` (raised by the
+    // caller from the returned list), so the box would only duplicate
+    // the error. Gating here — where `config` is the final, post-`updateConfig`
+    // value the strict-failure check also reads — keeps the warning and
+    // the error decision consistent without a reporter-side flag.
+    if ignored_builds.is_empty() || !config.strict_dep_builds {
+        Reporter::emit(&LogEvent::IgnoredScripts(IgnoredScriptsLog {
+            level: LogLevel::Debug,
+            package_names: ignored_builds.clone(),
+        }));
+    }
Evidence
The build-phase emitter explicitly suppresses pnpm:ignored-scripts under strict mode when there
are ignored builds, while the reporter schema documents the event as being emitted with the skipped
package list.

pacquet/crates/package-manager/src/install_frozen_lockfile.rs[477-494]
pacquet/crates/reporter/src/lib.rs[591-601]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`run_build_phase` currently gates `LogEvent::IgnoredScripts` behind `if ignored_builds.is_empty() || !config.strict_dep_builds`, which means the event is not emitted exactly when strict mode blocks builds (non-empty ignored list). This breaks the documented contract of `pnpm:ignored-scripts` being emitted with the skipped package list, and removes structured data from NDJSON output on the strict-failure path.
### Issue Context
- The log schema doc says `pnpm:ignored-scripts` is emitted with the names of every skipped package.
- The default reporter UI may want to suppress the warning box in strict mode, but that policy should not require dropping the structured event entirely.
### Fix Focus Areas
- pacquet/crates/package-manager/src/install_frozen_lockfile.rs[477-494]
- pacquet/crates/reporter/src/lib.rs[591-601]
- pacquet/crates/default-reporter/src/state.rs[846-859]
### Suggested fix approach
1. Emit `LogEvent::IgnoredScripts` unconditionally with the full `ignored_builds` list (including under strict mode).
2. Move the “suppress the warning box under strict” behavior into the default reporter (or into a separate dedicated event/field that indicates strict mode), so NDJSON/structured consumers still receive the ignored-build list.
3. If suppression-at-emitter is intentional, update the reporter schema documentation (and any tests/contracts) to explicitly state the event may be omitted under strict mode, and provide an alternate structured channel for the list on strict failures.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Strict gate ignores parse errors 🐞 Bug ☼ Reliability
Description
In Install::run and install_already_up_to_date, strictDepBuilds enforcement uses
read_modules_manifest(...).ok().flatten(), so a corrupted/unreadable node_modules/.modules.yaml is
treated as “missing” and the up-to-date fast paths can exit successfully even if ignored builds were
previously recorded. This bypasses ERR_PNPM_IGNORED_BUILDS on reruns specifically when .modules.yaml
cannot be parsed/read.
Code

pacquet/crates/package-manager/src/install.rs[R617-622]

+            if config.strict_dep_builds
+                && let Some(modules) =
+                    read_modules_manifest::<Host>(&config.modules_dir).ok().flatten()
+                && let Some(package_names) = unapproved_recorded_ignored_builds(&modules, config)
+            {
+                return Err(InstallError::IgnoredBuilds { package_names });
Evidence
The fast paths explicitly discard read/parse errors from .modules.yaml via .ok().flatten(), but
read_modules_manifest returns Err on YAML parse errors and non-NotFound I/O failures; discarding
these errors means strict-mode enforcement can be skipped when the manifest is corrupted/unreadable.

pacquet/crates/package-manager/src/install.rs[609-623]
pacquet/crates/package-manager/src/install.rs[1967-1979]
pacquet/crates/modules-yaml/src/lib.rs[371-386]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`strictDepBuilds` enforcement on the up-to-date fast paths currently does `read_modules_manifest(..).ok().flatten()`, which drops read/parse errors. If `.modules.yaml` is corrupted/unreadable, the install may incorrectly short-circuit as up-to-date under strict mode, bypassing the intended `ERR_PNPM_IGNORED_BUILDS` rerun enforcement.
### Issue Context
`read_modules_manifest` returns `Err(ReadModulesError::ReadFile|ParseYaml|...)` for non-NotFound failures. Those errors are currently swallowed by `.ok()`.
### Fix Focus Areas
- pacquet/crates/package-manager/src/install.rs[610-623]
- pacquet/crates/package-manager/src/install.rs[1967-1979]
### Suggested change
1) In `Install::run` optimistic fast-path block:
- Replace `.ok().flatten()` with explicit handling:
- `Ok(Some(modules))`: keep current behavior.
- `Ok(None)`: no recorded ignored builds.
- `Err(_)`: **do not** short-circuit as up-to-date; fall through to full install (e.g., set a `must_run_full_install = true` and skip the `Already up to date` early return).
2) In `install_already_up_to_date`:
- If `config.strict_dep_builds` and `read_modules_manifest` returns `Err(_)`, return `None` (force the full async install path), rather than allowing the pre-runtime up-to-date short-circuit.
This keeps strict enforcement conservative when the state file is unreadable, aligning with the intent of strictDepBuilds as an enforcement gate.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (5)
6. Eager modules.yaml parse 🐞 Bug ➹ Performance
Description
Install::run reads/parses node_modules/.modules.yaml before checking take_frozen_path, so
fresh-lockfile installs pay an extra filesystem read + YAML parse even though the value is only
needed for the frozen/up-to-date checks. This adds avoidable latency on the common install/add
path.
Code

pacquet/crates/package-manager/src/install.rs[R982-984]

+        // Parse `.modules.yaml` once and share it across the consistency,
+        // newly-allowed, and unapproved-ignored checks below.
+        let modules_manifest = read_modules_manifest::<Host>(&config.modules_dir).ok().flatten();
Evidence
The install pipeline parses .modules.yaml before the take_frozen_path condition, which means the
read/parse happens even on fresh-lockfile installs. The .modules.yaml reader does a filesystem
read and YAML parse, so this is measurable extra work on a hot path.

pacquet/crates/package-manager/src/install.rs[972-995]
pacquet/crates/modules-yaml/src/lib.rs[361-396]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`Install::run` unconditionally calls `read_modules_manifest` to build `modules_manifest`, even when `take_frozen_path` is false. This adds unnecessary disk I/O and YAML parsing on fresh-resolve/fresh-lockfile installs.
## Issue Context
`read_modules_manifest` performs a real file read and YAML parse, so calling it on paths that don’t use `.modules.yaml` is avoidable overhead.
## Fix Focus Areas
- Only parse `.modules.yaml` when needed (e.g., gate the parse behind `take_frozen_path`, or move it inside the `if take_frozen_path && ...` condition).
- pacquet/crates/package-manager/src/install.rs[982-995]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Detached store-index writer 🐞 Bug ☼ Reliability
Description
In InstallWithFreshLockfile::run, writer_task is only awaited after run_build_phase succeeds;
any error from the build phase (or earlier post-CreateVirtualStore steps) returns before the drain,
dropping the JoinHandle and detaching a task that still owns the SQLite connection. This can leave
background work/file handles alive past the error return, contrary to the store-index writer
contract and can cause flakiness/locks (notably on Windows) in long-running processes or in-process
tests.
Code

pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs[R1807-1850]

+        let ignored_builds = crate::install_frozen_lockfile::run_build_phase::<Reporter>(
+            &crate::install_frozen_lockfile::BuildPhaseInputs {
+                config,
+                workspace_root: lockfile_dir,
+                top_level_bin_root: symlink_root,
+                layout: &layout,
+                snapshots: built_lockfile.snapshots.as_ref(),
+                packages: built_lockfile.packages.as_ref(),
+                importers: &built_lockfile.importers,
+                dependency_groups: &dependency_groups,
+                allow_build_policy: &allow_build_policy,
+                side_effects_maps_by_snapshot: &side_effects_maps_by_snapshot,
+                requires_build_by_snapshot: &requires_build_by_snapshot,
+                engine_name: engine_name.as_deref(),
+                store_index_writer: &store_index_writer,
+                skipped: &skipped,
+                hoisted_pkg_root_by_key: hoisted_pkg_root_by_key.as_ref(),
+                is_hoisted,
+                publicly_hoisted_for_post_build: &publicly_hoisted_for_post_build,
+                logged_methods,
+            },
+        )
+        .map_err(InstallWithFreshLockfileError::BuildPhase)?;
+
+        // Drop the orchestration's writer handle so the channel closes,
+        // then wait for the final batch flush — now including any
+        // side-effects-cache rows the build phase queued. Errors are
+        // downgraded to `warn!` (see `create_virtual_store.rs`): the
+        // install is complete and a missed cache write just forces a
+        // re-fetch on the next install.
+        drop(store_index_writer);
+        match writer_task.await {
+            Ok(Ok(())) => {}
+            Ok(Err(error)) => tracing::warn!(
+                target: "pacquet::install",
+                ?error,
+                "store-index writer task returned an error; some rows may not be persisted",
+            ),
+            Err(error) => tracing::warn!(
+                target: "pacquet::install",
+                ?error,
+                "store-index writer task panicked; some rows may not be persisted",
+            ),
+        }
Evidence
The fresh-lockfile path awaits writer_task only after the fallible build-phase call; because that
call uses ?, failures bypass the drain and the join handle is dropped. The store-index writer
explicitly requires awaiting the task after dropping the last Arc to flush/close cleanly.

pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs[1807-1850]
pacquet/crates/store-dir/src/store_index.rs[112-116]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`InstallWithFreshLockfile::run` now awaits `writer_task` only on the success path after `run_build_phase`. Any `?`-propagated error after the writer is spawned can return early and drop the `JoinHandle`, detaching the task while it may still hold an open SQLite connection / WAL.
## Issue Context
`StoreIndexWriter::spawn` documents that callers must `await` the returned task after dropping the last `Arc` so the final batch flushes before returning.
## Fix Focus Areas
- Ensure the store-index writer is dropped + awaited on *all* exit paths after spawn (success and error), e.g. by:
- wrapping the remainder of the function in an inner closure, capturing its `Result`, then always running a `drop(store_index_writer); await writer_task;` cleanup before returning that `Result`, or
- using a scope-guard style cleanup that can `await` (structured as an explicit `match`/`let result = ...;` pattern).
- Preserve current warning behavior for writer-task failures.
### Code pointers
- pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs[1807-1850]
- pacquet/crates/store-dir/src/store_index.rs[112-116]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Patch hashes recomputed ✓ Resolved 🐞 Bug ➹ Performance
Description
run_build_phase re-resolves patchedDependencies via resolve_snapshot_patches, which re-hashes
patch files even on the fresh-lockfile path that already called
Config::resolved_patched_dependencies() earlier in the same install. This adds avoidable
patch-file I/O/CPU on installs that use patches.
Code

pacquet/crates/package-manager/src/install_frozen_lockfile.rs[R419-420]

+    let patches = resolve_snapshot_patches(config, snapshots)?;
+
Evidence
The build phase always calls resolve_snapshot_patches, which calls
Config::resolved_patched_dependencies() (hashing patch files). The fresh-lockfile install path
already calls resolved_patched_dependencies() earlier, so enabling the build phase on that path
introduces redundant hashing work within a single install.

pacquet/crates/package-manager/src/install_frozen_lockfile.rs[393-420]
pacquet/crates/package-manager/src/install_frozen_lockfile.rs[317-345]
pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs[966-975]
pacquet/crates/config/src/lib.rs[1600-1608]
pacquet/crates/patching/src/resolve.rs[64-85]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`run_build_phase()` calls `resolve_snapshot_patches()`, which always calls `config.resolved_patched_dependencies()` (hashing patch files). On the fresh-lockfile install path, `Config::resolved_patched_dependencies()` is already called earlier to feed the resolver, so the build phase re-hashes the same patch files again.
### Issue Context
This only affects installs with `patchedDependencies` configured, but those are already I/O-heavy; re-hashing can be noticeable and is easy to avoid.
### Fix Focus Areas
- pacquet/crates/package-manager/src/install_frozen_lockfile.rs[317-346]
- pacquet/crates/package-manager/src/install_frozen_lockfile.rs[352-420]
- pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs[966-985]
### Suggested approach
- Add an optional `patch_groups` (or already-resolved per-snapshot `patches`) field to `BuildPhaseInputs` so callers that already computed patch groups can pass them in.
- In the fresh-lockfile path, pass the already-computed `patched_dependencies` into the build-phase inputs.
- In the frozen-lockfile path, keep the current behavior (compute once), but route it through the same input field so `resolve_snapshot_patches` doesn’t re-hash when the value is already available.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Duplicate modules manifest parse ✓ Resolved 🐞 Bug ➹ Performance
Description
The no-op (up-to-date) frozen fast path now parses node_modules/.modules.yaml twice: once in
is_modules_yaml_consistent and again in has_newly_allowed_ignored_builds. This duplicates disk
I/O and YAML/JSON parsing on the most common “nothing to do” install path.
Code

pacquet/crates/package-manager/src/install.rs[R970-976]

&& let Some(current) = current_lockfile.as_ref()
&& wanted_lockfile == current
&& is_modules_yaml_consistent(&config.modules_dir, config, node_linker, included)
+            // An `allowBuilds` change that now permits a previously-ignored
+            // build must rebuild it, even though the lockfile and layout are
+            // unchanged. Mirrors pnpm's `runUnignoredDependencyBuilds`.
+            && !has_newly_allowed_ignored_builds(&config.modules_dir, config)
Evidence
The fast-path condition calls both helpers, and both helpers independently call
read_modules_manifest, causing two reads/parses of the same .modules.yaml.

pacquet/crates/package-manager/src/install.rs[958-977]
pacquet/crates/package-manager/src/install.rs[1638-1656]
pacquet/crates/package-manager/src/install.rs[1669-1683]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The frozen no-op fast path calls both `is_modules_yaml_consistent()` and `has_newly_allowed_ignored_builds()`, and each function calls `read_modules_manifest()` independently. That means `.modules.yaml` is read and parsed twice on the up-to-date path.
### Issue Context
This is a hot path (repeated installs in CI/dev). `.modules.yaml` can be large (hoistedDependencies, hoistedLocations), so a second parse can add measurable latency.
### Fix Focus Areas
- pacquet/crates/package-manager/src/install.rs[958-1019]
- pacquet/crates/package-manager/src/install.rs[1638-1683]
### Suggested approach
- Read/parse `.modules.yaml` once in `Install::run` before the fast-path `if`, then:
- Either pass `&Modules` into two helpers (`is_modules_yaml_consistent_with(modules, ...)` and `has_newly_allowed_ignored_builds_with(modules, ...)`),
- Or merge the two checks into a single helper that takes `&Modules` and returns whether the fast path is eligible.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. Duplicate node --version spawn ✓ Resolved 🐞 Bug ➹ Performance
Description
In InstallWithFreshLockfile::run, the new deferred detect_node_major() task can run alongside
the hoisted-linker installability host detection, resulting in two separate node --version process
spawns in a single hoisted install when installability constraints are present. This adds avoidable
latency/process churn on a hot path (hoisted installs), since both probes shell out to `node
--version`.
Code

pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs[R1388-1413]

+        // Detect the host node major once. The GVS layout needs it to
+        // compute slot paths; the build phase needs it for the
+        // side-effects-cache key. Probe only when one of those consumes
+        // it — when the side-effects cache is off and GVS is off, the
+        // build phase falls through to "rebuild" with no key to look
+        // up, so the `node --version` startup cost is pure waste.
+        //
+        // The GVS layout needs `engine_name` synchronously; the build
+        // phase only needs it for the side-effects-cache key. So under
+        // the default (non-GVS) config, defer the probe and let it
+        // overlap `CreateVirtualStore`'s I/O instead of serializing
+        // ahead of it — mirroring the frozen path's `deferred_engine_handle`.
+        let detect_engine = || {
+            pacquet_graph_hasher::detect_node_major()
+                .map(|major| pacquet_graph_hasher::engine_name(major, None, None))
+        };
+        let (engine_name, deferred_engine_handle): (
+            Option<String>,
+            Option<tokio::task::JoinHandle<Option<String>>>,
+        ) = if config.enable_global_virtual_store {
+            (tokio::task::spawn_blocking(detect_engine).await.ok().flatten(), None)
+        } else if config.side_effects_cache_read() || config.side_effects_cache_write() {
+            (None, Some(tokio::task::spawn_blocking(detect_engine)))
} else {
-            None
+            (None, None)
};
Evidence
The fresh-lockfile path now spawns a deferred engine probe when the side-effects cache is enabled,
and the hoisted linker branch can also spawn installability host detection; both detection routines
ultimately call node --version, so the two spawns can happen in the same hoisted install when
constraints exist.

pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs[481-491]
pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs[1389-1413]
pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs[1558-1576]
pacquet/crates/config/src/lib.rs[860-878]
pacquet/crates/graph-hasher/src/engine_name.rs[33-85]
pacquet/crates/package-manager/src/installability.rs[221-269]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`InstallWithFreshLockfile::run` can spawn `node --version` twice during hoisted installs: once via the newly-added deferred `detect_node_major()` (to build the side-effects-cache engine key) and again via `InstallabilityHost::detect` (to evaluate installability constraints).
### Issue Context
- The deferred probe is spawned when `side_effects_cache_read()`/`write()` is true (default config).
- The hoisted branch separately spawns `InstallabilityHost::detect` when any package has installability constraints.
- Both detection paths ultimately shell out to `node --version`.
### Fix Focus Areas
- pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs[1388-1413]
- pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs[1558-1574]
### Suggested fix approach
- When `nodeLinker == Hoisted` and `any_installability_constraint(...)` is true, spawn a *single* blocking task early (before/alongside `CreateVirtualStore`) to detect the full node version (or the full `InstallabilityHost`).
- Derive `engine_name` (major) from that same detected version for the side-effects-cache key, and reuse the same detected host info for the hoisted-linker installability check.
- Ensure the deferred handle is awaited once (and not duplicated) before the build phase / hoisted linker consumes it.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Review Change Stack

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

Implements pnpm's strictDepBuilds setting: adds strict_dep_builds: bool to Config, readable from pnpm-workspace.yaml and PNPM_CONFIG_STRICT_DEP_BUILDS. Extracts a shared run_build_phase helper from the frozen-lockfile path and wires both frozen and fresh install pipelines to propagate ignored_builds. The install dispatcher persists ignored builds to .modules.yaml, bypasses the fast-path when builds become newly allowed, and fails with ERR_PNPM_IGNORED_BUILDS under strict mode. Tests are migrated to workspace-YAML configuration with new regression coverage.

Changes

Strict Dependency Builds Feature

Layer / File(s) Summary
Config system: strict_dep_builds field, workspace YAML, and env overlay
pacquet/crates/config/src/lib.rs, pacquet/crates/config/src/workspace_yaml.rs, pacquet/crates/config/src/env_overlay.rs, pacquet/crates/config/src/pnpm_default_parity.rs
strict_dep_builds: bool added to Config (default true); WorkspaceSettings gains an optional field that apply_to copies into Config; PNPM_CONFIG_STRICT_DEP_BUILDS env var is parsed; parity test moves strict-dep-builds from NOT_PORTED to mapped_rows.
Frozen-lockfile: BuildPhaseError, run_build_phase, and ignored_builds output
pacquet/crates/package-manager/src/install_frozen_lockfile.rs
Four dedicated error variants collapsed into BuildPhase(BuildPhaseError); new BuildPhaseError, BuildPhaseInputs, resolve_snapshot_patches, and run_build_phase introduced; InstallFrozenLockfile::run delegates to run_build_phase; InstallFrozenLockfileOutput extended with ignored_builds: Vec<String>; parse_major_from_version and find_runtime_node_major made pub(crate).
Fresh-lockfile: wiring to shared build phase
pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
InstallWithFreshLockfileError::BuildPhase and ignored_builds field added to result; engine probe deferred; side_effects_maps_by_snapshot, requires_build_by_snapshot, hoisted_pkg_root_by_key, publicly_hoisted_for_post_build captured and passed to run_build_phase; store-index writer kept open until after build phase.
Install dispatcher: IgnoredBuilds error, fast-path bypass, .modules.yaml persistence
pacquet/crates/package-manager/src/install.rs, pacquet/crates/default-reporter/src/state.rs
InstallError::IgnoredBuilds variant added with ERR_PNPM_IGNORED_BUILDS code; ignored_builds collected from both install paths, persisted via updated build_modules_manifest; has_newly_allowed_ignored_builds bypasses the up-to-date fast-path; strict-mode enforcement fires after summary emission; reporter suppression documented.
Tests: workspace allowBuilds migration and ignored-build regression
pacquet/crates/cli/tests/lifecycle_scripts.rs, pacquet/crates/package-manager/src/install/tests.rs, pacquet/crates/package-manager/src/install_package_from_registry/tests.rs
Module renamed to dependency_build_scripts; workspace-YAML mutators allow_builds/set_strict_dep_builds replace package.json editing; build_deps_ran gating removed; JSON timestamp parsing fixed; add_reports_ignored_build_scripts regression test added; unit-test fixtures updated for dangerously_allow_all_builds, strict_dep_builds, and updated build_modules_manifest signature.

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant Install as Install::run
  participant HNAI as has_newly_allowed_ignored_builds
  participant BuildPhase as run_build_phase
  participant Modules as .modules.yaml

  User->>Install: pacquet install / pacquet add
  Install->>HNAI: read Modules, check allowBuilds policy
  HNAI-->>Install: fast-path bypassed if newly allowed
  Install->>BuildPhase: run lifecycle scripts (frozen or fresh path)
  BuildPhase-->>Install: ignored_builds Vec~String~
  Install->>Modules: build_modules_manifest(ignored_builds)
  alt strict_dep_builds=true and ignored_builds non-empty
    Install-->>User: ERR_PNPM_IGNORED_BUILDS
  else
    Install-->>User: success (optional warning)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • pnpm/pnpm#11904: Modifies the same "lockfile up-to-date" fast-path logic in install.rs and extends .modules.yaml state consultation for deciding whether Install::run can skip materialization.
  • pnpm/pnpm#11867: Modifies the fresh-lockfile phased pipeline after CreateVirtualStore to adjust symlinking and linking sequencing before lifecycle build execution, overlapping with this PR's build-phase integration refactoring.
  • pnpm/pnpm#11943: Modifies the installer's optimistic/"already up to date" short-circuit logic in the same install.rs file, adjusting the fast-path conditions that this PR extends with strict_dep_builds and ignored-build gating.

Poem

🐰 A rabbit once hopped through the scripts,
And found some builds blocked at the gate.
With strictDepBuilds set to true,
ERR_PNPM_IGNORED_BUILDS rang out on cue—
Now the warren knows which builds must wait! 🌿

🚥 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 describes the main change: fixing dependency build scripts execution on the fresh-lockfile path, which is the core objective of this PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/pacquet-fresh-path-build-phase

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

PR Summary by Qodo

pacquet: run dependency build phase for fresh-lockfile installs
🐞 Bug fix ✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

Walkthroughs

Description
• Run lifecycle-script build phase for fresh-lockfile installs and pacquet add, matching pnpm.
• Share build-phase tail across install modes, preserving ERR_PNPM_* error codes.
• Enable lifecycle-script integration tests and add regression for ignored build warnings.
Diagram
graph TD
cli(["pacquet CLI"]) --> fresh["Fresh install"] --> cvs1["Create store"] --> build["Build phase"] --> bm["BuildModules"] --> post["Report + relink"]
cli --> frozen["Frozen install"] --> cvs2["Create store"] --> build
build -- "fresh only" --> lock[("Write lockfile")]
subgraph Legend
direction LR
_cli(["Entry"]) ~~~ _proc["Process"] ~~~ _db[("Artifact")]
end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Inline build phase in fresh path
  • ➕ Smallest mechanical refactor; fewer new shared types
  • ➖ Duplicates build/ignored-scripts/bin-link logic; risks drift vs frozen path and pnpm parity over time
2. Create a new shared install orchestrator module
  • ➕ Could unify more of frozen/fresh install flow beyond build tail
  • ➖ Much larger change surface; higher regression risk unrelated to the reported bug
3. Always run synchronous engine probe
  • ➕ Simpler control flow; fewer join handles
  • ➖ Unnecessarily serializes node --version cost; loses the intended overlap with virtual-store I/O

Recommendation: The chosen approach (extracting a shared run_build_phase with a transparent BuildPhaseError) is the best trade-off: it removes duplication, guarantees pnpm-parity behaviors (running approved lifecycle scripts and reporting ignored builds) across both install modes, and keeps ERR_PNPM_* codes stable. Further unification via a larger shared orchestrator would be over-scoped for this bug fix.

Grey Divider

File Changes

Bug fix (1)
install_with_fresh_lockfile.rs Run shared build phase on fresh-lockfile path (and 'add') +136/-45

Run shared build phase on fresh-lockfile path (and 'add')

• Wires the fresh-lockfile install path to capture 'side_effects_maps_by_snapshot'/'requires_build_by_snapshot', surface hoisted roots and public-hoist aliases, and invoke the shared 'run_build_phase'. Reorders work so 'importing_done', store-index writer draining, and lockfile save happen after build success, and defers the node engine probe to overlap virtual-store I/O when possible.

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


Refactor (1)
install_frozen_lockfile.rs Extract shared build-phase tail into 'run_build_phase' +262/-196

Extract shared build-phase tail into 'run_build_phase'

• Introduces 'BuildPhaseInputs', 'BuildPhaseError', and 'run_build_phase' to encapsulate patched-dependency resolution, 'BuildModules', ignored-scripts emission, and post-build top-level bin linking. Refactors the frozen-lockfile install path to call the shared helper while preserving transparent diagnostics and existing error-code behavior.

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


Tests (1)
lifecycle_scripts.rs Enable dependency build-script tests and add 'pacquet add' regression +111/-87

Enable dependency build-script tests and add 'pacquet add' regression

• Renames the previous 'known_failures' module to actively run dependency lifecycle-script tests, moving 'allowBuilds' setup to 'pnpm-workspace.yaml' via a helper. Fixes timestamp parsing in dependency-order assertions and adds a regression test ensuring 'pacquet add' reports ignored build scripts and does not execute blocked scripts.

pacquet/crates/cli/tests/lifecycle_scripts.rs


Grey Divider

Qodo Logo

@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.05      5.6±0.29ms   772.0 KB/sec    1.00      5.4±0.45ms   807.0 KB/sec

Comment thread pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs Outdated
@codecov-commenter

codecov-commenter commented Jun 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.07921% with 24 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.00%. Comparing base (2b81344) to head (f49c75a).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
...tes/package-manager/src/install_frozen_lockfile.rs 87.78% 16 Missing ⚠️
...package-manager/src/install_with_fresh_lockfile.rs 94.68% 5 Missing ⚠️
pacquet/crates/package-manager/src/install.rs 95.71% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12436      +/-   ##
==========================================
+ Coverage   87.87%   88.00%   +0.13%     
==========================================
  Files         308      308              
  Lines       41210    41393     +183     
==========================================
+ Hits        36212    36429     +217     
+ Misses       4998     4964      -34     

☔ View full report in Codecov by Harness.
📢 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.

@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/cli/tests/lifecycle_scripts.rs`:
- Line 521: Remove the allow_known_failure! macro wrapper from the
warm_reinstall_detects_allow_builds_change() test call to expose the underlying
regression. The test failure will reveal a deps-status and workspace-state
invalidation issue where a warm --frozen-lockfile reinstall fails to react to an
allowBuilds policy change. Fix the invalidation logic to properly detect policy
changes so that the test passes normally without the known-failure gate,
ensuring users get correct behavior when approving previously blocked
dependencies.

In `@pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs`:
- Around line 1455-1463: The CreateVirtualStoreOutput destructuring in the
fresh-path install flow discards fetch_failed by binding it to an underscore,
whereas the frozen path folds optional fetch failures into SkippedSnapshots
before linking and building. Capture the fetch_failed result instead of
discarding it, and fold those snapshots into SkippedSnapshots before the
symlink, bin link, and BuildModules phases to prevent dangling links or build
attempts on missing snapshots. Apply this same fix pattern at the sibling
locations also mentioned (lines 1507-1509) to ensure the fresh and frozen paths
stay behaviorally synchronized.
- Around line 1388-1415: The current code detects the host Node version directly
via pacquet_graph_hasher::detect_node_major() in the detect_engine closure, but
the frozen path first checks for a runtime-pinned Node snapshot before falling
back to the host version. This creates a mismatch where fresh installs may
materialize GVS or side-effects-cache entries under the host Node key while
frozen installs use the pinned key, violating pnpm parity. Modify the
detect_engine closure to first check for a runtime-pinned Node (using the
existing frozen-path helper) and derive engine_name from that if present; only
probe the host Node version if no pinned version exists. Apply this consistently
to the engine_name assignment logic so that both GVS and side-effects-cache
cases use the same priority order (pinned first, then host).
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6a1dcf2a-5624-4362-a854-e67c32e20c7b

📥 Commits

Reviewing files that changed from the base of the PR and between 2b81344 and b28c3ca.

📒 Files selected for processing (3)
  • pacquet/crates/cli/tests/lifecycle_scripts.rs
  • pacquet/crates/package-manager/src/install_frozen_lockfile.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs

Comment thread pacquet/crates/cli/tests/lifecycle_scripts.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
@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Each scenario reports direct installs and pnpr installs. Bencher consumes pacquet@HEAD and pnpr@HEAD.

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.129 ± 0.189 3.977 4.433 1.92 ± 0.14
pacquet@main 4.176 ± 0.126 4.010 4.379 1.94 ± 0.12
pnpr@HEAD 2.202 ± 0.158 1.920 2.425 1.02 ± 0.09
pnpr@main 2.155 ± 0.123 2.008 2.297 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.128846565839999,
      "stddev": 0.1892519916995314,
      "median": 4.034592502540001,
      "user": 3.8456576999999994,
      "system": 3.5370765,
      "min": 3.97650178904,
      "max": 4.432853678040001,
      "times": [
        4.426610596040001,
        4.432853678040001,
        4.006685919040001,
        4.02450838604,
        4.33298937104,
        3.98181101904,
        4.0446766190400005,
        3.97650178904,
        4.06755195004,
        3.99427633104
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.1759889470400005,
      "stddev": 0.12567972830596547,
      "median": 4.17180777104,
      "user": 3.9156935999999996,
      "system": 3.5358456999999994,
      "min": 4.00956215204,
      "max": 4.37872657904,
      "times": [
        4.00956215204,
        4.06592009704,
        4.11427046304,
        4.3763098320400005,
        4.20635039504,
        4.13940508704,
        4.2049474380400005,
        4.06018697204,
        4.20421045504,
        4.37872657904
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.20185612864,
      "stddev": 0.15778136335444845,
      "median": 2.26526966854,
      "user": 2.5880654,
      "system": 2.9939004000000002,
      "min": 1.9197403920400002,
      "max": 2.42501641904,
      "times": [
        2.2556281450399998,
        1.9197403920400002,
        2.29806696004,
        2.27491119204,
        2.24070170804,
        2.27621060904,
        2.28091001904,
        2.0094590480399996,
        2.03791679404,
        2.42501641904
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.15467592544,
      "stddev": 0.12280376184505001,
      "median": 2.13640014954,
      "user": 2.6718437999999995,
      "system": 2.9618578999999996,
      "min": 2.0081639710399997,
      "max": 2.29692121604,
      "times": [
        2.0489911190399996,
        2.25598626104,
        2.2007262860399996,
        2.29257307004,
        2.03698433204,
        2.0081639710399997,
        2.29692121604,
        2.07207401304,
        2.2906027010399996,
        2.04373628504
      ]
    }
  ]
}

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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 627.3 ± 12.2 614.4 654.6 1.00
pacquet@main 642.2 ± 16.5 626.7 684.0 1.02 ± 0.03
pnpr@HEAD 676.1 ± 13.8 648.8 698.5 1.08 ± 0.03
pnpr@main 675.6 ± 14.4 659.9 711.0 1.08 ± 0.03
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.62733460576,
      "stddev": 0.012188480349408878,
      "median": 0.6230127786600002,
      "user": 0.36296644,
      "system": 1.3450928999999998,
      "min": 0.6144023591600001,
      "max": 0.65457373616,
      "times": [
        0.61954152516,
        0.65457373616,
        0.63772868916,
        0.6340625501600001,
        0.62968048216,
        0.6199436171600001,
        0.61738754116,
        0.6259584211600001,
        0.6144023591600001,
        0.6200671361600001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.6422389469600001,
      "stddev": 0.016538684150652277,
      "median": 0.6418975146600001,
      "user": 0.37219424,
      "system": 1.3490235,
      "min": 0.6266990961600001,
      "max": 0.6839891021600001,
      "times": [
        0.6311077261600001,
        0.6266990961600001,
        0.64349987616,
        0.6475460551600001,
        0.63377543416,
        0.62677843716,
        0.6446550241600001,
        0.6402951531600001,
        0.64404356516,
        0.6839891021600001
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6760774006600002,
      "stddev": 0.013822269902968835,
      "median": 0.6756643281600001,
      "user": 0.37140853999999995,
      "system": 1.3735849999999998,
      "min": 0.6487923911600001,
      "max": 0.6985042891600001,
      "times": [
        0.6985042891600001,
        0.6487923911600001,
        0.6815957921600001,
        0.69005502916,
        0.6753671691600001,
        0.66264742216,
        0.67596148716,
        0.6744640041600001,
        0.68226047316,
        0.6711259491600001
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.6756268324600001,
      "stddev": 0.014436245486357704,
      "median": 0.6746677026600001,
      "user": 0.38651264,
      "system": 1.3513084999999998,
      "min": 0.6598656881600001,
      "max": 0.7110467701600001,
      "times": [
        0.67867986816,
        0.6598656881600001,
        0.6627765451600001,
        0.66258289016,
        0.67997911716,
        0.67416311216,
        0.7110467701600001,
        0.6778389281600001,
        0.6750195331600001,
        0.6743158721600001
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.235 ± 0.053 4.123 4.283 1.92 ± 0.09
pacquet@main 4.253 ± 0.044 4.184 4.343 1.93 ± 0.09
pnpr@HEAD 2.206 ± 0.098 2.068 2.398 1.00
pnpr@main 2.241 ± 0.144 2.026 2.431 1.02 ± 0.08
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.23535109504,
      "stddev": 0.05264952114282649,
      "median": 4.25636112804,
      "user": 3.7171297999999995,
      "system": 3.4006093999999996,
      "min": 4.12342524554,
      "max": 4.28293845554,
      "times": [
        4.12342524554,
        4.18429635154,
        4.20046544854,
        4.27324732854,
        4.22180268054,
        4.28293845554,
        4.26806703054,
        4.24465522554,
        4.28149527154,
        4.27311791254
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.253436003740001,
      "stddev": 0.0438646828125624,
      "median": 4.25433366654,
      "user": 3.7853761999999995,
      "system": 3.4402128999999997,
      "min": 4.18437296454,
      "max": 4.34327830554,
      "times": [
        4.24699836854,
        4.34327830554,
        4.18437296454,
        4.22735210254,
        4.23608098454,
        4.28127421254,
        4.20980385154,
        4.26166896454,
        4.26762905254,
        4.27590123054
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.20635801464,
      "stddev": 0.09794325266736699,
      "median": 2.20660064754,
      "user": 2.4674521,
      "system": 2.9879644999999995,
      "min": 2.06808822454,
      "max": 2.39800240854,
      "times": [
        2.22598323254,
        2.2189668185399998,
        2.11721036954,
        2.12171182154,
        2.39800240854,
        2.06808822454,
        2.19423447654,
        2.3054954945399997,
        2.15766129854,
        2.25622600154
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.24105876944,
      "stddev": 0.14444427475106458,
      "median": 2.3101796220399997,
      "user": 2.546578099999999,
      "system": 2.9728608999999997,
      "min": 2.0262779485399998,
      "max": 2.43131189554,
      "times": [
        2.0605628075399998,
        2.30679281354,
        2.31356643054,
        2.08733595054,
        2.15390235354,
        2.0262779485399998,
        2.43131189554,
        2.34880789054,
        2.32562113554,
        2.3564084685399997
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.380 ± 0.023 1.323 1.399 2.11 ± 0.05
pacquet@main 1.387 ± 0.048 1.349 1.517 2.12 ± 0.08
pnpr@HEAD 0.666 ± 0.016 0.645 0.702 1.02 ± 0.03
pnpr@main 0.655 ± 0.013 0.636 0.675 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.37981285872,
      "stddev": 0.022936745267379804,
      "median": 1.38840563782,
      "user": 1.36840678,
      "system": 1.75888742,
      "min": 1.32261547082,
      "max": 1.39865139082,
      "times": [
        1.39124164382,
        1.37249897782,
        1.39744155582,
        1.38306139582,
        1.38937657382,
        1.39865139082,
        1.3874347018200002,
        1.36296800682,
        1.32261547082,
        1.39283886982
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.38657035832,
      "stddev": 0.04836572018991289,
      "median": 1.3786503673200001,
      "user": 1.3592317799999998,
      "system": 1.77700582,
      "min": 1.34905564782,
      "max": 1.51724910782,
      "times": [
        1.3546694378200002,
        1.34905564782,
        1.51724910782,
        1.3620160808200001,
        1.35961039182,
        1.37540352982,
        1.38570204382,
        1.39504850382,
        1.38189720482,
        1.38505163482
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.66623077852,
      "stddev": 0.0160145789007884,
      "median": 0.6622220373200001,
      "user": 0.33303807999999996,
      "system": 1.3069930199999997,
      "min": 0.6447097208200001,
      "max": 0.7021222138200001,
      "times": [
        0.6613171078200001,
        0.7021222138200001,
        0.66581744482,
        0.66312696682,
        0.67196824982,
        0.68125422682,
        0.6580052608200001,
        0.6533907228200001,
        0.66059587082,
        0.6447097208200001
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.65500832622,
      "stddev": 0.012659447681571019,
      "median": 0.65297461032,
      "user": 0.33728257999999994,
      "system": 1.30387372,
      "min": 0.6355342888200001,
      "max": 0.6754707318200001,
      "times": [
        0.65154464982,
        0.67274528682,
        0.6641287238200001,
        0.6516403488200001,
        0.64875266982,
        0.6355342888200001,
        0.6543088718200001,
        0.6414659758200001,
        0.65449171482,
        0.6754707318200001
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 3.065 ± 0.045 3.033 3.177 4.70 ± 0.10
pacquet@main 3.057 ± 0.042 3.018 3.158 4.69 ± 0.10
pnpr@HEAD 0.662 ± 0.008 0.649 0.674 1.01 ± 0.02
pnpr@main 0.652 ± 0.010 0.641 0.671 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 3.0652881574,
      "stddev": 0.04481049921486099,
      "median": 3.049316383,
      "user": 1.82405754,
      "system": 2.0057017200000002,
      "min": 3.0333767320000002,
      "max": 3.176861226,
      "times": [
        3.033887099,
        3.0333767320000002,
        3.062080243,
        3.0400730780000003,
        3.040817752,
        3.039184078,
        3.063100614,
        3.057815014,
        3.176861226,
        3.105685738
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 3.0570357868999998,
      "stddev": 0.04168967933701448,
      "median": 3.0476858565000002,
      "user": 1.79118924,
      "system": 2.0447553199999993,
      "min": 3.01822018,
      "max": 3.157880955,
      "times": [
        3.02118187,
        3.060319893,
        3.0341420980000002,
        3.0763600490000003,
        3.0262425470000003,
        3.01822018,
        3.080638564,
        3.054463433,
        3.157880955,
        3.04090828
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6619190423000001,
      "stddev": 0.00840302153285142,
      "median": 0.6610784435,
      "user": 0.34360073999999996,
      "system": 1.28757552,
      "min": 0.648723462,
      "max": 0.6736265410000001,
      "times": [
        0.66096392,
        0.652105606,
        0.665029267,
        0.661126596,
        0.648723462,
        0.6551327490000001,
        0.673448339,
        0.668003652,
        0.6736265410000001,
        0.661030291
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.652439516,
      "stddev": 0.009929338478055326,
      "median": 0.6529316390000001,
      "user": 0.34298043999999994,
      "system": 1.29109732,
      "min": 0.6408022800000001,
      "max": 0.670585147,
      "times": [
        0.657028059,
        0.656033806,
        0.670585147,
        0.644526679,
        0.663914981,
        0.6542787090000001,
        0.64443259,
        0.6515845690000001,
        0.6408022800000001,
        0.64120834
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12436
Testbedpacquet
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
milliseconds (ms)
(Result Δ%)
Upper Boundary
milliseconds (ms)
(Limit %)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
🚷 view threshold
4,235.35 ms
(+2.03%)Baseline: 4,151.08 ms
4,981.30 ms
(85.03%)
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
🚷 view threshold
3,065.29 ms
(+2.87%)Baseline: 2,979.70 ms
3,575.64 ms
(85.73%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,379.81 ms
(+6.18%)Baseline: 1,299.53 ms
1,559.43 ms
(88.48%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
4,128.85 ms
(+5.12%)Baseline: 3,927.58 ms
4,713.09 ms
(87.60%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
627.33 ms
(+1.54%)Baseline: 617.79 ms
741.35 ms
(84.62%)
🐰 View full continuous benchmarking report in Bencher

@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12436
Testbedpnpr

⚠️ WARNING: No Threshold found!

Without a Threshold, no Alerts will ever be generated.

Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the --ci-only-thresholds flag.

Click to view all benchmark results
BenchmarkLatencymilliseconds (ms)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,206.36 ms
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
661.92 ms
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
666.23 ms
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,201.86 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
676.08 ms
🐰 View full continuous benchmarking report in Bencher

Implements the `strictDepBuilds` setting (default `true`, matching pnpm)
so an install that blocks any dependency build script fails with
`ERR_PNPM_IGNORED_BUILDS` instead of only printing a warning and exiting
zero. This makes `pacquet add esbuild` behave like `pnpm add esbuild`:
the dependency is added and materialized, then the command exits
non-zero telling the user to approve the build.

- config: add `strict_dep_builds` (default `true`), read from
  `pnpm-workspace.yaml` (`strictDepBuilds`) and the env overlay; move it
  from the not-yet-implemented parity list into the asserted defaults.
- package-manager: `run_build_phase` returns the ignored-build list,
  threaded out of both install paths; `Install::run` raises
  `ERR_PNPM_IGNORED_BUILDS` after the artifacts are written when
  `strictDepBuilds` is on. The `pnpm:ignored-scripts` event is always
  emitted (empty list when nothing is ignored), mirroring pnpm.
- default-reporter: suppress the "Ignored build scripts" warning box
  under strict mode (the error replaces it), mirroring pnpm's
  `reportIgnoredBuilds` gate; the CLI seeds the flag from config.

Tests: `pacquet add` now fails under the strict default and warns (exit
zero) with `strictDepBuilds: false`. The selective-allow / ignore tests
that inspect a completed install set `strictDepBuilds: false`; the
optional-failing-postinstall unit test now allows the build so the
failure path is actually exercised.
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 572ac28

Comment thread pacquet/crates/cli/src/cli_args.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/cli/src/cli_args.rs`:
- Around line 271-276: The set_strict_dep_builds function is currently only
called in the install command path but not in the CliCommand::Dlx path, causing
inconsistent behavior between commands. Move the set_strict_dep_builds call to a
shared location in the config-loading path (likely within or immediately after
the config() function call) so that it applies to all command paths including
both install and dlx. This ensures the default reporter uses consistent strict
mode behavior across all commands and prevents non-strict warning behavior from
being used as a fallback in the dlx command.

In `@pacquet/crates/package-manager/src/install.rs`:
- Around line 254-270: The help text in the diagnostic attribute for the
IgnoredBuilds variant references the pnpm approve-builds command, which is not
implemented in pacquet (as noted in dlx.rs:38 where the interactive
approve-builds prompt is explicitly noted as not ported). Remove or replace the
reference to the approve-builds command in the help text of the IgnoredBuilds
diagnostic to accurately reflect what pacquet users can actually do when
encountering ignored build scripts.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0e398a54-e40c-4736-aef0-d1b51058e044

📥 Commits

Reviewing files that changed from the base of the PR and between b28c3ca and 572ac28.

📒 Files selected for processing (13)
  • pacquet/crates/cli/src/cli_args.rs
  • pacquet/crates/cli/tests/lifecycle_scripts.rs
  • pacquet/crates/config/src/env_overlay.rs
  • pacquet/crates/config/src/lib.rs
  • pacquet/crates/config/src/pnpm_default_parity.rs
  • pacquet/crates/config/src/workspace_yaml.rs
  • pacquet/crates/default-reporter/src/lib.rs
  • pacquet/crates/default-reporter/src/state.rs
  • pacquet/crates/package-manager/src/install.rs
  • pacquet/crates/package-manager/src/install/tests.rs
  • pacquet/crates/package-manager/src/install_frozen_lockfile.rs
  • pacquet/crates/package-manager/src/install_package_from_registry/tests.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
✅ Files skipped from review due to trivial changes (1)
  • pacquet/crates/package-manager/src/install_package_from_registry/tests.rs

Comment thread pacquet/crates/cli/src/cli_args.rs Outdated
Comment thread pacquet/crates/package-manager/src/install.rs
Resolves the PR review comments on the fresh-lockfile build phase:

- Rebuild on `allowBuilds` changes (ports pnpm's
  `runUnignoredDependencyBuilds`). The install now records the ignored
  builds in `.modules.yaml`; the frozen no-op fast path no longer
  short-circuits when a recorded ignored build is now allowed, so a warm
  `--frozen-lockfile` reinstall rebuilds the newly-approved package
  instead of skipping as up-to-date. Removes the corresponding
  `known_failure` gate.

- Single `node --version` probe. The fresh path detected the host once
  for the engine-name cache key and again (via `InstallabilityHost`) for
  the hoisted installability check; it now detects the host once and
  reuses it, mirroring the frozen path.

- Honor a runtime-pinned Node. `engine_name` now prefers a
  `node@runtime:` pin from the lockfile before probing the host, so a
  fresh install keys GVS slots / side-effects-cache rows under the same
  major a later frozen install uses. Exposes the frozen-path
  `find_runtime_node_major` / `parse_major_from_version` helpers.

- Fold swallowed optional-fetch failures into the skip set before the
  symlink / bin-link / build phases, matching the frozen path, so a
  fetch-failed snapshot can't leave a dangling link or a build attempt
  on a slot that was never extracted.
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit f0be33b

@zkochan

zkochan commented Jun 16, 2026

Copy link
Copy Markdown
Member Author

Addressed all four review findings in f0be33b:

  • Warm rebuild on allowBuilds change (CodeRabbit) — ported pnpm's runUnignoredDependencyBuilds. The install now records ignored_builds in .modules.yaml, and the frozen no-op fast path skips its short-circuit when a recorded ignored build is now allowed (has_newly_allowed_ignored_builds), so a warm --frozen-lockfile reinstall rebuilds the newly-approved package. The known_failure gate is removed and the test runs normally.

  • Duplicate node --version spawn (Qodo / CodeRabbit) — the fresh path now detects the host once and reuses it for both the engine-name cache key and the hoisted installability check, mirroring the frozen path.

  • Runtime-pinned Node (CodeRabbit) — engine_name now prefers a node@runtime: pin (via the now-pub(crate) find_runtime_node_major) before probing the host, so fresh and frozen installs key GVS / side-effects-cache entries under the same major.

  • Discarded fetch_failed (CodeRabbit) — optional fetch failures are folded into the skip set before the symlink / bin-link / build phases, matching the frozen path.

Verified: cargo fmt --check, clippy --all-targets, cargo doc -D warnings, cargo dylint, and 828 pacquet-package-manager + pacquet-cli tests pass.


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

Comment thread pacquet/crates/package-manager/src/install.rs
…r flag

The default reporter's `strictDepBuilds`-suppression was seeded into a
`OnceLock` from the CLI before `updateConfig` hooks ran, so a hook that
changed `strict_dep_builds` left the reporter stale (warning shown/hidden
inconsistently with the install's error/warning decision), and `dlx`
never seeded it at all.

Drop the reporter-side flag entirely and gate the emit in
`run_build_phase`, which already holds the final, post-`updateConfig`
`config` the strict-failure check reads. The empty-list event is still
always emitted (pnpm's NDJSON contract); a non-empty list is suppressed
under `strictDepBuilds` because the install fails with
`ERR_PNPM_IGNORED_BUILDS` and the box would only duplicate the error.
This keeps the warning and the failure decision consistent on every
command path without per-path CLI seeding.
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit f4b3d5c

Comment thread pacquet/crates/package-manager/src/install_frozen_lockfile.rs Outdated
Comment thread pacquet/crates/package-manager/src/install.rs Outdated
A strict install that blocked a build fails with
`ERR_PNPM_IGNORED_BUILDS` but still writes `node_modules`, the lockfile,
`.modules.yaml`, and the workspace-state first. A rerun then hit an
up-to-date fast path and exited 0, letting the user bypass the gate.

pnpm seeds `ignoredBuilds` from `.modules.yaml` on the up-to-date path
and `handleIgnoredBuilds` still throws. Mirror that: all three
short-circuits — the CLI pre-runtime fast path
(`install_already_up_to_date`), `Install::run`'s optimistic-repeat
branch, and the frozen no-op branch — now re-raise
`ERR_PNPM_IGNORED_BUILDS` when `.modules.yaml` records an ignored build
the current policy still leaves unapproved
(`unapproved_recorded_ignored_builds`). Builds an `allowBuilds` change
newly permits are excluded (they rebuild via the existing
`runUnignoredDependencyBuilds` path) and explicitly-denied builds are
excluded (silently skipped), matching a full install's `BuildModules`.
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 2d47350

Two performance findings from review:

- Parse `.modules.yaml` once on the frozen up-to-date fast path. The
  consistency, newly-allowed, and (new) unapproved-ignored-builds checks
  each re-read and re-parsed the manifest; they now share a single parse
  via `modules_consistent_with` / `&Modules`-taking helpers.

- Don't re-hash patch files in the build phase. `run_build_phase` resolved
  `patchedDependencies` again via `resolve_snapshot_patches`, even on the
  fresh path that already resolved the grouped record to feed the
  resolver. `BuildPhaseInputs` now carries an optional pre-resolved
  `PatchGroupRecord`; the fresh path passes its record and the frozen path
  resolves once internally as before.
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 3a11ef7

Comment thread pacquet/crates/package-manager/src/install.rs Outdated
Only the frozen-lockfile fast path consumes the parsed modules manifest,
so gate read_modules_manifest behind take_frozen_path. Fresh-resolve
install/add no longer pays an avoidable file read + YAML parse.
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 369ce9c

`modules_consistent_with`'s doc linked to `is_modules_yaml_consistent`,
which moved into the test module, breaking `cargo doc -D warnings` (the
Rust CI Doc job). Merge the two stacked doc blocks into one and drop the
dangling link.
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit f663da0

Comment thread pacquet/crates/package-manager/src/install.rs Outdated
Comment thread pacquet/crates/package-manager/src/install_frozen_lockfile.rs Outdated
Two review findings:

- Always emit `pnpm:ignored-scripts`. The emit was suppressed under
  `strictDepBuilds` with a non-empty list, dropping the structured list
  from NDJSON exactly on the strict-failure path. The event is now always
  emitted with the package names; it carries the final `strict_dep_builds`
  value (a `#[serde(skip)]` field, so the wire shape is unchanged) and the
  default reporter suppresses only the rendered warning box under strict
  mode — matching pnpm's split between `reportIgnoredBuilds` (display) and
  `handleIgnoredBuilds` (throw), without a stale reporter global.

- Don't swallow `.modules.yaml` read/parse errors in the strict gate. The
  optimistic and pre-runtime up-to-date fast paths used `.ok().flatten()`,
  so a corrupt/unreadable state file read as "no recorded ignored builds"
  and could short-circuit to exit 0 under strict mode. They now treat a
  read `Err` conservatively and fall through to the full install (which
  re-raises `ERR_PNPM_IGNORED_BUILDS`). The frozen no-op path was already
  safe (its consistency check needs a parsed manifest).
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 3e18430

`unparseable` → `unparsable` in a test doc comment.
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit f49c75a

Comment thread pacquet/crates/package-manager/src/install.rs Outdated
`unapproved_recorded_ignored_builds` resolved the build policy with
`AllowBuildPolicy::from_config(config).ok()?`, swallowing a malformed
`allowBuilds` spec (`ERR_PNPM_INVALID_VERSION_UNION` /
`ERR_PNPM_NAME_PATTERN_IN_VERSION_UNION`) into `None`. On the strict
optimistic and pre-runtime up-to-date fast paths that read as "no
unapproved ignored builds", hiding the config error and risking a
short-circuit to success.

The helper now returns `Result` and propagates the policy error; the two
vulnerable fast paths fall through to the full install on `Err`, which
re-evaluates the policy and reports the real error. The frozen no-op path
was already guarded by `has_newly_allowed_ignored_builds` (which returns
true on the same error and skips the branch). Adds a unit test.
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 16, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 02dd1e3

@zkochan zkochan merged commit d36b6f8 into main Jun 16, 2026
26 of 27 checks passed
@zkochan zkochan deleted the fix/pacquet-fresh-path-build-phase branch June 16, 2026 07:42
zkochan added a commit that referenced this pull request Jun 16, 2026
* feat(pacquet): add --ignore-scripts to install

`pacquet install` runs the dependency build phase on the fresh-lockfile
path (since #12436), so a project with blocked build scripts
(e.g. sharp, esbuild) fails with `ERR_PNPM_IGNORED_BUILDS` under the
default `strictDepBuilds`. pnpm's answer is `--ignore-scripts`; pacquet
had no equivalent, so there was no way to install such a project without
approving every build.

Add `--ignore-scripts`, mirroring pnpm: a `Config.ignore_scripts` field
fed by the CLI flag (merged enable-only at the Install dispatch, like
`--frozen-store`). When set, the during-install build loop bypasses its
allow-build gate entirely — no script runs and nothing is recorded as an
ignored build, so the install no longer fails under `strictDepBuilds`.
Patches still apply, and the project's own lifecycle scripts are skipped
too, matching pnpm's `ignoreScripts`.

This also unblocks the vlt.sh benchmark, whose pacquet command was the
only one missing the `--ignore-scripts` the pnpm command already passes,
making pacquet show up as DNF on every fixture.

* fix(pacquet): honor ignore_scripts for git deps and from workspace yaml/env

Two follow-ups from PR review:

- Git and git-hosted-tarball dependencies were fetched with
  `ignore_scripts: false` hardcoded, so their `prepare` script still ran
  during install even under `--ignore-scripts`. Thread
  `config.ignore_scripts` into `GitFetcher` / `GitHostedTarballFetcher`,
  and flip the git store-index key's `built` dimension to
  `!ignore_scripts` at both the write site (`install_package_by_snapshot`)
  and the warm-prefetch site (`snapshot_cache_key`) so the two stay in
  lock-step and address the same slot.

- `Config.ignore_scripts` was only set by the CLI flag; `ignoreScripts`
  in `pnpm-workspace.yaml` and `PNPM_CONFIG_IGNORE_SCRIPTS` were silently
  dropped. Wire it through `WorkspaceSettings` + `apply_to` and the env
  overlay, mirroring `strictDepBuilds`.
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