192 lines
5.4 KiB
TypeScript
192 lines
5.4 KiB
TypeScript
import { existsSync, promises as fs } from 'node:fs'
|
|
import path from 'node:path'
|
|
import process from 'node:process'
|
|
import chalk from 'chalk'
|
|
import { Command } from 'commander'
|
|
import { execa } from 'execa'
|
|
import ora from 'ora'
|
|
import prompts from 'prompts'
|
|
import * as z from 'zod'
|
|
import { transformImport } from '../utils/transformers/transform-import'
|
|
import { transformSFC } from '../utils/transformers/transform-sfc'
|
|
import { getConfig } from '@/src/utils/get-config'
|
|
import { getPackageManager } from '@/src/utils/get-package-manager'
|
|
import { handleError } from '@/src/utils/handle-error'
|
|
import { logger } from '@/src/utils/logger'
|
|
import {
|
|
fetchTree,
|
|
getItemTargetPath,
|
|
getRegistryBaseColor,
|
|
getRegistryIndex,
|
|
resolveTree,
|
|
} from '@/src/utils/registry'
|
|
|
|
const addOptionsSchema = z.object({
|
|
components: z.array(z.string()).optional(),
|
|
yes: z.boolean(),
|
|
overwrite: z.boolean(),
|
|
cwd: z.string(),
|
|
path: z.string().optional(),
|
|
})
|
|
|
|
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.', false)
|
|
.option('-o, --overwrite', 'overwrite existing files.', false)
|
|
.option(
|
|
'-c, --cwd <cwd>',
|
|
'the working directory. defaults to the current directory.',
|
|
process.cwd(),
|
|
)
|
|
.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)
|
|
}
|
|
|
|
const registryIndex = await getRegistryIndex()
|
|
|
|
let selectedComponents = options.components
|
|
if (!options.components?.length) {
|
|
const { components } = await prompts({
|
|
type: 'autocompleteMultiselect',
|
|
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,
|
|
})),
|
|
})
|
|
selectedComponents = components
|
|
}
|
|
|
|
if (!selectedComponents?.length) {
|
|
logger.warn('No components selected. Exiting.')
|
|
process.exit(0)
|
|
}
|
|
|
|
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()
|
|
const skippedDeps = new Set<string>()
|
|
for (const item of payload) {
|
|
spinner.text = `Installing ${item.name}...`
|
|
const targetDir = 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, item.name, file.name)),
|
|
)
|
|
|
|
if (existingComponent.length && !options.overwrite) {
|
|
if (selectedComponents.includes(item.name)) {
|
|
logger.warn(
|
|
`\nComponent ${
|
|
item.name
|
|
} already exists. Use ${chalk.green(
|
|
'--overwrite',
|
|
)} to overwrite.`,
|
|
)
|
|
spinner.stop()
|
|
process.exitCode = 1
|
|
return
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
for (const file of item.files) {
|
|
const componentDir = path.resolve(targetDir, item.name)
|
|
let filePath = path.resolve(
|
|
targetDir,
|
|
item.name,
|
|
file.name,
|
|
)
|
|
|
|
// Run transformers.
|
|
const content = await transformSFC(file, config)
|
|
|
|
if (!existsSync(componentDir))
|
|
await fs.mkdir(componentDir, { recursive: true })
|
|
|
|
if (!config.typescript)
|
|
filePath = filePath.replace(/\.ts$/, '.js')
|
|
|
|
await fs.writeFile(filePath, content)
|
|
}
|
|
|
|
// Install dependencies.
|
|
if (item.dependencies?.length) {
|
|
item.dependencies.forEach(dep =>
|
|
skippedDeps.add(dep),
|
|
)
|
|
|
|
const packageManager = await getPackageManager(cwd)
|
|
await execa(
|
|
packageManager,
|
|
[
|
|
packageManager === 'npm' ? 'install' : 'add',
|
|
...item.dependencies,
|
|
],
|
|
{
|
|
cwd,
|
|
},
|
|
)
|
|
}
|
|
}
|
|
spinner.succeed('Done.')
|
|
}
|
|
catch (error) {
|
|
handleError(error)
|
|
}
|
|
})
|