328 lines
9.0 KiB
TypeScript
328 lines
9.0 KiB
TypeScript
import { promises as fs } from 'node:fs'
|
|
import path from 'node:path'
|
|
import { preFlightInit } from '@/src/preflights/preflight-init'
|
|
import { addComponents } from '@/src/utils/add-components'
|
|
import {
|
|
type Config,
|
|
DEFAULT_COMPONENTS,
|
|
DEFAULT_TAILWIND_CONFIG,
|
|
DEFAULT_TAILWIND_CSS,
|
|
DEFAULT_UTILS,
|
|
getConfig,
|
|
rawConfigSchema,
|
|
resolveConfigPaths,
|
|
} from '@/src/utils/get-config'
|
|
import { getProjectConfig, getProjectInfo } from '@/src/utils/get-project-info'
|
|
import { handleError } from '@/src/utils/handle-error'
|
|
import { highlighter } from '@/src/utils/highlighter'
|
|
import { logger } from '@/src/utils/logger'
|
|
import { getRegistryBaseColors, getRegistryStyles } from '@/src/utils/registry'
|
|
import { spinner } from '@/src/utils/spinner'
|
|
import { updateTailwindContent } from '@/src/utils/updaters/update-tailwind-content'
|
|
import { Command } from 'commander'
|
|
import prompts from 'prompts'
|
|
import { z } from 'zod'
|
|
|
|
export const initOptionsSchema = z.object({
|
|
cwd: z.string(),
|
|
components: z.array(z.string()).optional(),
|
|
yes: z.boolean(),
|
|
defaults: z.boolean(),
|
|
force: z.boolean(),
|
|
silent: z.boolean(),
|
|
isNewProject: z.boolean(),
|
|
srcDir: z.boolean().optional(),
|
|
})
|
|
|
|
export const init = new Command()
|
|
.name('init')
|
|
.description('initialize your project and install dependencies')
|
|
.argument(
|
|
'[components...]',
|
|
'the components to add or a url to the component.',
|
|
)
|
|
.option('-y, --yes', 'skip confirmation prompt.', true)
|
|
.option('-d, --defaults,', 'use default configuration.', false)
|
|
.option('-f, --force', 'force overwrite of existing configuration.', false)
|
|
.option(
|
|
'-c, --cwd <cwd>',
|
|
'the working directory. defaults to the current directory.',
|
|
process.cwd(),
|
|
)
|
|
.option('-s, --silent', 'mute output.', false)
|
|
.option(
|
|
'--src-dir',
|
|
'use the src directory when creating a new project.',
|
|
false,
|
|
)
|
|
.action(async (components, opts) => {
|
|
try {
|
|
const options = initOptionsSchema.parse({
|
|
cwd: path.resolve(opts.cwd),
|
|
isNewProject: false,
|
|
components,
|
|
...opts,
|
|
})
|
|
|
|
await runInit(options)
|
|
|
|
logger.log(
|
|
`${highlighter.success(
|
|
'Success!',
|
|
)} Project initialization completed.\nYou may now add components.`,
|
|
)
|
|
logger.break()
|
|
}
|
|
catch (error) {
|
|
logger.break()
|
|
handleError(error)
|
|
}
|
|
})
|
|
|
|
export async function runInit(
|
|
options: z.infer<typeof initOptionsSchema> & {
|
|
skipPreflight?: boolean
|
|
},
|
|
) {
|
|
let projectInfo
|
|
if (!options.skipPreflight) {
|
|
const preflight = await preFlightInit(options)
|
|
// if (preflight.errors[ERRORS.MISSING_DIR_OR_EMPTY_PROJECT]) {
|
|
// const { projectPath } = await createProject(options)
|
|
// if (!projectPath) {
|
|
// process.exit(1)
|
|
// }
|
|
// options.cwd = projectPath
|
|
// options.isNewProject = true
|
|
// }
|
|
projectInfo = preflight.projectInfo
|
|
}
|
|
else {
|
|
projectInfo = await getProjectInfo(options.cwd)
|
|
}
|
|
|
|
const projectConfig = await getProjectConfig(options.cwd, projectInfo)
|
|
const config = projectConfig
|
|
? await promptForMinimalConfig(projectConfig, options)
|
|
: await promptForConfig(await getConfig(options.cwd))
|
|
|
|
if (!options.yes) {
|
|
const { proceed } = await prompts({
|
|
type: 'confirm',
|
|
name: 'proceed',
|
|
message: `Write configuration to ${highlighter.info(
|
|
'components.json',
|
|
)}. Proceed?`,
|
|
initial: true,
|
|
})
|
|
|
|
if (!proceed) {
|
|
process.exit(0)
|
|
}
|
|
}
|
|
|
|
// Write components.json.
|
|
const componentSpinner = spinner(`Writing components.json.`).start()
|
|
const targetPath = path.resolve(options.cwd, 'components.json')
|
|
await fs.writeFile(targetPath, JSON.stringify(config, null, 2), 'utf8')
|
|
componentSpinner.succeed()
|
|
|
|
// Add components.
|
|
const fullConfig = await resolveConfigPaths(options.cwd, config)
|
|
const components = ['index', ...(options.components || [])]
|
|
await addComponents(components, fullConfig, {
|
|
// Init will always overwrite files.
|
|
overwrite: true,
|
|
silent: options.silent,
|
|
isNewProject:
|
|
options.isNewProject || projectInfo?.framework.name === 'nuxt',
|
|
})
|
|
|
|
// If a new project is using src dir, let's update the tailwind content config.
|
|
// TODO: Handle this per framework.
|
|
if (options.isNewProject && options.srcDir) {
|
|
await updateTailwindContent(
|
|
['./src/**/*.{js,ts,jsx,tsx,mdx}'],
|
|
fullConfig,
|
|
{
|
|
silent: options.silent,
|
|
},
|
|
)
|
|
}
|
|
|
|
return fullConfig
|
|
}
|
|
|
|
async function promptForConfig(defaultConfig: Config | null = null) {
|
|
const [styles, baseColors] = await Promise.all([
|
|
getRegistryStyles(),
|
|
getRegistryBaseColors(),
|
|
])
|
|
|
|
logger.info('')
|
|
const options = await prompts([
|
|
{
|
|
type: 'toggle',
|
|
name: 'typescript',
|
|
message: `Would you like to use ${highlighter.info(
|
|
'TypeScript',
|
|
)} (recommended)?`,
|
|
initial: defaultConfig?.typescript ?? true,
|
|
active: 'yes',
|
|
inactive: 'no',
|
|
},
|
|
{
|
|
type: 'select',
|
|
name: 'style',
|
|
message: `Which ${highlighter.info('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 the ${highlighter.info(
|
|
'base color',
|
|
)}?`,
|
|
choices: baseColors.map(color => ({
|
|
title: color.label,
|
|
value: color.name,
|
|
})),
|
|
},
|
|
{
|
|
type: 'text',
|
|
name: 'tailwindCss',
|
|
message: `Where is your ${highlighter.info('global CSS')} file?`,
|
|
initial: defaultConfig?.tailwind.css ?? DEFAULT_TAILWIND_CSS,
|
|
},
|
|
{
|
|
type: 'toggle',
|
|
name: 'tailwindCssVariables',
|
|
message: `Would you like to use ${highlighter.info(
|
|
'CSS variables',
|
|
)} for theming?`,
|
|
initial: defaultConfig?.tailwind.cssVariables ?? true,
|
|
active: 'yes',
|
|
inactive: 'no',
|
|
},
|
|
{
|
|
type: 'text',
|
|
name: 'tailwindPrefix',
|
|
message: `Are you using a custom ${highlighter.info(
|
|
'tailwind prefix eg. tw-',
|
|
)}? (Leave blank if not)`,
|
|
initial: '',
|
|
},
|
|
{
|
|
type: 'text',
|
|
name: 'tailwindConfig',
|
|
message: `Where is your ${highlighter.info(
|
|
'tailwind.config.js',
|
|
)} located?`,
|
|
initial: defaultConfig?.tailwind.config ?? DEFAULT_TAILWIND_CONFIG,
|
|
},
|
|
{
|
|
type: 'text',
|
|
name: 'components',
|
|
message: `Configure the import alias for ${highlighter.info(
|
|
'components',
|
|
)}:`,
|
|
initial: defaultConfig?.aliases.components ?? DEFAULT_COMPONENTS,
|
|
},
|
|
{
|
|
type: 'text',
|
|
name: 'utils',
|
|
message: `Configure the import alias for ${highlighter.info('utils')}:`,
|
|
initial: defaultConfig?.aliases.utils ?? DEFAULT_UTILS,
|
|
},
|
|
])
|
|
|
|
return rawConfigSchema.parse({
|
|
$schema: 'https://ui.shadcn.com/schema.json',
|
|
style: options.style,
|
|
tailwind: {
|
|
config: options.tailwindConfig,
|
|
css: options.tailwindCss,
|
|
baseColor: options.tailwindBaseColor,
|
|
cssVariables: options.tailwindCssVariables,
|
|
prefix: options.tailwindPrefix,
|
|
},
|
|
typescript: options.typescript,
|
|
aliases: {
|
|
utils: options.utils,
|
|
components: options.components,
|
|
// TODO: fix this.
|
|
lib: options.components.replace(/\/components$/, 'lib'),
|
|
hooks: options.components.replace(/\/components$/, 'hooks'),
|
|
},
|
|
})
|
|
}
|
|
|
|
async function promptForMinimalConfig(
|
|
defaultConfig: Config,
|
|
opts: z.infer<typeof initOptionsSchema>,
|
|
) {
|
|
let style = defaultConfig.style
|
|
let baseColor = defaultConfig.tailwind.baseColor
|
|
let cssVariables = defaultConfig.tailwind.cssVariables
|
|
|
|
if (!opts.defaults) {
|
|
const [styles, baseColors] = await Promise.all([
|
|
getRegistryStyles(),
|
|
getRegistryBaseColors(),
|
|
])
|
|
|
|
const options = await prompts([
|
|
{
|
|
type: 'select',
|
|
name: 'style',
|
|
message: `Which ${highlighter.info('style')} would you like to use?`,
|
|
choices: styles.map(style => ({
|
|
title: style.label,
|
|
value: style.name,
|
|
})),
|
|
initial: styles.findIndex(s => s.name === style),
|
|
},
|
|
{
|
|
type: 'select',
|
|
name: 'tailwindBaseColor',
|
|
message: `Which color would you like to use as the ${highlighter.info(
|
|
'base color',
|
|
)}?`,
|
|
choices: baseColors.map(color => ({
|
|
title: color.label,
|
|
value: color.name,
|
|
})),
|
|
},
|
|
{
|
|
type: 'toggle',
|
|
name: 'tailwindCssVariables',
|
|
message: `Would you like to use ${highlighter.info(
|
|
'CSS variables',
|
|
)} for theming?`,
|
|
initial: defaultConfig?.tailwind.cssVariables,
|
|
active: 'yes',
|
|
inactive: 'no',
|
|
},
|
|
])
|
|
|
|
style = options.style
|
|
baseColor = options.tailwindBaseColor
|
|
cssVariables = options.tailwindCssVariables
|
|
}
|
|
|
|
return rawConfigSchema.parse({
|
|
$schema: defaultConfig?.$schema,
|
|
style,
|
|
tailwind: {
|
|
...defaultConfig?.tailwind,
|
|
baseColor,
|
|
cssVariables,
|
|
},
|
|
aliases: defaultConfig?.aliases,
|
|
iconLibrary: defaultConfig?.iconLibrary,
|
|
})
|
|
}
|