Upgrades and diffs
How to take an update to code you own and have already edited, without either losing your changes or silently keeping a bug.
Overview
In a copy-not-dependency model there is no npm update. A fix here does not
reach you; you go and get it, into a file you may have edited, in a codebase
somebody else now maintains.
This is the central cost of the distribution model and it is worth taking seriously rather than discovering during an incident. The good news is that it is a merge problem, and merge problems have known tools. The bad news is that nothing will tell you an upgrade exists unless you build the habit of asking.
Which version do I have?
There is no lockfile here, because there is no dependency to lock. Nothing in an
installed file records where it came from, and no tool can reconstruct it after
the fact. The record that exists is your own commit history, so make it carry
the answer: run add in its own commit, with nothing else in the diff, and put
the item name and the date in the message. Your own changes go in the next
commit. That one habit is the provenance of every copied file you own. It
gives a later three-way merge a base to work from, it tells a reviewer which
hunks arrived from upstream and which are yours, and it is the only thing a
colleague can read a year from now to find out what you took.
npx shadcn@latest add @opsinjs/<item> --diff is a different question and not a
substitute. It tells you how your file differs from upstream today. It cannot
tell you which upstream you started from, so it cannot separate a local edit
from an upstream change you have not taken yet. The commit answers where you
started; the diff answers where you have drifted to.
The upgrade loop
Find out what changed
Read the changelog, then read the commit that installed the file. Those two are the whole answer. Do this before touching anything; an upgrade you cannot describe is one you cannot review.
Diff before you fetch
npx shadcn@latest add @opsinjs/result-card --diffThis shows what would change in your file without writing anything. It is the single most useful flag in the CLI for this model, and the one people discover last.
Decide per hunk, not per file
Some hunks are upstream fixes you want. Some are upstream style changes you do not care about. Some collide with a deliberate local change. Treat them separately. The temptation to accept or reject the whole file is what produces both lost fixes and lost customisations.
Take it
npx shadcn@latest add @opsinjs/result-card --overwriteThen re-apply your local changes from the diff you just read. If your local changes are extensive, a three-way merge is better. See below.
Re-check
Type-check, run your tests, and re-run the accessibility and contrast checks that apply to your project. An upgrade that changes a token pair changes a measurement.
Three-way merge, when you have edited a lot
--overwrite is a two-way operation: upstream replaces yours. When you have
made substantial deliberate changes, do it as a proper three-way merge instead.
The three sides are: the upstream version you originally copied, the upstream version you want, and your current file. Any merge tool can take it from there, and the result is a merge with conflicts you resolve rather than a choice between two whole files.
This only works if you can identify and obtain the version you originally
copied, and nothing in the file tells you which that was. The commit that ran
add is the record, which is why it earns a commit of its own. It is also the
argument for opsinjs serving historical items rather than only the latest.
git merge-file --diff3 mine.tsx base.tsx theirs.tsxMake upgrades reviewable
Four habits that make the difference between a system that can be updated and one that quietly ossifies.
Commit the install. In its own commit, as set out under Which version do I have? above. It is first here because the other three are recoverable without it and nothing recovers it.
Keep local changes small and marked. A comment saying why the local change exists survives a merge; a silent divergence does not, and the next person will "fix" it back.
Prefer composition over editing. Wrapping a component, or passing different content, produces no merge conflict at all. Editing its internals produces one every time. The system is designed to be edited, but not editing it is still cheaper.
Schedule the ask. Nothing pushes an update to you. A quarterly check of what has changed upstream is a small habit that prevents a large migration.
What an upgrade can break
No upgrade path has been exercised in anger: every catalogue row is still
since: unreleased, so there is not yet a second version of anything to upgrade
to. The list below is the set of hazards the design anticipates, and it is a
specification of what upgrade notes will have to cover rather than a report of
observed breakage.
- A token pair moved. Your contrast measurements are stale. Re-run them.
- A data attribute changed. If you styled against
data-status, an attribute rename is a silent visual regression, not a type error. - A part was renamed in a compound component. Wrong nesting produces a component that renders and does nothing, which is worse than one that throws.
- A dependency was added.
registryDependenciesgrew, andaddinstalled something else into your tree. Read the install output. - A clinical default changed. The rarest and the most serious. Any such change carries a changelog entry with a safety note; it is worth reading the changelog for these alone.
Verify it worked
The diff is empty
Re-run add --diff after upgrading. It should report no differences except your
own deliberate local changes.
The install is recorded
Your commit should name the item and the date you took it, because the file will not. That record is what the next upgrade's three-way merge needs, and reconstructing it afterwards is guesswork.
Your local change is still there
Search for it. This is the check people skip and the one that catches a lost customisation.
Troubleshooting
--diff reports the whole file as changed. Line endings or formatting.
Normalise both sides before comparing, and consider whether your formatter runs
over installed files.
--overwrite destroyed a change I needed. Recover from git. Then adopt the
commit-the-install-separately habit, which makes this recoverable by
construction.
I cannot tell which version I originally copied. Nothing in the file says, so unless your commit history does, there is nothing to read. Diff against the current upstream and accept that this one is archaeology.
Upgrading one component pulled in three others. registryDependencies.
Composition is explicit, and this is what explicit looks like.
Next
- Changelog says what actually changed.
- Versioning policy says what a version number is allowed to imply.