Skip to content

fix(scripts): shadcn-sync refuses to silently delete local edits, and compiles the package after it writes - #3035

Merged
os-zhuang merged 1 commit into
mainfrom
claude/shadcn-sync-verify
Jul 30, 2026
Merged

fix(scripts): shadcn-sync refuses to silently delete local edits, and compiles the package after it writes#3035
os-zhuang merged 1 commit into
mainfrom
claude/shadcn-sync-verify

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Completes the work of #3033. That PR stopped the sync writing unresolvable import paths; it did not make --update-all safe 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.tsx carries local additions upstream has never had:

  • a sr-only DialogTitle / DialogDescription on CommandDialog — Radix requires an accessible name
  • contentProps, which is the ADR-0054 C4 testability contract: it lets callers attach a stable locator to the underlying dialog element, because a data-testid spread onto CommandDialog lands on the Radix root, which renders no DOM

Syncing 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. --force overrides.

✗ Refusing to overwrite command.tsx — 35 local line(s) upstream does not have:
    import { Dialog, DialogContent, DialogTitle, DialogDescription } from "./dialog"
    * stable locator + ARIA name to the underlying dialog element — e.g.
    * (ADR-0054 C4). Without this, `data-testid` spread onto `CommandDialog` lands on
    … and 27 more
  Overwriting deletes these. Port them onto the new upstream version by hand,
  or re-run with --force if they are genuinely obsolete.

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:

local-only lines components
0 — syncs cleanly 29
1–5 12
6–20 3
>20 (sidebar 24, command 22) 2

--update-all now reports skipped separately from failed.

2. Compile after writing. Both --update and --update-all then run pnpm --filter @object-ui/components type-check and say plainly whether the package still builds, printing the rollback command on failure. --no-verify skips 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

No changeset — tooling only.

🤖 Generated with Claude Code

… 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>
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Jul 30, 2026 1:00pm

Request Review

@os-zhuang
os-zhuang merged commit e25e300 into main Jul 30, 2026
15 checks passed
@os-zhuang
os-zhuang deleted the claude/shadcn-sync-verify branch July 30, 2026 13:07
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>
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