Skip to content

feat: add support for package maps#12430

Merged
zkochan merged 13 commits into
pnpm:mainfrom
arcanis:main
Jun 18, 2026
Merged

feat: add support for package maps#12430
zkochan merged 13 commits into
pnpm:mainfrom
arcanis:main

Conversation

@arcanis

@arcanis arcanis commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

This PR implements support for generating package maps, similar to what I implemented in Yarn in yarnpkg/berry#7184.

In practice it will:

  • Always generate a node_modules/.package-map.json file (so that third-party tooling can easily start experimenting with it, and since it's just a single file it shouldn't impact perfs).
  • Expose a new node-experimental-package-map boolean (if yes pnpm will auto-inject the --experimental-package-map flag in NODE_OPTIONS).
  • Expose a node-package-map-type setting to define whether the package map should be strict or loose (tolerate hoisting-like access).

I added tests and they pass locally, but they still require a nightly build version of Node.js until the next 26.x release.

Squash commit body

Generate a Node.js package map at `node_modules/.package-map.json` on every
isolated or hoisted install, including under the global virtual store, so that
third-party tooling can start experimenting with package maps. The file is
serialized compactly.

Two settings control how the map is consumed:

- `node-experimental-package-map` (default: off): inject
  `--experimental-package-map` into `NODE_OPTIONS` for the Node.js scripts pnpm
  runs — dependency lifecycle scripts, `pnpm exec`, and `pnpm run` (including
  recursive runs).
- `node-package-map-type` (`standard` | `loose`): choose between a strict map
  and one that tolerates hoisting-like access.

Covered by both the pnpm CLI and the pacquet (Rust) implementation.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added support for Node.js experimental package maps by generating node_modules/.package-map.json and injecting --experimental-package-map into NODE_OPTIONS.
    • Introduced node-experimental-package-map and node-package-map-type (standard/loose) across install, pnpm run, recursive execution, headless install, and to-pnp output.
  • Bug Fixes

    • Improved Yarn v2 lockfile import compatibility by updating Yarn v2 parsing to use js-yaml.
  • Tests

    • Added runtime and generation coverage for .package-map.json, standard vs loose behavior, and lifecycle/exec environment injection.

Copilot AI review requested due to automatic review settings June 15, 2026 15:52
@arcanis arcanis requested a review from zkochan as a code owner June 15, 2026 15:52
@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

This PR adds support for generating Node.js experimental package maps (node_modules/.package-map.json) for both isolated and hoisted installs, controlled by two new config options (node-experimental-package-map and node-package-map-type: standard|loose). The implementation spans TypeScript (pnpm) and Rust (pacquet) codebases, including config contracts, lockfile/graph converters, lifecycle environment injection, and install pipeline integration. A separate fix replaces Yarn v2 lockfile parsing from @yarnpkg/parsers to js-yaml.

Changes

Node.js Package Map Feature

Layer / File(s) Summary
TypeScript config contracts and CLI registration
config/reader/src/Config.ts, config/reader/src/configFileKey.ts, config/reader/src/index.ts, config/reader/src/types.ts, installing/commands/src/add.ts, installing/commands/src/install.ts, installing/commands/src/remove.ts, installing/commands/src/update/index.ts, installing/deps-installer/src/install/extendInstallOptions.ts, .changeset/package-maps.md
Adds nodeExperimentalPackageMap and nodePackageMapType to the TypeScript Config interface, config key sets, default values, type definitions, and rcOptionsTypes() of all installer commands; sets defaults to false and 'standard'.
Rust config enum and settings ingestion
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, pacquet/crates/config/src/tests.rs, pacquet/crates/config/src/workspace_yaml/tests.rs, pacquet/crates/config/src/env_overlay/tests.rs
Introduces NodePackageMapType enum and two Config fields in Rust; wires parsing from pnpm-workspace.yaml and PNPM_CONFIG_* env vars; adds parity assertions and Config::current load tests.
NODE_OPTIONS helper behavior and tests
exec/lifecycle/src/index.ts, exec/lifecycle/test/index.ts
Updates makeNodeRequireOption to accept an optional env, adds makeNodePackageMapOption that strips existing --experimental-package-map flags and appends a correctly quoted new one, and adds quotePathIfNeeded/removeNodePackageMapOption helpers; full unit test coverage including quoting, escaping, and replacement edge cases.
TypeScript packageMap conversion module
lockfile/to-pnp/src/packageMap.ts, lockfile/to-pnp/src/index.ts, lockfile/to-pnp/test/index.ts
Implements lockfileToPackageMap and dependenciesGraphToPackageMap with link resolution, relative URL building, loose-mode physical ancestor expansion, and async writers; re-exports the public API; tests cover aliased deps, link: deps, loose mode, Windows cross-drive paths, and path-based IDs.
TypeScript installer/restorer package-map wiring
installing/deps-installer/src/install/index.ts, installing/deps-restorer/src/index.ts, installing/deps-installer/test/install/misc.ts, installing/deps-installer/test/install/dedupeDirectDeps.ts, installing/deps-installer/test/lockfile.ts
Gates writePackageMap on isolated/non-PnP linker and modules enabled; injects makeNodePackageMapOption into lifecycle extraEnv for both dependency builds and top-level hooks; updates test expectations for .package-map.json presence; adds Node 27+ gated smoke tests for standard vs loose resolution.
TypeScript exec/run command environment flow
exec/commands/src/exec.ts, exec/commands/src/run.ts, exec/commands/src/runRecursive.ts, exec/commands/test/exec.ts
Updates exec, run, and recursive run to detect workspace/per-package .package-map.json, register new config option types, and merge package-map and PnP NODE_OPTIONS into child-process environments; adds exec command test for PnP + NODE_OPTIONS merging.
Rust package_map module and API exposure
pacquet/crates/package-manager/src/package_map.rs, pacquet/crates/package-manager/src/lib.rs, pacquet/crates/package-manager/Cargo.toml, pacquet/crates/package-manager/src/package_map/tests.rs
Introduces the Rust package_map module with PackageMap/PackageMapPackage structs, lockfile/graph converters, writers, make_node_package_map_option/package_map_path_for_execution helpers, and quote-aware NODE_OPTIONS tokenization; adds serde dependency; extensive unit tests for JSON output, URL helpers, and quoting.
Rust install/frozen/fresh pipeline integration and build env
pacquet/crates/package-manager/src/install.rs, pacquet/crates/package-manager/src/install_frozen_lockfile.rs, pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs, pacquet/crates/package-manager/src/build_modules.rs, pacquet/crates/package-manager/src/build_modules/tests.rs, pacquet/crates/package-manager/src/install/tests.rs, pacquet/crates/package-manager/src/install_package_from_registry/tests.rs
Threads project_manifests through frozen/hoisted flows; writes .package-map.json at install time for isolated and hoisted linkers; builds extra_env with NODE_OPTIONS for lifecycle hooks; adds WritePackageMap error variants; extends BuildModules with caller-supplied extra_env field.
Rust CLI execution wiring and hoisted runtime tests
pacquet/crates/cli/src/cli_args/exec.rs, pacquet/crates/cli/src/cli_args/run.rs, pacquet/crates/cli/src/cli_args/run/recursive.rs, pacquet/crates/cli/tests/hoisted_node_linker.rs
Updates Rust CLI exec/run/recursive commands to rewrite NODE_OPTIONS with make_node_package_map_option when a package-map path is found; adds Node ≥27 gated end-to-end tests asserting declared deps resolve in standard mode, undeclared deps fail, and undeclared deps succeed in loose mode.

Yarn v2 Lockfile Parsing Fix

Layer / File(s) Summary
js-yaml-based Yarn lock parsing and dependency updates
installing/commands/src/import/index.ts, installing/commands/package.json, pnpm-workspace.yaml, .changeset/fix-yarn-import-js-yaml.md
Replaces @yarnpkg/parsers' parseSyml with js-yaml FAILSAFE_SCHEMA parsing in parseYarn2Lock; adds shape validation with YARN_LOCKFILE_PARSE_FAILED error; updates package dependencies and workspace catalog.

Sequence Diagram(s)

sequenceDiagram
  participant CLI as pnpm install/run/exec
  participant Installer as deps-installer / deps-restorer
  participant pkgMap as packageMap (lockfileToPackageMap)
  participant lifecycle as exec.lifecycle (makeNodePackageMapOption)
  participant NodeProc as Node.js child process

  CLI->>Installer: install(opts: nodeExperimentalPackageMap, nodePackageMapType)
  Installer->>pkgMap: writePackageMap(lockfile, opts)
  pkgMap-->>Installer: writes node_modules/.package-map.json
  Installer->>lifecycle: makeNodePackageMapOption(packageMapPath, extraEnv)
  lifecycle-->>Installer: NODE_OPTIONS with --experimental-package-map
  Installer->>NodeProc: spawn lifecycle script with updated NODE_OPTIONS
  NodeProc-->>Installer: exit
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • pnpm/pnpm#11816: The main PR's node package-map generation/injection is added to pacquet's fresh-install pipeline (notably pacquet/crates/package-manager/src/install.rs and install_with_fresh_lockfile.rs), which is exactly the area refactored in #11816 to write pnpm-lock.yaml/<vsd>/lock.yaml—so they're code-level related.
  • pnpm/pnpm#12041: Both PRs touch the pacquet/crates/package-manager hoisted nodeLinker install pipeline (install_frozen_lockfile.rs / install_with_fresh_lockfile.rs)—main PR injects experimental Node .package-map config into NODE_OPTIONS and writes .package-map.json, while retrieved PR refactors hoisted fresh installs and adds hoistingLimits, making the changes intersect at the same hoisted-install execution points.
  • pnpm/pnpm#12436: Both PRs modify pacquet's package-manager install build/lifecycle phase plumbing in install_frozen_lockfile.rs/install_with_fresh_lockfile.rs (via shared run_build_phase/build-phase inputs): the retrieved PR focuses on strictDepBuilds/ignored dependency builds, while the main PR injects Node package-map configuration into the lifecycle NODE_OPTIONS through those same execution paths.

Suggested reviewers

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

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

❤️ Share

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds support for generating and optionally injecting Node.js package maps (node_modules/.package-map.json) so Node can resolve dependencies using --experimental-package-map during pnpm-managed installs and script executions.

Changes:

  • Generate .package-map.json during isolated/hoisted installs (Rust + TS implementations).
  • Add config/settings + CLI wiring for node-experimental-package-map and node-package-map-type.
  • Inject --experimental-package-map=... into Node script environments when enabled.

Reviewed changes

Copilot reviewed 24 out of 25 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
pacquet/crates/package-manager/src/package_map.rs New Rust package-map generator + serializer + unit tests
pacquet/crates/package-manager/src/install.rs Writes .package-map.json during isolated installs
pacquet/crates/package-manager/src/lib.rs Exposes package_map module + error type
pacquet/crates/package-manager/Cargo.toml Adds serde dependency for serialization derives
lockfile/to-pnp/src/packageMap.ts New TS package-map generation + writer utilities (incl. “loose” mode)
lockfile/to-pnp/src/index.ts Exports new package-map APIs
lockfile/to-pnp/test/index.ts Adds tests for lockfileToPackageMap + graph-based variant
installing/deps-restorer/src/index.ts Writes package map and injects env option for lifecycle scripts
installing/deps-installer/src/install/index.ts Writes package map after install + injects env option for scripts
installing/deps-installer/src/install/extendInstallOptions.ts Adds install options for experimental package map + type
installing/deps-installer/test/install/misc.ts E2E coverage for package map creation + Node 27+ runtime smoke tests
exec/lifecycle/src/index.ts Adds makeNodePackageMapOption() helper
exec/commands/src/{run.ts,runRecursive.ts,exec.ts} Injects --experimental-package-map into script env when enabled
config/reader/src/{types.ts,index.ts,configFileKey.ts,Config.ts} Adds config schema + defaults for new settings
installing/commands/src/{add.ts,install.ts,remove.ts,update/index.ts} Wires new rc options into command option typing
.changeset/package-maps.md Changeset entry documenting new feature + settings

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pacquet/crates/package-manager/src/package_map.rs
Comment thread pacquet/crates/package-manager/src/package_map.rs
Comment thread pacquet/crates/package-manager/src/package_map.rs
Comment thread lockfile/to-pnp/src/packageMap.ts
Comment thread exec/lifecycle/src/index.ts
Comment thread exec/lifecycle/src/index.ts
@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 (17) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Pacquet map write clobbers symlinks 🐞 Bug ⛨ Security
Description
pacquet writes .package-map.json via std::fs::write, which can follow a pre-existing symlink at
that path and truncate/overwrite the symlink target. This reintroduces a file-clobber primitive that
pacquet explicitly avoids for other critical files via hardened atomic writers.
Code

pacquet/crates/package-manager/src/package_map.rs[R58-77]

