Skip to content

Commit 9c5eb0d

Browse files
authored
feat(shadcn): add support for registries index (#8128)
* feat(shadcn): add support for registries index * fix * fix * chore: changeset * feat(shadcn): update handling of add commands * feat: add support for known registries * docs: update index docs
1 parent 2752ce1 commit 9c5eb0d

15 files changed

Lines changed: 361 additions & 96 deletions

File tree

.changeset/good-crews-smile.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"shadcn": minor
3+
---
4+
5+
add support for registries index

apps/v4/content/docs/registry/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"authentication",
88
"examples",
99
"mcp",
10+
"registry-index",
1011
"open-in-v0",
1112
"registry-json",
1213
"registry-item-json"

apps/v4/content/docs/registry/namespace.mdx

Lines changed: 1 addition & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ Registry namespaces are prefixed with `@` and provide a way to organize and refe
4141

4242
## Decentralized Namespace System
4343

44-
We intentionally designed the namespace system to be decentralized. There is no central registrar for namespaces. You are free to create and use any namespace you want.
44+
We intentionally designed the namespace system to be decentralized. There is [central registrar](/docs/registry/registrar) for open source namespaces but you are free to create and use any namespace you want.
4545

4646
This decentralized approach gives you complete flexibility to organize your resources however makes sense for your organization.
4747

@@ -615,82 +615,6 @@ Resolution order:
615615
2. **Circular Dependency Prevention**: Automatically detects and prevents circular dependencies
616616
3. **Smart Installation Order**: Dependencies are installed first, then the resources that use them
617617

618-
We intentionally designed the namespace system to be decentralized. There is no central registrar for namespaces. You are free to create and use any namespace you want.
619-
620-
This decentralized approach gives you complete flexibility to organize your resources however makes sense for your organization.
621-
622-
You can create multiple registries for different purposes:
623-
624-
```json title="components.json" showLineNumbers
625-
{
626-
"registries": {
627-
"@acme-ui": "https://registry.acme.com/ui/{name}.json",
628-
"@acme-docs": "https://registry.acme.com/docs/{name}.json",
629-
"@acme-ai": "https://registry.acme.com/ai/{name}.json",
630-
"@acme-themes": "https://registry.acme.com/themes/{name}.json",
631-
"@acme-internal": {
632-
"url": "https://internal.acme.com/registry/{name}.json",
633-
"headers": {
634-
"Authorization": "Bearer ${INTERNAL_TOKEN}"
635-
}
636-
}
637-
}
638-
}
639-
```
640-
641-
This allows you to:
642-
643-
- **Organize by type**: Separate UI components, documentation, AI resources, etc.
644-
- **Organize by team**: Different teams can maintain their own registries
645-
- **Organize by visibility**: Public vs. private resources
646-
- **Organize by version**: Stable vs. experimental registries
647-
- **No naming conflicts**: Since there's no central authority, you don't need to worry about namespace collisions
648-
649-
### Examples of Multi-Registry Setups
650-
651-
#### By Resource Type
652-
653-
```json title="components.json" showLineNumbers
654-
{
655-
"@components": "https://cdn.company.com/components/{name}.json",
656-
"@hooks": "https://cdn.company.com/hooks/{name}.json",
657-
"@utils": "https://cdn.company.com/utils/{name}.json",
658-
"@prompts": "https://cdn.company.com/ai-prompts/{name}.json"
659-
}
660-
```
661-
662-
#### By Team or Department
663-
664-
```json
665-
{
666-
"@design": "https://design.company.com/registry/{name}.json",
667-
"@engineering": "https://eng.company.com/registry/{name}.json",
668-
"@marketing": "https://marketing.company.com/registry/{name}.json"
669-
}
670-
```
671-
672-
#### By Stability
673-
674-
```json title="components.json" showLineNumbers
675-
{
676-
"@stable": "https://registry.company.com/stable/{name}.json",
677-
"@latest": "https://registry.company.com/beta/{name}.json",
678-
"@experimental": "https://registry.company.com/experimental/{name}.json"
679-
}
680-
```
681-
682-
---
683-
684-
## Built-in Registries
685-
686-
The `@shadcn` namespace is built-in and always available:
687-
688-
```bash
689-
npx shadcn@latest add @shadcn/button
690-
```
691-
692-
This is equivalent to installing from the default shadcn/ui registry.
693-
694618
---
695619

696620
## Versioning
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
---
2+
title: Index
3+
description: Open Source Registry Index
4+
---
5+
6+
The open source registry index is a list of all the open source registries that are available to use out of the box.
7+
8+
When you run `shadcn add` or `shadcn search`, the CLI will automatically check the registry index for the registry you are looking for and add it to your `components.json` file.
9+
10+
You can see the full list at [https://ui.shadcn.com/r/registries.json](https://ui.shadcn.com/r/registries.json).
11+
12+
## Adding a Registry
13+
14+
You can submit a PR to add a registry to the index by adding it to the [registries.json](https://github.com/shadcn-ui/ui/blob/main/apps/v4/public/r/registries.json) file.
15+
16+
Here's an example of how to add a registry to the index:
17+
18+
```json title="registries.json" showLineNumbers
19+
{
20+
"@acme": "https://registry.acme.com/r/{name}.json",
21+
"@example": "https://example.com/r/{name}"
22+
}
23+
```
24+
25+
### Requirements
26+
27+
1. The registry must be open source and publicly accessible.
28+
2. The registry must be a valid JSON file that conforms to the [registry schema specification](/docs/registry/registry-json).
29+
3. The registry is expected to be a flat registry with no nested items i.e `/registry.json` and `/component-name.json` files are expected to be in the root of the registry.
30+
4. The `files` array, if present, must NOT include a `content` property.
31+
32+
Here's an example of a valid registry:
33+
34+
```json title="registry.json" showLineNumbers
35+
{
36+
"$schema": "https://ui.shadcn.com/schema/registry.json",
37+
"name": "acme",
38+
"homepage": "https://acme.com",
39+
"items": [
40+
{
41+
"name": "login-form",
42+
"type": "registry:component",
43+
"title": "Login Form",
44+
"description": "A login form component.",
45+
"files": [
46+
{
47+
"path": "registry/new-york/auth/login-form.tsx",
48+
"type": "registry:component"
49+
}
50+
]
51+
},
52+
{
53+
"name": "example-login-form",
54+
"type": "registry:component",
55+
"title": "Example Login Form",
56+
"description": "An example showing how to use the login form component.",
57+
"files": [
58+
{
59+
"path": "registry/new-york/examples/example-login-form.tsx",
60+
"type": "registry:component"
61+
}
62+
]
63+
}
64+
}
65+
]
66+
}
67+
```
68+
69+
### Validation
70+
71+
At the root of the `shadcn/ui` project, you can run the following command to validate the `registries.json` file.
72+
73+
```bash
74+
pnpm validate:registries
75+
```
76+
77+
This will validate the registries.json file and output any errors.
78+
79+
Once you have submitted your PR, it will be validated and reviewed by the team.

packages/shadcn/src/commands/add.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { getProjectInfo } from "@/src/utils/get-project-info"
1414
import { handleError } from "@/src/utils/handle-error"
1515
import { highlighter } from "@/src/utils/highlighter"
1616
import { logger } from "@/src/utils/logger"
17+
import { ensureRegistriesInConfig } from "@/src/utils/registries"
1718
import { updateAppIndex } from "@/src/utils/update-app-index"
1819
import { Command } from "commander"
1920
import prompts from "prompts"
@@ -76,6 +77,17 @@ export const add = new Command()
7677
})
7778
}
7879

