ui-thing
Version:
CLI used to add Nuxt components to a project
168 lines (144 loc) • 5.35 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, BlockComponent } from "../types";
import { compareUIConfig } from "../utils/compareUIConfig";
import { getUIConfig } from "../utils/config";
import { fetchBlockCategories } from "../utils/fetchBlockCategories";
import { fetchBlocks } from "../utils/fetchBlocks";
import { fileExists } from "../utils/fileExists";
import { printFancyBoxMessage } from "../utils/printFancyBoxMessage";
import { writeFile } from "../utils/writeFile";
type BlockOptions = AddCommand & { category?: string };
let allBlocks: BlockComponent[] = [];
const currentDirectory = process.cwd();
/**
* Get the default blocks directory based on Nuxt version from config
*/
function getDefaultBlocksDir(uiConfig: any): string {
const isNuxt4 = uiConfig.componentsLocation?.startsWith("app/");
return isNuxt4 ? "app/components/Blocks" : "components/Blocks";
}
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;
}
const runBlockCommand = async (components: string[], options: BlockOptions) => {
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);
}
// Step 1: Fetch categories
const categories = await fetchBlockCategories();
// Step 2: Ask user to select category
let selectedCategory = options.category;
if (!selectedCategory && categories?.length) {
const { category } = await prompts({
type: "select",
name: "category",
message: "Choose a block category",
choices: [{ title: "All", value: "all" }, ...categories.map((c) => ({ title: c, value: c }))],
initial: 0,
});
selectedCategory = category;
}
// Step 3: Fetch all blocks
allBlocks = await fetchBlocks();
// Step 4: Filter blocks by category if not "all"
let blocksToSelect = allBlocks;
if (selectedCategory && selectedCategory !== "all") {
blocksToSelect = allBlocks.filter((b) => b.category === selectedCategory);
if (blocksToSelect.length === 0) {
consola.warn(
`No blocks found for category ${kleur.cyan(selectedCategory)}. Falling back to all.`
);
blocksToSelect = allBlocks;
}
}
// Step 5: Let user select blocks via autocomplete multi-select
const { selectedBlocks } = await prompts({
type: "autocompleteMultiselect",
name: "selectedBlocks",
message: "Select the blocks you want to add",
choices: blocksToSelect.map((b) => ({ title: b.name, value: b })),
});
if (!selectedBlocks || selectedBlocks.length === 0) {
consola.info("No blocks selected. Exiting...");
process.exit(0);
}
const found: BlockComponent[] = selectedBlocks;
// Step 6: Ask user where to add blocks (with default based on Nuxt version)
const defaultBlocksDir = getDefaultBlocksDir(uiConfig);
const { blocksDir } = await prompts({
type: "text",
name: "blocksDir",
message: "Where should we add the blocks?",
initial: defaultBlocksDir,
});
const blocksDirPath = blocksDir || defaultBlocksDir;
// Step 7: Write block files
for (const block of found) {
// Create full path: blocksDirPath + block.path + block.fileName
const fullPath = path.join(blocksDirPath, block.path);
const targetPath = path.join(currentDirectory, fullPath);
const fileWritten = await safeWriteFile(
targetPath,
block.file,
uiConfig.force,
`The block file ${kleur.bold(block.fileName)} already exists. Overwrite?`
);
if (!fileWritten) continue;
}
// Step 8: Handle components if present
const componentsToAdd = _.uniq(found.flatMap((b) => b.components || []));
if (componentsToAdd.length > 0) {
consola.info(`Adding ${componentsToAdd.length} component(s) required by blocks...`);
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}`);
}
}
// Step 9: Success message
printFancyBoxMessage(
"Blocks added!",
`Run the ${kleur.cyan("ui-thing@latest --help")} command to learn more.\n`,
{ box: { title: "Blocks Added" } }
);
};
export const block = new Command()
.name("block")
.command("block")
.description("Add UI Thing blocks to your project.")
.option("-c --category <category>", "Filter blocks by category before selection")
.action(runBlockCommand);