Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI: Multiple Registries #4950

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions apps/www/content/docs/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,57 @@ description: Latest updates and announcements.
toc: false
---

## September 2024 - CLI Registries

We've improved our support for multiple registries, making it easier to work with components from different sources.

1. Configure multiple registry sources in your `components.json` file. Switch between different component libraries or use private registries alongside the default one.

2. List all components from a specific registry. Get a quick overview of available components without checking documentation.

3. Install components by name once you've configured a registry. No need to use full URLs for each component anymore.

4. Use the new `-r` or `--registry` option to specify which registry to use when adding components.

5. The `-l` or `--list` option lets you view and select from all available registries configured in your project.

6. For named registries with CSS variables, we now add a `data-registry` attribute to your `globals.css`. This allows you to easily enable registry-specific styles by adding the attribute to a parent element in your HTML.

Try it now:

```bash
# Default registry
npx shadcn@latest init

# Another registry
npx shadcn@latest init -u https://platejs.org/r -n plate
```

After initializing, you can update your `components.json` file to customize `aliases.ui` for the new registry:

```json showLineNumbers {9-11} title="components.json"
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
// ...
"registries": {
"plate": {
"url": "https://platejs.org/r",
"style": "default",
"aliases": {
"ui": "@/components/plate-ui"
}
}
}
}
```

Now you can add components from the new registry:

```bash
npx shadcn@latest add -r plate
```

## August 2024 - npx shadcn init

The new CLI is now available. It's a complete rewrite with a lot of new features and improvements. You can now install components, themes, hooks, utils and more using `npx shadcn add`.
Expand Down
94 changes: 87 additions & 7 deletions apps/www/content/docs/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Use the CLI to add components to your project.

<Callout>

**Note:** We just released a new `shadcn` CLI. See the [changelog](/docs/changelog) for more information.
**Note:** We've updated the CLI with support for multiple registries. See the [changelog](/docs/changelog) for more information.

</Callout>

Expand Down Expand Up @@ -42,7 +42,9 @@ Options:
-f, --force force overwrite of existing components.json. (default: false)
-y, --yes skip confirmation prompt. (default: false)
-c, --cwd <cwd> the working directory. defaults to the current directory.
-h, --help display help for command
-u, --url <url> custom registry URL.
-n, --name <name> registry name (requires -u to be set).
-h, --help display help for command
```

## add
Expand Down Expand Up @@ -82,11 +84,13 @@ Arguments:
components the components to add or a url to the component.

Options:
-y, --yes skip confirmation prompt. (default: false)
-o, --overwrite overwrite existing files. (default: false)
-c, --cwd <cwd> the working directory. defaults to the current directory.
-p, --path <path> the path to add the component to.
-h, --help display help for command
-y, --yes skip confirmation prompt. (default: false)
-o, --overwrite overwrite existing files. (default: false)
-c, --cwd <cwd> the working directory. defaults to the current directory.
-p, --path <path> the path to add the component to.
-r, --registry <registry> registry name or URL. (default: https://ui.shadcn.com/r)
-l, --list list all available registries. (default: false)
-h, --help display help for command
```

## Monorepo
Expand All @@ -102,3 +106,79 @@ or
```bash
npx shadcn@latest add alert-dialog -c ./apps/www
```

## Multiple Registries

The CLI supports multiple registry configurations.

1. Initialize your project with a custom registry:
```bash
npx shadcn@latest init -u https://custom-registry.com -n my-registry
```
This adds a new registry named `my-registry` to your `components.json` file. The `-u` option is required when using `-n`.

2. Add components using a specific registry:
```bash
npx shadcn@latest add button -r my-registry
```
Or use the URL directly:
```bash
npx shadcn@latest add button -r https://another-registry.com
```

List available registries before adding a component:
```bash
npx shadcn@latest add -l
```
This prompts you to select from the available registries configured in your project.

4. Add more registries to your project:
```bash
npx shadcn@latest init -u https://another-registry.com -n another-registry
```
This adds another registry to your `components.json` without overwriting the existing configuration.

Registry configurations inherit from the top-level config in your `components.json` file. This is useful for customizing `style`, `aliases.ui`, or `tailwind.prefix` that could differ from the default config.

When you initialize a named registry with CSS variables, the CLI adds a `data-registry` attribute to your `globals.css` file. To enable the styling for a specific registry, use this attribute on a parent element:

```html
<div data-registry="my-registry">
<!-- Content using the "my-registry" components and styles -->
</div>
```

Example `components.json` with multiple registries:

```json
{
"style": "new-york",
"tailwind": {
"config": "tailwind.config.js",
"css": "app/globals.css",
"baseColor": "slate",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
},
"registries": {
"plate": {
"url": "https://platejs.org/r",
"style": "default",
"aliases": {
"ui": "@/components/plate-ui"
}
},
"themed": {
"url": "https://themed.shadcn.com/r",
"tailwind": {
"prefix": "themed-"
}
}
}
}
```

