feat: add @shadcn/ui cli (#112)

This commit is contained in:
shadcn
2023-03-08 11:37:15 +04:00
committed by GitHub
parent b043cf7f8c
commit be701cf139
28 changed files with 2833 additions and 70 deletions

8
.changeset/README.md Normal file
View File

@@ -0,0 +1,8 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)

11
.changeset/config.json Normal file
View File

@@ -0,0 +1,11 @@
{
"$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
"changelog": ["@changesets/changelog-github", { "repo": "shadcn/ui" }],
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["www", "playground", "**-template"]
}

View File

@@ -0,0 +1,5 @@
---
"@shadcn/ui": patch
---
Initial commit.

45
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
# Adapted from create-t3-app.
name: Publish
on:
push:
branches:
- main
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
build:
if: ${{ github.repository_owner == 'shadcn' }}
name: Create a PR for release workflow
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Use PNPM
uses: pnpm/action-setup@v2.2.4
- name: Use Node.js 18
uses: actions/setup-node@v3
with:
node-version: 18
cache: "pnpm"
- name: Install NPM Dependencies
run: pnpm install
- name: Create Version PR or Publish to NPM
id: changesets
uses: changesets/action@v1.4.1
with:
commit: "chore(release): version packages"
title: "chore(release): version packages"
publish: pnpm build:cli
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
NODE_ENV: "production"

View File

@@ -3,3 +3,4 @@ node_modules
.next
build
.contentlayer
apps/www/pages/api/components.json

View File

