mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-07-07 22:18:39 +00:00
feat: add @shadcn/ui cli (#112)
This commit is contained in:
2
packages/cli/.gitignore
vendored
Normal file
2
packages/cli/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
components
|
||||
dist
|
||||
58
packages/cli/package.json
Normal file
58
packages/cli/package.json
Normal 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
113
packages/cli/src/index.ts
Normal 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()
|
||||
34
packages/cli/src/utils/get-components.ts
Normal file
34
packages/cli/src/utils/get-components.ts
Normal 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")
|
||||
}
|
||||
}
|
||||
9
packages/cli/src/utils/get-package-info.ts
Normal file
9
packages/cli/src/utils/get-package-info.ts
Normal 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
|
||||
}
|
||||
17
packages/cli/src/utils/get-package-manager.ts
Normal file
17
packages/cli/src/utils/get-package-manager.ts
Normal 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"
|
||||
}
|
||||
16
packages/cli/src/utils/logger.ts
Normal file
16
packages/cli/src/utils/logger.ts
Normal 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))
|
||||
},
|
||||
}
|
||||
13
packages/cli/tsconfig.json
Normal file
13
packages/cli/tsconfig.json
Normal 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"]
|
||||
}
|
||||
11
packages/cli/tsup.config.ts
Normal file
11
packages/cli/tsup.config.ts
Normal 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",
|
||||
})
|
||||
Reference in New Issue
Block a user