Skip to content

Preserve resolved versions when generating NPM lockfile#176

Merged
0x80 merged 8 commits intomainfrom
thijs/0421-support-npm-lockfiles
Apr 22, 2026
Merged

Preserve resolved versions when generating NPM lockfile#176
0x80 merged 8 commits intomainfrom
thijs/0421-support-npm-lockfiles

Conversation

@0x80
Copy link
Copy Markdown
Owner

@0x80 0x80 commented Apr 21, 2026

Closes #111.

The NPM lockfile generator previously called arborist.buildIdealTree()
on the isolate directory, which ignores the monorepo's root
package-lock.json and resolves dependencies against the isolate's
adapted manifest. The isolated lockfile could therefore contain
different versions and integrity hashes than the monorepo's lockfile,
breaking reproducibility for deployments.

The new implementation uses Arborist only for graph traversal.
loadVirtual at the workspace root hydrates every Node with the
original resolved and integrity from the root lockfile (no registry
calls), and workspaceDependencySet identifies the transitive closure
needed by the target. The isolated package-lock.json is then built
by copying matching entries verbatim from the source lockfile, so
versions and integrity are preserved exactly.

Path remapping handles the target becoming the isolate root:

  • packages/<target> becomes ""
  • packages/<target>/node_modules/<dep> becomes node_modules/<dep>
    — this is the case from NPM lockfile versions do not match #111 where the target pins a different
    version than is hoisted at the workspace root
  • The target's own node_modules/<target> self-link is dropped
  • External node_modules/… paths and internal deps' paths are kept
    verbatim

The root "" entry and each internal dependency entry are overlaid
with their adapted manifest deps so workspace references become file:
references.

When no root package-lock.json exists (forceNpm from pnpm/bun/yarn,
or the modern-Yarn fallback), the generator falls back to the prior
buildIdealTree behavior unchanged.

A checked-in fixture reproduces the #111 scenario (real npm workspaces
monorepo where the target pins semver@^6 and a sibling pins semver@^7,
so npm hoists 7.7.4 at root and nests 6.3.1 under the target). The
integration test runs generateNpmLockfile against it and asserts the
isolated lockfile surfaces the nested 6.3.1 at the isolate root and
excludes the sibling's hoisted 7.7.4.

Scope: lockfile (npm)
Visibility: user-facing

Read the workspace root package-lock.json via Arborist.loadVirtual and
copy entries for the target workspace's transitive closure verbatim,
preserving the original version, resolved, and integrity fields. The
previous buildIdealTree path resolved dependencies against the isolate
manifest and produced lockfiles whose versions could drift from the
monorepo's root lockfile.

Falls back to the prior buildIdealTree behavior when no root
package-lock.json exists (forceNpm from pnpm/bun/yarn, modern Yarn).
Copilot AI review requested due to automatic review settings April 21, 2026 20:49
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Updates the NPM lockfile isolation flow to preserve the monorepo’s already-resolved versions and integrity metadata by subsetting the root package-lock.json, improving deployment reproducibility.

Changes:

  • Pass additional workspace/manifest context into the NPM lockfile generator from processLockfile.
  • Rework NPM lockfile generation to prefer rewriting from the root package-lock.json via Arborist loadVirtual + dependency graph traversal, with a fallback to buildIdealTree when no root lockfile exists.
  • Add unit tests for the pure JSON lockfile rewrite helper (buildIsolatedLockfileJson).

Reviewed changes

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

File Description
src/lib/lockfile/process-lockfile.ts Passes target workspace name/manifest and registry context into generateNpmLockfile.
src/lib/lockfile/process-lockfile.test.ts Updates expectations for the new generateNpmLockfile parameter set.
src/lib/lockfile/helpers/generate-npm-lockfile.ts Implements root-lockfile-based rewrite path, adds pure JSON rewrite helper and fallback path.
src/lib/lockfile/helpers/generate-npm-lockfile.test.ts Adds unit tests validating rewrite behavior (root remap, preserved entries, overrides, etc.).

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

Comment thread src/lib/lockfile/helpers/generate-npm-lockfile.ts
Comment thread src/lib/lockfile/helpers/generate-npm-lockfile.ts Outdated
Comment thread src/lib/lockfile/helpers/generate-npm-lockfile.ts
Comment thread src/lib/lockfile/helpers/generate-npm-lockfile.ts
0x80 added 2 commits April 21, 2026 22:55
When the target workspace has its own nested node_modules in the source
lockfile (a version override different from the hoisted root version),
those entries have source paths like packages/<target>/node_modules/<dep>.
Since the target becomes the isolate root, these need to be remapped
to node_modules/<dep>; otherwise they'd stay at a path that doesn't
exist in the isolate and the target would silently install the wrong
version.