@@ -7,6 +7,7 @@ import { allDocs } from "contentlayer/generated"
import { siteConfig } from "@/config/site"
import { cn } from "@/lib/utils"
import { Icons } from "@/components/icons"
import { buttonVariants } from "@/components/ui/button"
import {
NavigationMenu,
NavigationMenuContent,
@@ -16,8 +17,7 @@ import {
NavigationMenuTrigger,
navigationMenuTriggerStyle,
} from "@/components/ui/navigation-menu"
import { buttonVariants } from "./ui/button"
import { Separator } from "./ui/separator"
import { Separator } from "@/components/ui/separator"
export function MainNav() {
return (

View File

@@ -2,8 +2,8 @@
import * as React from "react"
import { DialogProps } from "@radix-ui/react-dialog"
import { Command as CommandPrimitive, useCommandState } from "cmdk"
import { ChevronsUpDown, Search } from "lucide-react"
import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react"
import { cn } from "@/lib/utils"
import { Dialog, DialogContent } from "@/components/ui/dialog"

View File

@@ -0,0 +1,295 @@
export const components = [
{
name: "Accordion",
dependencies: ["@radix-ui/react-accordion"],
files: [
{
name: "accordion.tsx",
dir: "components/ui",
},
],
},
{
name: "Alert Dialog",
dependencies: ["@radix-ui/react-alert-dialog"],
files: [
{
name: "alert-dialog.tsx",
dir: "components/ui",
},
],
},
{
name: "Aspect Ratio",
dependencies: ["@radix-ui/react-aspect-ratio"],
files: [
{
name: "aspect-ratio.tsx",
dir: "components/ui",
},
],
},
{
name: "Avatar",
dependencies: ["@radix-ui/react-avatar"],
files: [
{
name: "avatar.tsx",
dir: "components/ui",
},
],
},
{
name: "Button",
files: [
{
name: "button.tsx",
dir: "components/ui",
},
],
},
{
name: "Checkbox",
dependencies: ["@radix-ui/react-checkbox"],
files: [
{
name: "checkbox.tsx",
dir: "components/ui",
},
],
},
{
name: "Collapsible",
dependencies: ["@radix-ui/react-collapsible"],
files: [
{
name: "collapsible.tsx",
dir: "components/ui",
},
],
},
{
name: "Command",
dependencies: ["cmdk"],
files: [
{
name: "command.tsx",
dir: "components/ui",
},
],
},
{
name: "Context Menu",
dependencies: ["@radix-ui/react-context-menu"],
files: [
{
name: "context-menu.tsx",
dir: "components/ui",
},
],
},
{
name: "Dialog",
dependencies: ["@radix-ui/react-dialog"],
files: [
{
name: "dialog.tsx",
dir: "components/ui",
},
],
},
{
name: "Dropdown Menu",
dependencies: ["@radix-ui/react-dropdown-menu"],
files: [
{
name: "dropdown-menu.tsx",
dir: "components/ui",
},
],
},
{
name: "Hover Card",
dependencies: ["@radix-ui/react-hover-card"],
files: [
{
name: "hover-card.tsx",
dir: "components/ui",
},
],
},
{ name: "Input", files: [{ name: "input.tsx", dir: "components/ui" }] },
{
name: "Label",
dependencies: ["@radix-ui/react-label"],
files: [
{
name: "label.tsx",
dir: "components/ui",
},
],
},
{
name: "Menubar",
dependencies: ["@radix-ui/react-menubar"],
files: [
{
name: "menubar.tsx",
dir: "components/ui",
},
],
},
{
name: "Navigation Menu",
dependencies: ["@radix-ui/react-navigation-menu"],
files: [
{
name: "navigation-menu.tsx",
dir: "components/ui",
},
],
},
{
name: "Popover",
dependencies: ["@radix-ui/react-popover"],
files: [
{
name: "popover.tsx",
dir: "components/ui",
},
],
},
{
name: "Progress",
dependencies: ["@radix-ui/react-progress"],
files: [
{
name: "progress.tsx",
dir: "components/ui",
},
],
},
{
name: "Radio Group",
dependencies: ["@radix-ui/react-radio-group"],
files: [
{
name: "radio-group.tsx",
dir: "components/ui",
},
],
},
{
name: "Scroll-area",
dependencies: ["@radix-ui/react-scroll-area"],
files: [
{
name: "scroll-area.tsx",
dir: "components/ui",
},
],
},
{
name: "Select",
dependencies: ["@radix-ui/react-select"],
files: [
{
name: "select.tsx",
dir: "components/ui",
},
],
},
{
name: "Separator",
dependencies: ["@radix-ui/react-separator"],
files: [
{
name: "separator.tsx",
dir: "components/ui",
},
],
},
{
name: "Sheet",
dependencies: ["@radix-ui/react-dialog"],
files: [
{
name: "sheet.tsx",
dir: "components/ui",
},
],
},
{
name: "Slider",
dependencies: ["@radix-ui/react-slider"],
files: [
{
name: "slider.tsx",
dir: "components/ui",
},
],
},
{
name: "Switch",
dependencies: ["@radix-ui/react-switch"],
files: [
{
name: "switch.tsx",
dir: "components/ui",
},
],
},
{
name: "Tabs",
dependencies: ["@radix-ui/react-tabs"],
files: [
{
name: "tabs.tsx",
dir: "components/ui",
},
],
},
{
name: "Textarea",
files: [
{
name: "textarea.tsx",
dir: "components/ui",
},
],
},
{
name: "Toast",
dependencies: ["@radix-ui/react-toast"],
files: [
{
name: "toast.tsx",
dir: "components/ui",
},
{
name: "use-toast.ts",
dir: "hooks",
},
],
},
{
name: "Toggle",
dependencies: ["@radix-ui/react-toggle"],
files: [
{
name: "toggle.tsx",
dir: "components/ui",
},
],
},
{
name: "Tooltip",
dependencies: ["@radix-ui/react-tooltip"],
files: [
{
name: "tooltip.tsx",
dir: "components/ui",
},
],
},
]

View File

@@ -1,6 +1,7 @@
---
title: Context Menu
description: Displays a menu to the user — such as a set of actions or functions — triggered by a button.
component: true
radix:
link: https://www.radix-ui.com/docs/primitives/components/context-menu
api: https://www.radix-ui.com/docs/primitives/components/context-menu#api-reference

View File

@@ -1,6 +1,7 @@
---
title: Switch
description: A control that allows the user to toggle between checked and not checked.
component: true
radix:
link: https://www.radix-ui.com/docs/primitives/components/switch
api: https://www.radix-ui.com/docs/primitives/components/switch#api-reference

View File

@@ -4,7 +4,8 @@
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"build": "pnpm build:components && next build",
"build:components": "ts-node --esm --project ./tsconfig.scripts.json ./scripts/build-components.ts",
"start": "next start",
"lint": "next lint",
"preview": "next build && next start",

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,14 @@
import { NextApiRequest, NextApiResponse } from "next"
import components from "./components.json"
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== "GET") {
return res.status(405).end()
}
return res.status(200).json(components)
}

View File

@@ -0,0 +1,38 @@
import fs from "fs"
import path, { basename, dirname } from "path"
import { components } from "../config/components"
const payload = components
.map((component) => {
const files = component.files?.map((file) => {
const content = fs.readFileSync(
path.join(process.cwd(), file.dir, file.name),
"utf8"
)
return {
...file,
content,
}
})
return {
...component,
files,
}
})
.sort((a, b) => {
if (a.name < b.name) {
return -1
}
if (a.name > b.name) {
return 1
}
return 0
})
fs.writeFileSync(
path.join(process.cwd(), "pages/api/components.json"),
JSON.stringify(payload, null, 2)
)

View File

@@ -33,5 +33,5 @@
".next/types/**/*.ts",
".contentlayer/generated"
],
"exclude": ["node_modules"]
"exclude": ["node_modules", "./scripts/build-components.ts"]
}