80+
let hasNewRegistries = false
81+
if (components.length > 0) {
82+
const { config: updatedConfig, newRegistries } =
83+
await ensureRegistriesInConfig(components, initialConfig, {
84+
silent: options.silent,
85+
writeFile: false,
86+
})
87+
initialConfig = updatedConfig
88+
hasNewRegistries = newRegistries.length > 0
89+
}
90+
7991
if (components.length > 0) {
8092
const [registryItem] = await getRegistryItems([components[0]], {
8193
config: initialConfig,
@@ -134,6 +146,7 @@ export const add = new Command()
134146
let { errors, config } = await preFlightAdd(options)
135147

136148
// No components.json file. Prompt the user to run init.
149+
let initHasRun = false
137150
if (errors[ERRORS.MISSING_CONFIG]) {
138151
const { proceed } = await prompts({
139152
type: "confirm",
@@ -155,15 +168,18 @@ export const add = new Command()
155168
force: true,
156169
defaults: false,
157170
skipPreflight: false,
158-
silent: true,
171+
silent: options.silent || !hasNewRegistries,
159172
isNewProject: false,
160173
srcDir: options.srcDir,
161174
cssVariables: options.cssVariables,
162175
baseStyle: true,
176+
components: options.components,
163177
})
178+
initHasRun = true
164179
}
165180

166181
let shouldUpdateAppIndex = false
182+
167183
if (errors[ERRORS.MISSING_DIR_OR_EMPTY_PROJECT]) {
168184
const { projectPath, template } = await createProject({
169185
cwd: options.cwd,
@@ -187,12 +203,14 @@ export const add = new Command()
187203
force: true,
188204
defaults: false,
189205
skipPreflight: true,
190-
silent: true,
206+
silent: !hasNewRegistries && options.silent,
191207
isNewProject: true,
192208
srcDir: options.srcDir,
193209
cssVariables: options.cssVariables,
194210
baseStyle: true,
211+
components: options.components,
195212
})
213+
initHasRun = true
196214

197215
shouldUpdateAppIndex =
198216
options.components?.length === 1 &&
@@ -206,7 +224,18 @@ export const add = new Command()
206224
)
207225
}
208226

209-
await addComponents(options.components, config, options)
227+
const { config: updatedConfig } = await ensureRegistriesInConfig(
228+
options.components,
229+
config,
230+
{
231+
silent: options.silent || hasNewRegistries,
232+
}
233+
)
234+
config = updatedConfig
235+
236+
if (!initHasRun) {
237+
await addComponents(options.components, config, options)
238+
}
210239

211240
// If we're adding a single component and it's from the v0 registry,
212241
// let's update the app/page.tsx file to import the component.

packages/shadcn/src/commands/init.ts

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
} from "@/src/registry/api"
99
import { buildUrlAndHeadersForRegistryItem } from "@/src/registry/builder"
1010
import { configWithDefaults } from "@/src/registry/config"
11-
import { BASE_COLORS } from "@/src/registry/constants"
11+
import { BASE_COLORS, BUILTIN_REGISTRIES } from "@/src/registry/constants"
1212
import { clearRegistryContext } from "@/src/registry/context"
1313
import { rawConfigSchema } from "@/src/schema"
1414
import { addComponents } from "@/src/utils/add-components"
@@ -38,6 +38,7 @@ import {
3838
import { handleError } from "@/src/utils/handle-error"
3939
import { highlighter } from "@/src/utils/highlighter"
4040
import { logger } from "@/src/utils/logger"
41+
import { ensureRegistriesInConfig } from "@/src/utils/registries"
4142
import { spinner } from "@/src/utils/spinner"
4243
import { updateTailwindContent } from "@/src/utils/updaters/update-tailwind-content"
4344
import { Command } from "commander"
@@ -175,6 +176,16 @@ export const init = new Command()
175176
createFileBackup(componentsJsonPath)
176177
}
177178

179+
// Ensure all registries used in components are configured.
180+
const { config: updatedConfig } = await ensureRegistriesInConfig(
181+
components,
182+
shadowConfig,
183+
{
184+
silent: true,
185+
}
186+
)
187+
shadowConfig = updatedConfig
188+
178189
// This forces a shadowConfig validation early in the process.
179190
buildUrlAndHeadersForRegistryItem(components[0], shadowConfig)
180191

@@ -266,6 +277,31 @@ export async function runInit(
266277
}
267278
}
268279

