Skip to content

Commit

Permalink
feat(cli): rework of add command finished
Browse files Browse the repository at this point in the history
  • Loading branch information
stefan-karger committed Oct 1, 2024
1 parent da54bf8 commit 6591de2
Show file tree
Hide file tree
Showing 18 changed files with 862 additions and 29 deletions.
5 changes: 5 additions & 0 deletions .changeset/selfish-penguins-cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"solidui-cli": minor
---

complete rework of the add command to be more inline with shadcn/ui & others
2 changes: 1 addition & 1 deletion .changeset/warm-cheetahs-leave.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
"solidui-cli": minor
---

complete rework of the init command to be more inline with shadcn
complete rework of the init command to be more inline with shadcn/ui & others
3 changes: 3 additions & 0 deletions apps/docs/public/registry/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,9 @@
"dependencies": [
"@kobalte/core"
],
"registryDependencies": [
"button"
],
"files": [
"ui/pagination.tsx"
],
Expand Down
3 changes: 3 additions & 0 deletions apps/docs/public/registry/ui/pagination.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
"dependencies": [
"@kobalte/core"
],
"registryDependencies": [
"button"
],
"files": [
{
"name": "pagination.tsx",
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/src/__registry__/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ export const Index: Record<string, any> = {
"pagination": {
name: "pagination",
type: "ui",
registryDependencies: undefined,
registryDependencies: ["button"],
component: lazy(() => import("~/registry/ui/pagination")),
files: ["registry/ui/pagination.tsx"],
},
Expand Down
1 change: 1 addition & 0 deletions apps/docs/src/registry/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ const ui: Registry = [
name: "pagination",
type: "ui",
dependencies: ["@kobalte/core"],
registryDependencies: ["button"],
files: ["ui/pagination.tsx"]
},
{
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,19 @@
},
"dependencies": {
"@antfu/ni": "^0.23.0",
"@babel/core": "^7.25.2",
"@babel/parser": "^7.25.6",
"@babel/plugin-transform-typescript": "^7.25.2",
"@clack/prompts": "^0.7.0",
"chalk": "^5.3.0",
"commander": "^12.1.0",
"execa": "^9.4.0",
"recast": "^0.23.9",
"ts-morph": "^18.0.0",
"tsconfig-paths": "^4.2.0"
},
"devDependencies": {
"@types/babel__core": "^7.20.5",
"@types/node": "^20.14.12",
"tsup": "^8.3.0",
"typescript": "^5.6.2"
Expand Down
158 changes: 158 additions & 0 deletions packages/cli/src/commands/add.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { existsSync } from "node:fs"
import { mkdir, writeFile } from "node:fs/promises"
import path from "node:path"

import * as p from "@clack/prompts"
import { Command } from "commander"
import { execa } from "execa"
import * as v from "valibot"

import { getConfig } from "~/utils/config"
import { getPackageManager } from "~/utils/get-package-manager"
import { handleError, highlight } from "~/utils/logger"
import { fetchTree, getRegistryIndex, resolveTree } from "~/utils/registry"
import { transform } from "~/utils/transformers"

const addOptionsSchema = v.object({
components: v.optional(v.array(v.string()), []),
cwd: v.string(),
overwrite: v.boolean(),
all: v.boolean()
})

export const add = new Command()
.name("add")
.description("add components to your project")
.argument("[components...]", "the components to add")
.option("-c, --cwd <cwd>", "the working directory", process.cwd())
.option("-o --overwrite", "overwrite existing files", false)
.option("-a, --all", "add all available components", false)
.action(async (components, opts) => {
try {
const options = v.parse(addOptionsSchema, { components, ...opts })

const cwd = path.resolve(options.cwd)
if (!existsSync(cwd)) {
throw new Error(`The path ${cwd} does not exist. Please try again.`)
}

const config = await getConfig(cwd)
if (!config) {
p.log.warning(
`Configuration is missing. Please run ${highlight(`init`)} to create a components.json file.`
)
process.exit(1)
}

const registryIndex = await getRegistryIndex()

let selectedComponents = options.all ? registryIndex.map((v) => v.name) : options.components
if (!selectedComponents.length) {
const prompts = await p.group(
{
components: () =>
p.multiselect<{ label: string; value: string }[], string>({
message: `Which ${highlight("components")} would you like to add?`,
options: registryIndex.map((v) => ({ label: v.name, value: v.name }))
})
},
{
onCancel: () => {
p.cancel("Cancelled.")
process.exit(0)
}
}
)
selectedComponents = prompts.components
}

if (!selectedComponents.length) {
p.log.warn(`No components selected. Exiting.`)
process.exit(0)
}

const tree = await resolveTree(registryIndex, selectedComponents)
const payload = await fetchTree(tree)

if (!payload.length) {
p.log.warn(`Selected components not found. Exiting.`)
process.exit(0)
}

const spinner = p.spinner()
spinner.start("Installing...")

const targetDir = config.resolvedPaths.components
if (!existsSync(targetDir)) {
await mkdir(targetDir, { recursive: true })
}

for (const item of payload) {
spinner.message(`Installing ${highlight(item.name)}...`)

const existingComponent = item.files.filter((file) =>
existsSync(path.resolve(targetDir, file.name))
)
if (existingComponent.length && !options.overwrite) {
if (selectedComponents.includes(item.name)) {
spinner.stop()

const prompts = await p.group(
{
overwrite: () =>
p.confirm({
message: `Component ${item.name} already exists. Would you like to overwrite?`,
initialValue: false
})
},
{
onCancel: () => {
p.cancel("Cancelled.")
process.exit(0)
}
}
)
const overwrite = prompts.overwrite
if (!overwrite) {
p.log.info(
`Skipped ${item.name}. To overwrite, run with the ${highlight("--overwrite")} flag.`
)
continue
}

spinner.start(`Installing ${highlight(item.name)}...`)
} else {
continue
}
}

for (const file of item.files) {
let filePath = path.resolve(targetDir, file.name)

// run transformers
const content = await transform({
filename: file.name,
raw: file.content,
config: config
})

if (!config.tsx) {
filePath = filePath.replace(/\.tsx$/, ".jsx")
filePath = filePath.replace(/\.ts$/, ".js")
}

await writeFile(filePath, content, "utf-8")
}

// install dependencies
if (item.dependencies?.length) {
const packageManager = await getPackageManager(cwd)
await execa(packageManager, ["add", ...item.dependencies], { cwd })
}
}

spinner.stop("Done.")
} catch (e) {
handleError(e)
}
})
16 changes: 5 additions & 11 deletions packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { mkdir, writeFile } from "node:fs/promises"
import path from "node:path"

import * as p from "@clack/prompts"
import chalk from "chalk"
import { Command } from "commander"
import { execa } from "execa"
import * as v from "valibot"
Expand All @@ -20,7 +19,7 @@ import {
} from "~/utils/config"
import { getPackageInfo } from "~/utils/get-package-info"
import { getPackageManager } from "~/utils/get-package-manager"
import { handleError } from "~/utils/handle-error"
import { handleError, headline, highlight, subtle } from "~/utils/logger"
import * as templates from "~/utils/templates"

const PROJECT_DEPENDENCIES = [
Expand All @@ -30,9 +29,6 @@ const PROJECT_DEPENDENCIES = [
"tailwind-merge"
]

const headline = (text: string) => chalk.bgGreen.bold.black(text)
const highlight = (text: string) => chalk.bold.green(text)

const initOptionsSchema = v.object({
cwd: v.string()
})
Expand Down Expand Up @@ -123,12 +119,12 @@ async function promptForConfig(): Promise<RawConfig> {
}),
cssFile: () =>
p.text({
message: `Where is your ${highlight("global CSS")} file? ${chalk.gray("(this file will be overwritten)")}`,
message: `Where is your ${highlight("global CSS")} file? ${subtle("(this file will be overwritten)")}`,
initialValue: DEFAULT_CSS_FILE
}),
tailwindConfig: () =>
p.text({
message: `Where is your ${highlight("Tailwind config")} located? ${chalk.gray("(this file will be overwritten)")}`,
message: `Where is your ${highlight("Tailwind config")} located? ${subtle("(this file will be overwritten)")}`,
initialValue: DEFAULT_TAILWIND_CONFIG
}),
tailwindPrefix: () =>
Expand All @@ -149,13 +145,13 @@ async function promptForConfig(): Promise<RawConfig> {
},
{
onCancel: () => {
p.cancel("Operation cancelled.")
p.cancel("Cancelled.")
process.exit(0)
}
}
)

const config = v.parse(RawConfigSchema, {
return v.parse(RawConfigSchema, {
$schema: "https://solid-ui.com/schema.json",
tsx: options.typescript,
tailwind: {
Expand All @@ -168,6 +164,4 @@ async function promptForConfig(): Promise<RawConfig> {
utils: options.utils
}
})

return config
}
2 changes: 2 additions & 0 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#! /usr/bin/env node
import { Command } from "commander"

import { add } from "~/commands/add"
import { init } from "~/commands/init"
import { getPackageInfo } from "~/utils/get-package-info"

Expand All @@ -17,6 +18,7 @@ async function main() {
.description("add SolidUI components to your project")
.version(packageInfo.version || "0.0.0", "-v, --version", "display the version number")
.addCommand(init)
.addCommand(add)
.parse()
}

Expand Down
27 changes: 26 additions & 1 deletion packages/cli/src/utils/config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { existsSync, readFileSync } from "node:fs"
import path from "node:path"

import { loadConfig } from "tsconfig-paths"
import * as v from "valibot"

import { resolveImport } from "~/utils/resolve-import"

export const DEFAULT_COMPONENTS = "~/components"
export const DEFAULT_COMPONENTS = "~/components/ui"
export const DEFAULT_UTILS = "~/lib/utils"
export const DEFAULT_CSS_FILE = "src/app.css"
export const DEFAULT_TAILWIND_CONFIG = "tailwind.config.cjs"
Expand Down Expand Up @@ -58,3 +59,27 @@ export async function resolveConfigPaths(cwd: string, config: RawConfig) {
}
})
}

export function getRawConfig(cwd: string) {
try {
const configPath = path.resolve(cwd, "ui.config.json")

if (!existsSync(configPath)) {
return null
}

const config = JSON.parse(readFileSync(configPath).toString())

return v.parse(RawConfigSchema, config)
} catch (error) {
throw new Error(`Invalid configuration found in ${cwd}/ui.config.json.`)
}
}

export async function getConfig(cwd: string) {
const config = getRawConfig(cwd)
if (!config) {
return null
}
return await resolveConfigPaths(cwd, config)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import * as p from "@clack/prompts"
import chalk from "chalk"

export const headline = (text: string) => chalk.bgGreen.bold.black(text)
export const highlight = (text: string) => chalk.bold.green(text)
export const subtle = (text: string) => chalk.grey(text)

export function handleError(error: unknown) {
// provide a newline gap
Expand Down
Loading

0 comments on commit 6591de2

Please sign in to comment.