Merge 3132236449 into 1c7c60330a
This commit is contained in:
commit
7503f296b1
|
|
@ -1,23 +1,13 @@
|
||||||
import { existsSync, promises as fs, rmSync } from 'node:fs'
|
import { existsSync } from 'node:fs'
|
||||||
import process from 'node:process'
|
import process from 'node:process'
|
||||||
import path from 'pathe'
|
import path from 'pathe'
|
||||||
import { consola } from 'consola'
|
import { consola } from 'consola'
|
||||||
import { colors } from 'consola/utils'
|
import { colors } from 'consola/utils'
|
||||||
import { Command } from 'commander'
|
import { Command } from 'commander'
|
||||||
import ora from 'ora'
|
|
||||||
import prompts from 'prompts'
|
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { addDependency, addDevDependency } from 'nypm'
|
import { getProjectInfo } from '../utils/get-project-info'
|
||||||
import { transform } from '@/src/utils/transformers'
|
import frameworksCommands from './frameworks'
|
||||||
import { getConfig } from '@/src/utils/get-config'
|
|
||||||
import { handleError } from '@/src/utils/handle-error'
|
import { handleError } from '@/src/utils/handle-error'
|
||||||
import {
|
|
||||||
fetchTree,
|
|
||||||
getItemTargetPath,
|
|
||||||
getRegistryBaseColor,
|
|
||||||
getRegistryIndex,
|
|
||||||
resolveTree,
|
|
||||||
} from '@/src/utils/registry'
|
|
||||||
|
|
||||||
const addOptionsSchema = z.object({
|
const addOptionsSchema = z.object({
|
||||||
components: z.array(z.string()).optional(),
|
components: z.array(z.string()).optional(),
|
||||||
|
|
@ -55,166 +45,20 @@ export const add = new Command()
|
||||||
process.exit(1)
|
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) {
|
if (!config) {
|
||||||
consola.warn(`Configuration is missing. Please run ${colors.green('init')} to create a components.json file.`)
|
consola.warn(`Configuration is missing. Please run ${colors.green('init')} to create a components.json file.`)
|
||||||
|
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const registryIndex = await getRegistryIndex()
|
// Add components
|
||||||
|
await add(cwd, config, options)
|
||||||
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.')
|
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
handleError(error)
|
handleError(error)
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,13 @@
|
||||||
import { existsSync, promises as fs } from 'node:fs'
|
import { existsSync } from 'node:fs'
|
||||||
import process from 'node:process'
|
import process from 'node:process'
|
||||||
import path from 'pathe'
|
import path from 'pathe'
|
||||||
import { consola } from 'consola'
|
import { consola } from 'consola'
|
||||||
import { colors } from 'consola/utils'
|
import { colors } from 'consola/utils'
|
||||||
import { Command } from 'commander'
|
import { Command } from 'commander'
|
||||||
import { type Change, diffLines } from 'diff'
|
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import type { Config } from '@/src/utils/get-config'
|
import { getProjectInfo } from '../utils/get-project-info'
|
||||||
import { getConfig } from '@/src/utils/get-config'
|
import frameworksCommands from './frameworks'
|
||||||
import { handleError } from '@/src/utils/handle-error'
|
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({
|
const updateOptionsSchema = z.object({
|
||||||
component: z.string().optional(),
|
component: z.string().optional(),
|
||||||
|
|
@ -49,148 +40,22 @@ export const diff = new Command()
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await getConfig(cwd)
|
// Get the corresponding framework commands
|
||||||
|
const { isNuxt } = await getProjectInfo()
|
||||||
|
const framework = isNuxt ? 'nuxt' : 'vue'
|
||||||
|
const { loadConfig, diff } = frameworksCommands[framework]
|
||||||
|
|
||||||
|
// Load Config
|
||||||
|
const config = await loadConfig(cwd, options, false)
|
||||||
if (!config) {
|
if (!config) {
|
||||||
consola.warn(
|
consola.warn(`Configuration is missing. Please run ${colors.green('init')} to create a components.json file.`)
|
||||||
`Configuration is missing. Please run ${colors.green(
|
|
||||||
'init',
|
|
||||||
)} to create a components.json file.`,
|
|
||||||
)
|
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const registryIndex = await getRegistryIndex()
|
// Run Diff Command
|
||||||
|
await diff(cwd, config, options)
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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.`,
|
|
||||||
)
|
|
||||||
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('')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
handleError(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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
||||||
21
packages/cli/src/commands/frameworks/index.ts
Normal file
21
packages/cli/src/commands/frameworks/index.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import loadConfigVue from './vue/config'
|
||||||
|
import initVue from './vue/init'
|
||||||
|
import addVue from './vue/add'
|
||||||
|
import diffVue from './vue/diff'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
vue: {
|
||||||
|
loadConfig: loadConfigVue,
|
||||||
|
init: initVue,
|
||||||
|
add: addVue,
|
||||||
|
diff: diffVue,
|
||||||
|
},
|
||||||
|
// For now we run the same commands for Nuxt as for Vue
|
||||||
|
// TODO: replace with Nuxt-specific commands when required
|
||||||
|
nuxt: {
|
||||||
|
loadConfig: loadConfigVue,
|
||||||
|
init: initVue,
|
||||||
|
add: addVue,
|
||||||
|
diff: diffVue,
|
||||||
|
},
|
||||||
|
}
|
||||||
183
packages/cli/src/commands/frameworks/vue/add.ts
Normal file
183
packages/cli/src/commands/frameworks/vue/add.ts
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
import { existsSync, promises as fs, rmSync } from 'node:fs'
|
||||||
|
import { addDependency, addDevDependency } from 'nypm'
|
||||||
|
import { consola } from 'consola'
|
||||||
|
import path from 'pathe'
|
||||||
|
import ora from 'ora'
|
||||||
|
import prompts from 'prompts'
|
||||||
|
import { colors } from 'consola/utils'
|
||||||
|
import type { Config } from '../../../utils/get-config'
|
||||||
|
import { transform } from '@/src/utils/transformers'
|
||||||
|
import {
|
||||||
|
fetchTree,
|
||||||
|
getItemTargetPath,
|
||||||
|
getRegistryBaseColor,
|
||||||
|
getRegistryIndex,
|
||||||
|
resolveTree,
|
||||||
|
} from '@/src/utils/registry'
|
||||||
|
|
||||||
|
interface AddOptions {
|
||||||
|
|
||||||
|
yes: boolean
|
||||||
|
overwrite: boolean
|
||||||
|
cwd: string
|
||||||
|
all: boolean
|
||||||
|
path?: string | undefined
|
||||||
|
components?: string[] | undefined
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function (cwd: string, config: Config, options: AddOptions) {
|
||||||
|
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.')
|
||||||
|
}
|
||||||
173
packages/cli/src/commands/frameworks/vue/config.ts
Normal file
173
packages/cli/src/commands/frameworks/vue/config.ts
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
import { promises as fs } from 'node:fs'
|
||||||
|
import path from 'pathe'
|
||||||
|
import ora from 'ora'
|
||||||
|
import { colors } from 'consola/utils'
|
||||||
|
import { consola } from 'consola'
|
||||||
|
import prompts from 'prompts'
|
||||||
|
import { getRegistryBaseColors, getRegistryStyles } from '../../../utils/registry'
|
||||||
|
import {
|
||||||
|
type Config,
|
||||||
|
DEFAULT_COMPONENTS,
|
||||||
|
DEFAULT_TAILWIND_CONFIG,
|
||||||
|
DEFAULT_UTILS,
|
||||||
|
TAILWIND_CSS_PATH,
|
||||||
|
getConfig,
|
||||||
|
rawConfigSchema,
|
||||||
|
resolveConfigPaths,
|
||||||
|
} from '../../../utils/get-config'
|
||||||
|
|
||||||
|
export default async function (cwd: string, options: { yes: boolean, cwd: string }, prompt: boolean) {
|
||||||
|
const existingConfig = await getConfig(cwd)
|
||||||
|
return prompt ? await promptForConfig(cwd, existingConfig, options.yes) : existingConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
async function promptForConfig(
|
||||||
|
cwd: string,
|
||||||
|
defaultConfig: Config | null = null,
|
||||||
|
skip = false,
|
||||||
|
) {
|
||||||
|
const highlight = (text: string) => colors.cyan(text)
|
||||||
|
|
||||||
|
const styles = await getRegistryStyles()
|
||||||
|
const baseColors = await getRegistryBaseColors()
|
||||||
|
|
||||||
|
const options = await prompts([
|
||||||
|
{
|
||||||
|
type: 'toggle',
|
||||||
|
name: 'typescript',
|
||||||
|
message: `Would you like to use ${highlight('TypeScript')}? ${colors.gray('(recommended)')}?`,
|
||||||
|
initial: defaultConfig?.typescript ?? true,
|
||||||
|
active: 'yes',
|
||||||
|
inactive: 'no',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'select',
|
||||||
|
name: 'framework',
|
||||||
|
message: `Which ${highlight('framework')} are you using?`,
|
||||||
|
choices: [
|
||||||
|
{ title: 'Vite', value: 'vite' },
|
||||||
|
{ title: 'Nuxt', value: 'nuxt' },
|
||||||
|
{ title: 'Laravel', value: 'laravel' },
|
||||||
|
{ title: 'Astro', value: 'astro' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'select',
|
||||||
|
name: 'style',
|
||||||
|
message: `Which ${highlight('style')} would you like to use?`,
|
||||||
|
choices: styles.map(style => ({
|
||||||
|
title: style.label,
|
||||||
|
value: style.name,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'select',
|
||||||
|
name: 'tailwindBaseColor',
|
||||||
|
message: `Which color would you like to use as ${highlight(
|
||||||
|
'base color',
|
||||||
|
)}?`,
|
||||||
|
choices: baseColors.map(color => ({
|
||||||
|
title: color.label,
|
||||||
|
value: color.name,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'tsConfigPath',
|
||||||
|
message: (prev, values) => `Where is your ${highlight(values.typescript ? 'tsconfig.json' : 'jsconfig.json')} file?`,
|
||||||
|
initial: (prev, values) => {
|
||||||
|
const prefix = values.framework === 'nuxt' ? '.nuxt/' : './'
|
||||||
|
const path = values.typescript ? 'tsconfig.json' : 'jsconfig.json'
|
||||||
|
return prefix + path
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'tailwindCss',
|
||||||
|
message: `Where is your ${highlight('global CSS')} file? ${colors.gray('(this file will be overwritten)')}`,
|
||||||
|
initial: (prev, values) => defaultConfig?.tailwind.css ?? TAILWIND_CSS_PATH[values.framework as 'vite' | 'nuxt' | 'laravel' | 'astro'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'toggle',
|
||||||
|
name: 'tailwindCssVariables',
|
||||||
|
message: `Would you like to use ${highlight(
|
||||||
|
'CSS variables',
|
||||||
|
)} for colors?`,
|
||||||
|
initial: defaultConfig?.tailwind.cssVariables ?? true,
|
||||||
|
active: 'yes',
|
||||||
|
inactive: 'no',
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// type: 'text',
|
||||||
|
// name: 'tailwindPrefix',
|
||||||
|
// message: `Are you using a custom ${highlight(
|
||||||
|
// 'tailwind prefix eg. tw-',
|
||||||
|
// )}? (Leave blank if not)`,
|
||||||
|
// initial: '',
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'tailwindConfig',
|
||||||
|
message: `Where is your ${highlight('tailwind.config')} located? ${colors.gray('(this file will be overwritten)')}`,
|
||||||
|
initial: (prev, values) => {
|
||||||
|
if (defaultConfig?.tailwind.config)
|
||||||
|
return defaultConfig?.tailwind.config
|
||||||
|
if (values.framework === 'astro')
|
||||||
|
return 'tailwind.config.mjs'
|
||||||
|
else return DEFAULT_TAILWIND_CONFIG
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'components',
|
||||||
|
message: `Configure the import alias for ${highlight('components')}:`,
|
||||||
|
initial: defaultConfig?.aliases.components ?? DEFAULT_COMPONENTS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'utils',
|
||||||
|
message: `Configure the import alias for ${highlight('utils')}:`,
|
||||||
|
initial: defaultConfig?.aliases.utils ?? DEFAULT_UTILS,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const config = rawConfigSchema.parse({
|
||||||
|
$schema: 'https://shadcn-vue.com/schema.json',
|
||||||
|
style: options.style,
|
||||||
|
typescript: options.typescript,
|
||||||
|
tsConfigPath: options.tsConfigPath,
|
||||||
|
framework: options.framework,
|
||||||
|
tailwind: {
|
||||||
|
config: options.tailwindConfig,
|
||||||
|
css: options.tailwindCss,
|
||||||
|
baseColor: options.tailwindBaseColor,
|
||||||
|
cssVariables: options.tailwindCssVariables,
|
||||||
|
// prefix: options.tailwindPrefix,
|
||||||
|
},
|
||||||
|
aliases: {
|
||||||
|
utils: options.utils,
|
||||||
|
components: options.components,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!skip) {
|
||||||
|
const { proceed } = await prompts({
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'proceed',
|
||||||
|
message: `Write configuration to ${highlight('components.json')}. Proceed?`,
|
||||||
|
initial: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!proceed)
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to file.
|
||||||
|
consola.log('')
|
||||||
|
const spinner = ora('Writing components.json...').start()
|
||||||
|
const targetPath = path.resolve(cwd, 'components.json')
|
||||||
|
await fs.writeFile(targetPath, JSON.stringify(config, null, 2), 'utf8')
|
||||||
|
spinner.succeed()
|
||||||
|
|
||||||
|
return await resolveConfigPaths(cwd, config)
|
||||||
|
}
|
||||||
155
packages/cli/src/commands/frameworks/vue/diff.ts
Normal file
155
packages/cli/src/commands/frameworks/vue/diff.ts
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
import { existsSync, promises as fs } from 'node:fs'
|
||||||
|
import { consola } from 'consola'
|
||||||
|
import path from 'pathe'
|
||||||
|
import { colors } from 'consola/utils'
|
||||||
|
import { type Change, diffLines } from 'diff'
|
||||||
|
import type { z } from 'zod'
|
||||||
|
import type { Config } from '../../../utils/get-config'
|
||||||
|
import {
|
||||||
|
fetchTree,
|
||||||
|
getItemTargetPath,
|
||||||
|
getRegistryBaseColor,
|
||||||
|
getRegistryIndex,
|
||||||
|
} from '@/src/utils/registry'
|
||||||
|
import type { registryIndexSchema } from '@/src/utils/registry/schema'
|
||||||
|
import { transform } from '@/src/utils/transformers'
|
||||||
|
|
||||||
|
interface DiffOptions {
|
||||||
|
yes: boolean
|
||||||
|
cwd: string
|
||||||
|
path?: string
|
||||||
|
component?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function (cwd: string, config: Config, options: DiffOptions) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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.`,
|
||||||
|
)
|
||||||
|
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('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
120
packages/cli/src/commands/frameworks/vue/init.ts
Normal file
120
packages/cli/src/commands/frameworks/vue/init.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
import { existsSync, promises as fs } from 'node:fs'
|
||||||
|
import { template } from 'lodash-es'
|
||||||
|
import { addDependency, addDevDependency } from 'nypm'
|
||||||
|
import { gte } from 'semver'
|
||||||
|
import { consola } from 'consola'
|
||||||
|
import path from 'pathe'
|
||||||
|
import ora from 'ora'
|
||||||
|
import { applyPrefixesCss } from '../../../utils/transformers/transform-tw-prefix'
|
||||||
|
import { transformByDetype } from '../../../utils/transformers/transform-sfc'
|
||||||
|
import { transformCJSToESM } from '../../../utils/transformers/transform-cjs-to-esm'
|
||||||
|
import * as templates from '../../../utils/templates'
|
||||||
|
import { getProjectInfo } from '../../../utils/get-project-info'
|
||||||
|
import { getRegistryBaseColor } from '../../../utils/registry'
|
||||||
|
import type { Config } from '../../../utils/get-config'
|
||||||
|
|
||||||
|
const PROJECT_DEPENDENCIES = {
|
||||||
|
base: [
|
||||||
|
'tailwindcss-animate',
|
||||||
|
'class-variance-authority',
|
||||||
|
'clsx',
|
||||||
|
'tailwind-merge',
|
||||||
|
'radix-vue',
|
||||||
|
],
|
||||||
|
nuxt: [
|
||||||
|
'@nuxtjs/tailwindcss',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function (cwd: string, config: Config) {
|
||||||
|
const spinner = ora('Initializing project...')?.start()
|
||||||
|
|
||||||
|
// Check in in a Nuxt project.
|
||||||
|
const { isNuxt, shadcnNuxt } = await getProjectInfo()
|
||||||
|
if (isNuxt) {
|
||||||
|
consola.log('')
|
||||||
|
shadcnNuxt
|
||||||
|
? consola.info(`Detected a Nuxt project with 'shadcn-nuxt' v${shadcnNuxt.version}...`)
|
||||||
|
: consola.warn(`Detected a Nuxt project without 'shadcn-nuxt' module. It's recommended to install it.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure all resolved paths directories exist.
|
||||||
|
for (const [key, resolvedPath] of Object.entries(config.resolvedPaths)) {
|
||||||
|
// Determine if the path is a file or directory.
|
||||||
|
// TODO: is there a better way to do this?
|
||||||
|
let dirname = path.extname(resolvedPath)
|
||||||
|
? path.dirname(resolvedPath)
|
||||||
|
: resolvedPath
|
||||||
|
|
||||||
|
// If the utils alias is set to something like "@/lib/utils",
|
||||||
|
// assume this is a file and remove the "utils" file name.
|
||||||
|
// TODO: In future releases we should add support for individual utils.
|
||||||
|
if (key === 'utils' && resolvedPath.endsWith('/utils')) {
|
||||||
|
// Remove /utils at the end.
|
||||||
|
dirname = dirname.replace(/\/utils$/, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existsSync(dirname))
|
||||||
|
await fs.mkdir(dirname, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const extension = config.typescript ? 'ts' : 'js'
|
||||||
|
|
||||||
|
// Write tailwind config.
|
||||||
|
await fs.writeFile(
|
||||||
|
config.resolvedPaths.tailwindConfig,
|
||||||
|
transformCJSToESM(
|
||||||
|
config.resolvedPaths.tailwindConfig,
|
||||||
|
config.tailwind.cssVariables
|
||||||
|
? template(templates.TAILWIND_CONFIG_WITH_VARIABLES)({ extension, framework: config.framework, prefix: config.tailwind.prefix })
|
||||||
|
: template(templates.TAILWIND_CONFIG)({ extension, framework: config.framework, prefix: config.tailwind.prefix }),
|
||||||
|
),
|
||||||
|
'utf8',
|
||||||
|
)
|
||||||
|
|
||||||
|
// Write css file.
|
||||||
|
const baseColor = await getRegistryBaseColor(config.tailwind.baseColor)
|
||||||
|
if (baseColor) {
|
||||||
|
await fs.writeFile(
|
||||||
|
config.resolvedPaths.tailwindCss,
|
||||||
|
config.tailwind.cssVariables
|
||||||
|
? config.tailwind.prefix
|
||||||
|
? applyPrefixesCss(baseColor.cssVarsTemplate, config.tailwind.prefix)
|
||||||
|
: baseColor.cssVarsTemplate
|
||||||
|
: baseColor.inlineColorsTemplate,
|
||||||
|
'utf8',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write cn file.
|
||||||
|
await fs.writeFile(
|
||||||
|
`${config.resolvedPaths.utils}.${extension}`,
|
||||||
|
extension === 'ts' ? templates.UTILS : await transformByDetype(templates.UTILS, '.ts'),
|
||||||
|
'utf8',
|
||||||
|
)
|
||||||
|
|
||||||
|
spinner?.succeed()
|
||||||
|
|
||||||
|
// Install dependencies.
|
||||||
|
const dependenciesSpinner = ora('Installing dependencies...')?.start()
|
||||||
|
|
||||||
|
// Starting from `shadcn-nuxt` version 0.10.4, Base dependencies are handled by the module so no need to re-add them by the CLI
|
||||||
|
const baseDeps = gte(shadcnNuxt?.version || '0.0.0', '0.10.4') ? [] : PROJECT_DEPENDENCIES.base
|
||||||
|
const iconsDep = config.style === 'new-york' ? ['@radix-icons/vue'] : ['lucide-vue-next']
|
||||||
|
const deps = baseDeps.concat(iconsDep).filter(Boolean)
|
||||||
|
|
||||||
|
await Promise.allSettled(
|
||||||
|
[
|
||||||
|
config.framework === 'nuxt' && await addDevDependency(PROJECT_DEPENDENCIES.nuxt, {
|
||||||
|
cwd,
|
||||||
|
silent: true,
|
||||||
|
}),
|
||||||
|
await addDependency(deps, {
|
||||||
|
cwd,
|
||||||
|
silent: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
dependenciesSpinner?.succeed()
|
||||||
|
}
|
||||||
|
|
@ -1,49 +1,13 @@
|
||||||
import { existsSync, promises as fs } from 'node:fs'
|
import { existsSync } from 'node:fs'
|
||||||
import process from 'node:process'
|
import process from 'node:process'
|
||||||
import path from 'pathe'
|
import path from 'pathe'
|
||||||
import { Command } from 'commander'
|
import { Command } from 'commander'
|
||||||
import { template } from 'lodash-es'
|
|
||||||
import ora from 'ora'
|
|
||||||
import prompts from 'prompts'
|
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { addDependency, addDevDependency } from 'nypm'
|
|
||||||
import { consola } from 'consola'
|
import { consola } from 'consola'
|
||||||
import { colors } from 'consola/utils'
|
import { colors } from 'consola/utils'
|
||||||
import { gte } from 'semver'
|
|
||||||
import { getProjectInfo } from '../utils/get-project-info'
|
|
||||||
import * as templates from '../utils/templates'
|
|
||||||
import {
|
|
||||||
getRegistryBaseColor,
|
|
||||||
getRegistryBaseColors,
|
|
||||||
getRegistryStyles,
|
|
||||||
} from '../utils/registry'
|
|
||||||
import { handleError } from '../utils/handle-error'
|
import { handleError } from '../utils/handle-error'
|
||||||
import { transformByDetype } from '../utils/transformers/transform-sfc'
|
import { getProjectInfo } from '../utils/get-project-info'
|
||||||
import {
|
import frameworksCommands from './frameworks'
|
||||||
type Config,
|
|
||||||
DEFAULT_COMPONENTS,
|
|
||||||
DEFAULT_TAILWIND_CONFIG,
|
|
||||||
DEFAULT_UTILS,
|
|
||||||
TAILWIND_CSS_PATH,
|
|
||||||
getConfig,
|
|
||||||
rawConfigSchema,
|
|
||||||
resolveConfigPaths,
|
|
||||||
} from '../utils/get-config'
|
|
||||||
import { transformCJSToESM } from '../utils/transformers/transform-cjs-to-esm'
|
|
||||||
import { applyPrefixesCss } from '../utils/transformers/transform-tw-prefix'
|
|
||||||
|
|
||||||
const PROJECT_DEPENDENCIES = {
|
|
||||||
base: [
|
|
||||||
'tailwindcss-animate',
|
|
||||||
'class-variance-authority',
|
|
||||||
'clsx',
|
|
||||||
'tailwind-merge',
|
|
||||||
'radix-vue',
|
|
||||||
],
|
|
||||||
nuxt: [
|
|
||||||
'@nuxtjs/tailwindcss',
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
const initOptionsSchema = z.object({
|
const initOptionsSchema = z.object({
|
||||||
cwd: z.string(),
|
cwd: z.string(),
|
||||||
|
|
@ -70,11 +34,20 @@ export const init = new Command()
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read config.
|
// Get the corresponding framework commands
|
||||||
const existingConfig = await getConfig(cwd)
|
const { isNuxt } = await getProjectInfo()
|
||||||
const config = await promptForConfig(cwd, existingConfig, options.yes)
|
const framework = isNuxt ? 'nuxt' : 'vue'
|
||||||
|
const { loadConfig, init } = frameworksCommands[framework]
|
||||||
|
|
||||||
await runInit(cwd, config)
|
// Read config
|
||||||
|
const config = await loadConfig(cwd, options, true)
|
||||||
|
if (!config) {
|
||||||
|
consola.error(`Error loading config. Please run the ${colors.green('init')} command again.`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init
|
||||||
|
await init(cwd, config)
|
||||||
|
|
||||||
consola.log('')
|
consola.log('')
|
||||||
consola.info(
|
consola.info(
|
||||||
|
|
@ -86,247 +59,3 @@ export const init = new Command()
|
||||||
handleError(error)
|
handleError(error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function promptForConfig(
|
|
||||||
cwd: string,
|
|
||||||
defaultConfig: Config | null = null,
|
|
||||||
skip = false,
|
|
||||||
) {
|
|
||||||
const highlight = (text: string) => colors.cyan(text)
|
|
||||||
|
|
||||||
const styles = await getRegistryStyles()
|
|
||||||
const baseColors = await getRegistryBaseColors()
|
|
||||||
|
|
||||||
const options = await prompts([
|
|
||||||
{
|
|
||||||
type: 'toggle',
|
|
||||||
name: 'typescript',
|
|
||||||
message: `Would you like to use ${highlight('TypeScript')}? ${colors.gray('(recommended)')}?`,
|
|
||||||
initial: defaultConfig?.typescript ?? true,
|
|
||||||
active: 'yes',
|
|
||||||
inactive: 'no',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'select',
|
|
||||||
name: 'framework',
|
|
||||||
message: `Which ${highlight('framework')} are you using?`,
|
|
||||||
choices: [
|
|
||||||
{ title: 'Vite', value: 'vite' },
|
|
||||||
{ title: 'Nuxt', value: 'nuxt' },
|
|
||||||
{ title: 'Laravel', value: 'laravel' },
|
|
||||||
{ title: 'Astro', value: 'astro' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'select',
|
|
||||||
name: 'style',
|
|
||||||
message: `Which ${highlight('style')} would you like to use?`,
|
|
||||||
choices: styles.map(style => ({
|
|
||||||
title: style.label,
|
|
||||||
value: style.name,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'select',
|
|
||||||
name: 'tailwindBaseColor',
|
|
||||||
message: `Which color would you like to use as ${highlight(
|
|
||||||
'base color',
|
|
||||||
)}?`,
|
|
||||||
choices: baseColors.map(color => ({
|
|
||||||
title: color.label,
|
|
||||||
value: color.name,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
name: 'tsConfigPath',
|
|
||||||
message: (prev, values) => `Where is your ${highlight(values.typescript ? 'tsconfig.json' : 'jsconfig.json')} file?`,
|
|
||||||
initial: (prev, values) => {
|
|
||||||
const prefix = values.framework === 'nuxt' ? '.nuxt/' : './'
|
|
||||||
const path = values.typescript ? 'tsconfig.json' : 'jsconfig.json'
|
|
||||||
return prefix + path
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
name: 'tailwindCss',
|
|
||||||
message: `Where is your ${highlight('global CSS')} file? ${colors.gray('(this file will be overwritten)')}`,
|
|
||||||
initial: (prev, values) => defaultConfig?.tailwind.css ?? TAILWIND_CSS_PATH[values.framework as 'vite' | 'nuxt' | 'laravel' | 'astro'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'toggle',
|
|
||||||
name: 'tailwindCssVariables',
|
|
||||||
message: `Would you like to use ${highlight(
|
|
||||||
'CSS variables',
|
|
||||||
)} for colors?`,
|
|
||||||
initial: defaultConfig?.tailwind.cssVariables ?? true,
|
|
||||||
active: 'yes',
|
|
||||||
inactive: 'no',
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// type: 'text',
|
|
||||||
// name: 'tailwindPrefix',
|
|
||||||
// message: `Are you using a custom ${highlight(
|
|
||||||
// 'tailwind prefix eg. tw-',
|
|
||||||
// )}? (Leave blank if not)`,
|
|
||||||
// initial: '',
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
name: 'tailwindConfig',
|
|
||||||
message: `Where is your ${highlight('tailwind.config')} located? ${colors.gray('(this file will be overwritten)')}`,
|
|
||||||
initial: (prev, values) => {
|
|
||||||
if (defaultConfig?.tailwind.config)
|
|
||||||
return defaultConfig?.tailwind.config
|
|
||||||
if (values.framework === 'astro')
|
|
||||||
return 'tailwind.config.mjs'
|
|
||||||
else return DEFAULT_TAILWIND_CONFIG
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
name: 'components',
|
|
||||||
message: `Configure the import alias for ${highlight('components')}:`,
|
|
||||||
initial: defaultConfig?.aliases.components ?? DEFAULT_COMPONENTS,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
name: 'utils',
|
|
||||||
message: `Configure the import alias for ${highlight('utils')}:`,
|
|
||||||
initial: defaultConfig?.aliases.utils ?? DEFAULT_UTILS,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
const config = rawConfigSchema.parse({
|
|
||||||
$schema: 'https://shadcn-vue.com/schema.json',
|
|
||||||
style: options.style,
|
|
||||||
typescript: options.typescript,
|
|
||||||
tsConfigPath: options.tsConfigPath,
|
|
||||||
framework: options.framework,
|
|
||||||
tailwind: {
|
|
||||||
config: options.tailwindConfig,
|
|
||||||
css: options.tailwindCss,
|
|
||||||
baseColor: options.tailwindBaseColor,
|
|
||||||
cssVariables: options.tailwindCssVariables,
|
|
||||||
// prefix: options.tailwindPrefix,
|
|
||||||
},
|
|
||||||
aliases: {
|
|
||||||
utils: options.utils,
|
|
||||||
components: options.components,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!skip) {
|
|
||||||
const { proceed } = await prompts({
|
|
||||||
type: 'confirm',
|
|
||||||
name: 'proceed',
|
|
||||||
message: `Write configuration to ${highlight('components.json')}. Proceed?`,
|
|
||||||
initial: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!proceed)
|
|
||||||
process.exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write to file.
|
|
||||||
consola.log('')
|
|
||||||
const spinner = ora('Writing components.json...').start()
|
|
||||||
const targetPath = path.resolve(cwd, 'components.json')
|
|
||||||
await fs.writeFile(targetPath, JSON.stringify(config, null, 2), 'utf8')
|
|
||||||
spinner.succeed()
|
|
||||||
|
|
||||||
return await resolveConfigPaths(cwd, config)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function runInit(cwd: string, config: Config) {
|
|
||||||
const spinner = ora('Initializing project...')?.start()
|
|
||||||
|
|
||||||
// Check in in a Nuxt project.
|
|
||||||
const { isNuxt, shadcnNuxt } = await getProjectInfo()
|
|
||||||
if (isNuxt) {
|
|
||||||
consola.log('')
|
|
||||||
shadcnNuxt
|
|
||||||
? consola.info(`Detected a Nuxt project with 'shadcn-nuxt' v${shadcnNuxt.version}...`)
|
|
||||||
: consola.warn(`Detected a Nuxt project without 'shadcn-nuxt' module. It's recommended to install it.`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure all resolved paths directories exist.
|
|
||||||
for (const [key, resolvedPath] of Object.entries(config.resolvedPaths)) {
|
|
||||||
// Determine if the path is a file or directory.
|
|
||||||
// TODO: is there a better way to do this?
|
|
||||||
let dirname = path.extname(resolvedPath)
|
|
||||||
? path.dirname(resolvedPath)
|
|
||||||
: resolvedPath
|
|
||||||
|
|
||||||
// If the utils alias is set to something like "@/lib/utils",
|
|
||||||
// assume this is a file and remove the "utils" file name.
|
|
||||||
// TODO: In future releases we should add support for individual utils.
|
|
||||||
if (key === 'utils' && resolvedPath.endsWith('/utils')) {
|
|
||||||
// Remove /utils at the end.
|
|
||||||
dirname = dirname.replace(/\/utils$/, '')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!existsSync(dirname))
|
|
||||||
await fs.mkdir(dirname, { recursive: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
const extension = config.typescript ? 'ts' : 'js'
|
|
||||||
|
|
||||||
// Write tailwind config.
|
|
||||||
await fs.writeFile(
|
|
||||||
config.resolvedPaths.tailwindConfig,
|
|
||||||
transformCJSToESM(
|
|
||||||
config.resolvedPaths.tailwindConfig,
|
|
||||||
config.tailwind.cssVariables
|
|
||||||
? template(templates.TAILWIND_CONFIG_WITH_VARIABLES)({ extension, framework: config.framework, prefix: config.tailwind.prefix })
|
|
||||||
: template(templates.TAILWIND_CONFIG)({ extension, framework: config.framework, prefix: config.tailwind.prefix }),
|
|
||||||
),
|
|
||||||
'utf8',
|
|
||||||
)
|
|
||||||
|
|
||||||
// Write css file.
|
|
||||||
const baseColor = await getRegistryBaseColor(config.tailwind.baseColor)
|
|
||||||
if (baseColor) {
|
|
||||||
await fs.writeFile(
|
|
||||||
config.resolvedPaths.tailwindCss,
|
|
||||||
config.tailwind.cssVariables
|
|
||||||
? config.tailwind.prefix
|
|
||||||
? applyPrefixesCss(baseColor.cssVarsTemplate, config.tailwind.prefix)
|
|
||||||
: baseColor.cssVarsTemplate
|
|
||||||
: baseColor.inlineColorsTemplate,
|
|
||||||
'utf8',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write cn file.
|
|
||||||
await fs.writeFile(
|
|
||||||
`${config.resolvedPaths.utils}.${extension}`,
|
|
||||||
extension === 'ts' ? templates.UTILS : await transformByDetype(templates.UTILS, '.ts'),
|
|
||||||
'utf8',
|
|
||||||
)
|
|
||||||
|
|
||||||
spinner?.succeed()
|
|
||||||
|
|
||||||
// Install dependencies.
|
|
||||||
const dependenciesSpinner = ora('Installing dependencies...')?.start()
|
|
||||||
|
|
||||||
// Starting from `shadcn-nuxt` version 0.10.4, Base dependencies are handled by the module so no need to re-add them by the CLI
|
|
||||||
const baseDeps = gte(shadcnNuxt?.version || '0.0.0', '0.10.4') ? [] : PROJECT_DEPENDENCIES.base
|
|
||||||
const iconsDep = config.style === 'new-york' ? ['@radix-icons/vue'] : ['lucide-vue-next']
|
|
||||||
const deps = baseDeps.concat(iconsDep).filter(Boolean)
|
|
||||||
|
|
||||||
await Promise.allSettled(
|
|
||||||
[
|
|
||||||
config.framework === 'nuxt' && await addDevDependency(PROJECT_DEPENDENCIES.nuxt, {
|
|
||||||
cwd,
|
|
||||||
silent: true,
|
|
||||||
}),
|
|
||||||
await addDependency(deps, {
|
|
||||||
cwd,
|
|
||||||
silent: true,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
dependenciesSpinner?.succeed()
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import path from 'pathe'
|
||||||
import { addDependency } from 'nypm'
|
import { addDependency } from 'nypm'
|
||||||
import { afterEach, expect, it, vi } from 'vitest'
|
import { afterEach, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
import { runInit } from '../../src/commands/init'
|
import runInitVue from '../../src/commands/frameworks/vue/init'
|
||||||
import { getConfig } from '../../src/utils/get-config'
|
import { getConfig } from '../../src/utils/get-config'
|
||||||
import * as registry from '../../src/utils/registry'
|
import * as registry from '../../src/utils/registry'
|
||||||
|
|
||||||
|
|
@ -29,7 +29,7 @@ it('init config-full', async () => {
|
||||||
const targetDir = path.resolve(__dirname, '../fixtures/config-full')
|
const targetDir = path.resolve(__dirname, '../fixtures/config-full')
|
||||||
const config = await getConfig(targetDir)
|
const config = await getConfig(targetDir)
|
||||||
|
|
||||||
await runInit(targetDir, config!)
|
await runInitVue(targetDir, config!)
|
||||||
|
|
||||||
expect(mockMkdir).toHaveBeenNthCalledWith(
|
expect(mockMkdir).toHaveBeenNthCalledWith(
|
||||||
1,
|
1,
|
||||||
|
|
@ -98,7 +98,7 @@ it('init config-partial', async () => {
|
||||||
const targetDir = path.resolve(__dirname, '../fixtures/config-partial')
|
const targetDir = path.resolve(__dirname, '../fixtures/config-partial')
|
||||||
const config = await getConfig(targetDir)
|
const config = await getConfig(targetDir)
|
||||||
|
|
||||||
await runInit(targetDir, config!)
|
await runInitVue(targetDir, config!)
|
||||||
|
|
||||||
expect(mockMkdir).toHaveBeenNthCalledWith(
|
expect(mockMkdir).toHaveBeenNthCalledWith(
|
||||||
1,
|
1,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user