Skip to content

feat: read shadcn registry folders as read-only design systems, and make exclude add to the defaults - #183

Merged
dgesteves merged 3 commits into
mainfrom
fix/x2-37-registry-aware
Oct 16, 2026
Merged

dgesteves merged 3 commits into
mainfrom
fix/x2-37-registry-aware

Conversation

@dgesteves

Copy link
Copy Markdown
Owner

Roadmap item: X2-37 (registry-aware onsystem: registry folders as read-only secondary design systems, exclude additive, corpus fixture). Builds on #173 (init writes the registry folders), #175 (one exclude matcher) and #176 (installed packages).

What was wrong

Zero config took only the ui folder of components.json as the design system, so code that npx shadcn add copied from other registries was checked as the app's own. On VoltAgent's chatbot starter (pinned below), check . on main gives 7 errors, 7 warnings in 7 of 34 files checked (18 design-system files skipped), and 11 of those 14 findings are in components/ai-elements (no-hardcoded-color 8, prefer-design-system-component 3), code the app did not write. The baseline did not hold either: in a small app with @signoff-ui in components.json, after check . --update-baseline, adding components/ui/textarea.tsx (as shadcn add @ai-elements/prompt-input does) makes main report 1 error, 0 warnings in 1 of 2 files checked (1 in the baseline; …), a new Native <textarea> error in the untouched components/signoff-ui/diff-review.tsx; this branch reports No new problems in 1 file. Agents did not see the registry's components: on main, get_component DiffReview answers No component named "DiffReview", and <DiffReview tone="danger">, <DiffReviewPanel />, <Message size="lg"> and <Message.Header /> pass check without a finding.

Separately, a config's exclude replaced the default list, so "exclude": ["components/signoff-ui/**"] (the advice for registry code) left the folder out and started checking every test, story and .d.ts file.

What changed

Three commits:

  1. exclude adds to the defaults (src/config.ts, schema.json). A pattern that starts with ! takes out the default it names ("!**/*.d.ts"); one that names no default gets a warning. --include-tests still checks tests and stories for one run. A design-system package's exclude adds to the defaults for finding its own components too.
  2. Registry folders are read-only design systems (src/detect.ts, src/design-system.ts, src/explain.ts, src/lint/rules/components.ts, src/server/render.ts). For each registry in the registries of components.json, the folder of that name under the components alias (@signoff-ui → components/signoff-ui) that holds .tsx or .jsx files is added to components and to designSystems with a registry field, after every other design system:
    • check, the hook (check --skip-excluded <file>), ESLint and the Action skip its files, through the same componentFiles they already use for components/ui.
    • The app's code is checked against its components (<DiffReview> has no prop "tone", Message has no member "Header", "DiffReviewPanel" is not a design-system component … Did you mean <DiffReview>?).
    • prefer-design-system-component never suggests a registry component for a native element unless elements maps it, so signoff-ui's internal Img no longer replaces an app's <img>. Where a registry exports a name the primary has (Button), the primary wins, as with several design systems before.
    • get_component adds Installed from the shadcn registry @signoff-ui into components/signoff-ui: use it as it is. …; list_components marks each (from @signoff-ui, read-only); components in the folder's lib/ stay out of list_components and search_components.
    • inspect names the folders on its detected line (components.json (ui: @/components/ui → components/ui); from shadcn registries, read-only: components/ai-elements (@ai-elements, 31 files)), and inspect --explain has a registry @x step for each, with why it was not read (does not exist yet, holds no .tsx/.jsx file yet, inside the ui folder).
    • A config that lists such a folder in components, as init writes it since fix(init): add the shadcn registry folders to the components it writes #173, reads it the same way (rankedSources in src/design-system.ts), so the read-only treatment does not end at init. A folder components.json names in no registries entry is read as before.
    • Not read when a design system found is imported through a package's exports (@workspace/ui in shadcn's monorepo templates): init would then write a config whose importPath names that package for the registry code too. --explain says so.
  3. Corpus fixture (corpus/repos.json, corpus/snapshot.json): VoltAgent's examples/next-js-chatbot-starter-template at 72a46c7, a shadcn app whose components.json names @ai-elements and keeps 31 files in components/ai-elements.

docs/configuration.md: the "Code from shadcn registries" section now starts with what zero config does, and the sentence at line 88 that #173 left ("add the registry folders to its components") is rewritten: init writes them and a config that lists them keeps them read-only. The two configs it gives remain for code zero config does not set apart (loose files, unnamed registries, the monorepo layout). The exclude row says a list adds to the defaults, and the example no longer lists patterns the defaults cover.

init output

