fix(scripts): shadcn-sync refuses to silently delete local edits, and compiles the package after it writes - #3035
Merged
Merged
Conversation
… compiles the package after it writes #3033 stopped the sync writing unresolvable import paths. It did not make `--update-all` safe to run: the other failure mode is overwriting local edits, and I was wrong about how that shows up. I expected a loud type error. It is silent. `command.tsx` is the worked example. It carries local additions upstream has never had — a `sr-only` `DialogTitle`/`DialogDescription` on `CommandDialog` (Radix requires an accessible name) and `contentProps`, which is the ADR-0054 C4 testability contract letting callers attach a stable locator to the dialog element. Syncing it deletes 38 lines, removes both, and **type-checks clean**, because upstream drops each addition together with its usages. Nothing downstream notices. So two layers, not one: **Refuse before writing.** Any line the local file has that the incoming version does not is reported, and the write is skipped. Comments count — a comment explaining why we diverged is exactly what must not evaporate. `--force` overrides. Measured across all 46 registry entries this discriminates rather than crying wolf: 29 have no local-only lines and sync cleanly, 17 are protected. `--update-all` now reports skipped separately from failed. **Compile after writing.** `--update` and `--update-all` then run `pnpm --filter @object-ui/components type-check` and say plainly whether the package still builds, with the rollback command on failure. `--no-verify` skips it. This is the backstop for whatever the first layer does not model — it would not have caught `command`, which is precisely why the first layer exists. Reporting only; neither layer rolls back for you. Auto-reverting would throw away a sync someone may want to fix by hand. Verified end to end: `--update command` refuses and lists the ADR-0054 lines; `--force` overrides it; `--update button` (0 local-only lines) syncs and leaves the tree byte-identical; the #3033 unmapped-path guard and the `resizable` reclassification both still hold. Tree restored — no component file changes here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
os-zhuang
added a commit
that referenced
this pull request
Jul 30, 2026
…46 components "modified" (#3049) The heuristic was `lineDiff > 10 || localContent.includes('ObjectUI') || localContent.includes('data-slot')`. Every synced file carries the ObjectUI header this script prepends, so the second clause was true for all 46 of them: `--check` reported the entire tracked set as "modified", every time, which is the same as reporting nothing. #3035 added `localOnlyLines` for the pre-write guard. It is directional — lines in the first argument the second lacks — so calling it both ways answers the two questions that actually decide whether to sync: what would a sync DESTROY, and what would it GAIN. That replaces the heuristic, and splits the old "modified" bucket in two: ↑ outdated upstream moved, no local edits → safe to sync ⚠ modified local edits present → --update refuses Each line now carries counts, and the summary names the components in each bucket so the next step is obvious without running --diff 46 times. What it says about this repo right now: ✓ Identical: 29 ↑ Outdated: 0 ⚠ Modified: 17 ● Custom: 2 Zero outdated is the headline. Every component where upstream has moved also carries local edits, so there is currently no component `--update-all` could touch without deleting something — it has no safe work to do at all. That was invisible before. The per-component counts line up with the guard that enforces them (`command` 35, `select` 32, `sidebar` 24, `table` 17 …), because both sides call the same function. `--check` stays exit 0: it is a status report, and 17 locally-modified components is the steady state here, not a failure. Verified against the live registry. The `outdated` bucket is the one real traffic never exercises, so it was tested by deleting a line from an identical component (`separator`): it flips to `↑ … 1 upstream line(s) to pick up — safe to sync` and the summary offers the exact --update command. Restored after. `--list`, `--diff`, `--update` and both #3033 / #3035 guards regression-checked; no component file changes. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This was referenced Jul 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Completes the work of #3033. That PR stopped the sync writing unresolvable import paths; it did not make
--update-allsafe to run.I was wrong about how the remaining failure mode shows up
#3033's description said local named-import additions "break loudly rather than silently." Building the compile check disproved that — the first thing I did with it was run
--update command, expecting a type error. It passed.command.tsxcarries local additions upstream has never had:sr-onlyDialogTitle/DialogDescriptiononCommandDialog— Radix requires an accessible namecontentProps, which is the ADR-0054 C4 testability contract: it lets callers attach a stable locator to the underlying dialog element, because adata-testidspread ontoCommandDialoglands on the Radix root, which renders no DOMSyncing it deletes 38 lines, removes both, and type-checks clean — upstream drops each addition together with its usages, so the file stays self-consistent. A documented platform contract would have vanished with a green build.
So: two layers
1. Refuse before writing. Any line the local file has that the incoming version lacks is listed, and the write is skipped.
--forceoverrides.Comments count as local content — a comment explaining why we diverged is exactly what must not evaporate silently.
Measured across all 46 registry entries, so this discriminates rather than crying wolf:
sidebar24,command22)--update-allnow reports skipped separately from failed.2. Compile after writing. Both
--updateand--update-allthen runpnpm --filter @object-ui/components type-checkand say plainly whether the package still builds, printing the rollback command on failure.--no-verifyskips it.This is the backstop for whatever layer 1 doesn't model. It would not have caught
command— which is exactly why layer 1 exists. Keeping both is the point: line-level refusal catches deletions, compilation catches breakage.Neither layer rolls back for you. Auto-reverting would throw away a sync someone may want to fix by hand, so both print the command instead.
Verification
--update command→ refused, lists the ADR-0054 lines--update command --force→ overrides, writes, 38 lines lost as predicted--update button(0 local-only lines) → syncs, tree left byte-identicalresizablereclassification both still holdnode --check+eslintcleanNo changeset — tooling only.
🤖 Generated with Claude Code