Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

정유송: shadcn UI #32

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 225 additions & 0 deletions 정유송/shadcn-ui/add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
## shadcn/ui `add`

컴포넌트와 종속성을 추가하기 위해 사용

`npx shadcn-ui@latest add [component]`

## 코드 살펴보기

```tsx
export const add = new Command()
.name("add")
.description("add a component to your project")
.argument("[components...]", "the components to add")
.option("-y, --yes", "skip confirmation prompt.", true)
.option("-o, --overwrite", "overwrite existing files.", false)
.option(
"-c, --cwd <cwd>",
"the working directory. defaults to the current directory.",
process.cwd()
)
.option("-a, --all", "add all available components", false)
.option("-p, --path <path>", "the path to add the component to.")

.action(async (components, opts) => {
try {
const options = addOptionsSchema.parse({
components,
...opts,
})

const cwd = path.resolve(options.cwd)

if (!existsSync(cwd)) {
logger.error(`The path ${cwd} does not exist. Please try again.`)
process.exit(1)
}

const config = await getConfig(cwd)
if (!config) {
logger.warn(
`Configuration is missing. Please run ${chalk.green(
`init`
)} to create a components.json file.`
)
process.exit(1)
}
```

`commander`를 활용해 CLI를 작성

- `name`, `description`, `argument`, `option` : 명령어 정의 및 설명, 옵션 설정 등
- `action` : 명령어 실행 시 action 함수 호출

```tsx
const registryIndex = await getRegistryIndex()

let selectedComponents = options.all
? registryIndex.map((entry) => entry.name)
: options.components
if (!options.components?.length && !options.all) {
const { components } = await prompts({
type: "multiselect",
name: "components",
message: "Which components would you like to add?",
hint: "Space to select. A to toggle all. Enter to submit.",
instructions: false,
choices: registryIndex.map((entry) => ({
title: entry.name,
value: entry.name,
selected: options.all
? true
: options.components?.includes(entry.name),
})),
})
selectedComponents = components
}

if (!selectedComponents?.length) {
logger.warn("No components selected. Exiting.")
process.exit(0)
}
```

- registry에서 컴포넌트 목록을 가져옴
- 옵션에서 지정했다면? 사용자가 선택한 컴포넌트 설정
- 지정하지 않았다면? [`prompt`](https://github.com/terkelg/prompts)를 활용해 컴포넌트 추가 관련 유저 입력을 받게 됨

```tsx
const tree = await resolveTree(registryIndex, selectedComponents)
const payload = await fetchTree(config.style, tree)
const baseColor = await getRegistryBaseColor(config.tailwind.baseColor)

if (!payload.length) {
logger.warn("Selected components not found. Exiting.")
process.exit(0)
}

if (!options.yes) {
const { proceed } = await prompts({
type: "confirm",
name: "proceed",
message: `Ready to install components and dependencies. Proceed?`,
initial: true,
})

if (!proceed) {
process.exit(0)
}
}

const spinner = ora(`Installing components...`).start()
for (const item of payload) {
spinner.text = `Installing ${item.name}...`
const targetDir = await getItemTargetPath(
config,
item,
options.path ? path.resolve(cwd, options.path) : undefined
)

if (!targetDir) {
continue
}

if (!existsSync(targetDir)) {
await fs.mkdir(targetDir, { recursive: true })
}

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 { overwrite } = await prompts({
type: "confirm",
name: "overwrite",
message: `Component ${item.name} already exists. Would you like to overwrite?`,
initial: false,
})

if (!overwrite) {
logger.info(
`Skipped ${item.name}. To overwrite, run with the ${chalk.green(
"--overwrite"
)} flag.`
)
continue
}

spinner.start(`Installing ${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,
baseColor,
})

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

await fs.writeFile(filePath, content)
}

const packageManager = await getPackageManager(cwd)

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

// Install devDependencies.
if (item.devDependencies?.length) {
await execa(
packageManager,
[
packageManager === "npm" ? "install" : "add",
"-D",
...item.devDependencies,
],
{
cwd,
}
)
}
}
spinner.succeed(`Done.`)
}
```

- 선택한 컴포넌트 기반으로 파일 트리 구성, 파일 변환
- [ora 스피너](https://github.com/sindresorhus/ora)를 활용해 설치 진행 상태 표시
- 파일 존재 여부 확인 → 덮어 쓸 것인지?
- 파일 작성
- 종속성 설치
- 등등

```tsx
} catch (error) {
handleError(error)
}
})
```

- 에러 발생 시 handleError 호출해 에러 처리
Loading