diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 00000000000..9e4646c61cb
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,8 @@
+node_modules/
+target/
+.next/
+build/
+dist/
+
+/templates/
+/fixtures/
diff --git a/.github/workflows/prerelease-comment.yml b/.github/workflows/prerelease-comment.yml
index 7d4089ae1ac..41e7f693a25 100644
--- a/.github/workflows/prerelease-comment.yml
+++ b/.github/workflows/prerelease-comment.yml
@@ -49,7 +49,7 @@ jobs:
A new prerelease is available for testing:
```sh
- npx shadcn@${{ env.BETA_PACKAGE_VERSION }}
+ pnpm dlx shadcn@${{ env.BETA_PACKAGE_VERSION }}
```
- name: "Remove the autorelease label once published"
diff --git a/apps/www/components/announcement.tsx b/apps/www/components/announcement.tsx
index e1354e9e53c..e4406fffe5b 100644
--- a/apps/www/components/announcement.tsx
+++ b/apps/www/components/announcement.tsx
@@ -4,11 +4,11 @@ import { ArrowRight } from "lucide-react"
export function Announcement() {
return (
- New sidebar component
+ Monorepo support
diff --git a/apps/www/config/docs.ts b/apps/www/config/docs.ts
index 2b9ab00179f..92d4ac8075d 100644
--- a/apps/www/config/docs.ts
+++ b/apps/www/config/docs.ts
@@ -71,6 +71,12 @@ export const docsConfig: DocsConfig = {
href: "/docs/cli",
items: [],
},
+ {
+ title: "Monorepo",
+ href: "/docs/monorepo",
+ items: [],
+ label: "New",
+ },
{
title: "Next.js 15 + React 19",
href: "/docs/react-19",
@@ -141,12 +147,6 @@ export const docsConfig: DocsConfig = {
{
title: "Components",
items: [
- {
- title: "Sidebar",
- href: "/docs/components/sidebar",
- items: [],
- label: "New",
- },
{
title: "Accordion",
href: "/docs/components/accordion",
@@ -337,6 +337,11 @@ export const docsConfig: DocsConfig = {
href: "/docs/components/sheet",
items: [],
},
+ {
+ title: "Sidebar",
+ href: "/docs/components/sidebar",
+ items: [],
+ },
{
title: "Skeleton",
href: "/docs/components/skeleton",
diff --git a/apps/www/content/docs/monorepo.mdx b/apps/www/content/docs/monorepo.mdx
new file mode 100644
index 00000000000..94c3fdeb4e0
--- /dev/null
+++ b/apps/www/content/docs/monorepo.mdx
@@ -0,0 +1,175 @@
+---
+title: Monorepo
+description: Using shadcn/ui components and CLI in a monorepo.
+---
+
+
+ **Note:** We're releasing monorepo support in the CLI as __experimental__.
+ Help us improve it by testing it out and sending feedback. If you have any
+ questions, please [reach out to
+ us](https://github.com/shadcn-ui/ui/discussions).
+
+
+Until now, using shadcn/ui in a monorepo was a bit of a pain. You could add
+components using the CLI, but you had to manage where the components
+were installed and manually fix import paths.
+
+With the new monorepo support in the CLI, we've made it a lot easier to use
+shadcn/ui in a monorepo.
+
+The CLI now understands the monorepo structure and will install the components,
+dependencies and registry dependencies to the correct paths and handle imports
+for you.
+
+## Getting started
+
+
+
+### Create a new monorepo project
+
+To create a new monorepo project, run the `init` command. You will be prompted
+to select the type of project you are creating.
+
+```bash
+npx shadcn@canary init
+```
+
+Select the `Next.js (Monorepo)` option.
+
+```bash
+? Would you like to start a new project?
+ Next.js
+❯ Next.js (Monorepo)
+```
+
+This will create a new monorepo project with two workspaces: `web` and `ui`,
+and [Turborepo](https://turbo.build/repo/docs) as the build system.
+
+Everything is set up for you, so you can start adding components to your project.
+
+### Add components to your project
+
+To add components to your project, run the `add` command **in the path of your app**.
+
+```bash
+cd apps/web
+```
+
+```bash
+npx shadcn@canary add [COMPONENT]
+```
+
+The CLI will figure out what type of component you are adding and install the
+correct files to the correct path.
+
+For example, if you run `npx shadcn@canary add button`, the CLI will install the button component under `packages/ui` and update the import path for components in `apps/web`.
+
+If you run `npx shadcn@canary add login-01`, the CLI will install the `button`, `label`, `input` and `card` components under `packages/ui` and the `login-form` component under `apps/web/components`.
+
+### Importing components
+
+You can import components from the `@workspace/ui` package as follows:
+
+```tsx
+import { Button } from "@workspace/ui/components/button"
+```
+
+You can also import hooks and utilities from the `@workspace/ui` package.
+
+```tsx
+import { useTheme } from "@workspace/ui/hooks/use-theme"
+import { cn } from "@workspace/ui/lib/utils"
+```
+
+
+
+## File Structure
+
+When you create a new monorepo project, the CLI will create the following file structure:
+
+```txt
+apps
+└── web # Your app goes here.
+ ├── app
+ │ └── page.tsx
+ ├── components
+ │ └── login-form.tsx
+ ├── components.json
+ └── package.json
+packages
+└── ui # Your components and dependencies are installed here.
+ ├── src
+ │ ├── components
+ │ │ └── button.tsx
+ │ ├── hooks
+ │ ├── lib
+ │ │ └── utils.ts
+ │ └── styles
+ │ └── globals.css
+ ├── components.json
+ └── package.json
+package.json
+turbo.json
+```
+
+## Requirements
+
+1. Every workspace must have a `components.json` file. A `package.json` file tells npm how to install the dependencies. A `components.json` file tells the CLI how and where to install components.
+
+2. The `components.json` file must properly define aliases for the workspace. This tells the CLI how to import components, hooks, utilities, etc.
+
+```json title="apps/web/components.json"
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "../../packages/ui/tailwind.config.ts",
+ "css": "../../packages/ui/src/styles/globals.css",
+ "baseColor": "zinc",
+ "cssVariables": true
+ },
+ "iconLibrary": "lucide",
+ "aliases": {
+ "components": "@/components",
+ "hooks": "@/hooks",
+ "lib": "@/lib",
+ "utils": "@workspace/ui/lib/utils",
+ "ui": "@workspace/ui/components"
+ }
+}
+```
+
+```json title="packages/ui/components.json"
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "tailwind.config.ts",
+ "css": "src/styles/globals.css",
+ "baseColor": "zinc",
+ "cssVariables": true
+ },
+ "iconLibrary": "lucide",
+ "aliases": {
+ "components": "@workspace/ui/components",
+ "utils": "@workspace/ui/lib/utils",
+ "hooks": "@workspace/ui/hooks",
+ "lib": "@workspace/ui/lib",
+ "ui": "@workspace/ui/components"
+ }
+}
+```
+
+3. Ensure you have the same `style`, `iconLibrary` and `baseColor` in both `components.json` files.
+
+By following these requirements, the CLI will be able to install ui components, blocks, libs and hooks to the correct paths and handle imports for you.
+
+## Help us improve monorepo support
+
+We're releasing monorepo support in the CLI as **experimental**. Help us improve it by testing it out and sending feedback.
+
+If you have any questions, please reach out to us on [GitHub Discussions](https://github.com/shadcn-ui/ui/discussions).
diff --git a/package.json b/package.json
index 3dcad02a18d..ae9f892a570 100644
--- a/package.json
+++ b/package.json
@@ -10,8 +10,7 @@
},
"workspaces": [
"apps/*",
- "packages/*",
- "templates/*"
+ "packages/*"
],
"scripts": {
"build": "turbo run build",
diff --git a/packages/shadcn/src/commands/add.ts b/packages/shadcn/src/commands/add.ts
index eedbb6f6dbe..6a0b1e2b1f5 100644
--- a/packages/shadcn/src/commands/add.ts
+++ b/packages/shadcn/src/commands/add.ts
@@ -4,6 +4,7 @@ import { preFlightAdd } from "@/src/preflights/preflight-add"
import { addComponents } from "@/src/utils/add-components"
import { createProject } from "@/src/utils/create-project"
import * as ERRORS from "@/src/utils/errors"
+import { getConfig } from "@/src/utils/get-config"
import { handleError } from "@/src/utils/handle-error"
import { highlighter } from "@/src/utils/highlighter"
import { logger } from "@/src/utils/logger"
@@ -112,7 +113,7 @@ export const add = new Command()
let shouldUpdateAppIndex = false
if (errors[ERRORS.MISSING_DIR_OR_EMPTY_PROJECT]) {
- const { projectPath } = await createProject({
+ const { projectPath, projectType } = await createProject({
cwd: options.cwd,
force: options.overwrite,
srcDir: options.srcDir,
@@ -124,20 +125,25 @@ export const add = new Command()
}
options.cwd = projectPath
- config = await runInit({
- cwd: options.cwd,
- yes: true,
- force: true,
- defaults: false,
- skipPreflight: true,
- silent: true,
- isNewProject: true,
- srcDir: options.srcDir,
- })
-
- shouldUpdateAppIndex =
- options.components?.length === 1 &&
- !!options.components[0].match(/\/chat\/b\//)
+ if (projectType === "monorepo") {
+ options.cwd = path.resolve(options.cwd, "apps/web")
+ config = await getConfig(options.cwd)
+ } else {
+ config = await runInit({
+ cwd: options.cwd,
+ yes: true,
+ force: true,
+ defaults: false,
+ skipPreflight: true,
+ silent: true,
+ isNewProject: true,
+ srcDir: options.srcDir,
+ })
+
+ shouldUpdateAppIndex =
+ options.components?.length === 1 &&
+ !!options.components[0].match(/\/chat\/b\//)
+ }
}
if (!config) {
diff --git a/packages/shadcn/src/commands/init.ts b/packages/shadcn/src/commands/init.ts
index 0cb280cb574..d4311634bea 100644
--- a/packages/shadcn/src/commands/init.ts
+++ b/packages/shadcn/src/commands/init.ts
@@ -86,21 +86,28 @@ export async function runInit(
}
) {
let projectInfo
+ let newProjectType
if (!options.skipPreflight) {
const preflight = await preFlightInit(options)
if (preflight.errors[ERRORS.MISSING_DIR_OR_EMPTY_PROJECT]) {
- const { projectPath } = await createProject(options)
+ const { projectPath, projectType } = await createProject(options)
if (!projectPath) {
process.exit(1)
}
options.cwd = projectPath
options.isNewProject = true
+ newProjectType = projectType
}
projectInfo = preflight.projectInfo
} else {
projectInfo = await getProjectInfo(options.cwd)
}
+ if (newProjectType === "monorepo") {
+ options.cwd = path.resolve(options.cwd, "apps/web")
+ return await getConfig(options.cwd)
+ }
+
const projectConfig = await getProjectConfig(options.cwd, projectInfo)
const config = projectConfig
? await promptForMinimalConfig(projectConfig, options)
diff --git a/packages/shadcn/src/utils/add-components.ts b/packages/shadcn/src/utils/add-components.ts
index ee5342ee944..b0022fc820d 100644
--- a/packages/shadcn/src/utils/add-components.ts
+++ b/packages/shadcn/src/utils/add-components.ts
@@ -1,12 +1,28 @@
-import { type Config } from "@/src/utils/get-config"
+import path from "path"
+import {
+ configSchema,
+ findCommonRoot,
+ findPackageRoot,
+ getWorkspaceConfig,
+ workspaceConfigSchema,
+ type Config,
+} from "@/src/utils/get-config"
import { handleError } from "@/src/utils/handle-error"
import { logger } from "@/src/utils/logger"
-import { registryResolveItemsTree } from "@/src/utils/registry"
+import {
+ fetchRegistry,
+ getRegistryParentMap,
+ getRegistryTypeAliasMap,
+ registryResolveItemsTree,
+ resolveRegistryItems,
+} from "@/src/utils/registry"
+import { registryItemSchema } from "@/src/utils/registry/schema"
import { spinner } from "@/src/utils/spinner"
import { updateCssVars } from "@/src/utils/updaters/update-css-vars"
import { updateDependencies } from "@/src/utils/updaters/update-dependencies"
import { updateFiles } from "@/src/utils/updaters/update-files"
import { updateTailwindConfig } from "@/src/utils/updaters/update-tailwind-config"
+import { z } from "zod"
export async function addComponents(
components: string[],
@@ -24,6 +40,30 @@ export async function addComponents(
...options,
}
+ const workspaceConfig = await getWorkspaceConfig(config)
+ if (
+ workspaceConfig &&
+ workspaceConfig?.ui.resolvedPaths.cwd !== config.resolvedPaths.cwd
+ ) {
+ return await addWorkspaceComponents(components, config, workspaceConfig, {
+ ...options,
+ isRemote:
+ components?.length === 1 && !!components[0].match(/\/chat\/b\//),
+ })
+ }
+
+ return await addProjectComponents(components, config, options)
+}
+
+async function addProjectComponents(
+ components: string[],
+ config: z.infer,
+ options: {
+ overwrite?: boolean
+ silent?: boolean
+ isNewProject?: boolean
+ }
+) {
const registrySpinner = spinner(`Checking registry.`, {
silent: options.silent,
})?.start()
@@ -54,3 +94,166 @@ export async function addComponents(
logger.info(tree.docs)
}
}
+
+async function addWorkspaceComponents(
+ components: string[],
+ config: z.infer,
+ workspaceConfig: z.infer,
+ options: {
+ overwrite?: boolean
+ silent?: boolean
+ isNewProject?: boolean
+ isRemote?: boolean
+ }
+) {
+ const registrySpinner = spinner(`Checking registry.`, {
+ silent: options.silent,
+ })?.start()
+ let registryItems = await resolveRegistryItems(components, config)
+ let result = await fetchRegistry(registryItems)
+ const payload = z.array(registryItemSchema).parse(result)
+ if (!payload) {
+ registrySpinner?.fail()
+ return handleError(new Error("Failed to fetch components from registry."))
+ }
+ registrySpinner?.succeed()
+
+ const registryParentMap = getRegistryParentMap(payload)
+ const registryTypeAliasMap = getRegistryTypeAliasMap()
+
+ const filesCreated: string[] = []
+ const filesUpdated: string[] = []
+ const filesSkipped: string[] = []
+
+ const rootSpinner = spinner(`Installing components.`)?.start()
+
+ for (const component of payload) {
+ const alias = registryTypeAliasMap.get(component.type)
+ const registryParent = registryParentMap.get(component.name)
+
+ // We don't support this type of component.
+ if (!alias) {
+ continue
+ }
+
+ // A good start is ui for now.
+ // TODO: Add support for other types.
+ let targetConfig =
+ component.type === "registry:ui" || registryParent?.type === "registry:ui"
+ ? workspaceConfig.ui
+ : config
+
+ const workspaceRoot = findCommonRoot(
+ config.resolvedPaths.cwd,
+ targetConfig.resolvedPaths.ui
+ )
+ const packageRoot =
+ (await findPackageRoot(workspaceRoot, targetConfig.resolvedPaths.cwd)) ??
+ targetConfig.resolvedPaths.cwd
+
+ // 1. Update tailwind config.
+ if (component.tailwind?.config) {
+ await updateTailwindConfig(component.tailwind?.config, targetConfig, {
+ silent: true,
+ })
+ filesUpdated.push(
+ path.relative(workspaceRoot, targetConfig.resolvedPaths.tailwindConfig)
+ )
+ }
+
+ // 2. Update css vars.
+ if (component.cssVars) {
+ await updateCssVars(component.cssVars, targetConfig, {
+ silent: true,
+ })
+ filesUpdated.push(
+ path.relative(workspaceRoot, targetConfig.resolvedPaths.tailwindCss)
+ )
+ }
+
+ // 3. Update dependencies.
+ await updateDependencies(component.dependencies, targetConfig, {
+ silent: true,
+ })
+
+ // 4. Update files.
+ const files = await updateFiles(component.files, targetConfig, {
+ overwrite: options.overwrite,
+ silent: true,
+ rootSpinner,
+ isRemote: options.isRemote,
+ })
+
+ filesCreated.push(
+ ...files.filesCreated.map((file) =>
+ path.relative(workspaceRoot, path.join(packageRoot, file))
+ )
+ )
+ filesUpdated.push(
+ ...files.filesUpdated.map((file) =>
+ path.relative(workspaceRoot, path.join(packageRoot, file))
+ )
+ )
+ filesSkipped.push(
+ ...files.filesSkipped.map((file) =>
+ path.relative(workspaceRoot, path.join(packageRoot, file))
+ )
+ )
+ }
+
+ rootSpinner?.succeed()
+
+ // Sort files.
+ filesCreated.sort()
+ filesUpdated.sort()
+ filesSkipped.sort()
+
+ const hasUpdatedFiles = filesCreated.length || filesUpdated.length
+ if (!hasUpdatedFiles && !filesSkipped.length) {
+ spinner(`No files updated.`, {
+ silent: options.silent,
+ })?.info()
+ }
+
+ if (filesCreated.length) {
+ spinner(
+ `Created ${filesCreated.length} ${
+ filesCreated.length === 1 ? "file" : "files"
+ }:`,
+ {
+ silent: options.silent,
+ }
+ )?.succeed()
+ for (const file of filesCreated) {
+ logger.log(` - ${file}`)
+ }
+ }
+
+ if (filesUpdated.length) {
+ spinner(
+ `Updated ${filesUpdated.length} ${
+ filesUpdated.length === 1 ? "file" : "files"
+ }:`,
+ {
+ silent: options.silent,
+ }
+ )?.info()
+ for (const file of filesUpdated) {
+ logger.log(` - ${file}`)
+ }
+ }
+
+ if (filesSkipped.length) {
+ spinner(
+ `Skipped ${filesSkipped.length} ${
+ filesUpdated.length === 1 ? "file" : "files"
+ }: (use --overwrite to overwrite)`,
+ {
+ silent: options.silent,
+ }
+ )?.info()
+ for (const file of filesSkipped) {
+ logger.log(` - ${file}`)
+ }
+ }
+}
diff --git a/packages/shadcn/src/utils/create-project.ts b/packages/shadcn/src/utils/create-project.ts
index c8d6a12610a..366798f5f24 100644
--- a/packages/shadcn/src/utils/create-project.ts
+++ b/packages/shadcn/src/utils/create-project.ts
@@ -1,3 +1,4 @@
+import os from "os"
import path from "path"
import { initOptionsSchema } from "@/src/commands/init"
import { getPackageManager } from "@/src/utils/get-package-manager"
@@ -11,6 +12,9 @@ import fs from "fs-extra"
import prompts from "prompts"
import { z } from "zod"
+const MONOREPO_TEMPLATE_URL =
+ "https://codeload.github.com/shadcn-ui/ui/tar.gz/main"
+
export async function createProject(
options: Pick<
z.infer,
@@ -22,11 +26,14 @@ export async function createProject(
...options,
}
- let nextVersion = "14.2.16"
+ let projectType: "next" | "monorepo" = "next"
+ let projectName: string = "my-app"
+ let nextVersion = "15.1.0"
const isRemoteComponent =
options.components?.length === 1 &&
!!options.components[0].match(/\/chat\/b\//)
+
if (options.components && isRemoteComponent) {
try {
const [result] = await fetchRegistry(options.components)
@@ -45,40 +52,41 @@ export async function createProject(
}
if (!options.force) {
- const { proceed } = await prompts({
- type: "confirm",
- name: "proceed",
- message: `The path ${highlighter.info(
- options.cwd
- )} does not contain a package.json file. Would you like to start a new ${highlighter.info(
- "Next.js"
- )} project?`,
- initial: true,
- })
-
- if (!proceed) {
- return {
- projectPath: null,
- projectName: null,
- }
- }
+ const { type, name } = await prompts([
+ {
+ type: "select",
+ name: "type",
+ message: `The path ${highlighter.info(
+ options.cwd
+ )} does not contain a package.json file.\n Would you like to start a new project?`,
+ choices: [
+ { title: "Next.js", value: "next" },
+ { title: "Next.js (Monorepo)", value: "monorepo" },
+ ],
+ initial: 0,
+ },
+ {
+ type: "text",
+ name: "name",
+ message: "What is your project named?",
+ initial: projectName,
+ format: (value: string) => value.trim(),
+ validate: (value: string) =>
+ value.length > 128
+ ? `Name should be less than 128 characters.`
+ : true,
+ },
+ ])
+
+ projectType = type
+ projectName = name
}
const packageManager = await getPackageManager(options.cwd, {
withFallback: true,
})
- const { name } = await prompts({
- type: "text",
- name: "name",
- message: `What is your project named?`,
- initial: "my-app",
- format: (value: string) => value.trim(),
- validate: (value: string) =>
- value.length > 128 ? `Name should be less than 128 characters.` : true,
- })
-
- const projectPath = `${options.cwd}/${name}`
+ const projectPath = `${options.cwd}/${projectName}`
// Check if path is writable.
try {
@@ -95,16 +103,47 @@ export async function createProject(
process.exit(1)
}
- if (fs.existsSync(path.resolve(options.cwd, name, "package.json"))) {
+ if (fs.existsSync(path.resolve(options.cwd, projectName, "package.json"))) {
logger.break()
logger.error(
- `A project with the name ${highlighter.info(name)} already exists.`
+ `A project with the name ${highlighter.info(projectName)} already exists.`
)
logger.error(`Please choose a different name and try again.`)
logger.break()
process.exit(1)
}
+ if (projectType === "next") {
+ await createNextProject(projectPath, {
+ version: nextVersion,
+ cwd: options.cwd,
+ packageManager,
+ srcDir: !!options.srcDir,
+ })
+ }
+
+ if (projectType === "monorepo") {
+ await createMonorepoProject(projectPath, {
+ packageManager,
+ })
+ }
+
+ return {
+ projectPath,
+ projectName,
+ projectType,
+ }
+}
+
+async function createNextProject(
+ projectPath: string,
+ options: {
+ version: string
+ cwd: string
+ packageManager: string
+ srcDir: boolean
+ }
+) {
const createSpinner = spinner(
`Creating a new Next.js project. This may take a few minutes.`
).start()
@@ -117,17 +156,17 @@ export async function createProject(
"--app",
options.srcDir ? "--src-dir" : "--no-src-dir",
"--no-import-alias",
- `--use-${packageManager}`,
+ `--use-${options.packageManager}`,
]
- if (nextVersion.startsWith("15")) {
+ if (options.version.startsWith("15")) {
args.push("--turbopack")
}
try {
await execa(
"npx",
- [`create-next-app@${nextVersion}`, projectPath, "--silent", ...args],
+ [`create-next-app@${options.version}`, projectPath, "--silent", ...args],
{
cwd: options.cwd,
}
@@ -141,9 +180,60 @@ export async function createProject(
}
createSpinner?.succeed("Creating a new Next.js project.")
+}
- return {
- projectPath,
- projectName: name,
+async function createMonorepoProject(
+ projectPath: string,
+ options: {
+ packageManager: string
+ }
+) {
+ const createSpinner = spinner(
+ `Creating a new Next.js monorepo. This may take a few minutes.`
+ ).start()
+
+ try {
+ // Get the template.
+ const templatePath = path.join(os.tmpdir(), `shadcn-template-${Date.now()}`)
+ await fs.ensureDir(templatePath)
+ const response = await fetch(MONOREPO_TEMPLATE_URL)
+ if (!response.ok) {
+ throw new Error(`Failed to download template: ${response.statusText}`)
+ }
+
+ // Write the tar file
+ const tarPath = path.resolve(templatePath, "template.tar.gz")
+ await fs.writeFile(tarPath, Buffer.from(await response.arrayBuffer()))
+ await execa("tar", [
+ "-xzf",
+ tarPath,
+ "-C",
+ templatePath,
+ "--strip-components=2",
+ "ui-shadcn-cli-monorepo/templates/monorepo-next",
+ ])
+ const extractedPath = path.resolve(templatePath, "monorepo-next")
+ await fs.move(extractedPath, projectPath)
+ await fs.remove(templatePath)
+
+ // Run install.
+ await execa(options.packageManager, ["install"], {
+ cwd: projectPath,
+ })
+
+ // Try git init.
+ const cwd = process.cwd()
+ await execa("git", ["--version"], { cwd: projectPath })
+ await execa("git", ["init"], { cwd: projectPath })
+ await execa("git", ["add", "-A"], { cwd: projectPath })
+ await execa("git", ["commit", "-m", "Initial commit"], {
+ cwd: projectPath,
+ })
+ await execa("cd", [cwd])
+
+ createSpinner?.succeed("Creating a new Next.js monorepo.")
+ } catch (error) {
+ createSpinner?.fail("Something went wrong creating a new Next.js monorepo.")
+ handleError(error)
}
}
diff --git a/packages/shadcn/src/utils/get-config.ts b/packages/shadcn/src/utils/get-config.ts
index c82aa3c379e..d3477ae3272 100644
--- a/packages/shadcn/src/utils/get-config.ts
+++ b/packages/shadcn/src/utils/get-config.ts
@@ -2,6 +2,8 @@ import path from "path"
import { highlighter } from "@/src/utils/highlighter"
import { resolveImport } from "@/src/utils/resolve-import"
import { cosmiconfig } from "cosmiconfig"
+import fg from "fast-glob"
+import fs from "fs-extra"
import { loadConfig } from "tsconfig-paths"
import { z } from "zod"
@@ -59,6 +61,10 @@ export const configSchema = rawConfigSchema.extend({
export type Config = z.infer
+// TODO: type the key.
+// Okay for now since I don't want a breaking change.
+export const workspaceConfigSchema = z.record(configSchema)
+
export async function getConfig(cwd: string) {
const config = await getRawConfig(cwd)
@@ -137,3 +143,77 @@ export async function getRawConfig(cwd: string): Promise {
)
}
}
+
+// Note: we can check for -workspace.yaml or "workspace" in package.json.
+// Since cwd is not necessarily the root of the project.
+// We'll instead check if ui aliases resolve to a different root.
+export async function getWorkspaceConfig(config: Config) {
+ let resolvedAliases: any = {}
+
+ for (const key of Object.keys(config.aliases)) {
+ if (!isAliasKey(key, config)) {
+ continue
+ }
+
+ const resolvedPath = config.resolvedPaths[key]
+ const packageRoot = await findPackageRoot(
+ config.resolvedPaths.cwd,
+ resolvedPath
+ )
+
+ if (!packageRoot) {
+ resolvedAliases[key] = config
+ continue
+ }
+
+ resolvedAliases[key] = await getConfig(packageRoot)
+ }
+
+ const result = workspaceConfigSchema.safeParse(resolvedAliases)
+ if (!result.success) {
+ return null
+ }
+
+ return result.data
+}
+
+export async function findPackageRoot(cwd: string, resolvedPath: string) {
+ const commonRoot = findCommonRoot(cwd, resolvedPath)
+ const relativePath = path.relative(commonRoot, resolvedPath)
+
+ const packageRoots = await fg.glob("**/package.json", {
+ cwd: commonRoot,
+ deep: 3,
+ ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/public/**"],
+ })
+
+ const matchingPackageRoot = packageRoots
+ .map((pkgPath) => path.dirname(pkgPath))
+ .find((pkgDir) => relativePath.startsWith(pkgDir))
+
+ return matchingPackageRoot ? path.join(commonRoot, matchingPackageRoot) : null
+}
+
+function isAliasKey(
+ key: string,
+ config: Config
+): key is keyof Config["aliases"] {
+ return Object.keys(config.resolvedPaths)
+ .filter((key) => key !== "utils")
+ .includes(key)
+}
+
+export function findCommonRoot(cwd: string, resolvedPath: string) {
+ const parts1 = cwd.split(path.sep)
+ const parts2 = resolvedPath.split(path.sep)
+ const commonParts = []
+
+ for (let i = 0; i < Math.min(parts1.length, parts2.length); i++) {
+ if (parts1[i] !== parts2[i]) {
+ break
+ }
+ commonParts.push(parts1[i])
+ }
+
+ return commonParts.join(path.sep)
+}
diff --git a/packages/shadcn/src/utils/registry/index.ts b/packages/shadcn/src/utils/registry/index.ts
index 593d3165113..3c021610c29 100644
--- a/packages/shadcn/src/utils/registry/index.ts
+++ b/packages/shadcn/src/utils/registry/index.ts
@@ -281,17 +281,8 @@ export async function registryResolveItemsTree(
names.unshift("index")
}
- let registryDependencies: string[] = []
- for (const name of names) {
- const itemRegistryDependencies = await resolveRegistryDependencies(
- name,
- config
- )
- registryDependencies.push(...itemRegistryDependencies)
- }
-
- const uniqueRegistryDependencies = Array.from(new Set(registryDependencies))
- let result = await fetchRegistry(uniqueRegistryDependencies)
+ let registryItems = await resolveRegistryItems(names, config)
+ let result = await fetchRegistry(registryItems)
const payload = z.array(registryItemSchema).parse(result)
if (!payload) {
@@ -461,3 +452,44 @@ function isUrl(path: string) {
return false
}
}
+
+// TODO: We're double-fetching here. Use a cache.
+export async function resolveRegistryItems(names: string[], config: Config) {
+ let registryDependencies: string[] = []
+ for (const name of names) {
+ const itemRegistryDependencies = await resolveRegistryDependencies(
+ name,
+ config
+ )
+ registryDependencies.push(...itemRegistryDependencies)
+ }
+
+ return Array.from(new Set(registryDependencies))
+}
+
+export function getRegistryTypeAliasMap() {
+ return new Map([
+ ["registry:ui", "ui"],
+ ["registry:lib", "lib"],
+ ["registry:hook", "hooks"],
+ ["registry:block", "components"],
+ ["registry:component", "components"],
+ ])
+}
+
+// Track a dependency and its parent.
+export function getRegistryParentMap(
+ registryItems: z.infer[]
+) {
+ const map = new Map>()
+ registryItems.forEach((item) => {
+ if (!item.registryDependencies) {
+ return
+ }
+
+ item.registryDependencies.forEach((dependency) => {
+ map.set(dependency, item)
+ })
+ })
+ return map
+}
diff --git a/packages/shadcn/src/utils/transformers/index.ts b/packages/shadcn/src/utils/transformers/index.ts
index 3057e66e46d..c9adaf92652 100644
--- a/packages/shadcn/src/utils/transformers/index.ts
+++ b/packages/shadcn/src/utils/transformers/index.ts
@@ -19,6 +19,7 @@ export type TransformOpts = {
config: Config
baseColor?: z.infer
transformJsx?: boolean
+ isRemote?: boolean
}
export type Transformer