+    std::fs::create_dir_all(opts.modules_dir).map_err(WritePackageMapError::CreateDir)?;
+    let mut contents = serde_json::to_vec_pretty(&lockfile_to_package_map(lockfile, opts))
+        .map_err(WritePackageMapError::Serialize)?;
+    contents.push(b'\n');
+    std::fs::write(opts.modules_dir.join(PACKAGE_MAP_FILENAME), contents)
+        .map_err(WritePackageMapError::Write)
+}
+
+pub(crate) fn write_hoisted_package_map(
+    lockfile: &Lockfile,
+    graph: &LockfileToDepGraphResult,
+    opts: &HoistedPackageMapOptions<'_>,
+) -> Result<(), WritePackageMapError> {
+    std::fs::create_dir_all(opts.modules_dir).map_err(WritePackageMapError::CreateDir)?;
+    let mut contents =
+        serde_json::to_vec_pretty(&dependencies_graph_to_package_map(lockfile, graph, opts))
+            .map_err(WritePackageMapError::Serialize)?;
+    contents.push(b'\n');
+    std::fs::write(opts.modules_dir.join(PACKAGE_MAP_FILENAME), contents)
+        .map_err(WritePackageMapError::Write)
Evidence
The package map writer uses std::fs::write directly to the final filename. In contrast, pacquet’s
lockfile writer explicitly avoids direct create+truncate because it can follow symlinks/truncate
attacker-seeded paths, and instead uses a hardened atomic write strategy.

pacquet/crates/package-manager/src/package_map.rs[54-78]
pacquet/crates/lockfile/src/save_lockfile.rs[142-152]
pacquet/crates/package-manager/src/package_map.rs[371-390]

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

## Issue description
`pacquet/crates/package-manager/src/package_map.rs` writes `.package-map.json` using `std::fs::write(...)`. Direct writes can follow/truncate symlinks and expose a file-clobber vector if an attacker-controlled workspace can pre-seed `<modulesDir>/.package-map.json` as a symlink.
### Issue Context
The pacquet lockfile writer already documents and implements hardened atomic writes specifically to avoid following symlinks / truncating attacker-seeded paths.
### Fix Focus Areas
- Replace `std::fs::write(...)` with a temp-file + rename atomic write.
- Prefer reusing existing hardened helpers (e.g. `pacquet_fs::ensure_file::write_atomic`, or factor a shared helper analogous to the lockfile `write_atomic`).
- pacquet/crates/package-manager/src/package_map.rs[54-78]
- pacquet/crates/lockfile/src/save_lockfile.rs[142-152]

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


2. Unquoted require path 🐞 Bug ≡ Correctness
Description
makeNodeRequireOption() appends --require=${modulePath} into NODE_OPTIONS without
quoting/escaping, so paths with whitespace/quotes/backslashes can be split by Node’s NODE_OPTIONS
tokenizer and break PnP/lifecycle execution. This is especially likely on Windows or when projects
live under directories with spaces.
Code

exec/lifecycle/src/index.ts[R6-9]

+export function makeNodeRequireOption (modulePath: string, env?: Record<string, string | undefined>): { NODE_OPTIONS: string } {
+  let { NODE_OPTIONS } = env ?? process.env
+  NODE_OPTIONS = `${NODE_OPTIONS ?? process.env.NODE_OPTIONS ?? ''} --require=${modulePath}`.trim()
return { NODE_OPTIONS }
Evidence
The file itself documents that NODE_OPTIONS splits on whitespace and treats quotes/backslashes
specially, but makeNodeRequireOption() still appends the raw module path. Install/build flows call
it with .pnp.cjs paths derived from user-controlled filesystem locations, which commonly include
spaces (e.g., Windows user profile paths), so the breakage is practical.

exec/lifecycle/src/index.ts[6-26]
installing/deps-installer/src/install/index.ts[1777-1788]

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

## Issue description
`makeNodeRequireOption()` builds `NODE_OPTIONS` using an unquoted `--require=${modulePath}`. Node’s `NODE_OPTIONS` tokenization (which this PR explicitly documents) will mis-parse paths containing whitespace, quotes, or backslashes, causing `--require` to receive a truncated value and potentially making lifecycle/exec flows fail.
## Issue Context
This PR added robust quoting for `--experimental-package-map=...` via `quotePathIfNeeded()`, but `--require=...` is still built without using the same quoting/escaping logic.
## Fix Focus Areas
- exec/lifecycle/src/index.ts[6-26]

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


3. Pacquet modulesDir ignored 🐞 Bug ≡ Correctness
Description
package_map_path_for_execution() always checks /node_modules/.package-map.json instead of using
the configured config.modules_dir, so pacquet exec/run won’t inject
--experimental-package-map when modulesDir is not the default. Installs write the map to
config.modules_dir, so this becomes a silent feature break for non-standard layouts.
Code

pacquet/crates/package-manager/src/package_map.rs[R373-388]

+pub fn package_map_path_for_execution(config: &Config, dir: &Path) -> Option<PathBuf> {
+    if !config.node_experimental_package_map {
+        return None;
+    }
+    let workspace_path = config
+        .workspace_dir
+        .as_ref()
+        .map(|dir| dir.join("node_modules").join(PACKAGE_MAP_FILENAME));
+    if let Some(path) = workspace_path
+        && path.exists()
+    {
+        return Some(path);
+    }
+    let path = dir.join("node_modules").join(PACKAGE_MAP_FILENAME);
+    path.exists().then_some(path)
+}
Evidence
The install code writes .package-map.json into config.modules_dir, but the execution-time lookup
is hard-coded to node_modules, and the CLI exec/run code depends on that lookup to inject the
NODE_OPTIONS flag.

pacquet/crates/package-manager/src/install.rs[1468-1483]
pacquet/crates/package-manager/src/package_map.rs[373-388]
pacquet/crates/cli/src/cli_args/exec.rs[184-196]

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

## Issue description
`package_map_path_for_execution()` hard-codes `node_modules/.package-map.json` when searching for the package map, but installs write the map under `config.modules_dir`. This causes `pacquet exec`/`run` to skip injecting `--experimental-package-map` whenever `modulesDir` is configured to something other than `node_modules`.
## Issue Context
- Install path writes `PACKAGE_MAP_FILENAME` into `config.modules_dir`.
- Exec/run paths now rely on `package_map_path_for_execution()` to detect the map and decide whether to add `NODE_OPTIONS`.
## Fix Focus Areas
- pacquet/crates/package-manager/src/package_map.rs[373-388]
- pacquet/crates/package-manager/src/install.rs[1468-1483]
- pacquet/crates/cli/src/cli_args/exec.rs[184-196]
## Suggested fix
Update `package_map_path_for_execution()` to primarily check `config.modules_dir.join(PACKAGE_MAP_FILENAME)` (the actual install location). If you still want a per-invocation-dir fallback, compute it using the configured modules-dir basename (not the hard-coded `node_modules`).

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


View more (3)
4. Package-map prototype pollution 🐞 Bug ⛨ Security
Description
lockfileToPackageMap() / dependenciesGraphToPackageMap() build the packages table as a plain
object and assign entries using computed IDs (importerId / path-derived link IDs). If an ID
becomes __proto__ (e.g. project dir or link: target named __proto__), the assignment mutates
the object prototype, corrupting .package-map.json generation and possibly causing downstream
failures when Node uses the map.
Code

lockfile/to-pnp/src/packageMap.ts[R79-96]

+  const packages: PackageMap['packages'] = {}
+  const packageLocationsByModulesDir = isLoose ? new Map<string, Map<string, string>>() : undefined
+  const packageDirsById = isLoose ? new Map<string, string>() : undefined
+
+  const addPackage = (id: string, packageDir: string, dependencies: Map<string, string>) => {
+    packageDirsById?.set(id, packageDir)
+    packages[id] = {
+      url: toRelativeUrl(opts.rootModulesDir, packageDir),
+      dependencies: Object.fromEntries(Array.from(dependencies).sort(([a], [b]) => compareStrings(a, b))),
+    }
+  }
+
+  const addExternalLinkPackage = (target: LinkTarget) => {
+    packages[target.id] ??= {
+      url: toRelativeUrl(opts.rootModulesDir, target.dir),
+      dependencies: {},
+    }
+  }
Evidence
The generator uses a plain object for packages and assigns into it with computed IDs, while
resolveLinkTarget() derives IDs from relative filesystem paths without sanitizing reserved keys;
this can yield __proto__ and trigger prototype mutation. The affected code path is executed during
install when writing node_modules/.package-map.json.

lockfile/to-pnp/src/packageMap.ts[74-103]
lockfile/to-pnp/src/packageMap.ts[333-349]
installing/deps-installer/src/install/index.ts[1656-1667]
installing/deps-restorer/src/index.ts[545-570]

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 new package-map generator stores entries in a plain `{}` and writes entries via `packages[id] = ...` and `packages[target.id] ??= ...`. If `id` is `__proto__` (possible from filesystem-derived importer IDs or `link:` target IDs), JS treats it as a special accessor on normal objects and mutates the prototype instead of creating a regular key, corrupting the generated package map.
### Issue Context
This code runs during installs to write `node_modules/.package-map.json`, so a repository/workspace layout (or lockfile `link:` entries) that yields `__proto__` can break package-map generation.
### Fix Focus Areas
- lockfile/to-pnp/src/packageMap.ts[78-103]
- lockfile/to-pnp/src/packageMap.ts[216-229]
### How to fix
- Replace `const packages = {}` with `const packages = Object.create(null) as PackageMap['packages']` (and similarly any other object that may be keyed by attacker-controlled/path-derived IDs).
- Avoid using `??=` on plain objects for such keys unless the container is null-prototype (or switch to `Map<string, PackageMapPackage>` and serialize at the end).
- Ensure any nested objects that might receive computed keys are also null-prototype if applicable.

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


5. Yarn.lock prototype pollution 🐞 Bug ⛨ Security
Description
parseYarn2Yaml() loads attacker-controlled yarn.lock YAML into a plain object and
parseYarn2Lock() iterates it with for..in while assigning into a plain {} map, which allows
__proto__/prototype-chain tricks to inject inherited enumerable keys and/or prototype-pollute
dependencies during pnpm import. This can lead to unexpected dependency entries or crashes/DoS
when importing a malicious repository.
Code

installing/commands/src/import/index.ts[R243-253]

+function parseYarn2Yaml (lockFileContents: string): YarnLockYaml {
+  const parseYarnLock = yaml.load(lockFileContents, {
+    schema: yaml.FAILSAFE_SCHEMA,
+    json: true,
+  })
+  if (parseYarnLock == null) return {}
+  if (typeof parseYarnLock !== 'object' || Array.isArray(parseYarnLock)) {
+    throw new PnpmError('YARN_LOCKFILE_PARSE_FAILED', `Expected an indexed object, got a ${Array.isArray(parseYarnLock) ? 'array' : typeof parseYarnLock} instead. Does your file follow Yaml's rules?`)
+  }
+  return parseYarnLock as YarnLockYaml
+}
Evidence
The new YAML parsing returns a plain object from yaml.load(), and the existing loop uses for..in
(which can enumerate inherited enumerable properties) while writing into a plain {} map (which is
vulnerable to __proto__ key assignment semantics).

installing/commands/src/import/index.ts[219-236]
installing/commands/src/import/index.ts[243-253]

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

## Issue description
`parseYarn2Yaml()` uses `js-yaml` to load untrusted YAML into a normal JS object, and `parseYarn2Lock()` then uses `for..in` and writes into a plain `{}`. A crafted `yarn.lock` can exploit `__proto__`-style prototype pollution to introduce inherited enumerable keys into the iteration and/or mutate the prototype of the destination map.
### Issue Context
This is on the `pnpm import` path, where the repository’s `yarn.lock` should be treated as attacker-controlled input.
### Fix Focus Areas
- installing/commands/src/import/index.ts[219-253]
### Suggested fix approach
- Avoid `for..in` over the parsed YAML object; iterate only own keys via `Object.keys()` / `Object.entries()`.
- Build the output map with a null prototype to prevent `__proto__` assignment from mutating prototypes:
- `const dependencies = Object.create(null) as YarnPackageLock`
- After `yaml.load`, defensively copy only own enumerable properties into a null-prototype object (or otherwise ensure the returned structure is not prototype-pollutable) before further processing.
- Optionally, reject/ignore dangerous keys like `__proto__`, `prototype`, and `constructor` explicitly when copying/iterating.

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


6. Unsafe NODE_OPTIONS quoting 🐞 Bug ⛨ Security
Description
makeNodePackageMapOption()/make_node_package_map_option() only quotes the package-map path when
whitespace is present, so paths containing quotes can be misparsed by Node’s NODE_OPTIONS
tokenizer, potentially altering/breaking the intended flags when node-experimental-package-map is
enabled under exec/run flows.
Code

exec/lifecycle/src/index.ts[R12-20]

+export function makeNodePackageMapOption (packageMapPath: string, env?: Record<string, string | undefined>): { NODE_OPTIONS: string } {
+  let { NODE_OPTIONS } = env ?? process.env
+  NODE_OPTIONS = `${removeNodePackageMapOption(NODE_OPTIONS ?? process.env.NODE_OPTIONS ?? '')} --experimental-package-map=${quotePathIfNeeded(packageMapPath)}`.trim()
+  return { NODE_OPTIONS }
+}
+
+function quotePathIfNeeded (path: string): string {
+  return /\s/.test(path) ? JSON.stringify(path) : path
+}
Evidence
The implementations in both TS and Rust use a quoting helper (quotePathIfNeeded() /
quote_path_if_needed()) that returns the raw path string unless whitespace exists, and the
formatted --experimental-package-map= value is then directly concatenated into a reconstructed
NODE_OPTIONS string. Because the path is sourced from the filesystem (e.g., via existsInDir()
and workspace/project directory + node_modules/.package-map.json) and forwarded unmodified into
the option builder, any embedded quotes in the path can affect how Node tokenizes NODE_OPTIONS,
leading to unexpected argument boundaries or malformed quoting.

exec/lifecycle/src/index.ts[12-20]
exec/commands/src/run.ts[291-307]
pacquet/crates/package-manager/src/package_map.rs[362-370]
pacquet/crates/package-manager/src/package_map.rs[802-808]

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

## Issue description
`makeNodePackageMapOption()` (TS) and `make_node_package_map_option()` (Rust) build `NODE_OPTIONS` by string concatenation and only quote `packageMapPath` when it contains whitespace. If the path contains quotes (valid on POSIX), Node’s parsing of `NODE_OPTIONS` may treat the value as multiple arguments or malformed quoting, potentially changing effective runtime flags or failing Node startup.
## Issue Context
The package map path is derived from the filesystem (workspace/project directory + `node_modules/.package-map.json`, e.g. via `existsInDir()`) and is forwarded into `NODE_OPTIONS` for `pnpm run/exec` and pacquet CLI execution flows when `node_experimental_package_map` is enabled and `NODE_OPTIONS` is reconstructed from config/environment.
## Fix Focus Areas
- exec/lifecycle/src/index.ts[12-20]
- pacquet/crates/package-manager/src/package_map.rs[362-370]
- pacquet/crates/package-manager/src/package_map.rs[802-808]

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



Remediation recommended

7. Redundant map sorting 🐞 Bug ➹ Performance
Description
lockfileToPackageMap() and dependenciesGraphToPackageMap() perform multiple layers of sorting
(importers/packages, each dependency object, and again when serializing Maps), which is redundant
and adds avoidable CPU+allocations when generating .package-map.json during installs. This
overhead scales with lockfile size and dependency fanout and runs in the common
nodeLinker=isolated install path where the map is written.
Code

lockfile/to-pnp/src/packageMap.ts[R93-105]

+  const addPackage = (id: string, packageDir: string, dependencies: Map<string, string>) => {
+    packageDirsById?.set(id, packageDir)
+    packages[id] = {
+      url: toRelativeUrl(opts.rootModulesDir, packageDir),
+      dependencies: Object.fromEntries(Array.from(dependencies).sort(([a], [b]) => compareStrings(a, b))),
+    }
+  }
+
+  const addExternalLinkPackage = (target: LinkTarget) => {
+    packages[target.id] ??= {
+      url: toRelativeUrl(opts.rootModulesDir, target.dir),
+      dependencies: {},
+    }
Evidence
The writer sorts during traversal (Object.entries(...).sort(...)) and then sorts again when
serializing the dependencies Map to an object, causing redundant O(n log n) work per package.
.package-map.json is written in the default install path (nodeLinker=isolated) so this extra
work lands on a common hot path.

lockfile/to-pnp/src/packageMap.ts[93-105]
lockfile/to-pnp/src/packageMap.ts[120-140]
lockfile/to-pnp/src/packageMap.ts[171-180]
installing/deps-installer/src/install/index.ts[1688-1748]

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 package-map writer sorts the same data multiple times (e.g., sorting `Object.entries(...).sort(...)` during traversal, then sorting again when converting `Map` -> object). This increases CPU time and allocations on the install hot path where `.package-map.json` is generated.
### Issue Context
The map generator already enforces deterministic iteration order in multiple places, so some later sorts are redundant. You can keep determinism while removing redundant sorting by picking a single place to sort (either at traversal time or at serialization time), but not both.
### Fix Focus Areas
- lockfile/to-pnp/src/packageMap.ts[93-105]
- lockfile/to-pnp/src/packageMap.ts[120-140]
- lockfile/to-pnp/src/packageMap.ts[171-180]
- lockfile/to-pnp/src/packageMap.ts[255-268]
- lockfile/to-pnp/src/packageMap.ts[298-315]
Suggested approach:
- Keep a single sort layer:
- Either remove `.sort(...)` from the `Object.entries(...).sort(...)` loops and keep sorting only in `serializeDependencies`/final serialization, **or**
- Keep sorted traversal but remove `Array.from(dependencies).sort(...)` before `Object.fromEntries`.
- Consider also removing the importer/package outer-loop sorts if you still sort the final `packages` object (since final sorting already normalizes output order).

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


8. Malformed importer self key 🐞 Bug ≡ Correctness
Description
Package-map generation falls back to using the importer id when a project has no manifest.name,
which can produce non-package-name dependency keys (e.g. . or a path) in .package-map.json. This
makes the map internally inconsistent (the "self" mapping isn’t a real package name) and can
confuse/violate consumers that expect dependency keys to be actual package names.
Code

installing/deps-installer/src/install/index.ts[R1733-1735]

+      const importerNames = Object.fromEntries(
+        projects.map(({ manifest, id }) => [id, manifest.name ?? id])
+      )
Evidence
The PR-added package-map writer constructs importerNames using manifest.name ?? id, but name
is explicitly optional in manifest types. The package-map builder only adds the self-mapping when
the importer name is truthy, and pacquet’s analogous implementation stores Option and skips
missing names instead of using ids, indicating the intended semantics are “omit when unnamed”.

installing/deps-installer/src/install/index.ts[1732-1736]
installing/deps-restorer/src/index.ts[545-549]
core/types/src/package.ts[90-93]
lockfile/to-pnp/src/packageMap.ts[120-125]
pacquet/crates/package-manager/src/package_map.rs[689-702]

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

## Issue description
Package-map generation computes `importerNames` via `manifest.name ?? id`. Because `name` is optional in manifests, this can write non-package-name keys into `.package-map.json` (like `.` or workspace-relative paths) when a project omits `name`.
### Issue Context
`lockfileToPackageMap()` only intends to add the importer self-mapping when a real package name exists; other parts of the repo (pacquet) also treat missing names as “omit”.
### Fix Focus Areas
- installing/deps-installer/src/install/index.ts[1732-1736]
- installing/deps-restorer/src/index.ts[545-549]
### Suggested change
- Build `importerNames` without falling back to `id` (e.g. `[id, manifest.name]`). This ensures unnamed projects don’t emit invalid/non-package-name dependency keys in the generated package map.

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


9. Windows link URL wrong 🐞 Bug ≡ Correctness
Description
toRelativeUrl() selects path.win32 if either input looks Windows-absolute, then computes a
relative path even when from is a POSIX path (e.g. /repo/node_modules) and to is a
Windows-absolute link target (e.g. C:\x). This can emit an invalid relative URL instead of a
file:// URL, breaking package-map resolution for cross-platform lockfiles containing
Windows-absolute link: refs.
Code

lockfile/to-pnp/src/packageMap.ts[R366-371]

+function toRelativeUrl (from: string, to: string): string {
+  const pathUtils = getPathUtils(from, to)
+  const relative = pathUtils.relative(from, to)
+  if (pathUtils.isAbsolute(relative)) {
+    return pathToFileURL(to, { windows: pathUtils === path.win32 }).href
+  }
Evidence
The code intentionally supports Windows-absolute link: targets (even when running on POSIX), then
later computes URLs using a path utility chosen from the pair (from,to), which can mix POSIX
from with Windows to and produce an incorrect non-absolute relative result (therefore skipping
the file:// fallback).

lockfile/to-pnp/src/packageMap.ts[349-364]
lockfile/to-pnp/src/packageMap.ts[366-377]
lockfile/to-pnp/src/packageMap.ts[460-466]

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

## Issue description
`toRelativeUrl(from, to)` calls `getPathUtils(from, to)` and may end up using `path.win32.relative()` with a POSIX `from` and a Windows-absolute `to`. In that mixed case, `relative` can be a non-absolute string, so the code returns a bogus relative URL rather than a `file://` URL.
## Issue Context
`resolveLinkTarget()` explicitly supports Windows-absolute `link:` targets even on POSIX (it detects path flavor from `linkPath`). That makes it realistic for `toRelativeUrl()` to receive POSIX `rootModulesDir` + Windows-absolute `target.dir`.
## Fix Focus Areas
- lockfile/to-pnp/src/packageMap.ts[366-377]
- lockfile/to-pnp/src/packageMap.ts[460-466]
### Suggested fix
- In `toRelativeUrl()`, add an explicit mixed-flavor guard:
- If `to` is Windows-absolute and `from` is not Windows-absolute, return `pathToFileURL(to, { windows: true }).href` (skip `relative()` entirely).
- Symmetrically, if `from` is Windows-absolute and `to` is POSIX-absolute, return `pathToFileURL(to).href`.
- Alternatively, adjust `getPathUtils()` to avoid using `path.win32` when only *some* paths are Windows-absolute and others are POSIX paths, and force absolute-URL emission in such mismatched cases.

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


View more (8)
10. Non-atomic map file write 🐞 Bug ☼ Reliability
Description
writePackageMap() / writePackageMapFromDependenciesGraph() persist .package-map.json via
fs.writeFile, so an interrupted install or concurrent reader can observe truncated/invalid JSON
and break Node startup when --experimental-package-map is used. This is inconsistent with pnpm’s
existing practice of atomic writes for on-disk contracts (e.g. lockfiles).
Code

lockfile/to-pnp/src/packageMap.ts[R58-78]

+export async function writePackageMap (
+  lockfile: LockfileObject,
+  opts: PackageMapOptions
+): Promise<void> {
+  await fs.mkdir(opts.rootModulesDir, { recursive: true })
+  await fs.writeFile(
+    path.join(opts.rootModulesDir, PACKAGE_MAP_FILENAME),
+    `${JSON.stringify(lockfileToPackageMap(lockfile, opts), null, 2)}\n`,
+    'utf8'
+  )
+}
+
+export async function writePackageMapFromDependenciesGraph (
+  opts: DependenciesGraphPackageMapOptions
+): Promise<void> {
+  await fs.mkdir(opts.rootModulesDir, { recursive: true })
+  await fs.writeFile(
+    path.join(opts.rootModulesDir, PACKAGE_MAP_FILENAME),
+    `${JSON.stringify(dependenciesGraphToPackageMap(opts), null, 2)}\n`,
+    'utf8'
+  )
Evidence
The new package map writer uses a direct write to the final path, while existing lockfile writers in
this repo use atomic-write helpers to avoid torn/corrupt files on disk.

lockfile/to-pnp/src/packageMap.ts[58-78]
lockfile/fs/src/write.ts[58-83]

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

## Issue description
`lockfile/to-pnp/src/packageMap.ts` writes `.package-map.json` using `fs.writeFile(...)`, which can leave a partially-written JSON file if the process is interrupted or if another process reads while the write is in progress.
### Issue Context
The repo already treats key on-disk outputs as atomic-write-only (e.g. lockfile writers use `write-file-atomic`), which avoids torn reads and corrupted state.
### Fix Focus Areas
- Use an atomic temp-file + rename write for `.package-map.json` (likely `write-file-atomic`, already used elsewhere).
- Keep the output bytes identical (including trailing `\n`) to avoid churn.
- lockfile/to-pnp/src/packageMap.ts[58-79]
- lockfile/fs/src/write.ts[58-83]

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


11. Unneeded depPath location map 🐞 Bug ➹ Performance
Description
The installers always materialize locationByDepPath by iterating the whole dependency graph even
though package-map generation can derive package locations from depPath for the normal local virtual
store case. This adds avoidable extra CPU and allocations on the install hot path when
enableGlobalVirtualStore is not used.
Code

installing/deps-installer/src/install/index.ts[R1736-1741]

+      await writePackageMap(result.currentLockfile, {
+        importerNames,
+        lockfileDir: ctx.lockfileDir,
+        locationByDepPath: Object.fromEntries(
+          Object.values(dependenciesGraph).map((node) => [node.depPath, node.dir])
+        ),
Evidence
PackageMapOptions.locationByDepPath is documented as needed only for the global virtual store and
the writer explicitly falls back when it’s missing, yet deps-installer always builds the full map
from the dependencies graph before calling writePackageMap(). This makes the extra
traversal/object creation avoidable in the local virtual store case.

lockfile/to-pnp/src/packageMap.ts[24-38]
lockfile/to-pnp/src/packageMap.ts[138-145]
installing/deps-installer/src/install/index.ts[1732-1746]

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

## Issue description
`locationByDepPath` is always computed via `Object.fromEntries(Object.values(dependenciesGraph)...`, which is a full traversal + allocation of a large object. The package-map writer already falls back to the standard `<virtualStoreDir>/<depPathToFilename>/node_modules/<name>` layout when `locationByDepPath` is absent.
## Issue Context
`locationByDepPath` is documented as required only for the global virtual store (content-hashed locations). For the common local virtual store, passing it is redundant.
## Fix Focus Areas
- installing/deps-installer/src/install/index.ts[1732-1746]
- lockfile/to-pnp/src/packageMap.ts[24-38]
- lockfile/to-pnp/src/packageMap.ts[138-145]

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


12. Windows link path misparsed 🐞 Bug ≡ Correctness
Description
resolveLinkTarget() selects POSIX vs Win32 path handling using the full ref string (which includes
the link: prefix), so Windows-absolute refs like link:C:\... won’t be detected as Win32 on POSIX
and are treated as relative paths. This can generate incorrect package-map IDs/URLs for such link
targets, breaking package-map resolution for cross-platform lockfiles that contain Windows-absolute
link: entries.
Code

lockfile/to-pnp/src/packageMap.ts[R342-348]

+function resolveLinkTarget (lockfileDir: string, importerId: string | undefined, ref: string): LinkTarget {
+  const pathUtils = getPathUtils(lockfileDir, ref)
+  const importerDir = pathUtils.resolve(lockfileDir, importerId ?? '.')
+  const linkPath = ref.slice(5)
+  const dir = pathUtils.isAbsolute(linkPath)
+    ? linkPath
+    : pathUtils.resolve(importerDir, linkPath)
Evidence
The code selects the path implementation using ref (which includes the link: prefix), but
Windows-absolute detection is anchored to the beginning of the string; therefore link:C:\... will
not be classified as Windows-absolute, and path.posix.isAbsolute('C:\...') will return false,
causing the Windows absolute to be resolved as a relative path.

lockfile/to-pnp/src/packageMap.ts[342-353]
lockfile/to-pnp/src/packageMap.ts[433-456]

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

## Issue description
`resolveLinkTarget()` calls `getPathUtils(lockfileDir, ref)` before stripping the `link:` prefix. On POSIX, a Windows-absolute `link:` ref like `link:C:\foo` will not match the Windows-absolute regexp (because it doesn’t start with a drive letter due to the `link:` prefix), so the code will use POSIX path semantics and incorrectly treat `C:\foo` as a relative path.
## Issue Context
This logic lives in the newly added package-map writer and affects how `link:` dependency targets are resolved into (id, dir) pairs.
## Fix Focus Areas
- lockfile/to-pnp/src/packageMap.ts[342-356]
- lockfile/to-pnp/src/packageMap.ts[433-456]
## Suggested fix
- In `resolveLinkTarget()`, compute `linkPath = ref.slice(5)` first.
- Call `getPathUtils(lockfileDir, linkPath)` (or include `importerDir` + `linkPath`) instead of passing the raw `ref`.
- Then use the returned `pathUtils` to evaluate `isAbsolute(linkPath)` and to resolve the path.

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


13. Escape mishandled in pacquet 🐞 Bug ≡ Correctness
Description
pacquet’s split_node_options() and the related removal logic do not correctly honor
backslash-escaped quotes, treating any " inside a quoted NODE_OPTIONS token as a real quote
terminator. This can cause remove_node_package_map_option()/removeNodePackageMapOption() to fail
to fully strip an existing --experimental-package-map flag and lead
make_node_package_map_option()/makeNodePackageMapOption() to emit malformed NODE_OPTIONS,
potentially breaking exec/run/lifecycle script execution and idempotent rebuilds.
Code

pacquet/crates/package-manager/src/package_map.rs[R777-804]

+fn split_node_options(node_options: &str) -> Vec<String> {
+    let mut tokens = Vec::new();
+    let mut token = String::new();
+    let mut quote: Option<char> = None;
+    for ch in node_options.chars() {
+        if let Some(q) = quote {
+            token.push(ch);
+            if ch == q {
+                quote = None;
+            }
+            continue;
+        }
+        if ch == '"' || ch == '\'' {
+            quote = Some(ch);
+            token.push(ch);
+        } else if ch.is_whitespace() {
+            if !token.is_empty() {
+                tokens.push(std::mem::take(&mut token));
+            }
+        } else {
+            token.push(ch);
+        }
+    }
+    if !token.is_empty() {
+        tokens.push(token);
+    }
+    tokens
+}
Evidence
quote_path_if_needed() can emit escaped quotes (\") when a path contains ", but
split_node_options() has no escape-handling logic and will mis-tokenize by terminating a quoted
token at the escaped quote, which directly undermines remove_node_package_map_option() and
corrupts the final NODE_OPTIONS output. Separately, the code notes that Node treats backslash as
an escape, yet the regex approach for removal uses a fragment like "[^"]*", which necessarily
stops at an escaped quote and therefore cannot reliably match/remove the entire quoted
--experimental-package-map token, leaving trailing remnants that get concatenated with the newly
generated flag.

pacquet/crates/package-manager/src/package_map.rs[756-818]
exec/lifecycle/src/index.ts[12-33]

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

## Issue description
`NODE_OPTIONS` manipulation is not robust when quoted tokens contain backslash-escaped quotes (e.g. `\"`): the tokenizer/removal logic can terminate tokens early, fail to fully remove an existing `--experimental-package-map` option, and then produce malformed `NODE_OPTIONS` when appending the new option.
## Issue Context
- `quote_path_if_needed()` can legitimately produce `\"` sequences inside a quoted `--experimental-package-map=...` token, so the tokenization/removal logic must correctly round-trip such values.
- The codebase documents that Node’s `NODE_OPTIONS` parsing treats `\` as an escape character; however, current logic either lacks escape handling during tokenization (`split_node_options()`) or uses a regex pattern that cannot span escaped quotes, making removal unreliable and potentially breaking exec/run/lifecycle scripts when `NODE_OPTIONS` includes such tokens or when rebuilding it idempotently.
## Fix Focus Areas
- pacquet/crates/package-manager/src/package_map.rs[756-818]
- exec/lifecycle/src/index.ts[28-32]

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


14. modulesDir map mismatch 🐞 Bug ≡ Correctness
Description
pnpm exec injects --experimental-package-map only when node_modules/.package-map.json exists,
but installs write the map to ctx.rootModulesDir, which is derived from the configurable
modulesDir option. With modulesDir != node_modules, the map is generated but the flag won’t be
injected, so the feature silently doesn’t work for exec/run flows.
Code

exec/commands/src/exec.ts[R226-227]

+  const existsPackageMap = existsInDir.bind(null, path.join('node_modules', '.package-map.json'))
+  const workspacePackageMapPath = opts.nodeExperimentalPackageMap && opts.workspaceDir && existsPackageMap(opts.workspaceDir)
Evidence
exec.ts looks for node_modules/.package-map.json, while the install pipeline writes to
ctx.rootModulesDir, and ctx.rootModulesDir is computed from opts.modulesDir (not always
node_modules). Therefore, when modulesDir differs, the runtime injection can’t find the
generated map.

exec/commands/src/exec.ts[223-227]
installing/read-projects-context/src/index.ts[49-55]
installing/deps-installer/src/install/index.ts[1656-1667]

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

## Issue description
`exec/commands` hard-codes `node_modules/.package-map.json` when deciding whether to inject `--experimental-package-map`, but the installer writes `.package-map.json` to `rootModulesDir` computed from the configurable `modulesDir` option.
### Issue Context
This creates a layout mismatch: when users configure `modulesDir` to a non-default location (relative or absolute), pnpm will write the package map there, but exec/run won’t find it and won’t inject the flag.
### Fix Focus Areas
- exec/commands/src/exec.ts[226-253]
- exec/commands/src/run.ts[282-307]
- exec/commands/src/runRecursive.ts[76-151]

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


15. Map toggle ignored 🐞 Bug ⚙ Maintainability
Description
Pacquet writes node_modules/.package-map.json for isolated installs regardless of
config.node_experimental_package_map, even though the config is documented as opt-in. This
violates the documented contract and adds unnecessary package-map generation work and file writes on
the install hot path when the feature is disabled.
Code

pacquet/crates/package-manager/src/install.rs[R2129-2131]

+fn should_write_package_map(_config: &Config, node_linker: NodeLinker) -> bool {
+    node_linker == NodeLinker::Isolated
+}
Evidence
The pacquet config documents node_experimental_package_map as the gate for writing
.package-map.json, but the actual gate function ignores the config and only checks
NodeLinker::Isolated, and the install flow calls it before writing.

pacquet/crates/config/src/lib.rs[436-440]
pacquet/crates/package-manager/src/install.rs[1339-1355]
pacquet/crates/package-manager/src/install.rs[2129-2131]

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

## Issue description
`should_write_package_map` ignores `config.node_experimental_package_map` and returns `true` for every isolated install, causing `.package-map.json` to be written even when the feature is configured off.
### Issue Context
`Config` explicitly documents `node_experimental_package_map` as controlling whether the file is written. The install path currently contradicts that documentation.
### Fix Focus Areas
- pacquet/crates/package-manager/src/install.rs[1339-1355]
- pacquet/crates/package-manager/src/install.rs[2129-2131]
- pacquet/crates/config/src/lib.rs[436-444]

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


16. Pretty package-map serialization 🐞 Bug ➹ Performance
Description
Package-map writers generate .package-map.json with pretty-printed JSON (e.g.,
JSON.stringify(..., null, 2) / serde_json::to_vec_pretty), adding avoidable CPU, allocations,
and larger output on pnpm/pacquet’s install hot path when package maps are materialized. This extra
work scales with package-map/lockfile size and is exercised during normal install flows
(deps-installer and headless deps-restorer).
Code

lockfile/to-pnp/src/packageMap.ts[R51-71]

+export async function writePackageMap (
+  lockfile: LockfileObject,
+  opts: PackageMapOptions
+): Promise<void> {
+  await fs.mkdir(opts.rootModulesDir, { recursive: true })
+  await fs.writeFile(
+    path.join(opts.rootModulesDir, PACKAGE_MAP_FILENAME),
+    `${JSON.stringify(lockfileToPackageMap(lockfile, opts), null, 2)}\n`,
+    'utf8'
+  )
+}
+
+export async function writePackageMapFromDependenciesGraph (
+  opts: DependenciesGraphPackageMapOptions
+): Promise<void> {
+  await fs.mkdir(opts.rootModulesDir, { recursive: true })
+  await fs.writeFile(
+    path.join(opts.rootModulesDir, PACKAGE_MAP_FILENAME),
+    `${JSON.stringify(dependenciesGraphToPackageMap(opts), null, 2)}\n`,
+    'utf8'
+  )
Evidence
The cited writer implementations explicitly use pretty serialization—TypeScript via
JSON.stringify(..., null, 2) in writePackageMap() / writePackageMapFromDependenciesGraph() and
Rust via serde_json::to_vec_pretty in write_package_map() / write_hoisted_package_map()—which
necessarily performs extra formatting work and produces larger JSON than compact output. The call
sites in the install pipeline (deps-installer and deps-restorer) show these writers are invoked as
part of standard installs, meaning the formatting overhead is on the critical path and repeats
proportionally to the size of the generated package map.

lockfile/to-pnp/src/packageMap.ts[51-71]
installing/deps-installer/src/install/index.ts[1637-1649]
installing/deps-restorer/src/index.ts[545-571]
pacquet/crates/package-manager/src/package_map.rs[52-76]
pacquet/crates/package-manager/src/install.rs[1235-1258]
pacquet/crates/package-manager/src/install_frozen_lockfile.rs[961-980]

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 package-map writers are currently emitting `.package-map.json` using pretty-printed JSON (`JSON.stringify(..., null, 2)` / `serde_json::to_vec_pretty`), which adds avoidable formatting CPU, extra allocations, and larger output during installs.
## Issue Context
These package maps are generated/materialized as part of standard install flows (including deps-installer and headless deps-restorer), so the pretty-printing overhead sits on a performance-sensitive install hot path and scales with the size of the package map/lockfile.
## Fix Focus Areas
- lockfile/to-pnp/src/packageMap.ts[51-71]
- installing/deps-installer/src/install/index.ts[1637-1649]
- installing/deps-restorer/src/index.ts[545-571]
- pacquet/crates/package-manager/src/package_map.rs[52-76]

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


17. Hoisted map generation inconsistent 🐞 Bug ≡ Correctness
Description
The non-headless installer only writes .package-map.json for nodeLinker=isolated, while the
headless installer writes it for hoisted too, so hoisted installs may or may not produce the map
depending on which install codepath runs.
Code

installing/deps-installer/src/install/index.ts[R1593-1594]

+  const shouldWritePackageMap = opts.enableModulesDir !== false && opts.nodeLinker === 'isolated' && !opts.virtualStoreOnly
if (!opts.lockfileOnly && !isInstallationOnlyForLockfileCheck && opts.enableModulesDir) {
Evidence
The deps-installer path gates writing to isolated only, while deps-restorer headless writes a
hoisted map via writePackageMapFromDependenciesGraph, creating divergent outputs for the same
nodeLinker=hoisted configuration.

installing/deps-installer/src/install/index.ts[1591-1649]
installing/deps-restorer/src/index.ts[545-570]

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

## Issue description
Package-map generation differs across install implementations:
- `deps-installer` writes only when `opts.nodeLinker === 'isolated'`.
- `deps-restorer` headless path writes for `hoisted` as well.
This makes `.package-map.json` output non-deterministic for hoisted installs depending on whether pnpm takes the headless path, and can make `node-experimental-package-map` appear to “randomly” not work (exec/run code checks for file existence before injecting the flag).
### Issue Context
The PR description states pnpm will always generate `node_modules/.package-map.json`; the current implementation effectively makes this conditional for hoisted installs.
### Fix Focus Areas
- installing/deps-installer/src/install/index.ts[1593-1649]
- installing/deps-restorer/src/index.ts[545-570]
### Suggested fix
- Either:
1) Implement package-map generation for `nodeLinker=hoisted` in the non-headless path too (likely via a dependencies-graph-based writer, similar to headless), or
2) Explicitly scope/adjust behavior so both paths match (and update docs/notes accordingly).
- Add a test that asserts `.package-map.json` existence for hoisted installs across both headless and non-headless scenarios (or clearly asserts the intended behavior).

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


Grey Divider

Qodo Logo

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

Copy link
Copy Markdown

PR Summary by Qodo

feat: generate Node.js package maps and optionally inject --experimental-package-map
✨ Enhancement ⚙️ Configuration changes 🧪 Tests 🕐 40+ Minutes

Grey Divider

Walkthroughs

Description
• Generate node_modules/.package-map.json during installs for Node.js package-map resolution.
• Add config options to control package map injection and strictness (standard vs loose).
• Extend pnpm exec/run and install lifecycle environments to pass package map to Node.js.
Diagram
graph TD
  cfg["Config reader"] --> inst["Installer / restorer"] --> gen["Package map writer"] --> map[("node_modules/.package-map.json")]
  cfg --> cmd["pnpm exec/run"] --> life["exec.lifecycle env"] --> node{{"Node.js runtime"}}
  map --> cmd

  subgraph Legend
    direction LR
    _mod["Module"] ~~~ _svc["Service"] ~~~ _file[("File") ] ~~~ _ext{{"External"}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Only generate the package map when explicitly enabled
  • ➕ Avoids extra filesystem writes for users not experimenting with the feature
  • ➕ Reduces surface area for unexpected diffs in tooling that watches node_modules
  • ➖ Harder for third-party tooling to discover/experiment without opting in
  • ➖ Requires more branching in install paths and tests
2. Generate the package map on-demand (e.g. `pnpm package-map` command)
  • ➕ No install-time overhead; explicit user intent
  • ➕ Easier to iterate on format without tying to every install
  • ➖ Map can go stale relative to node_modules unless re-generated
  • ➖ Less seamless for lifecycle scripts that need the map immediately after install
3. Write to virtual store and symlink/copy into root node_modules
  • ➕ Keeps generated artifacts colocated with virtual store state
  • ➕ May reduce redundant writes in some workflows
  • ➖ More complex cross-platform handling (symlinks, permissions)
  • ➖ Harder for tools expecting a stable root location

Recommendation: Current approach (always writing a single root node_modules/.package-map.json, with separate opt-in for NODE_OPTIONS injection) is a pragmatic default for ecosystem experimentation while keeping runtime behavior gated. If install-time overhead becomes a concern, the most reasonable follow-up would be gating generation behind a config flag, but that would trade off discoverability.

Grey Divider

File Changes

Enhancement (12)
exec.ts Inject package map NODE_OPTIONS for pnpm exec +12/-3

Inject package map NODE_OPTIONS for pnpm exec

• Adds CLI/config plumbing for the new settings and, when enabled, detects 'node_modules/.package-map.json' and appends '--experimental-package-map=...' to NODE_OPTIONS for spawned processes (alongside existing PnP support).

exec/commands/src/exec.ts


run.ts Inject package map NODE_OPTIONS for pnpm run +14/-0

Inject package map NODE_OPTIONS for pnpm run

• Adds support for the new settings and augments lifecycle environment so scripts can run with Node’s experimental package-map resolution when a map file is present.

exec/commands/src/run.ts


runRecursive.ts Inject package map NODE_OPTIONS for recursive runs +11/-0

Inject package map NODE_OPTIONS for recursive runs

• Threads 'nodeExperimentalPackageMap' into recursive lifecycle execution and injects the package map flag per project when available.

exec/commands/src/runRecursive.ts


index.ts Add helper to append --experimental-package-map to NODE_OPTIONS +10/-0

Add helper to append --experimental-package-map to NODE_OPTIONS

• Introduces 'makeNodePackageMapOption', including path quoting for whitespace, enabling consistent injection behavior across exec/run/install lifecycle paths.

exec/lifecycle/src/index.ts


extendInstallOptions.ts Thread package map options through installer options +4/-0

Thread package map options through installer options

• Extends strict install options and defaults with 'nodeExperimentalPackageMap' and 'nodePackageMapType' so installer internals can generate maps and optionally inject flags.

installing/deps-installer/src/install/extendInstallOptions.ts


index.ts Write package map during isolated installs and inject into lifecycle scripts +27/-1

Write package map during isolated installs and inject into lifecycle scripts

• Generates 'node_modules/.package-map.json' (isolated linker, not virtual-store-only) via 'writePackageMap'. When 'nodeExperimentalPackageMap' is enabled (and not using PnP), injects '--experimental-package-map=...' into NODE_OPTIONS for dependency and project lifecycle scripts.

installing/deps-installer/src/install/index.ts


index.ts Generate package map in headless install for hoisted and isolated layouts +43/-1

Generate package map in headless install for hoisted and isolated layouts

• Adds headless options for package map settings, writes a map for non-PnP installs (isolated from lockfile; hoisted from the dependencies graph), and injects the Node flag into lifecycle environments when enabled.

installing/deps-restorer/src/index.ts


index.ts Re-export package map APIs from lockfile/to-pnp +14/-0

Re-export package map APIs from lockfile/to-pnp

• Exposes package map types and writer functions alongside existing PnP-related exports so callers can generate maps without deep imports.

lockfile/to-pnp/src/index.ts


packageMap.ts Implement lockfile/dependencies-graph to Node.js package map conversion +390/-0

Implement lockfile/dependencies-graph to Node.js package map conversion

• Adds full package map generation, including importer packages, snapshot packages, link: targets, and optional 'loose' behavior that augments dependencies based on physically accessible packages along ancestor node_modules directories.

lockfile/to-pnp/src/packageMap.ts


install.rs Generate package map during pacquet isolated installs +33/-2

Generate package map during pacquet isolated installs

• Adds an install error variant for package map persistence and writes 'node_modules/.package-map.json' from the (filtered) current/fresh lockfile for isolated installs. Also refactors frozen-lockfile handling to reuse the filtered lockfile for saving current lockfile state.

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


lib.rs Wire package_map module and export error type +2/-0

Wire package_map module and export error type

• Registers the new 'package_map' module and re-exports 'WritePackageMapError' for consumers.

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


package_map.rs Implement pacquet package map writer (Rust) + tests +477/-0

Implement pacquet package map writer (Rust) + tests

• Adds a Rust implementation that converts pacquet lockfiles into Node.js package maps (including link target resolution and stable relative URLs), writes the JSON file, and includes unit tests validating output structure.

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


Tests (2)
misc.ts Add end-to-end tests for package map generation and runtime resolution +222/-0

Add end-to-end tests for package map generation and runtime resolution

• Adds coverage for standard vs loose maps, isolated vs hoisted node linker behaviors, and Node.js runtime smoke tests (gated to Node >= 27) validating resolution and undeclared dependency blocking/allowing semantics.

installing/deps-installer/test/install/misc.ts


index.ts Add unit tests for package map generators +231/-1

Add unit tests for package map generators

• Introduces tests for 'lockfileToPackageMap' and 'dependenciesGraphToPackageMap' (path-based ids), ensuring stable URLs/dependency mappings including link targets.

lockfile/to-pnp/test/index.ts


Documentation (1)
package-maps.md Add release notes for Node.js package map support +12/-0

Add release notes for Node.js package map support

• Introduces a changeset announcing package map generation and the two new configuration settings controlling injection and strictness.

.changeset/package-maps.md


Other (9)
Config.ts Expose package map settings on the Config interface +2/-0

Expose package map settings on the Config interface

• Adds 'nodeExperimentalPackageMap' and 'nodePackageMapType' to the strongly-typed config surface so downstream commands can consume them.

config/reader/src/Config.ts


configFileKey.ts Allow new package map keys in pnpm config files +2/-0

Allow new package map keys in pnpm config files

• Registers 'node-experimental-package-map' and 'node-package-map-type' as valid pnpm config keys.

config/reader/src/configFileKey.ts


index.ts Provide defaults for package map options +2/-0

Provide defaults for package map options

• Defaults 'node-experimental-package-map' to false and 'node-package-map-type' to 'standard'. Ensures consistent behavior without explicit user configuration.

config/reader/src/index.ts


types.ts Define config types for new package map settings +2/-0

Define config types for new package map settings

• Adds schema/type validation for the boolean enable flag and the enum ('standard' | 'loose') map type.

config/reader/src/types.ts


add.ts Expose package map options on 'pnpm add' +2/-0

Expose package map options on 'pnpm add'

• Adds the new config keys to the rc options picked up by the 'add' command.

installing/commands/src/add.ts


install.ts Expose package map options on 'pnpm install' +2/-0

Expose package map options on 'pnpm install'

• Adds the new config keys to the rc options for 'install', allowing CLI/config control of map type and injection behavior.

installing/commands/src/install.ts


remove.ts Expose package map options on 'pnpm remove' +2/-0

Expose package map options on 'pnpm remove'

• Adds the new config keys to the rc options for 'remove' to keep behavior consistent across install mutations.

installing/commands/src/remove.ts


index.ts Expose package map options on 'pnpm update' +2/-0

Expose package map options on 'pnpm update'

• Adds the new config keys to the rc options for 'update' to propagate package-map behavior into update flows.

installing/commands/src/update/index.ts


Cargo.toml Add serde dependency for package map serialization +1/-0

Add serde dependency for package map serialization

• Adds 'serde' to support JSON serialization of the new package map structures in pacquet’s package-manager crate.

pacquet/crates/package-manager/Cargo.toml


Grey Divider

Qodo Logo

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

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

⚠️ Outside diff range comments (1)
installing/deps-installer/src/install/index.ts (1)

2074-2079: ⚠️ Potential issue | 🔴 Critical

Pacquet lifecycle scripts don't receive NODE_OPTIONS with --experimental-package-map during materialization.

When pnpm delegates to pacquet (lines 2074–2079), the isolated install resolves via pnpm (lockfileOnly pass), then materializes via pacquet. Pacquet writes .package-map.json correctly, but its run_projects_lifecycle_scripts function (pacquet line 1774) initializes extra_env as empty and never injects config.node_options. Pnpm's non-delegated lifecycle script path injects NODE_OPTIONS with --experimental-package-map before running scripts (lines 1848–1852). Lifecycle scripts running under pacquet therefore cannot access the package-map during execution, breaking parity.

🤖 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 `@installing/deps-installer/src/install/index.ts` around lines 2074 - 2079, The
issue is that when pnpm delegates to pacquet for materialization via the
opts.runPacquet call at line 2078, the NODE_OPTIONS environment variable
containing --experimental-package-map is not being passed to pacquet. Pacquet's
run_projects_lifecycle_scripts function initializes extra_env as empty and never
receives config.node_options, so lifecycle scripts cannot access the
package-map. Fix this by ensuring that config.node_options (which contains the
--experimental-package-map flag) is included in the parameters passed to the
runPacquet function call, so pacquet can inject NODE_OPTIONS into the
environment when running lifecycle scripts, maintaining parity with pnpm's
non-delegated lifecycle script path.

Source: Coding guidelines

🧹 Nitpick comments (1)
installing/deps-installer/test/install/misc.ts (1)

58-277: ⚡ Quick win

Add regression tests for package-map skip-write branches.

The new suite doesn’t cover virtualStoreOnly / enableModulesDir: false, where package-map materialization is skipped and env-injection predicates are easy to regress. Adding those two cases would harden this feature significantly.

🤖 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 `@installing/deps-installer/test/install/misc.ts` around lines 58 - 277, Add
regression tests to cover the cases where package-map materialization is
skipped. Create two new test cases: one that sets virtualStoreOnly to true in
testDefaults() and another that sets enableModulesDir to false in
testDefaults(), verifying that the package-map file is not written in these
scenarios or that dependencies are still resolvable without the package-map.
These tests should be added alongside the existing test cases (such as those
using testOnNode27Plus and the various hoisted/loose package map tests) to
ensure that env-injection predicates in these skip-write branches are not
inadvertently regressed during future changes.
🤖 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 `@exec/commands/src/exec.ts`:
- Around line 244-250: The issue is that when both pnpPath and opts.nodeOptions
are set, the NODE_OPTIONS property spread from the second condition (line 247)
overwrites the one from makeNodeRequireOption(pnpPath) (line 246), losing the
PnP --require flag. Instead of relying on object spread order to combine
NODE_OPTIONS, you need to merge them explicitly by checking if both values exist
and concatenating them with a space separator. Construct the NODE_OPTIONS value
by combining the result from makeNodeRequireOption(pnpPath) with
opts.nodeOptions when both are present, rather than using sequential spreads
that cause the later one to overwrite the earlier one.

In `@installing/deps-installer/src/install/index.ts`:
- Around line 1685-1690: The block at line 1685 that injects the NODE_OPTIONS
with the experimental-package-map option does not check the same
virtualStoreOnly condition that guards the creation of the .package-map.json
file at line 1636, causing the injected NODE_OPTIONS to reference a non-existent
file during virtualStoreOnly installs. Wrap the extraEnv assignment block that
calls makeNodePackageMapOption (around line 1685) with an additional check for
!opts.virtualStoreOnly so it only injects the package-map option when the file
was actually created. Apply the same fix at
installing/deps-restorer/src/index.ts line 613 where the same predicate
misalignment exists.

In `@installing/deps-restorer/src/index.ts`:
- Around line 545-570: The code generates package-map files without checking if
modules directory is enabled, which can cause unexpected filesystem side effects
when enableModulesDir is false. Add a check for the enableModulesDir option to
the condition at lines 545-570 that wraps the calls to
writePackageMapFromDependenciesGraph and writePackageMap, ensuring these
functions are only called when modules directory is actually enabled. Apply the
same enableModulesDir check at the second affected location (lines 754-759)
where similar package-map writing logic exists.

In `@lockfile/to-pnp/src/packageMap.ts`:
- Around line 178-188: In the addPhysicalDependencyLocations function, `link:`
refs are currently being skipped with a continue statement that prevents them
from being added to the physical-location index. Remove the condition that skips
refs starting with 'link:' (the if statement checking ref.startsWith('link:'))
so that linked dependencies are processed and added to the physical dependency
locations via the addDependencyLocation call, ensuring they are included in the
physical-location index used by loose-mode expansion, consistent with how linked
deps are added to the direct package map elsewhere in the codebase.

In `@pacquet/crates/package-manager/src/install.rs`:
- Around line 1242-1256: The condition for writing the package-map in the
install function currently only checks for NodeLinker::Isolated, but it should
also support the hoisted linker to match the TypeScript implementation. Modify
the if condition that checks node_linker to include both the Isolated variant
and the Hoisted variant (using an OR condition or pattern matching to handle
both cases), so that the write_package_map function is called for both isolated
and hoisted node linker configurations.

---

Outside diff comments:
In `@installing/deps-installer/src/install/index.ts`:
- Around line 2074-2079: The issue is that when pnpm delegates to pacquet for
materialization via the opts.runPacquet call at line 2078, the NODE_OPTIONS
environment variable containing --experimental-package-map is not being passed
to pacquet. Pacquet's run_projects_lifecycle_scripts function initializes
extra_env as empty and never receives config.node_options, so lifecycle scripts
cannot access the package-map. Fix this by ensuring that config.node_options
(which contains the --experimental-package-map flag) is included in the
parameters passed to the runPacquet function call, so pacquet can inject
NODE_OPTIONS into the environment when running lifecycle scripts, maintaining
parity with pnpm's non-delegated lifecycle script path.

---

Nitpick comments:
In `@installing/deps-installer/test/install/misc.ts`:
- Around line 58-277: Add regression tests to cover the cases where package-map
materialization is skipped. Create two new test cases: one that sets
virtualStoreOnly to true in testDefaults() and another that sets
enableModulesDir to false in testDefaults(), verifying that the package-map file
is not written in these scenarios or that dependencies are still resolvable
without the package-map. These tests should be added alongside the existing test
cases (such as those using testOnNode27Plus and the various hoisted/loose
package map tests) to ensure that env-injection predicates in these skip-write
branches are not inadvertently regressed during future changes.
🪄 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: 33b20f72-0bcc-40bd-9c4c-4e467df6b4f7

📥 Commits

Reviewing files that changed from the base of the PR and between b6826b7 and f417039.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (24)
  • .changeset/package-maps.md
  • config/reader/src/Config.ts
  • config/reader/src/configFileKey.ts
  • config/reader/src/index.ts
  • config/reader/src/types.ts
  • exec/commands/src/exec.ts
  • exec/commands/src/run.ts
  • exec/commands/src/runRecursive.ts
  • exec/lifecycle/src/index.ts
  • installing/commands/src/add.ts
  • installing/commands/src/install.ts
  • installing/commands/src/remove.ts
  • installing/commands/src/update/index.ts
  • installing/deps-installer/src/install/extendInstallOptions.ts
  • installing/deps-installer/src/install/index.ts
  • installing/deps-installer/test/install/misc.ts
  • installing/deps-restorer/src/index.ts
  • lockfile/to-pnp/src/index.ts
  • lockfile/to-pnp/src/packageMap.ts
  • lockfile/to-pnp/test/index.ts
  • pacquet/crates/package-manager/Cargo.toml
  • pacquet/crates/package-manager/src/install.rs
  • pacquet/crates/package-manager/src/lib.rs
  • pacquet/crates/package-manager/src/package_map.rs

Comment thread exec/commands/src/exec.ts
Comment thread installing/deps-installer/src/install/index.ts Outdated
Comment thread installing/deps-restorer/src/index.ts Outdated
Comment thread lockfile/to-pnp/src/packageMap.ts
Comment thread pacquet/crates/package-manager/src/install.rs Outdated
Comment thread lockfile/to-pnp/src/packageMap.ts Outdated
@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.077 ± 0.177 3.850 4.382 1.96 ± 0.13
pacquet@main 3.864 ± 0.074 3.783 4.011 1.86 ± 0.10
pnpr@HEAD 2.137 ± 0.117 1.992 2.360 1.03 ± 0.08
pnpr@main 2.082 ± 0.109 1.942 2.251 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.0765190402,
      "stddev": 0.17678428428122978,
      "median": 4.0803585756,
      "user": 3.8210029999999997,
      "system": 3.4545260399999997,
      "min": 3.8504428201,
      "max": 4.3821363351,
      "times": [
        4.3821363351,
        3.8968582101,
        4.0389247751,
        3.9654136881,
        4.1505584021,
        4.1217923761,
        3.9162421850999998,
        4.3094976911,
        4.1333239191,
        3.8504428201
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 3.8641933048999997,
      "stddev": 0.07400980263738809,
      "median": 3.8509771886,
      "user": 3.7583662,
      "system": 3.4089585399999995,
      "min": 3.7833505951,
      "max": 4.0114060341,
      "times": [
        3.8067531181,
        3.8862118681,
        3.9260282671,
        4.0114060341,
        3.7833505951,
        3.8384989811,
        3.9290755981,
        3.8634553961,
        3.7991297871,
        3.7980234041
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.136928469,
      "stddev": 0.1165347949003075,
      "median": 2.1219597046,
      "user": 2.6617607,
      "system": 2.9420014399999994,
      "min": 1.9919510431,
      "max": 2.3602239251,
      "times": [
        2.0842286991,
        1.9919510431,
        2.0282815061,
        2.2096550351,
        2.0579165521,
        2.3602239251,
        2.0449375041,
        2.1596907101,
        2.2591072171,
        2.1732924981
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.082070166,
      "stddev": 0.10923134677165727,
      "median": 2.0648463866,
      "user": 2.6138700999999998,
      "system": 2.9431938399999997,
      "min": 1.9421110081,
      "max": 2.2509691351,
      "times": [
        2.2509691351,
        1.9421110081,
        2.0163917611,
        1.9958463581,
        2.2062672081,
        2.1836266761,
        2.1336537041,
        1.9678175261000002,
        2.0107172711,
        2.1133010121
      ]
    }
  ]
}

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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 637.1 ± 14.7 613.7 658.3 1.02 ± 0.03
pacquet@main 624.1 ± 14.7 603.2 640.1 1.00
pnpr@HEAD 683.0 ± 14.7 661.3 711.7 1.09 ± 0.03
pnpr@main 722.6 ± 26.1 696.2 789.6 1.16 ± 0.05
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.63711435794,
      "stddev": 0.014656741639673914,
      "median": 0.64088466734,
      "user": 0.3757738,
      "system": 1.32341996,
      "min": 0.6137358823400001,
      "max": 0.65830619934,
      "times": [
        0.65830619934,
        0.64573037434,
        0.6137358823400001,
        0.61759820434,
        0.64507056334,
        0.62967871034,
        0.6418258143400001,
        0.6269544073400001,
        0.63994352034,
        0.6522999033400001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.62410553784,
      "stddev": 0.014745425288494663,
      "median": 0.63151934434,
      "user": 0.3692619999999999,
      "system": 1.3052173599999999,
      "min": 0.6032284173400001,
      "max": 0.6401174043400001,
      "times": [
        0.6081833963400001,
        0.6032284173400001,
        0.60582662034,
        0.6126633893400001,
        0.6383636073400001,
        0.63411621334,
        0.6304269693400001,
        0.63261171934,
        0.6401174043400001,
        0.6355176413400001
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6829526736400002,
      "stddev": 0.014718994213038286,
      "median": 0.68268271034,
      "user": 0.39501069999999994,
      "system": 1.3447090599999998,
      "min": 0.66133266134,
      "max": 0.71174772034,
      "times": [
        0.66576065634,
        0.71174772034,
        0.6814937053400001,
        0.6841505643400001,
        0.67415267934,
        0.69510447434,
        0.6785354853400001,
        0.66133266134,
        0.6933770743400001,
        0.68387171534
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.7225582656400001,
      "stddev": 0.026062574756706116,
      "median": 0.7194535843400001,
      "user": 0.39540349999999996,
      "system": 1.3756718600000002,
      "min": 0.6962499883400001,
      "max": 0.78955310434,
      "times": [
        0.72817139034,
        0.72898167834,
        0.71277174434,
        0.71604956034,
        0.7013608073400001,
        0.7060764153400001,
        0.72285760834,
        0.6962499883400001,
        0.7235103593400001,
        0.78955310434
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.209 ± 0.036 4.165 4.261 1.96 ± 0.09
pacquet@main 4.191 ± 0.034 4.146 4.264 1.95 ± 0.09
pnpr@HEAD 2.151 ± 0.095 2.008 2.320 1.00
pnpr@main 2.164 ± 0.114 2.065 2.364 1.01 ± 0.07
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.20874728818,
      "stddev": 0.03555369444887688,
      "median": 4.20387434348,
      "user": 3.7705514199999994,
      "system": 3.3517832,
      "min": 4.1653991239799995,
      "max": 4.261234786979999,
      "times": [
        4.25028851798,
        4.261234786979999,
        4.19993463198,
        4.1653991239799995,
        4.187699149979999,
        4.20781405498,
        4.1747131369799995,
        4.25302878798,
        4.17220764298,
        4.2151530479799995
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.190652739980001,
      "stddev": 0.03358652958659078,
      "median": 4.183322092979999,
      "user": 3.67932012,
      "system": 3.3207296,
      "min": 4.14603688498,
      "max": 4.2640927049799995,
      "times": [
        4.18479640298,
        4.14603688498,
        4.2184305129799995,
        4.21357423998,
        4.181847782979999,
        4.18809965798,
        4.17322127798,
        4.2640927049799995,
        4.16361522598,
        4.17281270898
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.15054849378,
      "stddev": 0.09528598470856657,
      "median": 2.13148429598,
      "user": 2.51718352,
      "system": 2.8483145,
      "min": 2.0079157809800003,
      "max": 2.31960928398,
      "times": [
        2.1195695379800004,
        2.16443744198,
        2.14339905398,
        2.2129335289800003,
        2.06232664898,
        2.2725327119800003,
        2.0914988989800003,
        2.0079157809800003,
        2.31960928398,
        2.11126204998
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.16390420908,
      "stddev": 0.1142076181807464,
      "median": 2.1130242314800003,
      "user": 2.4573688199999997,
      "system": 2.8739628999999995,
      "min": 2.06462044098,
      "max": 2.3637411699800004,
      "times": [
        2.3637411699800004,
        2.2431877899800003,
        2.0858078259800004,
        2.1096985579800003,
        2.34650697598,
        2.07787829498,
        2.1163499049800003,
        2.16362485598,
        2.06762627398,
        2.06462044098
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.315 ± 0.017 1.294 1.339 1.99 ± 0.14
pacquet@main 1.320 ± 0.034 1.287 1.399 1.99 ± 0.15
pnpr@HEAD 0.662 ± 0.045 0.634 0.786 1.00
pnpr@main 0.663 ± 0.075 0.626 0.876 1.00 ± 0.13
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.3151249603800002,
      "stddev": 0.016539266144537695,
      "median": 1.31229530028,
      "user": 1.3050233800000002,
      "system": 1.6994350399999998,
      "min": 1.29415406478,
      "max": 1.33925247078,
      "times": [
        1.29775348778,
        1.29415406478,
        1.31434470178,
        1.29769457678,
        1.32770715878,
        1.3328414527799999,
        1.30611223778,
        1.3102458987799999,
        1.33114355378,
        1.33925247078
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.31970859098,
      "stddev": 0.03383182861035359,
      "median": 1.31121257728,
      "user": 1.3105328799999998,
      "system": 1.7184925399999997,
      "min": 1.28736656778,
      "max": 1.39886538978,
      "times": [
        1.30936685978,
        1.35558278178,
        1.29698929678,
        1.28736656778,
        1.39886538978,
        1.31305829478,
        1.32620772178,
        1.30126710878,
        1.31344580678,
        1.29493608178
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.66179015468,
      "stddev": 0.045281807194821416,
      "median": 0.64331079028,
      "user": 0.3308316799999999,
      "system": 1.26604454,
      "min": 0.63406511078,
      "max": 0.7855863737800001,
      "times": [
        0.66689808378,
        0.63406511078,
        0.6445755347800001,
        0.64204604578,
        0.7855863737800001,
        0.66634963878,
        0.66230531478,
        0.64160967578,
        0.63979292878,
        0.63467283978
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.6633938699799999,
      "stddev": 0.07545429700607648,
      "median": 0.63852522328,
      "user": 0.3267050799999999,
      "system": 1.27389234,
      "min": 0.62573248078,
      "max": 0.8760425737800001,
      "times": [
        0.63680212678,
        0.6354163377800001,
        0.64729885278,
        0.6390266007800001,
        0.63802384578,
        0.63132429378,
        0.62573248078,
        0.63921205478,
        0.8760425737800001,
        0.6650595327800001
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.988 ± 0.040 2.936 3.077 4.68 ± 0.13
pacquet@main 3.010 ± 0.091 2.918 3.225 4.71 ± 0.18
pnpr@HEAD 0.663 ± 0.014 0.639 0.681 1.04 ± 0.03
pnpr@main 0.638 ± 0.016 0.625 0.677 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.98765523984,
      "stddev": 0.04020411510018309,
      "median": 2.98317263874,
      "user": 1.7554402399999998,
      "system": 1.9480684400000001,
      "min": 2.93646624424,
      "max": 3.0765032582400003,
      "times": [
        2.94004634224,
        2.97106068224,
        2.9719521192400005,
        2.97572294424,
        2.99850261624,
        2.93646624424,
        2.9988671732400003,
        2.99062233324,
        3.0765032582400003,
        3.01680868524
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 3.00963830004,
      "stddev": 0.09118145112823023,
      "median": 2.99137943824,
      "user": 1.7603272399999998,
      "system": 1.9574325399999999,
      "min": 2.91811052224,
      "max": 3.22527445624,
      "times": [
        2.9681974272400002,
        2.93723265824,
        2.9457411402400004,
        2.91811052224,
        3.01456144924,
        3.02381658124,
        3.0539743012400002,
        3.0624800862400003,
        3.22527445624,
        2.9469943782400003
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6631646606400002,
      "stddev": 0.01368127381373223,
      "median": 0.6662157502400001,
      "user": 0.33667023999999995,
      "system": 1.2788295399999998,
      "min": 0.6392432582400001,
      "max": 0.6809748612400001,
      "times": [
        0.6659569152400001,
        0.6526152192400001,
        0.6753776152400001,
        0.6809748612400001,
        0.6690670632400001,
        0.6735651062400001,
        0.66647458524,
        0.6438971952400001,
        0.6644747872400001,
        0.6392432582400001
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.6384548190400001,
      "stddev": 0.015527351147230998,
      "median": 0.63401830374,
      "user": 0.32244194000000004,
      "system": 1.26799874,
      "min": 0.6250774932400001,
      "max": 0.6774146282400001,
      "times": [
        0.6362173082400001,
        0.6289934822400001,
        0.62839144924,
        0.64399614524,
        0.6356449562400001,
        0.62816587824,
        0.6250774932400001,
        0.6323916512400001,
        0.6774146282400001,
        0.6482551982400001
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12430
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,208.75 ms
(-0.09%)Baseline: 4,212.54 ms
5,055.05 ms
(83.26%)
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
🚷 view threshold
2,987.66 ms
(-0.44%)Baseline: 3,000.79 ms
3,600.95 ms
(82.97%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,315.12 ms
(-0.97%)Baseline: 1,328.07 ms
1,593.68 ms
(82.52%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
4,076.52 ms
(-2.10%)Baseline: 4,163.79 ms
4,996.55 ms
(81.59%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
637.11 ms
(+3.33%)Baseline: 616.58 ms
739.90 ms
(86.11%)
🐰 View full continuous benchmarking report in Bencher

@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12430
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,150.55 ms
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
663.16 ms
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
661.79 ms
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,136.93 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
682.95 ms
🐰 View full continuous benchmarking report in Bencher

@zkochan

zkochan commented Jun 15, 2026

Copy link
Copy Markdown
Member

I added tests and they pass locally, but they still require a nightly build version of Node.js until the next 26.x release.

so we should wait for the node release first?

performance seems ok. So, I don't have objections.

@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 was updated up to the latest commit 73e7d26

Comment thread installing/deps-restorer/src/index.ts Outdated
@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results

# Scenario main HEAD
1 Isolated linker: fresh restore, hot cache + hot store 2.23s ± 0.138s 2.146s ± 0.031s
2 Isolated linker: fresh add new dep, hot cache + hot store 6.163s ± 0.076s 6.234s ± 0.047s
3 Isolated linker: fresh install, hot cache + hot store 6.307s ± 0.09s 6.208s ± 0.089s
4 Isolated linker: fresh restore, cold cache + cold store 8.228s ± 0.112s 8.191s ± 0.263s
5 Isolated linker: fresh install, cold cache + cold store 12.556s ± 0.13s 12.42s ± 0.221s
6 GVS linker: fresh restore, hot cache + hot store 1.094s ± 0.013s 1.092s ± 0.011s

Run 27748494396 · runs per scenario · triggered by @zkochan

@codecov-commenter

codecov-commenter commented Jun 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.00000% with 80 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.16%. Comparing base (83f06a6) to head (9ee374b).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
pacquet/crates/package-manager/src/package_map.rs 93.91% 40 Missing ⚠️
...tes/package-manager/src/install_frozen_lockfile.rs 80.00% 10 Missing ⚠️
...package-manager/src/install_with_fresh_lockfile.rs 72.97% 10 Missing ⚠️
pacquet/crates/package-manager/src/install.rs 72.00% 7 Missing ⚠️
pacquet/crates/cli/src/cli_args/run.rs 28.57% 5 Missing ⚠️
pacquet/crates/cli/src/cli_args/run/recursive.rs 28.57% 5 Missing ⚠️
pacquet/crates/cli/src/cli_args/exec.rs 66.66% 2 Missing ⚠️
pacquet/crates/config/src/pnpm_default_parity.rs 85.71% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12430      +/-   ##
==========================================
+ Coverage   88.12%   88.16%   +0.03%     
==========================================
  Files         311      313       +2     
  Lines       41924    42981    +1057     
==========================================
+ Hits        36947    37893     +946     
- Misses       4977     5088     +111     

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

Caution

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

⚠️ Outside diff range comments (2)
pacquet/crates/package-manager/src/install.rs (1)

1235-1245: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Generate the package map for auto-frozen installs too.

When take_frozen_path is true via preferFrozenLockfile but frozen_lockfile is false, the frozen branch returns fresh_lockfile = None and filtered_current_lockfile stays None, so this package-map block skips .package-map.json even after materializing node_modules.

Suggested fix
-        let filtered_current_lockfile = if frozen_lockfile {
+        let filtered_current_lockfile = if take_frozen_path {
             lockfile.map(|lockfile| {
                 crate::filter_lockfile_for_current(lockfile, included, &frozen_skipped)
             })
         } else {
             None

As per coding guidelines, user-visible on-disk artifact behavior must stay in sync between pnpm and pacquet.

🤖 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` around lines 1235 - 1245, The
package map is not being generated for auto-frozen installs when
take_frozen_path is true but frozen_lockfile is false, because both
filtered_current_lockfile and fresh_lockfile remain None in this scenario. To
fix this, ensure that package_map_lockfile has access to a valid lockfile
reference in the auto-frozen case by making the lockfile available for package
map generation even when frozen_lockfile is false but the frozen path is being
taken. This may require using the current lockfile or adjusting the conditional
logic that determines which lockfile to use for the package map check in the
should_write_package_map block.

Source: Coding guidelines

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

1559-1575: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Generate the package map on the fresh isolated path too.

Right now the fresh install only writes .package-map.json when is_hoisted routes through run_hoisted_linker; the default isolated fresh path falls through without any write_package_map call, so first installs can miss the user-visible artifact.

Suggested direction
+        let project_manifests = importer_manifests
+            .iter()
+            .map(|(id, manifest)| (lockfile_dir.join(id), *manifest))
+            .collect::<Vec<_>>();
+
         let (hoisted_dependencies, hoisted_locations) = if is_hoisted {
-            let project_manifests = importer_manifests
-                .iter()
-                .map(|(id, manifest)| (lockfile_dir.join(id), *manifest))
-                .collect::<Vec<_>>();
             let output = crate::install_frozen_lockfile::run_hoisted_linker::<Reporter>(
                 crate::install_frozen_lockfile::HoistedLinkerInputs {
                     config,
                     lockfile: &built_lockfile,
@@
 
             (hoisted_dependencies, BTreeMap::new())
         };
+
+        if matches!(node_linker, NodeLinker::Isolated) {
+            let included = pacquet_modules_yaml::IncludedDependencies {
+                dependencies: dependency_groups.contains(&DependencyGroup::Prod),
+                dev_dependencies: dependency_groups.contains(&DependencyGroup::Dev),
+                optional_dependencies: dependency_groups.contains(&DependencyGroup::Optional),
+            };
+            let filtered_lockfile =
+                crate::filter_lockfile_for_current(&built_lockfile, included, &skipped);
+            crate::package_map::write_package_map(
+                &filtered_lockfile,
+                &crate::package_map::PackageMapOptions {
+                    lockfile_dir,
+                    modules_dir: &config.modules_dir,
+                    package_map_type: config.node_package_map_type,
+                    virtual_store_dir: &config.virtual_store_dir,
+                    virtual_store_dir_max_length: config.virtual_store_dir_max_length as usize,
+                    project_manifests: &project_manifests,
+                },
+            )
+            .map_err(InstallWithFreshLockfileError::WritePackageMap)?;
+        }

As per coding guidelines, this PR’s user-visible on-disk artifact behavior must stay in sync between pnpm and pacquet, and the PR objective says package maps are automatically generated.

Also applies to: 1715-1716

🤖 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_with_fresh_lockfile.rs` around
lines 1559 - 1575, The fresh install path for non-hoisted (isolated) layout does
not generate the `.package-map.json` artifact, unlike the hoisted path which
generates it through run_hoisted_linker. Ensure that write_package_map is called
in the isolated fresh install code path to maintain consistency with pnpm
behavior and guarantee that package maps are generated for all first installs
regardless of whether the layout is hoisted or isolated.

Source: Coding guidelines

🤖 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/package-manager/src/install.rs`:
- Around line 1778-1788: The extra_env HashMap is only seeded with NODE_OPTIONS
from config.node_options, but it should also preserve any NODE_OPTIONS value
inherited from the process environment before the package-map flag is injected.
Before initializing extra_env, check the actual process environment using
std::env::var("NODE_OPTIONS") to retrieve any inherited NODE_OPTIONS value. When
constructing the NODE_OPTIONS entry in extra_env, ensure it includes both the
inherited value from the process environment and the configured value from
config.node_options. This way, when crate::make_node_package_map_option is
called with the NODE_OPTIONS from extra_env, it will correctly append the
package-map flag to the complete NODE_OPTIONS (containing both inherited and
configured values) rather than losing the inherited environment variable value.

In `@pacquet/crates/package-manager/src/package_map.rs`:
- Around line 243-244: The issue is that package_ids_by_dep_path uses a
first-seen or_insert pattern at line 243 to store only one package ID per
dep_path string, but in hoisted layouts the same dep_path can exist at multiple
physical locations. When this single cached ID is used for importer dependency
edges at lines 258-272 and 491-507, it may bind importers to non-nearest package
copies, causing incorrect runtime resolution. Replace the global dep_path-to-ID
mapping approach with logic that derives importer dependency IDs based on
importer-scoped graph placement or nearest-location selection instead. This
means at each location where package IDs are resolved for importer dependencies
(the three affected sections), determine the correct ID by considering the
importer's context and proximity rather than consulting the single-entry
package_ids_by_dep_path map.

---

Outside diff comments:
In `@pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs`:
- Around line 1559-1575: The fresh install path for non-hoisted (isolated)
layout does not generate the `.package-map.json` artifact, unlike the hoisted
path which generates it through run_hoisted_linker. Ensure that
write_package_map is called in the isolated fresh install code path to maintain
consistency with pnpm behavior and guarantee that package maps are generated for
all first installs regardless of whether the layout is hoisted or isolated.

In `@pacquet/crates/package-manager/src/install.rs`:
- Around line 1235-1245: The package map is not being generated for auto-frozen
installs when take_frozen_path is true but frozen_lockfile is false, because
both filtered_current_lockfile and fresh_lockfile remain None in this scenario.
To fix this, ensure that package_map_lockfile has access to a valid lockfile
reference in the auto-frozen case by making the lockfile available for package
map generation even when frozen_lockfile is false but the frozen path is being
taken. This may require using the current lockfile or adjusting the conditional
logic that determines which lockfile to use for the package map check in the
should_write_package_map block.
🪄 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: 44faed5c-b5e3-4f57-ae04-fb954c24336d

📥 Commits

Reviewing files that changed from the base of the PR and between f417039 and 73e7d26.

📒 Files selected for processing (31)
  • exec/commands/src/exec.ts
  • exec/commands/src/run.ts
  • exec/commands/src/runRecursive.ts
  • exec/commands/test/exec.ts
  • exec/lifecycle/src/index.ts
  • exec/lifecycle/test/index.ts
  • installing/deps-installer/src/install/index.ts
  • installing/deps-installer/test/install/misc.ts
  • installing/deps-restorer/src/index.ts
  • lockfile/to-pnp/src/packageMap.ts
  • lockfile/to-pnp/test/index.ts
  • pacquet/crates/cli/src/cli_args/exec.rs
  • pacquet/crates/cli/src/cli_args/run.rs
  • pacquet/crates/cli/src/cli_args/run/recursive.rs
  • pacquet/crates/cli/tests/hoisted_node_linker.rs
  • pacquet/crates/config/src/env_overlay.rs
  • pacquet/crates/config/src/env_overlay/tests.rs
  • pacquet/crates/config/src/lib.rs
  • pacquet/crates/config/src/pnpm_default_parity.rs
  • pacquet/crates/config/src/tests.rs
  • pacquet/crates/config/src/workspace_yaml.rs
  • pacquet/crates/config/src/workspace_yaml/tests.rs
  • pacquet/crates/package-manager/src/build_modules.rs
  • pacquet/crates/package-manager/src/build_modules/tests.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
  • pacquet/crates/package-manager/src/lib.rs
  • pacquet/crates/package-manager/src/package_map.rs
🚧 Files skipped from review as they are similar to previous changes (6)
  • exec/lifecycle/src/index.ts
  • installing/deps-restorer/src/index.ts
  • installing/deps-installer/test/install/misc.ts
  • exec/commands/src/runRecursive.ts
  • exec/commands/src/run.ts
  • lockfile/to-pnp/src/packageMap.ts

Comment thread pacquet/crates/package-manager/src/install.rs
Comment thread pacquet/crates/package-manager/src/package_map.rs Outdated
Copilot AI review requested due to automatic review settings June 15, 2026 21:46
@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

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 48 out of 50 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file
Comments suppressed due to low confidence (3)

pacquet/crates/package-manager/src/package_map.rs:1

  • package_map_path_for_execution() hard-codes node_modules instead of using the configured modules directory. If config.modules_dir is customized (or differs per-workspace), this can fail to locate an existing .package-map.json and silently skip injecting --experimental-package-map. Prefer deriving the lookup path from config.modules_dir (and/or its basename relative to workspace_dir/dir) so execution-time resolution matches where write_package_map() wrote the file.
    pacquet/crates/config/src/lib.rs:1
  • This doc comment is now inaccurate: the PR adds support for Loose maps (including physical ancestor deps) and a hoisted writer (write_hoisted_package_map). Update the comment to reflect the current behavior (what is supported for isolated vs hoisted, and what Standard vs Loose means).
    pacquet/crates/package-manager/src/build_modules/tests.rs:1
  • Using &HashMap::new() here creates a reference to a temporary. If the BuildModules { ... } struct is stored in a local variable (common in tests), this typically triggers E0716: temporary value dropped while borrowed. Bind the empty map to a named variable (e.g., let extra_env = HashMap::new();) and pass &extra_env instead (and apply the same fix to the other occurrences in this file).

@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 was updated up to the latest commit 1acfdae

@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

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

752-800: 💤 Low value

split_node_options does not handle escape sequences inside quotes.

The tokenizer correctly handles quoted strings but doesn't process escape sequences like \" inside double-quoted values. If NODE_OPTIONS contains --flag="path with \"quotes\"", the inner escaped quotes would incorrectly terminate the quoted region.

This is unlikely in practice since Node's own NODE_OPTIONS parsing has similar limitations, and the primary use case (paths with spaces) is handled correctly. The current behavior matches the TypeScript implementation's approach.

🤖 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/package_map.rs` around lines 752 - 800,
The split_node_options function does not handle escape sequences inside quoted
strings, so escaped quotes like \" inside double-quoted values would incorrectly
terminate the quoted region. To fix this, modify the logic inside the
quote-handling section (when quote is Some(q)) to detect and handle backslash
escape sequences: when a backslash is encountered inside quotes, check if the
next character matches the quote character, and if so, treat both characters as
part of the token content without closing the quote. This requires converting
the char iteration to use a peekable iterator or manually handling the character
lookahead to properly process escape sequences like \" and \' within quoted
strings.
🤖 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 `@installing/commands/src/import/index.ts`:
- Around line 249-250: The error message in the PnpmError thrown when
parseYarnLock fails has a grammar issue: when the ternary operator evaluates to
'array', it produces "got a array" which should be "got an array". Modify the
ternary expression to use the correct article "an" when the type is "array" and
"a" for other types. The simplest approach is to change the template string to
conditionally include either "an array" or "a ${typeof parseYarnLock}" based on
whether Array.isArray(parseYarnLock) is true, ensuring proper grammar regardless
of the parsed type.

In `@pacquet/crates/package-manager/src/package_map.rs`:
- Around line 747-750: The condition in the code at line 749 checks only for an
exact match to `".."` but should also handle relative paths starting with
`"../"` (like `"../foo"`). Update the condition from checking `relative == ".."`
to instead use `relative.starts_with("../")` to match the correct pattern
already used in the `link_target_id` function at line 734. This ensures all
upward-traversing paths receive proper handling.

---

Nitpick comments:
In `@pacquet/crates/package-manager/src/package_map.rs`:
- Around line 752-800: The split_node_options function does not handle escape
sequences inside quoted strings, so escaped quotes like \" inside double-quoted
values would incorrectly terminate the quoted region. To fix this, modify the
logic inside the quote-handling section (when quote is Some(q)) to detect and
handle backslash escape sequences: when a backslash is encountered inside
quotes, check if the next character matches the quote character, and if so,
treat both characters as part of the token content without closing the quote.
This requires converting the char iteration to use a peekable iterator or
manually handling the character lookahead to properly process escape sequences
like \" and \' within quoted strings.
🪄 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: db56482b-ac83-4f0d-904e-fe3f081e69db

📥 Commits

Reviewing files that changed from the base of the PR and between 73e7d26 and f6f1814.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (8)
  • .changeset/fix-yarn-import-js-yaml.md
  • installing/commands/package.json
  • installing/commands/src/import/index.ts
  • pacquet/crates/cli/tests/hoisted_node_linker.rs
  • pacquet/crates/package-manager/src/build_modules.rs
  • pacquet/crates/package-manager/src/package_map.rs
  • pacquet/crates/package-manager/src/package_map/tests.rs
  • pnpm-workspace.yaml
💤 Files with no reviewable changes (1)
  • pnpm-workspace.yaml
✅ Files skipped from review due to trivial changes (1)
  • .changeset/fix-yarn-import-js-yaml.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • pacquet/crates/cli/tests/hoisted_node_linker.rs

Comment thread installing/commands/src/import/index.ts Outdated
Comment thread pacquet/crates/package-manager/src/package_map.rs
Comment thread exec/lifecycle/src/index.ts
Comment thread installing/deps-installer/src/install/index.ts
Copilot AI review requested due to automatic review settings June 16, 2026 06:25
@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 2084a79

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 50 out of 52 changed files in this pull request and generated 2 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file
Comments suppressed due to low confidence (3)

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

  • This writes node_modules/.package-map.json from the outer install layer, but InstallFrozenLockfile also writes a package map for isolated installs (see install_frozen_lockfile.rs around the matches!(node_linker, NodeLinker::Isolated) block). For frozen_lockfile installs this likely causes redundant serialization + disk IO. Consider consolidating to a single write location (mandatory): either remove the isolated write inside InstallFrozenLockfile and rely on this block, or gate this block to only run when fresh_lockfile.is_some() (or otherwise ensure it doesn't run on the path where the map has already been persisted).
    pacquet/crates/config/src/lib.rs:1
  • The doc comment is now inaccurate: this PR adds loose-mode support for isolated package maps and a hoisted package-map writer. Please update the comment to reflect current behavior (mandatory), or clarify any remaining limitations (e.g., if some modes/layouts are still unsupported).
    pacquet/crates/package-manager/src/install.rs:1
  • should_write_package_map takes config but never uses it, which is confusing for callers/tests and makes the function name read like it applies broader policy than it currently does. Consider (mandatory) either removing the unused config parameter, or using it to encode the intended policy (e.g., any config flag that should control whether package maps are written).

Comment thread installing/commands/src/import/index.ts Outdated
Comment thread lockfile/to-pnp/src/packageMap.ts
Comment thread lockfile/to-pnp/src/packageMap.ts
Copilot AI review requested due to automatic review settings June 16, 2026 07:00
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

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

Port the pnpm CLI fix to pacquet. lockfile_to_package_map derived each
package directory from to_virtual_store_name (the flat local layout), so
under the global virtual store every url pointed at a non-existent path.
Resolve each snapshot through VirtualStoreLayout::slot_dir instead, which
returns the correct location for both the flat and content-hashed layouts.

Also collapse the redundant double-write: the map was written once in
InstallFrozenLockfile::run and again in Install::run_inner. Each install
path now writes once, from where its VirtualStoreLayout is in scope (the
fresh path gains its own write), and the central run_inner write is removed.

Written by an agent (Claude Code, claude-opus-4-8).
Copilot AI review requested due to automatic review settings June 18, 2026 00:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread lockfile/to-pnp/src/packageMap.ts
Comment thread pacquet/crates/package-manager/src/package_map.rs
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 5917ef5

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

Caution

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

⚠️ Outside diff range comments (3)
pacquet/crates/package-manager/src/install.rs (2)

1461-1486: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Persist the current lockfile for auto-frozen installs too.

take_frozen_path is true for both explicit --frozen-lockfile and the default preferFrozenLockfile auto-frozen path, but frozen_lockfile is only the explicit flag. With the current gate, an auto-frozen materialization can install node_modules without updating <virtual_store_dir>/lock.yaml, leaving the next install unable to use the current-lockfile fast path.

Suggested fix
-        let filtered_current_lockfile = if frozen_lockfile {
+        let filtered_current_lockfile = if take_frozen_path {
             lockfile.map(|lockfile| {
                 crate::filter_lockfile_for_current(lockfile, included, &frozen_skipped)
             })
         } else {
             None
         };
@@
-        if frozen_lockfile && let Some(lockfile) = filtered_current_lockfile.as_ref() {
+        if take_frozen_path && let Some(lockfile) = filtered_current_lockfile.as_ref() {

As per coding guidelines, pacquet must keep persisted install artifacts aligned with pnpm behavior.

🤖 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` around lines 1461 - 1486, The
condition gating the current lockfile persistence logic is checking only the
explicit `frozen_lockfile` flag, but it should also cover the auto-frozen path.
Change the condition in the if statement that persists the
filtered_current_lockfile to use `take_frozen_path` instead of `frozen_lockfile`
so that both explicit frozen installs and auto-frozen materializations properly
update the virtual_store_dir/lock.yaml file and enable the current-lockfile fast
path for subsequent installs.

Source: Coding guidelines


660-668: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t let up-to-date fast paths skip package-map creation.

Both early returns can report success without ensuring node_modules/.package-map.json exists. If a project was installed before this feature, or the file was deleted, pacquet install can now exit “up to date” while the new always-generated on-disk artifact is still missing. Gate these fast paths on the package-map file being present for non-PnP linkers, or write it before returning.

Suggested direction
+        let package_map_ready = resolve_only
+            || matches!(node_linker, NodeLinker::Pnp)
+            || config
+                .modules_dir
+                .join(crate::package_map::PACKAGE_MAP_FILENAME)
+                .is_file();
+
         if optimistic_decision {
@@
-            if fast_path_safe {
+            if fast_path_safe && package_map_ready {
@@
         if take_frozen_path
@@
             && !has_newly_allowed_ignored_builds(modules, config)
+            && package_map_ready
         {

As per coding guidelines, on-disk artifacts like .package-map.json must stay in sync between pnpm and pacquet; the PR objective also states package maps are automatically generated.

Also applies to: 1080-1108

🤖 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` around lines 660 - 668, The
fast path early returns in the function (where fast_path_safe is checked, and
the similar locations mentioned at lines 1080-1108) exit without ensuring the
package-map file exists on disk. Either add a check to verify that
node_modules/.package-map.json exists before taking the fast path return, or
ensure the package-map is written to disk before returning early from these fast
path branches. This ensures the on-disk artifact remains in sync and is always
present after pacquet install completes, even on up-to-date installs.

Source: Coding guidelines

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

1806-1818: ⚠️ Potential issue | 🟠 Major

Move .package-map.json write before lifecycle scripts that consume NODE_OPTIONS.

When node_experimental_package_map is enabled with isolated linking, lifecycle scripts receive NODE_OPTIONS pointing at .package-map.json before it's written. The package-map write currently happens ~70 lines after run_build_phase starts, causing build scripts to fail if they try to read the map file.

Move the write to after materialization/linking (where layout is finalized) and before run_build_phase. This is safe: all required data (layout, built_lockfile, importer_manifests, config, node_linker) is available at that point, and the hoisted linker path uses a separate write_hoisted_package_map function so there's no duplication.

🤖 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_with_fresh_lockfile.rs` around
lines 1806 - 1818, The `.package-map.json` file is being written approximately
70 lines after `run_build_phase` starts, but the `NODE_OPTIONS` environment
variable is already being configured to point to this file in the
build_extra_env HashMap. This causes lifecycle scripts to fail because they
receive NODE_OPTIONS pointing to a file that doesn't exist yet. Move the
package-map file write operation to occur after the materialization and linking
phase (where layout is finalized) but before run_build_phase is called. At this
point, all required data (layout, built_lockfile, importer_manifests, config,
and node_linker) will be available. Since the hoisted linker uses the separate
write_hoisted_package_map function, there is no code duplication concern with
this refactoring.
🤖 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.

Outside diff comments:
In `@pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs`:
- Around line 1806-1818: The `.package-map.json` file is being written
approximately 70 lines after `run_build_phase` starts, but the `NODE_OPTIONS`
environment variable is already being configured to point to this file in the
build_extra_env HashMap. This causes lifecycle scripts to fail because they
receive NODE_OPTIONS pointing to a file that doesn't exist yet. Move the
package-map file write operation to occur after the materialization and linking
phase (where layout is finalized) but before run_build_phase is called. At this
point, all required data (layout, built_lockfile, importer_manifests, config,
and node_linker) will be available. Since the hoisted linker uses the separate
write_hoisted_package_map function, there is no code duplication concern with
this refactoring.

In `@pacquet/crates/package-manager/src/install.rs`:
- Around line 1461-1486: The condition gating the current lockfile persistence
logic is checking only the explicit `frozen_lockfile` flag, but it should also
cover the auto-frozen path. Change the condition in the if statement that
persists the filtered_current_lockfile to use `take_frozen_path` instead of
`frozen_lockfile` so that both explicit frozen installs and auto-frozen
materializations properly update the virtual_store_dir/lock.yaml file and enable
the current-lockfile fast path for subsequent installs.
- Around line 660-668: The fast path early returns in the function (where
fast_path_safe is checked, and the similar locations mentioned at lines
1080-1108) exit without ensuring the package-map file exists on disk. Either add
a check to verify that node_modules/.package-map.json exists before taking the
fast path return, or ensure the package-map is written to disk before returning
early from these fast path branches. This ensures the on-disk artifact remains
in sync and is always present after pacquet install completes, even on
up-to-date installs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 957bee58-0209-4442-a1c4-7b9c3dd18242

📥 Commits

Reviewing files that changed from the base of the PR and between fb27ba2 and 5917ef5.

📒 Files selected for processing (5)
  • pacquet/crates/package-manager/src/install.rs
  • pacquet/crates/package-manager/src/install_frozen_lockfile.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
  • pacquet/crates/package-manager/src/package_map.rs
  • pacquet/crates/package-manager/src/package_map/tests.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • pacquet/crates/package-manager/src/package_map/tests.rs
  • pacquet/crates/package-manager/src/package_map.rs

- Detect the path flavor of a `link:` target from the path after the prefix,
  so a Windows-absolute target (`link:C:\...`) is not misread as relative on a
  POSIX lockfile dir.
- Quote the `--require` module path the same way the package-map path is
  quoted, so paths with whitespace/quotes/backslashes survive Node's
  NODE_OPTIONS tokenizer.
- Write pacquet's `.package-map.json` through the hardened atomic writer
  (`pacquet_fs::ensure_file`) instead of `std::fs::write`, so it never follows
  a pre-seeded symlink or leaves a torn file.

Written by an agent (Claude Code, claude-opus-4-8).
Comment thread lockfile/to-pnp/src/packageMap.ts
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

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

…nk url

- Move the fresh install path's `.package-map.json` write to before the build
  phase. The build env points lifecycle scripts' NODE_OPTIONS at the map, so
  writing it afterwards left isolated-linker scripts pointing at a missing file.
  Now mirrors the frozen path, which already writes before its build phase.
- toRelativeUrl: when `from` and `to` have different path flavors (POSIX dir vs
  Windows-absolute link target), emit a file:// URL instead of letting
  path.win32.relative produce a bogus relative string.

Written by an agent (Claude Code, claude-opus-4-8).
Copilot AI review requested due to automatic review settings June 18, 2026 07:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@zkochan

zkochan commented Jun 18, 2026

Copy link
Copy Markdown
Member

Addressing the three CodeRabbit outside diff range comments from this review:

1. install_with_fresh_lockfile.rs — write .package-map.json before the build phase — fixed in 99e498842c. This was a real bug I introduced when adding the fresh-path write: build_extra_env points lifecycle scripts' NODE_OPTIONS at the map, but the write happened after run_build_phase, so isolated-linker scripts saw a missing file. The write now runs after materialization and before the build phase, mirroring the frozen path.

2. install.rs:1461-1486 — persist the current lockfile for auto-frozen installs (take_frozen_path vs frozen_lockfile) — flagging, not fixing here. This looks like a legitimate correctness gap, but it concerns <virtual_store_dir>/lock.yaml persistence / frozen-path dispatch, which is orthogonal to package-map generation. Changing the gate alters when the current lockfile is persisted and thus the up-to-date fast path on subsequent installs, which I'd want to validate against the auto-frozen materialization path before touching. Happy to fix it in this PR if you'd like, otherwise it seems better as a focused follow-up.

3. install.rs:660-668 / 1080-1108 — up-to-date fast paths skip package-map creation — flagging for a decision. This is a real feature-completeness gap relative to the "always generate" objective: a project installed before this feature (or with the map deleted) can report "up to date" without .package-map.json. The pnpm CLI has the same gap on its checkDepsStatus fast path, so a parity-preserving fix would gate both stacks' fast paths on the map's presence (a missing map forces a normal install). Since that changes up-to-date detection on the hot path in both stacks, I'd like your call on whether to include it here or split it out.


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

Comment thread installing/deps-installer/src/install/index.ts
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 99e4988

…self-key

- Restore the `.package-map.json` entry in the deps-restorer "no symlinks but
  with PnP" test assertion, which a rebase auto-merge had dropped (the install
  writes the map for this isolated-linker-with-PnP case).
- Build the package-map `importerNames` from `manifest.name` only, without
  falling back to the importer id. An unnamed project would otherwise emit a
  non-package-name self key (e.g. `.`) in `.package-map.json`. Matches pacquet,
  which already omits the self-mapping when a project has no name.

Written by an agent (Claude Code, claude-opus-4-8).
Comment thread lockfile/to-pnp/src/packageMap.ts
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

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

`.package-map.json` is written on every materializing install and is consumed
by tooling, not read by humans, so drop the indentation: pretty-printing
roughly doubles the on-disk size and adds serialization CPU on the install
path for no benefit. Applies to both the pnpm CLI and pacquet writers.

Written by an agent (Claude Code, claude-opus-4-8).
Copilot AI review requested due to automatic review settings June 18, 2026 08:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 9ee374b

@zkochan zkochan merged commit 0474a9c into pnpm:main Jun 18, 2026
27 of 29 checks passed
@welcome

welcome Bot commented Jun 18, 2026

Copy link
Copy Markdown

Congrats on merging your first pull request! 🎉🎉🎉

@zkochan

zkochan commented Jun 18, 2026

Copy link
Copy Markdown
Member

Thanks @arcanis!

zkochan added a commit that referenced this pull request Jun 18, 2026
The 'exec should merge node options with PnP require option' test (added in
#12430) hardcoded an unquoted '--require=<path>' expectation. On Windows the
.pnp.cjs path contains backslashes, which makeNodeRequireOption quotes and
escapes for Node's NODE_OPTIONS tokenizer, so the assertion mismatched and the
test failed on Windows runners.

Derive the expected NODE_OPTIONS from makeNodeRequireOption itself so the
expectation matches the implementation's quoting on every platform.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants