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

refactor: restructure cli commands to support different frameworks templates #618

Open
wants to merge 5 commits into
base: dev
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
178 changes: 11 additions & 167 deletions packages/cli/src/commands/add.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,13 @@
import { existsSync, promises as fs, rmSync } from 'node:fs'
import { existsSync } from 'node:fs'
import process from 'node:process'
import path from 'pathe'
import { consola } from 'consola'
import { colors } from 'consola/utils'
import { Command } from 'commander'
import ora from 'ora'
import prompts from 'prompts'
import { z } from 'zod'
import { addDependency, addDevDependency } from 'nypm'
import { transform } from '@/src/utils/transformers'
import { getConfig } from '@/src/utils/get-config'
import { getProjectInfo } from '../utils/get-project-info'
import frameworksCommands from './frameworks'
import { handleError } from '@/src/utils/handle-error'
import {
fetchTree,
getItemTargetPath,
getRegistryBaseColor,
getRegistryIndex,
resolveTree,
} from '@/src/utils/registry'

const addOptionsSchema = z.object({
components: z.array(z.string()).optional(),
Expand Down Expand Up @@ -55,166 +45,20 @@ export const add = new Command()
process.exit(1)
}

const config = await getConfig(cwd)
// Get the corresponding framework commands
const { isNuxt } = await getProjectInfo()
const framework = isNuxt ? 'nuxt' : 'vue'
const { loadConfig, add } = frameworksCommands[framework]

// Read config
const config = await loadConfig(cwd, options, false)
if (!config) {
consola.warn(`Configuration is missing. Please run ${colors.green('init')} to create a components.json file.`)

process.exit(1)
}

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) {
consola.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) {
consola.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 = 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)) {
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) {
consola.info(
`Skipped ${item.name}. To overwrite, run with the ${colors.green(
'--overwrite',
)} flag.`,
)
continue
}

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

// Install dependencies.
await Promise.allSettled(
[
item.dependencies?.length && await addDependency(item.dependencies, {
cwd,
silent: true,
}),
item.devDependencies?.length && await addDevDependency(item.devDependencies, {
cwd,
silent: true,
}),
],
)

const componentDir = path.resolve(targetDir, item.name)
if (!existsSync(componentDir))
await fs.mkdir(componentDir, { recursive: true })

const files = item.files.map(file => ({
...file,
path: path.resolve(
targetDir,
item.name,
file.name,
),
}))

// We need to write original files to disk if we're not using TypeScript.
// Rewrite or delete added files after transformed
if (!config.typescript) {
for (const file of files)
await fs.writeFile(file.path, file.content)
}

for (const file of files) {
// Run transformers.
const content = await transform({
filename: file.path,
raw: file.content,
config,
baseColor,
})

let filePath = file.path

if (!config.typescript) {
// remove original .ts file if we're not using TypeScript.
if (file.path.endsWith('.ts')) {
if (existsSync(file.path))
rmSync(file.path)
}
filePath = file.path.replace(/\.ts$/, '.js')
}

await fs.writeFile(filePath, content)
}
}
spinner.succeed('Done.')
// Add components
await add(cwd, config, options)
}
catch (error) {
handleError(error)
Expand Down
161 changes: 13 additions & 148 deletions packages/cli/src/commands/diff.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,13 @@
import { existsSync, promises as fs } from 'node:fs'
import { existsSync } from 'node:fs'
import process from 'node:process'
import path from 'pathe'
import { consola } from 'consola'
import { colors } from 'consola/utils'
import { Command } from 'commander'
import { type Change, diffLines } from 'diff'
import { z } from 'zod'
import type { Config } from '@/src/utils/get-config'
import { getConfig } from '@/src/utils/get-config'
import { getProjectInfo } from '../utils/get-project-info'
import frameworksCommands from './frameworks'
import { handleError } from '@/src/utils/handle-error'
import {
fetchTree,
getItemTargetPath,
getRegistryBaseColor,
getRegistryIndex,
} from '@/src/utils/registry'
import type { registryIndexSchema } from '@/src/utils/registry/schema'
import { transform } from '@/src/utils/transformers'

const updateOptionsSchema = z.object({
component: z.string().optional(),
Expand Down Expand Up @@ -49,148 +40,22 @@ export const diff = new Command()
process.exit(1)
}

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

const registryIndex = await getRegistryIndex()

if (!options.component) {
const targetDir = config.resolvedPaths.components

// Find all components that exist in the project.
const projectComponents = registryIndex.filter((item) => {
for (const file of item.files) {
const filePath = path.resolve(targetDir, file)
if (existsSync(filePath))
return true
}

return false
})

// Check for updates.
const componentsWithUpdates = []
for (const component of projectComponents) {
const changes = await diffComponent(component, config)
if (changes.length) {
componentsWithUpdates.push({
name: component.name,
changes,
})
}
}

if (!componentsWithUpdates.length) {
consola.info('No updates found.')
process.exit(0)
}
// Get the corresponding framework commands
const { isNuxt } = await getProjectInfo()
const framework = isNuxt ? 'nuxt' : 'vue'
const { loadConfig, diff } = frameworksCommands[framework]

consola.info('The following components have updates available:')
for (const component of componentsWithUpdates) {
consola.info(`- ${component.name}`)
for (const change of component.changes)
consola.info(` - ${change.filePath}`)
}

consola.log('')
consola.info(
`Run ${colors.green('diff <component>')} to see the changes.`,
)
process.exit(0)
}

// Show diff for a single component.
const component = registryIndex.find(
item => item.name === options.component,
)

if (!component) {
consola.error(
`The component ${colors.green(options.component)} does not exist.`,
)
// Load Config
const config = await loadConfig(cwd, options, false)
if (!config) {
consola.warn(`Configuration is missing. Please run ${colors.green('init')} to create a components.json file.`)
process.exit(1)
}

const changes = await diffComponent(component, config)

if (!changes.length) {
consola.info(`No updates found for ${options.component}.`)
process.exit(0)
}

for (const change of changes) {
consola.info(`- ${change.filePath}`)
printDiff(change.patch)
consola.log('')
}
// Run Diff Command
await diff(cwd, config, options)
}
catch (error) {
handleError(error)
}
})

async function diffComponent(
component: z.infer<typeof registryIndexSchema>[number],
config: Config,
) {
const payload = await fetchTree(config.style, [component])
const baseColor = await getRegistryBaseColor(config.tailwind.baseColor)

const changes = []

for (const item of payload) {
const targetDir = await getItemTargetPath(config, item)

if (!targetDir)
continue

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

if (!existsSync(filePath))
continue

const fileContent = await fs.readFile(filePath, 'utf8')

const registryContent = await transform({
filename: file.name,
raw: file.content,
config,
baseColor,
})

const patch = diffLines(registryContent as string, fileContent)
if (patch.length > 1) {
changes.push({
file: file.name,
filePath,
patch,
})
}
}
}

return changes
}

// TODO: Does is it need to async?
function printDiff(diff: Change[]) {
diff.forEach((part) => {
if (part) {
if (part.added)
return process.stdout.write(colors.green(part.value))

if (part.removed)
return process.stdout.write(colors.red(part.value))

return process.stdout.write(part.value)
}
})
}
Loading