UNPKG

ui-thing

Version:

CLI used to add Nuxt components to a project

205 lines (181 loc) 6.44 kB
import { spawnSync } from "node:child_process"; import path from "node:path"; import { Command } from "commander"; import { consola } from "consola"; import kleur from "kleur"; import _ from "lodash"; import prompts from "prompts"; import { AddCommand, ProseComponent, TemplateFile } from "../types"; import { compareUIConfig } from "../utils/compareUIConfig"; import { addModuleToConfig, getUIConfig } from "../utils/config"; import { fetchProseComponents } from "../utils/fetchProseComponents"; import { fileExists } from "../utils/fileExists"; import { installPackages } from "../utils/installPackages"; import { printFancyBoxMessage } from "../utils/printFancyBoxMessage"; import { promptUserForComponents } from "../utils/promptForComponents"; import { writeFile } from "../utils/writeFile"; let allProse: ProseComponent[] = []; const currentDirectory = process.cwd(); const findProse = (name: string) => allProse.find((c) => c.value.toLowerCase() === name.toLowerCase()); async function safeWriteFile( targetPath: string, content: string, forceOverwrite: boolean, promptMessage: string ) { const exists = await fileExists(targetPath); if (exists && !forceOverwrite) { const { value: overwrite } = await prompts({ type: "confirm", name: "value", message: promptMessage, initial: false, }); if (!overwrite) { consola.info(`Skipped: ${kleur.cyan(path.basename(targetPath))}`); return false; } } await writeFile(targetPath, content); return true; } async function writeCategoryFiles( category: string, items: TemplateFile[] | undefined, baseDir: string, forceOverwrite: boolean ) { if (!items || items.length === 0) return; for (const item of items) { const targetPath = path.join(currentDirectory, baseDir, item.fileName); await safeWriteFile( targetPath, item.fileContent, forceOverwrite, `The ${category} file ${kleur.bold(item.fileName)} already exists. Overwrite?` ); } } const runProseCommand = async (components: string[], options: AddCommand) => { let uiConfig = await getUIConfig(); if (!(await compareUIConfig())) { uiConfig = await getUIConfig({ force: true }); } if (_.isEmpty(uiConfig)) { consola.error("Config file not set. Exiting..."); process.exit(1); } allProse = await fetchProseComponents(); let componentNames = components; if (componentNames.length === 0) { const response = await promptUserForComponents(options.all, allProse); if (!response || response.length === 0) { consola.info("No components selected. Exiting..."); process.exit(0); } componentNames = response; } const notFound = componentNames.filter((name) => !findProse(name)); if (notFound.length > 0) { consola.error(`Not found: ${kleur.bgRed(notFound.join(", "))}`); } const found: ProseComponent[] = componentNames .map((name) => findProse(name)) .filter(Boolean) as ProseComponent[]; // Handle prose dependencies for (const comp of [...found]) { if (comp.prose && comp.prose.length > 0) { comp.prose.forEach((proseName) => { if (!found.find((c) => c.value === proseName)) { const proseComp = findProse(proseName); if (proseComp) { found.push(proseComp); } } }); } } for (const component of found) { const componentFile = component.file; let dirPath = uiConfig.componentsLocation; let filePath = path.join(currentDirectory, dirPath, componentFile.fileName); if (!uiConfig.useDefaultFilename) { const { value: newDir } = await prompts({ type: "text", name: "value", message: `Where should we add the file ${kleur.cyan(componentFile.fileName)}?`, initial: dirPath, }); if (newDir) { dirPath = newDir; filePath = path.join(currentDirectory, dirPath, componentFile.fileName); } } const fileWritten = await safeWriteFile( filePath, componentFile.fileContent, uiConfig.force, `The file ${kleur.bold(componentFile.fileName)} already exists. Overwrite?` ); if (!fileWritten) continue; await writeCategoryFiles("utils", component.utils, uiConfig.utilsLocation, uiConfig.force); await writeCategoryFiles( "composables", component.composables, uiConfig.composablesLocation, uiConfig.force ); await writeCategoryFiles( "plugins", component.plugins, uiConfig.pluginsLocation ?? "", uiConfig.force ); } await addModuleToConfig(_.uniq(found.flatMap((c) => c.modules || []))); const deps = _.uniq(found.flatMap((c) => c.deps || [])); const devDeps = _.uniq(found.flatMap((c) => c.devDeps || [])); if (deps.length > 0 || devDeps.length > 0) { if (options.all) { await installPackages(uiConfig.packageManager, deps, devDeps); } else { const { confirmInstall } = await prompts({ type: "confirm", name: "confirmInstall", message: `Install packages: ${kleur.cyan([...deps, ...devDeps].join(", "))}?`, initial: true, }); if (confirmInstall) { await installPackages(uiConfig.packageManager, deps, devDeps); } } } // Collect and add components via the add command const componentsToAdd = _.uniq(found.flatMap((c) => c.components || [])); if (componentsToAdd.length > 0) { consola.info(`Adding ${componentsToAdd.length} component(s) required by prose...`); const result = spawnSync("npx", ["ui-thing@latest", "add", ...componentsToAdd], { cwd: currentDirectory, stdio: "inherit", }); if (result.error) { consola.error("Failed to spawn component installer:", result.error.message); } else if (result.status !== 0) { consola.error(`Component installer exited with code ${result.status}`); } } printFancyBoxMessage( "Prose added!", `Run the ${kleur.cyan("ui-thing@latest --help")} command to learn more.\n`, { box: { title: "Prose Components Added" } } ); found.filter((c) => c.docsUrl).forEach((c) => consola.info(`Docs: ${c.docsUrl}`)); }; export const prose = new Command() .name("prose") .command("prose") .description("Add prose components to your project.") .option("-a --all", "Add all prose components to your project.", false) .argument("[componentNames...]") .action(runProseCommand);