280+
// Prepare the list of components to be added.
281+
const components = [
282+
// "index" is the default shadcn style.
283+
// Why index? Because when style is true, we read style from components.json and fetch that.
284+
// i.e new-york from components.json then fetch /styles/new-york/index.
285+
// TODO: Fix this so that we can extend any style i.e --style=new-york.
286+
...(options.baseStyle ? ["index"] : []),
287+
...(options.components ?? []),
288+
]
289+
290+
// Ensure registries are configured for the components we're about to add.
291+
const fullConfigForRegistry = await resolveConfigPaths(options.cwd, config)
292+
const { config: configWithRegistries } = await ensureRegistriesInConfig(
293+
components,
294+
fullConfigForRegistry,
295+
{
296+
silent: true,
297+
}
298+
)
299+
300+
// Update config with any new registries found.
301+
if (configWithRegistries.registries) {
302+
config.registries = configWithRegistries.registries
303+
}
304+
269305
const componentSpinner = spinner(`Writing components.json.`).start()
270306
const targetPath = path.resolve(options.cwd, "components.json")
271307
const backupPath = `${targetPath}${FILE_BACKUP_SUFFIX}`
@@ -279,20 +315,20 @@ export async function runInit(
279315
config = { ...merged, registries }
280316
}
281317

318+
// Make sure to filter out built-in registries.
319+
// TODO: fix this in ensureRegistriesInConfig.
320+
config.registries = Object.fromEntries(
321+
Object.entries(config.registries || {}).filter(
322+
([key]) => !Object.keys(BUILTIN_REGISTRIES).includes(key)
323+
)
324+
)
325+
282326
// Write components.json.
283327
await fs.writeFile(targetPath, `${JSON.stringify(config, null, 2)}\n`, "utf8")
284328
componentSpinner.succeed()
285329