This is exactly the mono-ts firebase-admin scenario from #111.
Checks in a real npm workspaces monorepo as a fixture (root manifest,
package-lock.json from a real npm install, and workspace package.json
files) with the target pinning semver@^6 and a sibling pinning
semver@^7 — so npm hoists 7.7.4 to the root and nests 6.3.1 under the
target package.

The test copies the fixture to a tmp directory and runs the real
generateNpmLockfile (with Arborist loadVirtual) against it, asserting
the isolated lockfile surfaces the nested 6.3.1 at the isolate root's
node_modules and excludes the hoisted 7.7.4 that only the sibling
needs. Reverting either the loadVirtual seed or the nested-path
remap regresses this test.
Copilot AI review requested due to automatic review settings April 21, 2026 20:59
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 10 out of 11 changed files in this pull request and generated 3 comments.

Files not reviewed (1)
  • src/lib/lockfile/helpers/fixtures/nested-version-override/workspace/package-lock.json: Language not supported

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

Comment thread src/lib/lockfile/helpers/generate-npm-lockfile.ts
Comment thread src/lib/lockfile/helpers/generate-npm-lockfile.ts
Comment thread src/lib/lockfile/helpers/generate-npm-lockfile.ts
Comment thread src/lib/lockfile/helpers/generate-npm-lockfile.ts
0x80 added 2 commits April 21, 2026 23:08
- Rename targetLink to targetImporterNode and compute targetLinkLoc
  directly from the package name. workspaceNodes returns the real
  importer Node (not a Link); Node.target getter returns this, which
  let the old code pass tests by coincidence.
- Throw when the target importer entry is missing from srcData.packages,
  and when any reachable node has no corresponding source entry. These
  are invariants that should fail fast rather than produce a malformed
  lockfile silently.
- Spread top-level fields from the source lockfile so unknown fields
  survive isolation, and drop the legacy v2 dependencies tree which
  would be stale after subsetting.
- Add an integration test fixture with three internal workspaces
  (api depends on shared, shared depends on utils) that exercises the
  adapted-manifest overlay path for internal dep lockfile entries.
- When two source entries remap to the same output path with different
  content, throw a descriptive error rather than silently dropping one.
  This happens when the target pins a nested version override that
  collides with a hoisted version still needed by another reachable
  dependency. Identical colliding entries are still allowed (no-op).
- When Arborist's virtual tree has no packages map (e.g. unusual
  lockfile shapes), fall back to buildIdealTree instead of throwing.
- Add unit tests for both the conflict and the no-conflict collision
  cases.
Copilot AI review requested due to automatic review settings April 21, 2026 21:11
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 14 out of 16 changed files in this pull request and generated 2 comments.

Files not reviewed (2)
  • src/lib/lockfile/helpers/fixtures/internal-deps/workspace/package-lock.json: Language not supported
  • src/lib/lockfile/helpers/fixtures/nested-version-override/workspace/package-lock.json: Language not supported

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

Comment thread src/lib/lockfile/helpers/generate-npm-lockfile.ts
Comment thread src/lib/lockfile/helpers/generate-npm-lockfile.ts Outdated
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0937972. Configure here.

Comment thread src/lib/lockfile/helpers/generate-npm-lockfile.ts
0x80 added 2 commits April 22, 2026 07:22
- Only remap nested paths under the target when they live under
  `<target>/node_modules/` — preserves nested workspace importer paths
  like `packages/api/lib/core` so the internal-dep overlay loop finds
  them and so install paths stay correct.
- Throw when the target importer didn't end up in the reachable set
  before overlaying the root entry.
- Stop defaulting `requires: true` when the source lockfile omitted
  it; propagate via the srcData spread only when it was there.
Copilot AI review requested due to automatic review settings April 22, 2026 06:05
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 14 out of 16 changed files in this pull request and generated 1 comment.

Files not reviewed (2)
  • src/lib/lockfile/helpers/fixtures/internal-deps/workspace/package-lock.json: Language not supported
  • src/lib/lockfile/helpers/fixtures/nested-version-override/workspace/package-lock.json: Language not supported

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

Comment thread src/lib/lockfile/helpers/generate-npm-lockfile.ts
@0x80 0x80 merged commit 42a5b06 into main Apr 22, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NPM lockfile versions do not match

2 participants