UNPKG

known-ui

Version:

A CLI tool for integrating Known UI components into your Next.js projects.

281 lines (236 loc) • 8.44 kB
import inquirer from "inquirer"; import fs from "fs-jetpack"; import path from "path"; import { fileURLToPath } from "url"; import { execSync } from "child_process"; import chalk from "chalk"; import ora from "ora"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const REGISTRY_BASE_URL = "https://anvy.vercel.app/registry"; const REGISTRY_INDEX_URL = `${REGISTRY_BASE_URL}/index.json`; const ProjectTypes = { NEXT: "next", REACT: "react", VITE: "vite", }; const spinner = ora(); async function fetchJson(url) { try { spinner.start(`Fetching data from registry...`); const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); spinner.succeed(`Successfully fetched data`); return data; } catch (error) { spinner.fail(`Failed to fetch from ${url}`); throw error; } } async function getRegistryIndex() { return await fetchJson(REGISTRY_INDEX_URL); } async function getComponentContent(componentName, config) { const format = config.language === "typescript" ? "tsx" : "jsx"; const styleUrl = `${REGISTRY_BASE_URL}/styles/default/${format}/${componentName}.json`; try { spinner.start(`Fetching ${componentName} component...`); const componentData = await fetchJson(styleUrl); spinner.succeed(`Successfully fetched ${componentName}`); return componentData.files[0].content; } catch (error) { spinner.fail(`Failed to fetch component ${componentName}`); throw error; } } const detectProjectConfig = (projectDir) => { spinner.start("Analyzing project configuration..."); const packageJsonPath = path.join(projectDir, "package.json"); if (!fs.exists(packageJsonPath)) { spinner.warn("No package.json found, defaulting to React configuration"); return { type: ProjectTypes.REACT, language: "javascript", extension: ".jsx", }; } const packageJson = fs.read(packageJsonPath, "json"); const isNextJs = !!packageJson?.dependencies?.next; const isVite = !!packageJson?.dependencies?.["@vitejs/plugin-react"] || !!packageJson?.devDependencies?.["@vitejs/plugin-react"]; const hasTypeScript = !!packageJson?.dependencies?.typescript || !!packageJson?.devDependencies?.typescript || fs.exists(path.join(projectDir, "tsconfig.json")); const config = { type: isNextJs ? ProjectTypes.NEXT : isVite ? ProjectTypes.VITE : ProjectTypes.REACT, language: hasTypeScript ? "typescript" : "javascript", extension: hasTypeScript ? ".tsx" : ".jsx", }; spinner.succeed(`Detected ${config.type} project with ${config.language}`); return config; }; const isSrcBasedProject = (projectDir) => { const srcPath = path.join(projectDir, "src"); if (!fs.exists(srcPath)) { return false; } // If src exists and contains any files/folders, consider it src-based const srcContents = fs.list(srcPath); return srcContents && srcContents.length > 0; }; const determineComponentsPath = (projectDir) => { const hasSrc = isSrcBasedProject(projectDir); const basePath = hasSrc ? path.join(projectDir, "src") : projectDir; const componentsPath = path.join(basePath, "components", "ui"); spinner.info( `Detected ${hasSrc ? "src-based" : "root-based"} project structure` ); fs.dir(componentsPath); return componentsPath; }; const validateRegistryData = (data) => { if (!data) return false; if (Array.isArray(data)) return data.length > 0; if (data.components && Array.isArray(data.components)) return data.components.length > 0; return false; }; const getComponentsList = (registryData) => { if (Array.isArray(registryData)) { return registryData.map((c) => ({ name: c.name, value: c.name, description: c.description, })); } if (registryData.components) { return registryData.components.map((c) => ({ name: c.name, value: c.name, description: c.description, })); } return []; }; const getDependencies = (registryData, component) => { if (Array.isArray(registryData)) { return registryData.find((c) => c.name === component)?.dependencies || []; } if (registryData.components) { return ( registryData.components.find((c) => c.name === component)?.dependencies || [] ); } return []; }; const installDependencies = async (projectDir, component, registryData) => { try { const dependencies = getDependencies(registryData, component); if (dependencies.length === 0) { spinner.info(`No dependencies found for ${component}`); return; } spinner.start(`Installing dependencies for ${component}...`); execSync(`npm install ${dependencies.join(" ")}`, { cwd: projectDir, stdio: "inherit", }); spinner.succeed( `Installed dependencies: ${chalk.green(dependencies.join(", "))}` ); } catch (err) { spinner.warn(`Dependencies installation warning: ${err.message}`); } }; const processComponentContent = (content, config) => { spinner.start("Processing component content..."); let processedContent = content; if (config.type !== ProjectTypes.NEXT && content.includes("'use client'")) { processedContent = content.replace(/'use client'\n*/g, ""); } if (config.type === ProjectTypes.REACT || config.type === ProjectTypes.VITE) { processedContent = processedContent .replace(/import Link from ['"]next\/link['"];?/g, "") .replace(/<Link\s+href/g, "<a href") .replace(/<\/Link>/g, "</a>"); } processedContent = processedContent.replace( /@\/components\/ui\/(.*?)['"]/g, (_, componentPath) => `'./${componentPath}'` ); spinner.succeed("Component content processed successfully"); return processedContent; }; const add = async (component) => { console.log(chalk.bold.cyan("\n🚀 Known UI Component Installer\n")); try { const projectDir = process.cwd(); const config = detectProjectConfig(projectDir); const componentsDir = determineComponentsPath(projectDir); spinner.start("Fetching registry data..."); const registryData = await getRegistryIndex(); if (!validateRegistryData(registryData)) { throw new Error("Invalid registry data format"); } spinner.succeed("Registry data fetched successfully"); let selectedComponents = component ? [component] : []; if (!selectedComponents.length) { const availableComponents = getComponentsList(registryData); const answer = await inquirer.prompt([ { type: "checkbox", name: "components", message: "Select components to install:", choices: availableComponents, pageSize: 15, }, ]); selectedComponents = answer.components; } for (const comp of selectedComponents) { const destPath = path.join(componentsDir, `${comp}${config.extension}`); try { const content = await getComponentContent(comp, config); const processedContent = processComponentContent(content, config); if (fs.exists(destPath)) { const { overwrite } = await inquirer.prompt([ { type: "confirm", name: "overwrite", message: `${comp} already exists. Overwrite?`, default: false, }, ]); if (!overwrite) { console.log(chalk.yellow(`⚠️ Skipped ${comp}`)); continue; } } fs.write(destPath, processedContent); console.log( chalk.green( `✓ Added ${comp} to ${path.relative(projectDir, destPath)}` ) ); await installDependencies(projectDir, comp, registryData); } catch (err) { console.error(chalk.red(`✗ Failed to process ${comp}: ${err.message}`)); } } console.log(chalk.bold.green("\n✨ Installation complete!\n")); } catch (error) { console.error(chalk.red(`\n✗ Error: ${error.message}\n`)); process.exit(1); } }; export default add;