feat(shadcn): monorepo support (#6104)

This commit is contained in:
shadcn
2024-12-20 19:07:17 +04:00
committed by GitHub
parent ea677cc74e
commit 811bb59a8f
65 changed files with 6663 additions and 86 deletions

8
.eslintignore Normal file
View File

@@ -0,0 +1,8 @@
node_modules/
target/
.next/
build/
dist/
/templates/
/fixtures/

View File

@@ -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"

View File

@@ -4,11 +4,11 @@ import { ArrowRight } from "lucide-react"
export function Announcement() {
return (
<Link
href="/docs/components/sidebar"
href="/docs/monorepo"
className="group mb-2 inline-flex items-center px-0.5 text-sm font-medium"
>
<span className="underline-offset-4 group-hover:underline">
New sidebar component
Monorepo support
</span>
<ArrowRight className="ml-1 h-4 w-4" />
</Link>

View File

@@ -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",

View File

@@ -0,0 +1,175 @@
---
title: Monorepo
description: Using shadcn/ui components and CLI in a monorepo.
---
<Callout>
**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).
</Callout>
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
<Steps>
### 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"
```
</Steps>
## 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).

View File

@@ -10,8 +10,7 @@
},
"workspaces": [
"apps/*",
"packages/*",
"templates/*"
"packages/*"
],
"scripts": {
"build": "turbo run build",

View File

@@ -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,
})
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\//)
shouldUpdateAppIndex =
options.components?.length === 1 &&
!!options.components[0].match(/\/chat\/b\//)
}
}
if (!config) {

View File

@@ -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)

View File

@@ -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<typeof configSchema>,
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<typeof configSchema>,
workspaceConfig: z.infer<typeof workspaceConfigSchema>,
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}`)
}
}
}

View File

@@ -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<typeof initOptionsSchema>,
@@ -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,
})
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,
},
])
if (!proceed) {
return {
projectPath: null,
projectName: null,
}
}
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)
}
}

View File

@@ -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<typeof configSchema>
// 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<RawConfig | null> {
)
}
}
// 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)
}

View File