286330
// Add components.
287331
const fullConfig = await resolveConfigPaths(options.cwd, config)
288-
const components = [
289-
// "index" is the default shadcn style.
290-
// Why index? Because when style is true, we read style from components.json and fetch that.
291-
// i.e new-york from components.json then fetch /styles/new-york/index.
292-
// TODO: Fix this so that we can extend any style i.e --style=new-york.
293-
...(options.baseStyle ? ["index"] : []),
294-
...(options.components ?? []),
295-
]
296332
await addComponents(components, fullConfig, {
297333
// Init will always overwrite files.
298334
overwrite: true,

packages/shadcn/src/commands/search.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { rawConfigSchema } from "@/src/schema"
77
import { loadEnvFiles } from "@/src/utils/env-loader"
88
import { createConfig, getConfig } from "@/src/utils/get-config"
99
import { handleError } from "@/src/utils/handle-error"
10+
import { ensureRegistriesInConfig } from "@/src/utils/registries"
1011
import { Command } from "commander"
1112
import fsExtra from "fs-extra"
1213
import { z } from "zod"
@@ -84,6 +85,19 @@ export const search = new Command()
8485
// Use shadow config if getConfig fails (partial components.json).
8586
}
8687

88+
const { config: updatedConfig, newRegistries } =
89+
await ensureRegistriesInConfig(
90+
registries.map((registry) => `${registry}/registry`),
91+
config,
92+
{
93+
silent: true,
94+
writeFile: false,
95+
}
96+
)
97+
if (newRegistries.length > 0) {
98+
config.registries = updatedConfig.registries
99+
}
100+
87101
// Validate registries early for better error messages.
88102
validateRegistryConfigForItems(registries, config)
89103

0 commit comments

Comments
 (0)