ui-thing
Version:
CLI used to add Nuxt components to a project
205 lines (181 loc) • 6.44 kB
text/typescript
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);