UNPKG

@chakra-ui/cli

Version:

Generate theme typings for autocomplete

225 lines (222 loc) 8.49 kB
"use strict"; import * as p from '@clack/prompts'; import { boxen } from '@visulima/boxen'; import { Command } from 'commander'; import createDebug from 'debug'; import { existsSync } from 'fs'; import { writeFile } from 'fs/promises'; import { join } from 'node:path/posix'; import { z } from 'zod'; import { getProjectContext } from '../utils/context.js'; import { convertTsxToJsx } from '../utils/convert-tsx-to-jsx.js'; import { fetchProBlocks, fetchProBlock } from '../utils/fetch.js'; import { formatCliError } from '../utils/format-error.js'; import { ensureDir } from '../utils/io.js'; import { addCommandFlagsSchema } from '../utils/schema.js'; import { tasks } from '../utils/tasks.js'; const debug = createDebug("chakra:blocks"); function handleCancel(value) { if (p.isCancel(value)) { p.cancel("Operation cancelled"); process.exit(0); } } function ensureApiKey() { const apiKey = process.env.CHAKRA_UI_PRO_API_KEY; if (!apiKey) { p.log.error("CHAKRA_UI_PRO_API_KEY environment variable is required"); p.outro("Set your API key and try again"); process.exit(1); } return apiKey; } const BlocksCommand = new Command("blocks").description("Add Chakra UI Pro blocks to your project").addCommand( new Command("add").description("Add a new block from Chakra UI Pro").argument("[blockId]", "block ID to add").option("-v, --variant <variant>", "Specific variant to add").option("-d, --dry-run", "Dry run").option("--outdir <dir>", "Output directory to write the blocks").option("-f, --force", "Overwrite existing files").option("--tsx", "Convert to TSX").action(async (blockId, flags) => { const parsedFlags = addCommandFlagsSchema.extend({ variant: z.string().optional() }).parse(flags); const { dryRun, force, tsx, variant } = parsedFlags; const ctx = await getProjectContext({ cwd: parsedFlags.outdir || process.cwd(), tsx }); debug("context", ctx); const jsx = !ctx.isTypeScript; const baseOutdir = parsedFlags.outdir || "src/components/blocks"; const apiKey = ensureApiKey(); const blocksResponse = await fetchProBlocks(); const allBlocks = blocksResponse.data; let blocksToDownload = []; if (!blockId) { const blockOptions = allBlocks.flatMap( (block) => block.variants.map((v) => ({ value: `${block.id}/${v.id}`, label: `${block.group}/${block.name} - ${v.name}` })) ); const selectedBlock = await p.select({ message: "Select a block:", options: blockOptions }); handleCancel(selectedBlock); blocksToDownload = [selectedBlock]; } else { const targetBlock = allBlocks.find( (block) => block.id === blockId ); if (!targetBlock) { p.log.error(`Block "${blockId}" not found`); const availableBlocks = allBlocks.map((block) => block.id).join(", "); p.log.info(`Available blocks: ${availableBlocks}`); process.exit(1); } if (variant) { const targetVariant = targetBlock.variants.find( (v) => v.id === variant ); if (!targetVariant) { p.log.error( `Variant "${variant}" not found for block "${blockId}"` ); const availableVariants = targetBlock.variants.map((v) => v.id).join(", "); p.log.info(`Available variants: ${availableVariants}`); process.exit(1); } blocksToDownload = [`${blockId}/${variant}`]; } else { blocksToDownload = targetBlock.variants.map( (v) => `${blockId}/${v.id}` ); } } debug("blocksToDownload", blocksToDownload); p.log.info(`Adding ${blocksToDownload.length} block(s)...`); let skippedFiles = []; await tasks([ { title: "Downloading selected blocks", task: async () => { await Promise.all( blocksToDownload.map(async (blockId2) => { const [category, id] = blockId2.split("/"); if (!category || !id) { p.log.warn( `Invalid block ID format: ${blockId2}. Expected format: category/id` ); return; } try { const blockData = await fetchProBlock(category, id, apiKey); const files = blockData.files; if (!files || files.length === 0) { p.log.error(`No files found for block ${blockId2}`); return; } const blockOutdir = join(baseOutdir, category, id); ensureDir(blockOutdir); for (const fileData of files) { let filename = fileData.filename || `${id}.tsx`; let content = fileData.content; if (!content) { p.log.warn( `No content found for file ${filename} in block ${blockId2}` ); continue; } if (jsx) { filename = filename.replace(".tsx", ".jsx"); content = await convertTsxToJsx(content); } if (existsSync(join(blockOutdir, filename)) && !force) { skippedFiles.push(filename); continue; } if (dryRun) { printBlockSync(content, filename); } else { const outPath = join(blockOutdir, filename); await writeFile(outPath, content, "utf-8"); } } } catch (error) { p.log.error( `Failed to fetch block ${blockId2}: ${formatCliError(error)}` ); } }) ); } } ]); if (skippedFiles.length) { p.log.warn( `Skipping ${skippedFiles.length} file(s) that already exist. Use the --force flag to overwrite.` ); } if (blocksToDownload.length === 1) { const [category, id] = blocksToDownload[0].split("/"); const blockOutdir = join(baseOutdir, category, id); p.outro(`\u{1F389} Added block to ${blockOutdir}`); } else { p.outro(`\u{1F389} Added ${blocksToDownload.length} blocks to ${baseOutdir}`); } }) ).addCommand( new Command("list").description("List available Chakra UI Pro blocks").option("-c, --category <category>", "Filter by category").action(async (flags) => { const { default: Table } = await import('cli-table'); const table = new Table({ head: ["Category", "Name"], colWidths: [20, 50], style: { compact: true } }); try { const blocksResponse = await fetchProBlocks(); const blocks = blocksResponse.data.filter( (block) => block.group.toLowerCase() !== "documentation" ); const filteredBlocks = flags.category ? blocks.filter( (block) => block.group.toLowerCase() === flags.category?.toLowerCase() || block.id === flags.category ) : blocks; if (filteredBlocks.length === 0) { if (flags.category) { p.log.warn(`No blocks found for category: ${flags.category}`); const categories = [ ...new Set(blocks.map((block) => block.group)) ].join(", "); p.log.info(`Available categories: ${categories}`); } else { p.log.warn("No blocks found"); } return; } filteredBlocks.sort((a, b) => a.group.localeCompare(b.group)).forEach((block) => { const blockCount = block.variants.length; const nameWithCount = `${block.name} (${blockCount} block${blockCount === 1 ? "" : "s"})`; table.push([block.group, nameWithCount]); }); const totalVariants = filteredBlocks.reduce( (sum, block) => sum + block.variants.length, 0 ); p.log.info( `Found ${totalVariants} block(s) across ${filteredBlocks.length} component(s)` ); p.log.info(table.toString()); } catch (error) { p.log.error("Failed to fetch blocks list"); if (error instanceof Error) { debug("error", error.message); } } p.outro("\u{1F389} Done!"); }) ); function printBlockSync(content, filename) { const boxText = boxen(content, { headerText: `${filename} `, borderStyle: "none" }); p.log.info(boxText); } export { BlocksCommand };