Skip to content

feat(packages): merge package.json keys three-way against the merge-base - #25

Open
flipvh wants to merge 3 commits into
mainfrom
feat/package-json-three-way
Open

flipvh wants to merge 3 commits into
mainfrom
feat/package-json-three-way

Conversation

@flipvh

@flipvh flipvh commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

The three sync CLI items from raak's 2026-09-24 feedback list (the cella-side items are in cellajs/cella#1201 (cellajs/cella#1201)).

Three-way package.json key merge

packageJsonSync was add-and-bump only (safeMergeRecord): an entry upstream dropped stayed forever, a script body never updated, a range upstream lowered on purpose never followed. After one sync raak still carried 22 dependency entries upstream removed in cella#1190, pg-format in three workspaces, a geoip:download script whose target the same sync deleted, and boot:build --target node24 after the Node 26 migration.

Every record key and packageManager now merge against the merge-base package.json, read with git show <base>:<path> where base is getEffectiveMergeBase (the recorded sync point, else git's merge-base):

Entry state Result
new upstream added
fork value equals base, upstream changed it follows upstream (update, downgrade, rewritten script)
fork value equals base, upstream dropped it removed, unless fork code still uses it (below)
fork changed it kept; a strictly higher upstream version still bumps it, never for scripts
fork removed it (in base, absent in fork) stays removed
no base available previous add-and-bump behaviour

exports and pnpm.packageExtensions stay add-only. README section updated.

A new upstream workspace arrives with its package.json

Every package.json is managed (always ignored by the file merge) and the sync only visited locations the fork already had, so oauth/ arrived with README.md, src/ and tsconfig.json but no package.json while pnpm-workspace.yaml already listed it. A location the fork lacks whose base also lacks it is a workspace upstream added since the last sync: its package.json is copied verbatim when the merge brought the directory and the path is not ignored. A package.json the base had and the fork deleted stays deleted.

analyze no longer flags managed package.jsons as masking pins

isPinned() treats managed files as pinned, and findMaskingPins did not exclude them, so analyze warned about cdc/, sdk/, studio/ and yjs/package.json although the package sync reconciles their keys.

A dropped entry stays while fork code still uses it

The fork can use an inherited entry without touching it in package.json: an import in a fork-only module, a CLI in a fork script, a script name in a fork workflow. The three-way merge alone would remove it. Before removing a dependency or script upstream dropped, the sync searches fork-authored content, meaning files that differ from upstream after the file merge (package.json files and the lockfile excluded) plus scripts the fork added or changed. Upstream no longer uses what it dropped, so a match there is the fork's.

  • Dependencies: a quoted specifier or subpath (imports, requires, config strings), the package an @types/* entry types, and the package's CLI names as whole words, read from its installed bin (typescript is found through tsc). Scoped to the workspace's directory; root dependencies search the whole repo.
  • Scripts: a package manager command that runs the script (pnpm --filter backend geoip:download), searched repo-wide, workflows and lefthook included.

A false match keeps the entry, which is the previous behaviour. Not covered: packages loaded from a computed import(name), scripts started through a glob runner (run-s geoip:*), and pnpm.overrides/patchedDependencies, which code does not import.

The sync now lists every package.json change and warns about each kept entry with the file that uses it:

  backend/package.json
    dependencies.hono: updated
    dependencies.pg-format: removed
  package.json
    scripts.build: updated
    scripts.old:task: removed

⚠ kept 2 entries upstream dropped, the fork still uses them:
    → backend/package.json dependencies.postgres: used in backend/db.ts
    → package.json scripts.geoip:download: used in .github/workflows/g.yml

Verification

pnpm ts, pnpm lint, pnpm build and the full vitest run (18 files, 180 tests) are green. New tests: dependency removal when untouched, kept when changed, not re-added when the fork removed it, downgrade follows upstream, script update when untouched, new-workspace copy, fork-deleted workspace package.json left out, managed files excluded from masking pins, a dependency kept while a fork module imports it, a devDependency kept through the tsc bin a fork script runs, the search scoped to the workspace, a script kept while a fork workflow runs it. The four usage tests fail with the check disabled.

🤖 Generated with Claude Code

flipvh and others added 2 commits September 25, 2026 15:33
The key sync was add-and-bump only: an entry upstream dropped stayed forever, a
script body never updated, and a range upstream lowered on purpose never followed.
After one sync raak still carried 22 dependency entries upstream had removed,
`pg-format` in three workspaces, a script whose target the same sync deleted, and
`--target node24` after the Node 26 migration.

Every record key (dependencies, devDependencies, scripts, engines, overrides,
pnpm.overrides, pnpm.patchedDependencies) and `packageManager` now merge against
the merge-base package.json (the recorded sync point, else git's merge-base). An
entry the fork never touched follows upstream: update, downgrade or removal. An
entry the fork added or changed stays, except that a strictly higher upstream
version still bumps it (never for scripts). An entry the fork removed is not
re-added. Without a base the old add-and-bump behaviour applies.

A workspace upstream added since the base (raak: `oauth/`) used to arrive without
its package.json, since every package.json is managed and the sync only visited
locations the fork already had. Its package.json now lands verbatim when the merge
brought the directory; a package.json the base had and the fork deleted stays
deleted.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Managed files (package.json, the lockfile, the sync config) are always pinned, so
`analyze` warned that `cdc/`, `sdk/`, `studio/` and `yjs/package.json` "match the
previous upstream but changed upstream" although the package sync reconciles their
keys. `findMaskingPins` skips them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The three-way merge removed a dependency or script upstream dropped whenever the fork's
package.json entry still matched the merge-base, even when a fork-only module imported the
package, a fork script ran its CLI, or a fork workflow ran the script. Before removing such an
entry the sync now searches fork-authored content (files that differ from upstream after the
file merge, plus scripts the fork added or changed) and keeps the entry when it finds a use.
Packages are searched within their own workspace, scripts repo-wide.

The sync also prints every package.json change (added, updated, removed) and warns about each
entry it kept, with the file that uses it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.

1 participant