UNPKG

leshi-ui

Version:

Modern CLI for building and managing React Native UI components with copy-paste simplicity, custom theming, and open source design system support

345 lines (344 loc) 15.7 kB
import { Command } from 'commander'; import inquirer from 'inquirer'; import ora from 'ora'; import { Logger } from '../utils/logger.js'; import { FileUtils } from '../utils/file-utils.js'; import { ProjectService } from '../services/project-service.js'; import { ComponentRegistryService } from '../services/component-registry.js'; import { GitHubProjectService } from '../services/github-project-service.js'; import { GitHubService } from '../services/github-service.js'; import { colors, icons } from '../utils/colors.js'; export function createAddCommand() { return new Command('add') .description('Add components or themes to your project'); } export function createAddComponentCommand() { return new Command('component') .description('Add UI components to your project') .argument('[components...]', 'Component names to add') .option('--rn', 'Use React Native StyleSheet implementation (default)') .option('--unistyles', 'Use Unistyles implementation') .option('-o, --overwrite', 'Overwrite existing files') .option('-y, --yes', 'Skip confirmation prompts') .option('-c, --cwd <path>', 'Working directory', process.cwd()) .action(async (components, options) => { try { await addComponents(components, options); } catch (error) { Logger.error(`Failed to add components: ${error instanceof Error ? error.message : 'Unknown error'}`); process.exit(1); } }); } export function createAddThemeCommand() { return new Command('theme') .description('Add themes to your project') .argument('[themes...]', 'Theme names to add') .option('--rn', 'Use React Native StyleSheet implementation (default)') .option('--unistyles', 'Use Unistyles implementation') .option('-o, --overwrite', 'Overwrite existing files') .option('-y, --yes', 'Skip confirmation prompts') .option('-c, --cwd <path>', 'Working directory', process.cwd()) .action(async (themes, options) => { try { await addThemes(themes, options); } catch (error) { Logger.error(`Failed to add themes: ${error instanceof Error ? error.message : 'Unknown error'}`); process.exit(1); } }); } async function addComponents(componentNames, options) { const cwd = options.cwd || process.cwd(); let framework = 'rn'; if (options.unistyles) { framework = 'unistyles'; } if (componentNames.length === 0) { const availableComponents = await ComponentRegistryService.getComponentsWithDetails(); const { selectedComponents } = await inquirer.prompt([ { type: 'checkbox', name: 'selectedComponents', message: 'Select components to add:', choices: availableComponents.map(comp => ({ name: `${comp.name} - ${colors.dim(comp.description)}`, value: comp.name, })), }, ]); if (selectedComponents.length === 0) { Logger.info('No components selected.'); return; } componentNames = selectedComponents; } Logger.title(`${icons.package} Adding components`); Logger.subtitle(`Framework: ${framework === 'rn' ? 'React Native StyleSheet' : 'Unistyles'}`); const githubRef = await GitHubService.getCurrentRef(); Logger.subtitle(`Source: GitHub @ ${githubRef}`); Logger.break(); const invalidComponents = []; for (const name of componentNames) { if (!(await ComponentRegistryService.componentExists(name))) { invalidComponents.push(name); } } if (invalidComponents.length > 0) { Logger.error(`Components not found: ${invalidComponents.join(', ')}`); Logger.tip(`Run ${colors.primary('npx leshi-ui@latest list component')} to see available components`); return; } let spinner = ora('Resolving dependencies...').start(); try { const allResolvedComponents = await ComponentRegistryService.resolveDependencies(componentNames); const requestedComponents = componentNames; const potentialDependencies = allResolvedComponents.filter(comp => !requestedComponents.includes(comp)); spinner.stop(); let componentsToInstall = [...requestedComponents]; if (potentialDependencies.length > 0 && !options.yes) { Logger.break(); Logger.info(`${icons.package} Components to install:`); requestedComponents.forEach(name => { Logger.log(` ${colors.primary('✓')} ${colors.primary(name)} ${colors.dim('(requested)')}`); }); Logger.log(''); Logger.info(`${icons.info} The following dependencies are available but not required:`); potentialDependencies.forEach(name => { Logger.log(` ${colors.dim('•')} ${colors.dim(name)}`); }); Logger.break(); Logger.log(`${colors.dim('Note:')} Dependencies will be automatically imported if they exist in your project.`); Logger.break(); const { installDependencies } = await inquirer.prompt([ { type: 'confirm', name: 'installDependencies', message: `${icons.question} Do you also want to install the dependencies?`, default: false, }, ]); if (installDependencies) { componentsToInstall = allResolvedComponents; } Logger.break(); } else if (options.yes && potentialDependencies.length > 0) { Logger.info(`${icons.info} Installing only requested components. Dependencies will be auto-imported if they exist.`); Logger.break(); } spinner = ora('Checking for existing files...').start(); const config = await ProjectService.getProjectConfig(cwd); const existingComponents = []; const newComponents = []; for (const componentName of componentsToInstall) { const targetPath = FileUtils.join(config.componentsDir, `${componentName}.tsx`); if (await FileUtils.exists(targetPath)) { existingComponents.push(componentName); } else { newComponents.push(componentName); } } spinner.stop(); let componentsToOverwrite = []; if (existingComponents.length > 0) { if (options.overwrite) { componentsToOverwrite = existingComponents; Logger.info(`${icons.info} Overwriting ${existingComponents.length} existing component${existingComponents.length === 1 ? '' : 's'}.`); } else if (!options.yes) { Logger.break(); Logger.info(`${icons.info} The following components already exist and will be skipped:`); existingComponents.forEach(name => { Logger.log(` • ${colors.dim(name)} ${colors.dim('(exists)')}`); }); if (newComponents.length > 0) { Logger.log(''); Logger.info(`${icons.package} New components to install:`); newComponents.forEach(name => { Logger.log(` • ${colors.primary(name)} ${colors.dim('(new)')}`); }); } Logger.break(); const { shouldOverwriteExisting } = await inquirer.prompt([ { type: 'confirm', name: 'shouldOverwriteExisting', message: `${icons.question} Do you want to overwrite the existing components?`, default: false, }, ]); if (shouldOverwriteExisting) { componentsToOverwrite = existingComponents; } Logger.break(); } else { Logger.info(`${icons.info} Skipping ${existingComponents.length} existing component${existingComponents.length === 1 ? '' : 's'}.`); } } const finalComponentsToInstall = [...newComponents, ...componentsToOverwrite]; if (finalComponentsToInstall.length === 0) { Logger.info(`${icons.success} All requested components already exist. Nothing to install.`); return; } const installSpinner = ora(`Installing ${finalComponentsToInstall.length} component${finalComponentsToInstall.length === 1 ? '' : 's'}...`).start(); spinner = installSpinner; await FileUtils.ensureDir(config.componentsDir); const installedComponents = []; const componentDetails = []; for (const componentName of finalComponentsToInstall) { const targetPath = FileUtils.join(config.componentsDir, `${componentName}.tsx`); const success = await GitHubProjectService.downloadComponent(framework, componentName, targetPath, options.overwrite || false); if (success) { installedComponents.push(componentName); const componentInfo = await ComponentRegistryService.getComponent(componentName); if (componentInfo) { componentDetails.push({ name: componentName, dependencies: componentInfo.dependencies, externalDeps: componentInfo.externalDeps, utilities: componentInfo.utilities, }); } } } for (const detail of componentDetails) { if (detail.utilities) { for (const utilityFile of detail.utilities) { const targetPath = FileUtils.join(config.libDir, FileUtils.basename(utilityFile)); await GitHubProjectService.downloadUtility(framework, utilityFile, targetPath, options.overwrite || false); } } } spinner.succeed(`Successfully added ${installedComponents.length} component${installedComponents.length === 1 ? '' : 's'}!`); Logger.break(); Logger.success('Components installed successfully!'); Logger.break(); Logger.log(`${icons.package} Installed components:`); installedComponents.forEach(name => { Logger.log(` • ${colors.primary(name)}`); }); const externalDeps = new Set(); const setupInstructions = []; for (const detail of componentDetails) { if (detail.externalDeps.length > 0) { detail.externalDeps.forEach(dep => externalDeps.add(dep)); } } if (externalDeps.size > 0) { Logger.break(); Logger.warning('External dependencies required:'); Array.from(externalDeps).forEach(dep => { Logger.log(` • ${colors.primary(dep)}`); }); Logger.tip(`Install with: ${colors.primary(`npm install ${Array.from(externalDeps).join(' ')}`)}`); } for (const detail of componentDetails) { const componentInfo = await ComponentRegistryService.getComponent(detail.name); if (componentInfo?.setup && componentInfo.setup.length > 0) { if (componentInfo.setup.some(instruction => instruction.includes('ModalProvider'))) { Logger.break(); Logger.info(`${icons.gear} Setup required for ${colors.primary(detail.name)}:`); Logger.log(` Wrap your app with ${colors.primary('<ModalProvider>')}`); Logger.tip('Check the component guide for detailed setup instructions'); break; } } } } catch (error) { spinner.fail('Failed to add components'); throw error; } } async function addThemes(themeNames, options) { const cwd = options.cwd || process.cwd(); let framework = 'rn'; if (options.unistyles) { framework = 'unistyles'; } if (themeNames.length === 0) { const availableThemes = await GitHubProjectService.getAvailableThemes(framework); if (availableThemes.length === 0) { Logger.error('No themes available'); return; } const { selectedThemes } = await inquirer.prompt([ { type: 'checkbox', name: 'selectedThemes', message: 'Select themes to add:', choices: availableThemes.map(theme => ({ name: theme, value: theme, })), }, ]); if (selectedThemes.length === 0) { Logger.info('No themes selected.'); return; } themeNames = selectedThemes; } Logger.title(`${icons.theme} Adding themes`); Logger.subtitle(`Framework: ${framework === 'rn' ? 'React Native StyleSheet' : 'Unistyles'}`); Logger.break(); const spinner = ora('Installing themes...').start(); try { const config = await ProjectService.getProjectConfig(cwd); const themesDir = FileUtils.join(config.stylesDir, 'themes'); await FileUtils.ensureDir(themesDir); const installedThemes = []; const skippedThemes = []; for (const themeName of themeNames) { const targetPath = FileUtils.join(themesDir, `${themeName}.ts`); if (await FileUtils.exists(targetPath) && !options.overwrite && !options.yes) { const { shouldOverwrite } = await inquirer.prompt([ { type: 'confirm', name: 'shouldOverwrite', message: `Theme '${themeName}' already exists. Overwrite?`, default: false, }, ]); if (!shouldOverwrite) { skippedThemes.push(themeName); continue; } } const success = await GitHubProjectService.downloadTheme(framework, themeName, targetPath, options.overwrite || false); if (success) { installedThemes.push(themeName); } else { skippedThemes.push(themeName); } } spinner.succeed(`Successfully added ${installedThemes.length} theme${installedThemes.length === 1 ? '' : 's'}!`); Logger.break(); Logger.success('Themes installed successfully!'); Logger.break(); if (installedThemes.length > 0) { Logger.log(`${icons.theme} Installed themes:`); installedThemes.forEach(name => { Logger.log(` • ${colors.primary(name)}`); }); } if (skippedThemes.length > 0) { Logger.break(); Logger.warning(`Skipped themes: ${skippedThemes.join(', ')}`); } Logger.break(); Logger.tip('Next steps:'); Logger.log(` 1. Update ${colors.primary('styles/themes/index.ts')} to include the new theme${installedThemes.length === 1 ? '' : 's'}`); Logger.log(` 2. Run ${colors.primary('npx leshi-ui@latest guide theme')} to learn how to use themes`); } catch (error) { spinner.fail('Failed to add themes'); throw error; } }