@@ -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<string, string>([
["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<typeof registryItemSchema>[]
) {
const map = new Map<string, z.infer<typeof registryItemSchema>>()
registryItems.forEach((item) => {
if (!item.registryDependencies) {
return
}
item.registryDependencies.forEach((dependency) => {
map.set(dependency, item)
})
})
return map
}

View File

@@ -19,6 +19,7 @@ export type TransformOpts = {
config: Config
baseColor?: z.infer<typeof registryBaseColorSchema>
transformJsx?: boolean
isRemote?: boolean
}
export type Transformer<Output = SourceFile> = (

View File

@@ -1,24 +1,39 @@
import { Config } from "@/src/utils/get-config"
import { Transformer } from "@/src/utils/transformers"
export const transformImport: Transformer = async ({ sourceFile, config }) => {
const COMMON_CN_IMPORTS = {
"@/lib/utils": /^@\/lib\/utils/,
"@workspace/lib/utils": /^@workspace\/lib\/utils/,
}
export const transformImport: Transformer = async ({
sourceFile,
config,
isRemote,
}) => {
const importDeclarations = sourceFile.getImportDeclarations()
for (const importDeclaration of importDeclarations) {
const moduleSpecifier = updateImportAliases(
importDeclaration.getModuleSpecifierValue(),
config
config,
isRemote
)
importDeclaration.setModuleSpecifier(moduleSpecifier)
// Replace `import { cn } from "@/lib/utils"`
if (moduleSpecifier == "@/lib/utils") {
if (COMMON_CN_IMPORTS[moduleSpecifier as keyof typeof COMMON_CN_IMPORTS]) {
const namedImports = importDeclaration.getNamedImports()
const cnImport = namedImports.find((i) => i.getName() === "cn")
if (cnImport) {
importDeclaration.setModuleSpecifier(
moduleSpecifier.replace(/^@\/lib\/utils/, config.aliases.utils)
moduleSpecifier.replace(
COMMON_CN_IMPORTS[
moduleSpecifier as keyof typeof COMMON_CN_IMPORTS
],
config.aliases.utils
)
)
}
}
@@ -27,12 +42,21 @@ export const transformImport: Transformer = async ({ sourceFile, config }) => {
return sourceFile
}
function updateImportAliases(moduleSpecifier: string, config: Config) {
function updateImportAliases(
moduleSpecifier: string,
config: Config,
isRemote: boolean = false
) {
// Not a local import.
if (!moduleSpecifier.startsWith("@/")) {
if (!moduleSpecifier.startsWith("@/") && !isRemote) {
return moduleSpecifier
}
// This treats the remote as coming from a faux registry.
if (isRemote && moduleSpecifier.startsWith("@/")) {
moduleSpecifier = moduleSpecifier.replace(/^@\//, `@/registry/new-york/`)
}
// Not a registry import.
if (!moduleSpecifier.startsWith("@/registry/")) {
// We fix the alias and return.

View File

@@ -66,6 +66,7 @@ export async function updateDependencies(
cwd: config.resolvedPaths.cwd,
}
)
dependenciesSpinner?.succeed()
}

View File

@@ -38,15 +38,22 @@ export async function updateFiles(
overwrite?: boolean
force?: boolean
silent?: boolean
rootSpinner?: ReturnType<typeof spinner>
isRemote?: boolean
}
) {
if (!files?.length) {
return
return {
filesCreated: [],
filesUpdated: [],
filesSkipped: [],
}
}
options = {
overwrite: false,
force: false,
silent: false,
isRemote: false,
...options,
}
const filesCreatedSpinner = spinner(`Updating files.`, {
@@ -83,8 +90,12 @@ export async function updateFiles(
}
const existingFile = existsSync(filePath)
if (existingFile && !options.overwrite) {
filesCreatedSpinner.stop()
if (options.rootSpinner) {
options.rootSpinner.stop()
}
const { overwrite } = await prompts({
type: "confirm",
name: "overwrite",
@@ -96,9 +107,15 @@ export async function updateFiles(
if (!overwrite) {
filesSkipped.push(path.relative(config.resolvedPaths.cwd, filePath))
if (options.rootSpinner) {
options.rootSpinner.start()
}
continue
}
filesCreatedSpinner?.start()
if (options.rootSpinner) {
options.rootSpinner.start()
}
}
// Create the target directory if it doesn't exist.
@@ -114,6 +131,7 @@ export async function updateFiles(
config,
baseColor,
transformJsx: !config.tsx,
isRemote: options.isRemote,
},
[
transformImport,
@@ -170,7 +188,7 @@ export async function updateFiles(
spinner(
`Skipped ${filesSkipped.length} ${
filesUpdated.length === 1 ? "file" : "files"
}:`,
}: (use --overwrite to overwrite)`,
{
silent: options.silent,
}
@@ -185,4 +203,10 @@ export async function updateFiles(
if (!options.silent) {
logger.break()
}
return {
filesCreated,
filesUpdated,
filesSkipped,
}
}

View File

@@ -1,5 +1,4 @@
packages:
- "apps/*"
- "packages/*"
- "templates/*"
- "!**/test/**"

View File

@@ -10,6 +10,8 @@ module.exports = {
"^(next/(.*)$)|^(next$)",
"<THIRD_PARTY_MODULES>",
"",
"^@workspace/(.*)$",
"",
"^types$",
"^@/types/(.*)$",
"^@/config/(.*)$",

View File

@@ -1,2 +0,0 @@
> [!NOTE]
> The `next-template` has been deprecated. Use `npx shadcn@latest init` to create a new project.

View File

@@ -0,0 +1,10 @@
// This configuration only applies to the package manager root.
/** @type {import("eslint").Linter.Config} */
module.exports = {
ignorePatterns: ["apps/**", "packages/**"],
extends: ["@workspace/eslint-config/library.js"],
parser: "@typescript-eslint/parser",
parserOptions: {
project: true,
},
}

36
templates/monorepo-next/.gitignore vendored Normal file
View File

@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Dependencies
node_modules
.pnp
.pnp.js
# Local env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Testing
coverage
# Turbo
.turbo
# Vercel
.vercel
# Build Outputs
.next/
out/
build
dist
# Debug
npm-debug.log*
# Misc
.DS_Store
*.pem

View File

View File

@@ -0,0 +1,31 @@
# shadcn/ui monorepo template
This template is for creating a monorepo with shadcn/ui.
## Usage
```bash
pnpm dlx shadcn@latest init
```
## Adding components
To add components to your app, run the following command at the root of your `web` app:
```bash
pnpm dlx shadcn@latest add button -c apps/web
```
This will place the ui components in the `packages/ui/src/components` directory.
## Tailwind
Your `tailwind.config.ts` and `globals.css` are already set up to use the components from the `ui` package.
## Using components
To use the components in your app, import them from the `ui` package.
```tsx
import { Button } from "@workspace/ui/components/ui/button"
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,30 @@
import { Geist, Geist_Mono } from "next/font/google"
import "@workspace/ui/globals.css"
import { Providers } from "@/components/providers"
const fontSans = Geist({
subsets: ["latin"],
variable: "--font-sans",
})
const fontMono = Geist_Mono({
subsets: ["latin"],
variable: "--font-mono",
})
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body
className={`${fontSans.variable} ${fontMono.variable} font-sans antialiased `}
>
<Providers>{children}</Providers>
</body>
</html>
)
}

View File

@@ -0,0 +1,12 @@
import { Button } from "@workspace/ui/components/button"
export default function Page() {
return (
<div className="flex items-center justify-center min-h-svh">
<div className="flex flex-col items-center justify-center gap-4">
<h1 className="text-2xl font-bold">Hello World</h1>
<Button size="sm">Button</Button>
</div>
</div>
)
}

View File

@@ -0,0 +1,20 @@
{
"$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"
}
}

View File

@@ -0,0 +1,18 @@
"use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
export function Providers({ children }: { children: React.ReactNode }) {
return (
<NextThemesProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
enableColorScheme
>
{children}
</NextThemesProvider>
)
}

View File

@@ -0,0 +1,4 @@
import { nextJsConfig } from "@workspace/eslint-config/next-js"
/** @type {import("eslint").Linter.Config} */
export default nextJsConfig

View File

@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ["@workspace/ui"],
}
export default nextConfig

View File

@@ -0,0 +1,30 @@
{
"name": "web",
"version": "0.0.1",
"type": "module",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@workspace/ui": "workspace:*",
"lucide-react": "0.456.0",
"next-themes": "^0.4.3",
"next": "^15.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "18.3.0",
"@types/react-dom": "18.3.1",
"@workspace/eslint-config": "workspace:^",
"@workspace/typescript-config": "workspace:*",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}

View File

@@ -0,0 +1 @@
export { default } from "@workspace/ui/postcss.config";

View File

@@ -0,0 +1 @@
export * from "@workspace/ui/tailwind.config";

View File

@@ -0,0 +1,23 @@
{
"extends": "@workspace/typescript-config/nextjs.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"],
"@workspace/ui/*": ["../../packages/ui/src/*"]
},
"plugins": [
{
"name": "next"
}
]
},
"include": [
"next-env.d.ts",
"next.config.mjs",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": ["node_modules"]
}

View File

@@ -0,0 +1,22 @@
{
"name": "shadcn-ui-monorepo",
"version": "0.0.1",
"private": true,
"scripts": {
"build": "turbo build",
"dev": "turbo dev",
"lint": "turbo lint",
"format": "prettier --write \"**/*.{ts,tsx,md}\""
},
"devDependencies": {
"@workspace/eslint-config": "workspace:*",
"@workspace/typescript-config": "workspace:*",
"prettier": "^3.2.5",
"turbo": "^2.3.0",
"typescript": "5.5.4"
},
"packageManager": "pnpm@9.12.3",
"engines": {
"node": ">=20"
}
}

View File

@@ -0,0 +1,3 @@
# `@workspace/eslint-config`
Shared eslint configuration for the workspace.

View File

@@ -0,0 +1,32 @@
import js from "@eslint/js"
import eslintConfigPrettier from "eslint-config-prettier"
import onlyWarn from "eslint-plugin-only-warn"
import turboPlugin from "eslint-plugin-turbo"
import tseslint from "typescript-eslint"
/**
* A shared ESLint configuration for the repository.
*
* @type {import("eslint").Linter.Config}
* */
export const config = [
js.configs.recommended,
eslintConfigPrettier,
...tseslint.configs.recommended,
{
plugins: {
turbo: turboPlugin,
},
rules: {
"turbo/no-undeclared-env-vars": "warn",
},
},
{
plugins: {
onlyWarn,
},
},
{
ignores: ["dist/**"],
},
]

View File

@@ -0,0 +1,51 @@
import js from "@eslint/js"
import pluginNext from "@next/eslint-plugin-next"
import eslintConfigPrettier from "eslint-config-prettier"
import pluginReact from "eslint-plugin-react"
import pluginReactHooks from "eslint-plugin-react-hooks"
import globals from "globals"
import tseslint from "typescript-eslint"
import { config as baseConfig } from "./base.js"
/**
* A custom ESLint configuration for libraries that use Next.js.
*
* @type {import("eslint").Linter.Config}
* */
export const nextJsConfig = [
...baseConfig,
js.configs.recommended,
eslintConfigPrettier,
...tseslint.configs.recommended,
{
...pluginReact.configs.flat.recommended,
languageOptions: {
...pluginReact.configs.flat.recommended.languageOptions,
globals: {
...globals.serviceworker,
},
},
},
{
plugins: {
"@next/next": pluginNext,
},
rules: {
...pluginNext.configs.recommended.rules,
...pluginNext.configs["core-web-vitals"].rules,
},
},
{
plugins: {
"react-hooks": pluginReactHooks,
},
settings: { react: { version: "detect" } },
rules: {
...pluginReactHooks.configs.recommended.rules,
// React scope no longer necessary with new JSX transform.
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
},
},
]

View File

@@ -0,0 +1,25 @@
{
"name": "@workspace/eslint-config",
"version": "0.0.0",
"type": "module",
"private": true,
"exports": {
"./base": "./base.js",
"./next-js": "./next.js",
"./react-internal": "./react-internal.js"
},
"devDependencies": {
"@next/eslint-plugin-next": "^15.1.0",
"@typescript-eslint/eslint-plugin": "^8.15.0",
"@typescript-eslint/parser": "^8.15.0",
"eslint": "^9.15.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-only-warn": "^1.1.0",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-turbo": "^2.3.0",
"globals": "^15.12.0",
"typescript": "^5.3.3",
"typescript-eslint": "^8.15.0"
}
}

View File

@@ -0,0 +1,41 @@
import js from "@eslint/js"
import eslintConfigPrettier from "eslint-config-prettier"
import pluginReact from "eslint-plugin-react"
import pluginReactHooks from "eslint-plugin-react-hooks"
import globals from "globals"
import tseslint from "typescript-eslint"
import { config as baseConfig } from "./base.js"
/**
* A custom ESLint configuration for libraries that use React.
*
* @type {import("eslint").Linter.Config} */
export const config = [
...baseConfig,
js.configs.recommended,
eslintConfigPrettier,
...tseslint.configs.recommended,
pluginReact.configs.flat.recommended,
{
languageOptions: {
...pluginReact.configs.flat.recommended.languageOptions,
globals: {
...globals.serviceworker,
...globals.browser,
},
},
},
{
plugins: {
"react-hooks": pluginReactHooks,
},
settings: { react: { version: "detect" } },
rules: {
...pluginReactHooks.configs.recommended.rules,
// React scope no longer necessary with new JSX transform.
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
},
},
]

View File

@@ -0,0 +1,3 @@
# `@workspace/typescript-config`
Shared typescript configuration for the workspace.

View File

@@ -0,0 +1,20 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Default",
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"incremental": false,
"isolatedModules": true,
"lib": ["es2022", "DOM", "DOM.Iterable"],
"module": "NodeNext",
"moduleDetection": "force",
"moduleResolution": "NodeNext",
"noUncheckedIndexedAccess": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"target": "ES2022"
}
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Next.js",
"extends": "./base.json",
"compilerOptions": {
"plugins": [{ "name": "next" }],
"module": "ESNext",
"moduleResolution": "Bundler",
"allowJs": true,
"jsx": "preserve",
"noEmit": true
}
}

View File

@@ -0,0 +1,9 @@
{
"name": "@workspace/typescript-config",
"version": "0.0.0",
"private": true,
"license": "PROPRIETARY",
"publishConfig": {
"access": "public"
}
}

View File

@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "React Library",
"extends": "./base.json",
"compilerOptions": {
"jsx": "react-jsx",
}
}

View File

@@ -0,0 +1,20 @@
{
"$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"
}
}

View File

@@ -0,0 +1,4 @@
import { config } from "@workspace/eslint-config/react-internal"
/** @type {import("eslint").Linter.Config} */
export default config

View File

@@ -0,0 +1,42 @@
{
"name": "@workspace/ui",
"version": "0.0.0",
"type": "module",
"private": true,
"scripts": {
"lint": "eslint . --max-warnings 0"
},
"dependencies": {
"@radix-ui/react-slot": "^1.1.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"lucide-react": "0.456.0",
"next-themes": "^0.4.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.23.8"
},
"devDependencies": {
"@turbo/gen": "^2.2.3",
"@types/node": "^22.9.0",
"@types/react": "18.3.0",
"@types/react-dom": "18.3.1",
"@workspace/eslint-config": "workspace:*",
"@workspace/typescript-config": "workspace:*",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.47",
"react": "^18.3.1",
"tailwindcss": "^3.4.14",
"typescript": "^5.6.3"
},
"exports": {
"./globals.css": "./src/styles/globals.css",
"./postcss.config": "./postcss.config.mjs",
"./tailwind.config": "./tailwind.config.ts",
"./lib/*": "./src/lib/*.ts",
"./components/*": "./src/components/*.tsx",
"./hooks/*": "./src/hooks/*.ts"
}
}

View File

@@ -0,0 +1,9 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
export default config;

View File

@@ -0,0 +1,56 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@workspace/ui/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@@ -0,0 +1,68 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 10% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -0,0 +1,63 @@
import type { Config } from "tailwindcss"
import tailwindcssAnimate from "tailwindcss-animate"
import { fontFamily } from "tailwindcss/defaultTheme"
const config = {
darkMode: ["class"],
content: [
"app/**/*.{ts,tsx}",
"components/**/*.{ts,tsx}",
"../../packages/ui/src/components/**/*.{ts,tsx}",
],
theme: {
extend: {
fontFamily: {
sans: ["var(--font-sans)", ...fontFamily.sans],
mono: ["var(--font-mono)", ...fontFamily.mono],
},
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
},
},
plugins: [tailwindcssAnimate],
} satisfies Config
export default config

View File

@@ -0,0 +1,12 @@
{
"extends": "@workspace/typescript-config/react-library.json",
"compilerOptions": {
//"outDir": "dist"
"baseUrl": ".",
"paths": {
"@workspace/ui/*": ["./src/*"]
}
},
"include": ["."],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,8 @@
{
"extends": "@workspace/typescript-config/react-library.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src", "turbo"],
"exclude": ["node_modules", "dist"]
}

5153
templates/monorepo-next/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
packages:
- "apps/*"
- "packages/*"

View File

@@ -0,0 +1,4 @@
{
"extends": "@workspace/typescript-config/base.json"
}

View File

@@ -0,0 +1,21 @@
{
"$schema": "https://turbo.build/schema.json",
"ui": "tui",
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": [".next/**", "!.next/cache/**"]
},
"lint": {
"dependsOn": ["^lint"]
},
"check-types": {
"dependsOn": ["^check-types"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}