src/init.ts is not touched (OS2-38, #182, holds it this wave). Since zero config now finds the registry folders, init writes them from config.components and names them on its Found the design system through … line; its own "Added components/… to components" note now prints only for folders that do not exist yet or hold no component file. The written config is the same set of globs, with a folder that does not exist yet last instead of in alphabetical order (test/init-registry-folders.test.ts updated for both).

Changesets (minor, two)

Behaviour changes for existing configs:

  • A config whose exclude lists folders without repeating the defaults now also leaves out tests, stories, __tests__/__stories__/__mocks__ folders and .d.ts files.
  • A config that lists a registry folder components.json names: its components are no longer suggested for native elements (map one in elements to keep that), get_component labels them, and their lib/ helpers leave list_components and search_components.
  • Zero config in an app whose components.json names registries: their folders' files are no longer reported, and the app's code gets new findings where it misuses their components, such as the icon-only PromptInputButton below.

Files outside the item's listed files

Corpus

pnpm corpus --update (all runs, against a cached checkout), then pnpm corpus: Counts match corpus/snapshot.json. Before adding the new run, pnpm corpus on this branch also printed Counts match corpus/snapshot.json: neither change moves any of the 24 existing runs. The --update table:

Run Rule Before After
voltagent-chatbot icon-button-accessible-name 0 1 (0 fixable)
voltagent-chatbot no-hardcoded-color 0 3 (0 fixable)

The run is new, so "before" is empty. With this branch it checks 3 files (1 error, 3 warnings in 1 of 3 files checked (49 design-system files skipped)); main's CLI on the same checkout gives 7 errors and 7 warnings, as above. The error is new: Icon-only <PromptInputButton> has no accessible name at components/chat-interface.tsx:188, a submit button whose only child is a <Send> icon. It is reported now because the rule knows PromptInputButton renders a button. The 3 warnings (bg-green-500/10, bg-green-500, text-green-600) are reported on main as well. node scripts/corpus.mjs eslint --only voltagent: same as check.

Labels: 290, 244 still reported (as on main); the blind sample has 80 of its 80 labels still reported. corpus/sample-stale.json is kept, so the README still quotes the sample with the corpus it was drawn from (12 repositories, 24 runs); no new labels. The snapshot's version field moved from 0.6.0 to 0.7.0 because --update writes the current package version.

There is no public app with signoff-ui installed through shadcn add to pin yet; test/registry-aware.test.ts covers that shape (a lib/primitives.tsx with Img), and the follow-ups name the corpus part.

How it was tested

New tests, both failing on main (12 of their 15 tests fail there; the other 3 are guards that pass before and after):

  • test/registry-aware.test.ts: a shadcn app with @signoff-ui, @ai-elements and @react-bits in components.json. Registry files skipped by check . and when named (the hook's path), the next shadcn add keeps check passing, invented props and parts reported against registry components, <img> not told to use Img while <button> is still told to use Button, a same-name Button in a registry loses to components/ui's, inspect and --explain text, get_component/search_components/list_components, the ESLint plugin skipping the folders, no registry read without registries or with a workspace package's exports, a config that lists the folders keeping them read-only (and elements mapping Img back), and a config with its own components left as it is.
  • test/exclude-additive.test.ts: defaults kept with a folder in exclude, --include-tests, a list that repeats the defaults, ! taking one out, the warning for a ! that names no default, and an empty list.

Gate, all passing: pnpm install --frozen-lockfile --prefer-offline, pnpm format:check, pnpm lint, pnpm typecheck, pnpm test (78 files passed, 1 skipped; 1014 tests passed, 3 skipped), pnpm build (schema.json committed), pnpm smoke (smoke test passed), pnpm corpus (above).

Follow-ups

  • init: a note of its own for the registry folders zero config already found, with how to check that code as your own (needs src/init.ts, after fix(hook): check the files a Codex apply_patch changes; init offers an AGENTS.md section #182).
  • check_ui on a registry file: a one-line "installed from @x, use it as it is" answer instead of findings (needs src/server/index.ts).
  • The monorepo layout (ui folder from a workspace package's exports): read its registry folders once init keeps them out of a config whose importPath names that package.
  • A public example app with signoff-ui and AI Elements installed through shadcn add, pinned in the corpus, so each release checks the two together; and the CI step from the roadmap row that applies every fix and runs tsc --noEmit.
  • README.md:79 and docs/quickstart.md:18 still describe registry folders as something to set apart by hand; they could say zero config reads the ones components.json names.
  • The MCP server re-runs detection when components.json changes; a registry folder that shadcn add creates while it runs is read at its next start (check and the hook read it at once).

A config's `exclude` replaced the default list, so `"exclude":
["components/signoff-ui/**"]`, the advice for a folder installed from a
registry, left that folder out and started checking every test, story
and .d.ts file. The list now adds to the defaults; a pattern that starts
with `!` takes out the default it names, and one that names no default
gets a warning. A design-system package's list adds to them too.
Zero config took only the ui folder of components.json as the design
system, so code `npx shadcn add` copied from other registries
(components/ai-elements, components/signoff-ui) was checked as the
app's own, and the next `shadcn add` could bring findings in files
nobody edited.

For each registry in the `registries` of components.json, the folder
of that name under the components alias is now a design system of its
own, after the others: check, the hook, ESLint and the Action skip its
files, the app's code is checked against its components, agents find
them (get_component says which registry each came from), none is
suggested for a native element, and its lib/ helpers stay out of
list_components and search_components. inspect names the folders on
its detected line, and --explain says why each was read or not. A
config that lists such a folder, as init writes it, reads it the same
way. Not when the components come from a workspace package imported
through its exports.
…s from a shadcn registry

The app keeps 31 files from @ai-elements in components/ai-elements, as
its components.json names the registry. With this branch, zero config
reads that folder as a read-only design system: the run checks 3 files
and reports 1 error and 3 warnings in components/chat-interface.tsx.
`pnpm corpus --update` wrote the run into the snapshot; the counts of
the other 24 runs are unchanged.
@dgesteves
dgesteves merged commit 7c6ba2b into main Oct 16, 2026
12 checks passed
@dgesteves
dgesteves deleted the fix/x2-37-registry-aware branch October 16, 2026 09:34
@dgesteves-release dgesteves-release Bot mentioned this pull request Oct 16, 2026
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