fix(scripts): shadcn-sync rewrites the registry paths Shadcn actually serves, and refuses to write a file when it cannot - #3033
Merged
Conversation
… serves, and refuses to write a file when it cannot
`updateComponent` rewrote two alias shapes: `@/lib/utils` and
`@/components/ui/x`. Surveying all 46 registry entries this manifest
points at, the second one **never appears**. Shadcn moved to a different
layout, and what it serves today is:
42 components @/lib/utils ← rewritten
8 components @/registry/default/ui/<name> ← missed
2 components @/registry/default/hooks/<name> ← missed
1 component @/registry/new-york/ui/<name> ← missed
1 component @/registry/default/lib/<name> ← missed
So syncing `calendar`, `carousel`, `command`, `form`, `pagination`,
`sidebar`, `toggle-group` or `chart` left `@/registry/...` specifiers
that do not resolve from `src/ui/` — the script swapped working code for
code that cannot compile, and said `✓ Updated`. That is how `resizable`
ended up hand-patched and eventually reclassified (#3029).
Two changes:
**The rewrite table now covers what is served.** `ui/` maps to a sibling
(`./x`), `hooks/` and `lib/` step up a level (`../hooks/x`, `../lib/x`),
across any `<style>` segment. The older `@/components/ui/x` and `@/hooks/x`
forms stay in the table so a re-pointed source keeps working. Matching is
on the quoted specifier rather than on `from "…"`, so `export … from`,
dynamic `import()` and `vi.mock()` are covered.
**It fails closed.** Any `@/…` left after rewriting is now a hard refusal
to write, naming the specifier and pointing at the table. An enumerated
list of paths silently goes stale the next time Shadcn reorganises; the
guard is what makes that visible instead of catastrophic. Verified with a
hypothetical future layout (`@/registry/tokyo/blocks/button`): flagged,
while known aliases still rewrite.
Also: `--check` and `--diff` now rewrite before comparing, so import-path
style alone no longer reads as drift, and `--diff` warns when `--update`
would refuse. A registry entry shipping more than one file (`toast` ships
3) now says which files it is not writing rather than silently dropping
them.
Verified against all 46 live registry entries: every one rewrites to zero
unmapped aliases, and every rewritten relative specifier resolves on disk.
End-to-end `--update sidebar` — the entry exercising all three shapes —
now reproduces the import block already on main **exactly**, zero
difference; the remaining 21/-24 line delta is genuine upstream drift in
the component body. Restored afterwards; 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
… compiles the package after it writes (#3035) #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: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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 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.
Root-causes the landmine #3029 defused for one component.
pnpm shadcn:update-allcurrently reports✓ Updatedwhile writing components that cannot compile.What was wrong
updateComponentrewrote exactly two alias shapes —@/lib/utilsand@/components/ui/x. I surveyed all 46 registry entries this manifest points at. The second shape never appears. Shadcn reorganised its layout, and what the endpoints serve today is:@/lib/utils@/registry/default/ui/<name>@/registry/default/hooks/<name>@/registry/new-york/ui/<name>@/registry/default/lib/<name>So syncing
calendar,carousel,command,form,pagination,sidebar,toggle-grouporchartleft@/registry/…specifiers behind. Those don't resolve fromsrc/ui/, so the script replaced working code with code that cannot compile — and reported success. That is the mechanism behindresizablebeing hand-patched and eventually reclassified in #3029.The fix
1. The table covers what's actually served.
ui/→ sibling (./x),hooks/→../hooks/x,lib/→../lib/x, across any<style>segment. The older@/components/ui/xand@/hooks/xforms stay in so a re-pointed source keeps working. Matching is on the quoted specifier rather thanfrom "…", soexport … from, dynamicimport()andvi.mock()are covered.2. It fails closed — the part that actually matters. Any
@/…surviving the rewrite is now a hard refusal to write, naming the specifier and pointing at the table:An enumerated list of paths goes stale the next time Shadcn reorganises — that's precisely how this bug arose. The guard converts that from a silent build break into a clear message.
3.
--check/--diffrewrite before comparing, so import-path style alone no longer reads as drift, and--diffwarns when--updatewould refuse.4. Multi-file entries are no longer silently truncated. The script only writes
files[0];toastships 3. It now names the files it isn't writing.Verification
@/registry/tokyo/blocks/buttonis flagged, while known aliases still rewrite--update sidebar— the entry exercising all three shapes — now reproduces the import block already onmainexactly, zero difference. The remaining 21/−24 line delta is genuine upstream drift in the component body, which is what a sync is for. Restored afterwards: no component file changes in this PR--update resizablestill refuses, so fix(components): resizable is a diverged file, not a synced one — stop the sync from breaking the build, and finish the v4 migration in it #3029's defusal is intactnode --check+eslintcleanStill open (not this PR)
Re-syncing wholesale still discards local named-import additions —
commandhas addedDialogTitle/DialogDescription,sonnerhas addedtoast. Those break loudly rather than silently, but they mean--update-allremains unsafe to run unattended. Worth a follow-up.No changeset — tooling only, no package output.
🤖 Generated with Claude Code