View File

@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./tsconfig.json",
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"esModuleInterop": true,
"isolatedModules": false
},
"include": [".contentlayer/generated", "scripts/**/*.ts"],
"exclude": ["node_modules"]
}

View File

@@ -15,6 +15,7 @@
],
"scripts": {
"build": "turbo run build",
"build:cli": "turbo --filter=@shadcn/ui build",
"dev": "turbo run dev --parallel",
"lint": "turbo run lint",
"preview": "turbo run preview",
@@ -22,11 +23,13 @@
"format:write": "turbo run format",
"format:check": "turbo run format:check",
"sync:templates": "./scripts/sync-templates.sh \"templates/*\"",
"prepare": "husky install"
"prepare": "husky install",
"release": "changeset version"
},
"packageManager": "pnpm@7.13.5",
"dependencies": {
"@babel/core": "^7.20.7",
"@changesets/cli": "^2.26.0",
"@ianvs/prettier-plugin-sort-imports": "^3.7.1",
"@tailwindcss/line-clamp": "^0.4.2",
"eslint": "^8.31.0",
@@ -36,6 +39,7 @@
"eslint-plugin-react": "^7.31.11",
"eslint-plugin-tailwindcss": "^3.8.0",
"tailwindcss-animate": "^1.0.5",
"ts-node": "^10.9.1",
"turbo": "^1.6.3"
},
"devDependencies": {

2
packages/cli/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
components
dist

58
packages/cli/package.json Normal file
View File

@@ -0,0 +1,58 @@
{
"name": "@shadcn/ui",
"version": "0.0.1",
"description": "Add @shadcn/ui components to your app.",
"license": "MIT",
"author": {
"name": "shadcn",
"url": "https://twitter.com/shadcn"
},
"repository": {
"type": "git",
"url": "https://github.com/shadcn/ui.git",
"directory": "packages/cli"
},
"keywords": [
"components",
"ui",
"tailwind",
"radix-ui",
"shadcn"
],
"type": "module",
"exports": "./dist/index.js",
"bin": {
"@shadcn/cli": "./dist/index.js"
},
"scripts": {
"dev": "tsup --watch",
"build": "tsup",
"typecheck": "tsc",
"clean": "rimraf dist && rimraf components",
"start": "node dist/index.js",
"format:write": "prettier --write \"**/*.{ts,tsx,mdx}\" --cache",
"format:check": "prettier --check \"**/*.{ts,tsx,mdx}\" --cache",
"release": "changeset version",
"pub:beta": "pnpm build && npm publish --tag beta",
"pub:next": "pnpm build && npm publish --tag next",
"pub:release": "pnpm build && npm publish"
},
"dependencies": {
"chalk": "5.2.0",
"commander": "^10.0.0",
"execa": "^7.0.0",
"fs-extra": "^11.1.0",
"node-fetch": "^3.3.0",
"ora": "^6.1.2",
"prompts": "^2.4.2",
"zod": "^3.20.2"
},
"devDependencies": {
"@types/fs-extra": "^11.0.1",
"@types/prompts": "^2.4.2",
"rimraf": "^4.1.3",
"tsup": "^6.6.3",
"type-fest": "^3.6.1",
"typescript": "^4.9.5"
}
}

113
packages/cli/src/index.ts Normal file
View File

@@ -0,0 +1,113 @@
#!/usr/bin/env node
import { existsSync, promises as fs } from "fs"
import path from "path"
import { Command } from "commander"
import { execa } from "execa"
import ora from "ora"
import prompts from "prompts"
import { Component, getAvailableComponents } from "./utils/get-components"
import { getPackageInfo } from "./utils/get-package-info"
import { getPackageManager } from "./utils/get-package-manager"
import { logger } from "./utils/logger"
process.on("SIGINT", () => process.exit(0))
process.on("SIGTERM", () => process.exit(0))
async function main() {
const packageInfo = await getPackageInfo()
const program = new Command()
.name("@shadcn/ui")
.description("Add @shadcn/ui components to your project")
.version(
packageInfo.version || "1.0.0",
"-v, --version",
"display the version number"
)
program
.command("add")
.description("add components to your project")
.action(async () => {
const { components, dir } = await promptForAddOptions()
if (!components?.length) {
logger.warn("No components selected. Nothing to install.")
process.exit(0)
}
// Create componentPath directory if it doesn't exist.
const destinationDir = path.resolve(dir)
if (!existsSync(destinationDir)) {
const spinner = ora(`Creating ${dir}...`).start()
await fs.mkdir(destinationDir, { recursive: true })
spinner.succeed()
}
const packageManager = getPackageManager()
logger.success(`Installing components...`)
for (const component of components) {
const componentSpinner = ora(`${component.name}...`).start()
// Write the files.
for (const file of component.files) {
const filePath = path.resolve(dir, file.name)
await fs.writeFile(filePath, file.content)
}
// Install dependencies.
if (component.dependencies?.length) {
const dependencies = component.dependencies.join(" ")
await execa(packageManager, [
packageManager === "npm" ? "install" : "add",
dependencies,
])
}
componentSpinner.succeed(component.name)
}
})
program.parse()
}
type AddOptions = {
components: Component[]
dir: string
}
async function promptForAddOptions() {
const availableComponents = await getAvailableComponents()
if (!availableComponents?.length) {
logger.error(
"An error occurred while fetching components. Please try again."
)
process.exit(0)
}
const options = await prompts([
{
type: "multiselect",
name: "components",
message: "Which component(s) would you like to add?",
hint: "Space to select. A to select all. I to invert selection.",
instructions: false,
choices: availableComponents.map((component) => ({
title: component.name,
value: component,
})),
},
{
type: "text",
name: "dir",
message: "Where would you like to install the component(s)?",
initial: "./components/ui",
},
])
return options as AddOptions
}
main()

View File

@@ -0,0 +1,34 @@
import fetch from "node-fetch"
import * as z from "zod"
const baseUrl =
process.env.NODE_ENV === "production"
? "https://ui.shadcn.com"
: "http://localhost:3000"
const componentSchema = z.object({
name: z.string(),
dependencies: z.array(z.string()).optional(),
files: z.array(
z.object({
name: z.string(),
dir: z.string(),
content: z.string(),
})
),
})
export type Component = z.infer<typeof componentSchema>
const componentsSchema = z.array(componentSchema)
export async function getAvailableComponents() {
try {
const response = await fetch(`${baseUrl}/api/components`)
const components = await response.json()
return componentsSchema.parse(components)
} catch (error) {
throw new Error("Failed to fetch components")
}
}

View File

@@ -0,0 +1,9 @@
import path from "path"
import fs from "fs-extra"
import { type PackageJson } from "type-fest"
export function getPackageInfo() {
const packageJsonPath = path.join("package.json")
return fs.readJSONSync(packageJsonPath) as PackageJson
}

View File

@@ -0,0 +1,17 @@
export function getPackageManager() {
const userAgent = process.env.npm_config_user_agent
if (!userAgent) {
return "npm"
}
if (userAgent.startsWith("yarn")) {
return "yarn"
}
if (userAgent.startsWith("pnpm")) {
return "pnpm"
}
return "npm"
}

View File

@@ -0,0 +1,16 @@
import chalk from "chalk"
export const logger = {
error(...args: unknown[]) {
console.log(chalk.red(...args))
},
warn(...args: unknown[]) {
console.log(chalk.yellow(...args))
},
info(...args: unknown[]) {
console.log(chalk.cyan(...args))
},
success(...args: unknown[]) {
console.log(chalk.green(...args))
},
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../tsconfig.json",
"compilerOptions": {
"isolatedModules": false,
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
}
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}

View File

@@ -0,0 +1,11 @@
import { defineConfig } from "tsup"
export default defineConfig({
clean: true,
dts: true,
entry: ["src/index.ts"],
format: ["esm"],
sourcemap: true,
target: "esnext",
outDir: "dist",
})

1841
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,8 @@
"env": [
"NEXT_PUBLIC_APP_URL",
"UPSTASH_REDIS_REST_URL",
"UPSTASH_REDIS_REST_TOKEN"
"UPSTASH_REDIS_REST_TOKEN",
"npm_config_user_agent"
],
"outputs": ["dist/**", ".next/**"]
},