feat: add support for package maps#12430
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR adds support for generating Node.js experimental package maps ( ChangesNode.js Package Map Feature
Yarn v2 Lockfile Parsing Fix
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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
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.jsonduring isolated/hoisted installs (Rust + TS implementations). - Add config/settings + CLI wiring for
node-experimental-package-mapandnode-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.
Code Review by Qodo
1. Pacquet map write clobbers symlinks
|
PR Summary by Qodofeat: generate Node.js package maps and optionally inject --experimental-package-map WalkthroughsDescription• 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. Diagramgraph 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
High-Level AssessmentThe following are alternative approaches to this PR: 1. Only generate the package map when explicitly enabled
2. Generate the package map on-demand (e.g. `pnpm package-map` command)
3. Write to virtual store and symlink/copy into root node_modules
Recommendation: Current approach (always writing a single root File ChangesEnhancement (12)
Tests (2)
Documentation (1)
Other (9)
|
There was a problem hiding this comment.
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 | 🔴 CriticalPacquet 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.jsoncorrectly, but itsrun_projects_lifecycle_scriptsfunction (pacquet line 1774) initializesextra_envas empty and never injectsconfig.node_options. Pnpm's non-delegated lifecycle script path injects NODE_OPTIONS with--experimental-package-mapbefore 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 winAdd 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
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (24)
.changeset/package-maps.mdconfig/reader/src/Config.tsconfig/reader/src/configFileKey.tsconfig/reader/src/index.tsconfig/reader/src/types.tsexec/commands/src/exec.tsexec/commands/src/run.tsexec/commands/src/runRecursive.tsexec/lifecycle/src/index.tsinstalling/commands/src/add.tsinstalling/commands/src/install.tsinstalling/commands/src/remove.tsinstalling/commands/src/update/index.tsinstalling/deps-installer/src/install/extendInstallOptions.tsinstalling/deps-installer/src/install/index.tsinstalling/deps-installer/test/install/misc.tsinstalling/deps-restorer/src/index.tslockfile/to-pnp/src/index.tslockfile/to-pnp/src/packageMap.tslockfile/to-pnp/test/index.tspacquet/crates/package-manager/Cargo.tomlpacquet/crates/package-manager/src/install.rspacquet/crates/package-manager/src/lib.rspacquet/crates/package-manager/src/package_map.rs
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
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
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
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
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
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
]
}
]
} |
|
| Branch | pr/12430 |
| Testbed | pacquet |
Click to view all benchmark results
| Benchmark | Latency | Benchmark Result milliseconds (ms) (Result Δ%) | Upper Boundary milliseconds (ms) (Limit %) |
|---|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 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%) |
|
| Branch | pr/12430 |
| Testbed | pnpr |
⚠️ 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-thresholdsflag.
Click to view all benchmark results
| Benchmark | Latency | milliseconds (ms) |
|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot | 2,150.55 ms |
| isolated-linker.fresh-install.cold-cache.hot-store | 📈 view plot | 663.16 ms |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot | 661.79 ms |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot | 2,136.93 ms |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot | 682.95 ms |
so we should wait for the node release first? performance seems ok. So, I don't have objections. |
|
Code review by qodo was updated up to the latest commit 73e7d26 |
Benchmark Results
Run 27748494396 · runs per scenario · triggered by @zkochan |
Codecov Report❌ Patch coverage is 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. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
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 winGenerate the package map for auto-frozen installs too.
When
take_frozen_pathis true viapreferFrozenLockfilebutfrozen_lockfileis false, the frozen branch returnsfresh_lockfile = Noneandfiltered_current_lockfilestaysNone, so this package-map block skips.package-map.jsoneven after materializingnode_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 { NoneAs 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 winGenerate the package map on the fresh isolated path too.
Right now the fresh install only writes
.package-map.jsonwhenis_hoistedroutes throughrun_hoisted_linker; the default isolated fresh path falls through without anywrite_package_mapcall, 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
📒 Files selected for processing (31)
exec/commands/src/exec.tsexec/commands/src/run.tsexec/commands/src/runRecursive.tsexec/commands/test/exec.tsexec/lifecycle/src/index.tsexec/lifecycle/test/index.tsinstalling/deps-installer/src/install/index.tsinstalling/deps-installer/test/install/misc.tsinstalling/deps-restorer/src/index.tslockfile/to-pnp/src/packageMap.tslockfile/to-pnp/test/index.tspacquet/crates/cli/src/cli_args/exec.rspacquet/crates/cli/src/cli_args/run.rspacquet/crates/cli/src/cli_args/run/recursive.rspacquet/crates/cli/tests/hoisted_node_linker.rspacquet/crates/config/src/env_overlay.rspacquet/crates/config/src/env_overlay/tests.rspacquet/crates/config/src/lib.rspacquet/crates/config/src/pnpm_default_parity.rspacquet/crates/config/src/tests.rspacquet/crates/config/src/workspace_yaml.rspacquet/crates/config/src/workspace_yaml/tests.rspacquet/crates/package-manager/src/build_modules.rspacquet/crates/package-manager/src/build_modules/tests.rspacquet/crates/package-manager/src/install.rspacquet/crates/package-manager/src/install/tests.rspacquet/crates/package-manager/src/install_frozen_lockfile.rspacquet/crates/package-manager/src/install_package_from_registry/tests.rspacquet/crates/package-manager/src/install_with_fresh_lockfile.rspacquet/crates/package-manager/src/lib.rspacquet/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
There was a problem hiding this comment.
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-codesnode_modulesinstead of using the configured modules directory. Ifconfig.modules_diris customized (or differs per-workspace), this can fail to locate an existing.package-map.jsonand silently skip injecting--experimental-package-map. Prefer deriving the lookup path fromconfig.modules_dir(and/or its basename relative toworkspace_dir/dir) so execution-time resolution matches wherewrite_package_map()wrote the file.
pacquet/crates/config/src/lib.rs:1- This doc comment is now inaccurate: the PR adds support for
Loosemaps (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 whatStandardvsLoosemeans).
pacquet/crates/package-manager/src/build_modules/tests.rs:1 - Using
&HashMap::new()here creates a reference to a temporary. If theBuildModules { ... }struct is stored in a local variable (common in tests), this typically triggersE0716: temporary value dropped while borrowed. Bind the empty map to a named variable (e.g.,let extra_env = HashMap::new();) and pass&extra_envinstead (and apply the same fix to the other occurrences in this file).
|
Code review by qodo was updated up to the latest commit 1acfdae |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
pacquet/crates/package-manager/src/package_map.rs (1)
752-800: 💤 Low value
split_node_optionsdoes not handle escape sequences inside quotes.The tokenizer correctly handles quoted strings but doesn't process escape sequences like
\"inside double-quoted values. IfNODE_OPTIONScontains--flag="path with \"quotes\"", the inner escaped quotes would incorrectly terminate the quoted region.This is unlikely in practice since Node's own
NODE_OPTIONSparsing 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (8)
.changeset/fix-yarn-import-js-yaml.mdinstalling/commands/package.jsoninstalling/commands/src/import/index.tspacquet/crates/cli/tests/hoisted_node_linker.rspacquet/crates/package-manager/src/build_modules.rspacquet/crates/package-manager/src/package_map.rspacquet/crates/package-manager/src/package_map/tests.rspnpm-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
|
Code review by qodo was updated up to the latest commit 2084a79 |
There was a problem hiding this comment.
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.jsonfrom the outer install layer, butInstallFrozenLockfilealso writes a package map for isolated installs (seeinstall_frozen_lockfile.rsaround thematches!(node_linker, NodeLinker::Isolated)block). Forfrozen_lockfileinstalls this likely causes redundant serialization + disk IO. Consider consolidating to a single write location (mandatory): either remove the isolated write insideInstallFrozenLockfileand rely on this block, or gate this block to only run whenfresh_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_maptakesconfigbut 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 unusedconfigparameter, or using it to encode the intended policy (e.g., any config flag that should control whether package maps are written).
|
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).
|
Code review by qodo was updated up to the latest commit 5917ef5 |
There was a problem hiding this comment.
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 winPersist the current lockfile for auto-frozen installs too.
take_frozen_pathis true for both explicit--frozen-lockfileand the defaultpreferFrozenLockfileauto-frozen path, butfrozen_lockfileis only the explicit flag. With the current gate, an auto-frozen materialization can installnode_moduleswithout 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 winDon’t let up-to-date fast paths skip package-map creation.
Both early returns can report success without ensuring
node_modules/.package-map.jsonexists. If a project was installed before this feature, or the file was deleted,pacquet installcan 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.jsonmust 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 | 🟠 MajorMove
.package-map.jsonwrite before lifecycle scripts that consumeNODE_OPTIONS.When
node_experimental_package_mapis enabled with isolated linking, lifecycle scripts receiveNODE_OPTIONSpointing at.package-map.jsonbefore it's written. The package-map write currently happens ~70 lines afterrun_build_phasestarts, causing build scripts to fail if they try to read the map file.Move the write to after materialization/linking (where
layoutis finalized) and beforerun_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 separatewrite_hoisted_package_mapfunction 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
📒 Files selected for processing (5)
pacquet/crates/package-manager/src/install.rspacquet/crates/package-manager/src/install_frozen_lockfile.rspacquet/crates/package-manager/src/install_with_fresh_lockfile.rspacquet/crates/package-manager/src/package_map.rspacquet/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).
|
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).
|
Addressing the three CodeRabbit outside diff range comments from this review: 1. 2. 3. Written by an agent (Claude Code, claude-opus-4-8). |
|
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).
|
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).
|
Code review by qodo was updated up to the latest commit 9ee374b |
|
Congrats on merging your first pull request! 🎉🎉🎉 |
|
Thanks @arcanis! |
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.
This PR implements support for generating package maps, similar to what I implemented in Yarn in yarnpkg/berry#7184.
In practice it will:
node_modules/.package-map.jsonfile (so that third-party tooling can easily start experimenting with it, and since it's just a single file it shouldn't impact perfs).node-experimental-package-mapboolean (if yes pnpm will auto-inject the--experimental-package-mapflag in NODE_OPTIONS).node-package-map-typesetting 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
Summary by CodeRabbit
Release Notes
New Features
node_modules/.package-map.jsonand injecting--experimental-package-mapintoNODE_OPTIONS.node-experimental-package-mapandnode-package-map-type(standard/loose) across install,pnpm run, recursive execution, headless install, and to-pnp output.Bug Fixes
js-yaml.Tests
.package-map.json,standardvsloosebehavior, and lifecycle/exec environment injection.