Back to core workflows Fix dependency resolution Tune package metadata Validate before publishing

Resolving package-lock.json Merge Conflicts

Two branches that both change dependencies will almost always conflict in package-lock.json, and the conflict markers land in a generated file thousands of lines long. Editing it by hand is slow and error-prone; accepting one side wholesale silently drops the other branch's changes. The reliable fix is to let npm regenerate the lockfile from the merged manifests, starting from the target branch's lockfile. This guide shows the exact sequence, how to automate it with a git merge driver, and how to verify the result.

Exact symptoms and error messages

Git reports the conflict during a merge or rebase:

Auto-merging package-lock.json
CONFLICT (content): Merge conflict in package-lock.json
Auto-merging package.json
Automatic merge failed; fix conflicts and then commit the result.

If conflict markers are committed by mistake, every later install fails to parse the file:

npm error code EJSONPARSE
npm error JSON.parse Invalid package.json: JSONParseError: Unexpected token "<" (0x3C) in JSON at position 48213 while parsing near "...    },\n<<<<<<< HEAD\n    \"node_modules/zod..."
npm error JSON.parse Failed to parse JSON data.

And if a conflict is resolved by picking one side, the next CI run fails with the drift error described in Fixing 'npm ci' Lockfile Out of Sync Errors:

npm error `npm ci` can only install packages when your package.json and package-lock.json are in sync.
npm error Missing: date-fns@4.1.0 from lock file

Root cause analysis

package-lock.json is a serialised tree. A single dependency change can touch dozens of lines: the package entry, its integrity hash, entries for its new transitive dependencies, and the packages[""] block that mirrors the root manifest. Two unrelated dependency changes on different branches often edit neighbouring lines — especially in the alphabetically sorted packages map — so git cannot merge them textually even though they are logically independent. The lockfile's role in the install is covered in Lockfile Management Strategies.

Why two independent changes conflict in the lockfile A main branch and a feature branch both descend from a common commit; each changes a different dependency, but both edit adjacent entries in the sorted packages map. common ancestor lockfile v3 main adds date-fns 4.1.0 feature branch upgrades dayjs 1.11.13 merge conflict in node_modules/d* entries
Logically independent changes collide because they rewrite adjacent lines of one generated file.

The manifests usually merge cleanly because each branch changed a different line of package.json. That is the key insight: the merged manifests are the source of truth, and the lockfile can be derived from them.

Resolution and configuration patch

Resolve package.json conflicts by hand (they are small and meaningful), then rebuild the lockfile.

Regenerating the lockfile after a conflicted merge Resolve package.json by hand, take the target branch lockfile as a base, run npm install to add the other branch's changes, verify with npm ci and continue the merge. resolve package.json by hand, keep both changes checkout --theirs/ours lock take the target branch lockfile npm install adds the other branch's changes npm ci and continue verify, then commit
Start from the target branch's lockfile so only the incoming branch's changes are re-resolved.

During a merge of main into your branch

git merge origin/main
# CONFLICT in package-lock.json (and maybe package.json)

# 1. Fix package.json conflicts manually, keeping both sides' intended changes
$EDITOR package.json

# 2. Start from main's lockfile — the version that is already reviewed
git checkout origin/main -- package-lock.json

# 3. Let npm add your branch's dependency changes on top
npm install

# 4. Verify and conclude
npm ci
git add package.json package-lock.json
git commit

Starting from the target branch's lockfile matters. npm install keeps every locked version that still satisfies the merged manifests, so only your branch's dependency changes are re-resolved. Deleting the lockfile instead would re-resolve the entire tree to the newest allowed versions.

During a rebase

During git rebase, "ours" and "theirs" are swapped: "ours" is the branch you are rebasing onto. The command that takes the upstream lockfile is therefore git checkout --ours package-lock.json. To avoid remembering, name the branch explicitly as above (git checkout origin/main -- package-lock.json), which is unambiguous in both merges and rebases.

npm's built-in conflict handling

npm can repair a lockfile that contains conflict markers by itself: running npm install on a conflicted package-lock.json makes npm parse both sides, merge them, and write a resolved file. It works for simple cases and is a reasonable first attempt. The explicit "take main's lockfile and reinstall" approach is more predictable for large workspaces, because it always produces the same result as a fresh resolution from the merged manifests.

Automating it with a merge driver

A custom git merge driver can perform the regeneration automatically whenever git would otherwise conflict:

# .gitattributes
package-lock.json merge=npm-lockfile
# One-time setup per clone (or add to a bootstrap script)
git config merge.npm-lockfile.name "Regenerate package-lock.json"
git config merge.npm-lockfile.driver "cp %A %A.ours && cp %B %A && npm install --package-lock-only --ignore-scripts && rm %A.ours"

