Repository navigation
fix(arborist): skip legacy metadata work for v3+ lockfiles - #10130
jeffwidman wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
🔵 Needs a closer look
Maintainer judgment is needed on whether removing synthesized node.meta.data.dependencies and changing meta.get() fallback after v3 commits affects supported API behavior and requires a compatibility or release plan.
No open issues 🎉
What changed in this PR
This PR reduces Arborist’s v3 lockfile generation work by skipping legacy dependency traversal while preserving root metadata and v1/v2 generation. Serialized v3 output is intended to remain unchanged; the draft explicitly leaves compatibility of the changed in-memory metadata open for discussion.
Changes:
- Skip legacy descendants for v3 commits and remove stale legacy dependency data.
- Add regression coverage for format compatibility, metadata lookup behavior, and legacy output; update six raw-metadata snapshots.
- Document
commit()andtoJSON()as ways to obtain data in the selected lockfile format.
| File | Description |
|---|---|
workspaces/arborist/test/shrinkwrap.js |
Tests skipped traversal, legacy compatibility, and changed v3 lookup behavior. |
workspaces/arborist/tap-snapshots/test/shrinkwrap.js.test.cjs |
Removes legacy dependency blocks from six v3 metadata snapshots. |
workspaces/arborist/README.md |
Clarifies format-specific metadata access. |
workspaces/arborist/lib/shrinkwrap.js |
Stops legacy traversal for v3 after root processing and deletes legacy dependencies. |
Review effort: Balanced
4174b03 to
fae00da
Compare
fae00da to
d1251fc
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Shrinkwrap handling can return metadata for the wrong node, fail to restore legacy resolution and integrity values, and discard legacy dependency data before a tree is attached.
3 open issues
1 resolved since last review
Review effort: Max
d1251fc to
98c5f89
Compare
Co-authored-by: martin <43253759+martinrrm@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
98c5f89 to
f773a22
Compare
| const inventoryLocation = useTree && this.tree && | ||
| relpath(this.tree.root.realpath, nodePathResolved) | ||
| const node = inventoryLocation && | ||
| this.tree.root.inventory.get(inventoryLocation) |
| const inventoryLocation = useTree && this.tree && | ||
| relpath(this.tree.root.realpath, nodePathResolved) | ||
| const node = inventoryLocation && | ||
| this.tree.root.inventory.get(inventoryLocation) |
| if (node) { | ||
| return Shrinkwrap.metaFromNode(node, this.path, this.resolveOptions) |

Blocked on #10276
Avoid doing legacy metadata work that package-only lockfiles do not use, while preserving older lockfile formats.
Review and landing order
The standalone live-tree metadata lookup bug fix in npm/cli#10276 should be reviewed and decided before this optimization. That PR isolates the independent API behavior question of whether
node.meta.get()should fall back to metadata from the attached live tree when persisted lockfile metadata is absent.This PR currently remains based on
latestbecause GitHub cannot keep it as an npm/cli pull request while targeting the fork-only branch used by #10276. As a result, the current diff temporarily includes the prerequisite bug-fix changes from #10276. After #10276 lands, this branch will be rebased and resquashed so #10130 contains only the version 3+ traversal and serialization optimization described below.Optimization implementation
For lockfile version 3, the legacy builder still performs the required root and header work, then returns before recursively traversing descendants. Version 4 already bypasses the legacy builder because it does not need that representation.
When a tree is attached, the shared version 3+ serialization path removes stale legacy
dependenciesmetadata from retained in-memory state, and returned version 3+ output omits it. Before a tree is attached, legacy input remains available internally for lookup and node hydration, whilecommit()andtoJSON()still omit it from version 3+ returned output. This also lets the same metadata instance switch back to version 1 or version 2 output without losing its legacy source.Lockfile versions 1 and 2 remain readable, and their output retains its existing legacy metadata generation.
Temporary prerequisite changes in the current diff
The following behavior belongs to #10276 and is present here only until that prerequisite lands and this PR is rebased.
meta.get()checks pending updates and modernpackagesmetadata first. If that lookup misses, it derives metadata from the current tree inventory using the inventory root's coordinate system, without caching or serializing the derived value, then falls back to legacy lockfile metadata. This preserves relative and absolute lookups for linked tops as well as the documented live-tree behavior for omitted nodes, such as a versionless inert optional dependency.During
add(), hydration intentionally bypasses the incomplete node already present in the live inventory, so version 1 and version 2 legacyresolvedandintegrityvalues can still decorate newly loaded nodes.Performance evidence
The performance measurements below are specifically for the version 3 flow that skips recursive legacy descendant generation. Version 4 already bypassed that builder, so this PR does not claim an additional version 4 speedup.
In an offline, warmed large-workspace
--package-lock-onlywriter benchmark, adding the version 3 traversal change to a baseline already containing adaptations of #10127, #10128, and npm/hosted-git-info#347 produced:These are three measured alternating pairs after one warmup per arm. All 4,686 manifests/lockfiles plus the npm configuration file were byte-identical across all eight runs, and normalized command output matched. Separate instrumentation showed 4,358 -> 0 generated legacy entries. A public synthetic 128-dependency graph also reduced descendant bundle-membership reads from 256 -> 0 for version 3, while versions 1 and 2 retained their traversal.
The benchmark runtime was Node 24.22.0, npm 11.17.0, and Arborist 9.8.0. This PR currently remains based on
latestfor the review constraint described above. These measurements describe the incremental native writer benefit after the earlier optimizations, not this PR alone versus unmodified npm, nor end-to-end Dependabot job savings. A dry-run checker does not serialize a lockfile and is not expected to benefit.A tiny control over 21 pairs had medians of +2.9% CPU and +0.4% wall time; paired bootstrap intervals crossed zero. That result is inconclusive, not proof of regression-free performance.
Validation
Latest targeted validation of the current combined diff:
test/node.jsandtest/shrinkwrap.js— passed; the Shrinkwrap suite passed all 53 subtests.node.jssnapshot diff is limited to the three linked-top metadata expectations, which now contain the intended live child metadata from the temporary fix(arborist): use live metadata for shrinkwrap lookups #10276 prerequisite changes../node_modules/.bin/eslint workspaces/arborist/lib/shrinkwrap.js workspaces/arborist/test/shrinkwrap.js— passed.git diff --check— passed.add()hydration to use the live-tree fallback made its legacyresolvedandintegrityrestoration regression fail.commit()andtoJSON()retention regressions fail.Earlier validation of the original version 3 traversal optimization ran
node bin/npm-cli.js test --workspace=@npmcli/arborist --ignore-scripts -- --jobs=4and passed all 59 test files with 100% statements, branches, functions, and lines. The full npm CLI suite was not run.The optimization tests cover skipped version 3 descendant work, retained root metadata, version 3 and version 4 stale legacy cleanup, no-tree legacy retention, legacy input, equivalent version 1 and version 2 output, git metadata, aliases, mixed dependency flags, and cyclic links. The temporary #10276 prerequisite tests cover linked-top relative and absolute lookup coordinates, non-caching live-tree lookup for omitted inert nodes, and legacy hydration during
add().References