In this example, `plate` registry uses a different path for UI components, while `themed` one uses a different Tailwind prefix.
9 changes: 8 additions & 1 deletion packages/shadcn/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,14 @@
"pub:next": "pnpm build && pnpm publish --no-git-checks --access public --tag next",
"pub:release": "pnpm build && pnpm publish --access public",
"test": "vitest run",
"test:dev": "REGISTRY_URL=http://localhost:3333/r vitest run"
"test:dev": "REGISTRY_URL=http://localhost:3333/r vitest run",
"dev:r1": "cd test/fixtures/next && pnpm dev",
"init:r0": "REGISTRY_URL=http://localhost:3333/r node dist/index.js init -c test/fixtures/next -f",
"init:r1": "node dist/index.js init -c test/fixtures/next -n r1 -u http://localhost:3334/r -f",
"init:r2": "node dist/index.js init -c test/fixtures/next -n r2 -u http://localhost:3334/r -f",
"add:l": "node dist/index.js add -c test/fixtures/next -l",
"add:r0": "node dist/index.js add -c test/fixtures/next",
"add:r1": "node dist/index.js add -c test/fixtures/next -r r1"
},
"dependencies": {
"@antfu/ni": "^0.21.4",
Expand Down
94 changes: 87 additions & 7 deletions packages/shadcn/src/commands/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@ import { logger } from "@/src/utils/logger"
import { getRegistryIndex } from "@/src/utils/registry"
import { updateAppIndex } from "@/src/utils/update-app-index"
import { Command } from "commander"
import deepmerge from "deepmerge"
import { merge } from "diff"
import prompts from "prompts"
import { z } from "zod"

import { resolveConfigPaths, type Config } from "../utils/get-config"

export const addOptionsSchema = z.object({
components: z.array(z.string()).optional(),
yes: z.boolean(),
Expand All @@ -22,6 +26,8 @@ export const addOptionsSchema = z.object({
path: z.string().optional(),
silent: z.boolean(),
srcDir: z.boolean().optional(),
registry: z.string().optional(),
list: z.boolean().optional(),
})

export const add = new Command()
Expand All @@ -46,6 +52,8 @@ export const add = new Command()
"use the src directory when creating a new project.",
false
)
.option("-r, --registry <registry>", "registry name or url")
.option("-l, --list", "list all available registries", false)
.action(async (components, opts) => {
try {
const options = addOptionsSchema.parse({
Expand Down Expand Up @@ -76,10 +84,6 @@ export const add = new Command()
}
}

if (!options.components?.length) {
options.components = await promptForRegistryComponents(options)
}

let { errors, config } = await preFlightAdd(options)

// No components.json file. Prompt the user to run init.
Expand Down Expand Up @@ -107,9 +111,19 @@ export const add = new Command()
silent: true,
isNewProject: false,
srcDir: options.srcDir,
url: options.registry,
})
}

const registryConfig = await getRegistryConfig(config as any, options)

if (!options.components?.length) {
options.components = await promptForRegistryComponents(
options,
registryConfig.url
)
}

let shouldUpdateAppIndex = false
if (errors[ERRORS.MISSING_DIR_OR_EMPTY_PROJECT]) {
const { projectPath } = await createProject({
Expand All @@ -132,6 +146,7 @@ export const add = new Command()
silent: true,
isNewProject: true,
srcDir: options.srcDir,
url: options.registry,
})

shouldUpdateAppIndex =
Expand All @@ -145,7 +160,7 @@ export const add = new Command()
)
}

await addComponents(options.components, config, options)
await addComponents(options.components, registryConfig, options)

// If we're adding a single component and it's from the v0 registry,
// let's update the app/page.tsx file to import the component.
Expand All @@ -158,10 +173,75 @@ export const add = new Command()
}
})

async function getRegistryConfig(
config: Config,
opts: z.infer<typeof addOptionsSchema>
): Promise<Config> {
const { registry } = opts

if (
opts.list &&
config.registries &&
Object.keys(config.registries).length > 0
) {
const { selectedRegistry } = await prompts({
type: "select",
name: "selectedRegistry",
message: "Select a registry:",
choices: [
{ title: "default", value: "default" },
...Object.entries(config.registries).map(([name, reg]) => ({
title: name,
value: name,
})),
],
})

if (selectedRegistry === "default") {
return { ...config }
} else {
return await resolveConfigPaths(opts.cwd, deepmerge(config, config.registries[selectedRegistry]) as Config)
}
}

// If a registry is specified
if (registry) {
// If it's a URL, use it directly
if (registry.startsWith("http://") || registry.startsWith("https://")) {
// Find registry by url
if (config.registries) {
const registryConfig = Object.values(config.registries)?.find(
(reg) => reg.url === registry
)
if (registryConfig) {
return await resolveConfigPaths(opts.cwd, deepmerge(config, registryConfig) as Config)
}
}

return { ...config, url: registry }
}

// If it's a named registry in the config, use that
if (config.registries?.[registry]) {
return await resolveConfigPaths(opts.cwd, deepmerge(config, config.registries[registry]) as Config)
}

// If it's neither a URL nor a known registry name, warn the user and fallback to the default config
logger.warn(
`Registry "${registry}" not found in configuration. Using the default registry.`
)
return { ...config }
}

// If no registry is specified and no registries in config, use the default config
return { ...config }
}

async function promptForRegistryComponents(
options: z.infer<typeof addOptionsSchema>
options: z.infer<typeof addOptionsSchema>,
registryUrl?: string
) {
const registryIndex = await getRegistryIndex()
const registryIndex = await getRegistryIndex(registryUrl)
if (!registryIndex) {
logger.break()
handleError(new Error("Failed to fetch registry index."))
Expand Down
4 changes: 3 additions & 1 deletion packages/shadcn/src/commands/diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const updateOptionsSchema = z.object({
yes: z.boolean(),
cwd: z.string(),
path: z.string().optional(),
registry: z.string().optional(),
})

export const diff = new Command()
Expand All @@ -33,6 +34,7 @@ export const diff = new Command()
"the working directory. defaults to the current directory.",
process.cwd()
)
.option("--registry <url>", "custom registry URL")
.action(async (name, opts) => {
try {
const options = updateOptionsSchema.parse({
Expand All @@ -57,7 +59,7 @@ export const diff = new Command()
process.exit(1)
}

const registryIndex = await getRegistryIndex()
const registryIndex = await getRegistryIndex(options.registry)

if (!registryIndex) {
handleError(new Error("Failed to fetch registry index."))
Expand Down
Loading