The driver takes the incoming lockfile as the base and asks npm to reconcile it with the working tree's merged package.json using --package-lock-only, which updates the lockfile without touching node_modules. Merge drivers run only after package.json is merged, so a conflict in the manifest still needs manual resolution first. Treat the driver as a convenience; CI's npm ci check remains the safety net.

Workspaces: one lockfile, many manifests

npm workspaces keep a single package-lock.json at the repository root that covers every package, so a conflict in the root lockfile may come from changes to any workspace manifest. Resolve every conflicted package.json first — root and packages — and only then regenerate the lockfile from the root. Running npm install from inside a package directory still operates on the root lockfile, but it can also create a stray nested package-lock.json in older npm versions, which then shadows nothing and confuses reviewers. Delete any nested lockfile you find.

The packages[""] entry at the top of the lockfile mirrors the root manifest, and each workspace appears as a packages["packages/ui"] entry mirroring its manifest. When reviewing a regenerated lockfile, these mirror entries are a quick sanity check: their dependencies blocks should match the merged manifests exactly. If a range there differs from the manifest, npm install did not run against the merged files — usually because the command ran before the manifest conflicts were saved.

Reviewing the result in the pull request

A regenerated lockfile still deserves review, even though a machine produced it. Three questions cover most of what can go wrong. Did any package unrelated to either branch change version? That points to a regeneration that started from the wrong base. Did the resolved URLs stay on the expected registry? A developer with a personal registry mirror can inject a different host into the file. Did integrity hashes change for versions that did not change? That should never happen, and it is worth investigating before merging. Tools such as lockfile-lint automate the registry and protocol checks, as described in Configuring lockfile-lint for Supply-Chain Safety.

Reducing how often it happens

Conflicts scale with the number of branches changing dependencies at once. A few habits cut them sharply:

  • Batch dependency updates. Grouped Renovate or Dependabot pull requests land many updates in one change instead of twenty competing ones — see Reducing Dependabot Noise with Grouping and Schedules.
  • Merge dependency updates quickly. Long-lived branches accumulate lockfile drift; rebasing daily keeps each conflict small.
  • Keep dependency changes out of feature branches when possible, and land them separately first.
  • Use one lockfile format. Pinning npm with packageManager prevents format churn that turns every merge into a conflict.
Lockfile conflicts per week before and after grouping updates Example counts of package-lock.json conflicts per week for ungrouped bot updates, grouped weekly updates, and grouped updates with a merge driver. ungrouped bot PRs 14 weekly grouped PRs 4 grouped + merge driver 1
Grouping bot updates removes most conflicts; a merge driver resolves many of the rest automatically.

CLI validation and debug commands

# Make sure no conflict markers survived
grep -nE "^(<<<<<<<|=======|>>>>>>>)" package-lock.json && echo "markers remain" || echo "clean"

# The lockfile must parse and satisfy the manifests
node -e "JSON.parse(require('fs').readFileSync('package-lock.json','utf8'))" && npm ci --dry-run

# Review what the merge changed compared with main
git diff origin/main -- package-lock.json | grep -E '^\+\s+"node_modules/' | head -40

The last command lists every package entry added relative to main; each should correspond to a dependency change you expect from your branch.

Prevention and CI/CD guardrails

  • Require npm ci on every pull request so a wrongly resolved lockfile cannot merge.
  • Reject conflict markers with a pre-commit hook or a CI grep on package-lock.json.
  • Document the "take main's lockfile, reinstall" procedure in the contributing guide so everyone resolves the same way.
  • Group and schedule dependency updates to reduce concurrent lockfile edits.

Frequently Asked Questions

Should I just delete package-lock.json and run npm install? Only as a last resort. It resolves every dependency afresh, which can upgrade hundreds of transitive packages in a pull request that was supposed to change one. Starting from the target branch's lockfile keeps the change minimal.

Can I hand-edit the conflict if it is tiny? You can, but integrity hashes and nested dependency entries make it easy to produce a file that parses and still does not match the manifests. Regenerating takes a minute and is always correct.

Does this approach work for pnpm and Yarn? Yes. The same idea — resolve manifests, take the target branch's lockfile, reinstall — applies to pnpm-lock.yaml and yarn.lock. pnpm also resolves conflict markers automatically during pnpm install, as described in Fixing pnpm-lock.yaml Merge Conflicts.

What if package.json itself has a conflict in the same dependency? Decide which version the merged code needs — usually the higher of the two if both branches upgraded it — write that range into package.json, and then regenerate the lockfile. The lockfile follows the manifest, so the decision belongs in the manifest.

Why did my merge succeed without conflicts but CI now fails? Git merged the two lockfiles textually without conflicts, yet the combination does not correspond to any real resolution — for example, two branches added different transitive versions that the merged manifests do not permit together. Regenerate the lockfile with npm install and commit the result.

Related

Lockfile Management Strategies