UNPKG

anvy

Version:

A CLI tool for integrating Anvy UI components into your React.js and Next.js projects.

472 lines 19.6 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"; import { processComponentContent, ProjectTypes, } from './process-component-content.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const spinner = ora(); const REGISTRY_BASE_URL = "https://anvy.vercel.app/registry"; const REGISTRY_INDEX_URL = `${REGISTRY_BASE_URL}/index.json`; const isValidUrl = (string) => { try { new URL(string); return true; } catch (_) { return false; } }; const parseComponentSource = (input) => { if (isValidUrl(input)) { return { type: "url", value: input, }; } return { type: "name", value: input, }; }; const extractImports = (content) => { const importRegex = /@\/components\/ui\/([a-zA-Z-]+)/g; const matches = [...content.matchAll(importRegex)]; return matches.map((match) => match[1]); }; async function fetchJson(url) { try { spinner.start(`Fetching data from ${url}...`); 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 data`); throw new Error(`Error fetching data: ${error.message}`); } } async function getRegistryIndex() { return await fetchJson(REGISTRY_INDEX_URL); } async function checkComponentInRegistry(componentName) { try { const registryData = await getRegistryIndex(); const components = Array.isArray(registryData) ? registryData : registryData.components || []; return components.some((c) => c.name === componentName); } catch { return false; } } async function getComponentFromUrl(url, config) { try { const data = await fetchJson(url); if (!data.files || !Array.isArray(data.files) || data.files.length === 0) { throw new Error("Invalid component data format"); } const content = data.files[0].content; const subComponents = extractImports(content); return { content, dependencies: data.dependencies || [], registryDependencies: data.registryDependencies || [], name: data.name || path.basename(url, path.extname(url)), isExternalRegistry: true, tailwind: data.tailwind || null, subComponents, }; } catch (error) { throw new Error(`Failed to fetch component from URL: ${error.message}`); } } async function getComponentFromRegistry(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 from registry...`); const componentData = await fetchJson(styleUrl); const content = componentData.files[0].content; const subComponents = extractImports(content); spinner.succeed(`Successfully fetched ${componentName}`); return { content, dependencies: componentData.dependencies || [], registryDependencies: componentData.registryDependencies || [], name: componentName, isExternalRegistry: false, tailwind: componentData.tailwind || null, subComponents, }; } catch (error) { spinner.fail(`Failed to fetch component ${componentName}`); throw error; } } async function installSubComponents(subComponents, config, componentsDir) { if (!subComponents || subComponents.length === 0) return; spinner.info("Checking for sub-components..."); for (const componentName of subComponents) { try { const exists = await checkComponentInRegistry(componentName); if (!exists) { spinner.info(`Sub-component ${componentName} not found in registry, skipping...`); continue; } const componentPath = path.join(componentsDir, `${componentName}${config.extension}`); if (fs.exists(componentPath)) { spinner.info(`Sub-component ${componentName} already exists locally, skipping...`); continue; } const componentData = await getComponentFromRegistry(componentName, config); const processedContent = processComponentContent(componentData.content, config, componentData.isExternalRegistry); fs.write(componentPath, processedContent); console.log(chalk.green(`✓ Added sub-component ${componentName}`)); if (componentData.dependencies?.length > 0) { spinner.start(`Installing dependencies for ${componentName}...`); execSync(`npm install ${componentData.dependencies.join(" ")}`, { cwd: process.cwd(), stdio: "inherit", }); spinner.succeed(`Installed dependencies for ${componentName}`); } if (componentData.tailwind?.config) { await mergeTailwindConfig(process.cwd(), componentData.tailwind.config); } if (componentData.subComponents && componentData.subComponents.length > 0) { await installSubComponents(componentData.subComponents, config, componentsDir); } } catch (error) { console.error(chalk.yellow(`⚠️ Failed to install sub-component ${componentName}: ${error.message}`)); } } } async function installRegistryDependencies(dependencies, config, componentsDir) { if (!dependencies || dependencies.length === 0) return; spinner.start("Installing registry dependencies..."); for (const dependencyUrl of dependencies) { try { const dependencyData = await getComponentFromUrl(dependencyUrl, config); const destPath = path.join(componentsDir, `${dependencyData.name}${config.extension}`); const processedContent = processComponentContent(dependencyData.content, config, dependencyData.isExternalRegistry); fs.write(destPath, processedContent); console.log(chalk.green(`✓ Added dependency ${dependencyData.name}`)); if (dependencyData.tailwind?.config) { await mergeTailwindConfig(process.cwd(), dependencyData.tailwind.config); } if (dependencyData.dependencies?.length > 0) { spinner.start(`Installing npm dependencies...`); execSync(`npm install ${dependencyData.dependencies.join(" ")}`, { cwd: process.cwd(), stdio: "inherit", }); spinner.succeed(`Installed npm dependencies`); } if (dependencyData.registryDependencies) { await installRegistryDependencies(dependencyData.registryDependencies, config, componentsDir); } if (dependencyData.subComponents && dependencyData.subComponents.length > 0) { await installSubComponents(dependencyData.subComponents, config, componentsDir); } } catch (error) { console.error(chalk.yellow(`⚠️ Failed to install registry dependency ${dependencyUrl}: ${error.message}`)); } } spinner.succeed("Registry dependencies installed"); } 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; } 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 generateConfigContent = (originalContent, componentTailwindConfig) => { try { // Remove any import statements and comments const cleanContent = originalContent .replace(/import[^;]+;?\s*/g, "") .replace(/require\([^)]+\)/g, "") .replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, "") .trim(); let existingConfig; if (cleanContent.includes("export default") || cleanContent.includes("export const")) { // Handle ESM format const configString = cleanContent .replace(/export\s+default\s*/, "") .replace(/export\s+const\s+config\s*=\s*/, "") .replace(/export\s+const\s*=\s*/, "") .replace(/;$/, "") // Remove trailing semicolon .trim(); // Safely evaluate the configuration existingConfig = Function(`"use strict";return (${configString})`)(); } else { // Handle CommonJS format const configString = cleanContent .replace(/module\.exports\s*=\s*/, "") .replace(/exports\s*=\s*/, "") .replace(/;$/, "") // Remove trailing semicolon .trim(); // Safely evaluate the configuration existingConfig = Function(`"use strict";return (${configString})`)(); } // Merge configurations const mergedTheme = { ...existingConfig.theme, extend: { ...existingConfig.theme?.extend, animation: { ...existingConfig.theme?.extend?.animation, ...componentTailwindConfig.theme?.extend?.animation, }, keyframes: { ...existingConfig.theme?.extend?.keyframes, ...componentTailwindConfig.theme?.extend?.keyframes, }, }, }; const updatedConfig = { ...existingConfig, theme: mergedTheme, }; // Determine the export format based on the original content const isESModule = originalContent.includes("export default") || originalContent.includes("export const"); return isESModule ? `export default ${JSON.stringify(updatedConfig, null, 2)}` : `module.exports = ${JSON.stringify(updatedConfig, null, 2)}`; } catch (error) { // Log the error but don't throw, allowing the installation to continue console.log(chalk.yellow(`⚠️ Warning: Could not merge Tailwind configuration. Using original config.`)); return originalContent; } }; const findTailwindConfig = (projectDir) => { const possiblePaths = [ path.join(projectDir, "tailwind.config.js"), path.join(projectDir, "tailwind.config.cjs"), path.join(projectDir, "tailwind.config.mjs"), path.join(projectDir, "tailwind.config.ts"), ]; for (const configPath of possiblePaths) { if (fs.exists(configPath)) { try { const content = fs.read(configPath, "utf8") || ""; const isESModule = content.includes("export default") || content.includes("export const"); // Basic validation of the content if (!content.includes("module.exports") && !content.includes("export default") && !content.includes("export const")) { continue; // Skip invalid config files } return { path: configPath, content, isESModule, }; } catch (error) { continue; // Skip files that can't be read } } } return null; }; const mergeTailwindConfig = async (projectDir, componentTailwindConfig) => { try { const existingConfig = findTailwindConfig(projectDir); if (!existingConfig) { console.log(chalk.yellow("⚠️ No Tailwind config found. Skipping configuration merge.")); return; } const updatedContent = generateConfigContent(existingConfig.content, componentTailwindConfig); if (updatedContent !== existingConfig.content) { fs.write(existingConfig.path, updatedContent); console.log(chalk.green("✓ Updated Tailwind configuration")); } } catch (error) { // Log the error but don't throw, allowing the installation to continue console.log(chalk.yellow("⚠️ Warning: Tailwind configuration update skipped")); } }; export const addCommand = async (component) => { console.log(chalk.bold.cyan("\n🚀 Anvy UI Component Installer\n")); try { const projectDir = process.cwd(); const config = detectProjectConfig(projectDir); const componentsDir = determineComponentsPath(projectDir); let selectedComponents = []; if (component) { const parsedComponent = parseComponentSource(component); selectedComponents = [parsedComponent]; } else { 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"); 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.map((comp) => ({ type: "name", value: comp, })); } for (const comp of selectedComponents) { try { let componentData; if (comp.type === "url") { componentData = await getComponentFromUrl(comp.value, config); if (componentData.registryDependencies && componentData.registryDependencies.length > 0) { await installRegistryDependencies(componentData.registryDependencies, config, componentsDir); } } else { componentData = await getComponentFromRegistry(comp.value, config); } const destPath = path.join(componentsDir, `${componentData.name}${config.extension}`); const processedContent = processComponentContent(componentData.content, config, componentData.isExternalRegistry); if (fs.exists(destPath)) { const { overwrite } = await inquirer.prompt([ { type: "confirm", name: "overwrite", message: `${componentData.name} already exists. Overwrite?`, default: false, }, ]); if (!overwrite) { console.log(chalk.yellow(`⚠️ Skipped ${componentData.name}`)); continue; } } fs.write(destPath, processedContent); console.log(chalk.green(`✓ Added ${componentData.name} to ${path.relative(projectDir, destPath)}`)); // Install sub-components if they exist if (componentData.subComponents && componentData.subComponents.length > 0) { await installSubComponents(componentData.subComponents, config, componentsDir); } if (componentData.tailwind?.config) { await mergeTailwindConfig(projectDir, componentData.tailwind.config); } if (componentData.dependencies && componentData.dependencies.length > 0) { spinner.start(`Installing dependencies for ${componentData.name}...`); execSync(`npm install ${componentData.dependencies.join(" ")}`, { cwd: projectDir, stdio: "inherit", }); spinner.succeed(`Installed dependencies: ${chalk.green(componentData.dependencies.join(", "))}`); } } catch (err) { console.error(chalk.red(`✗ Failed to process ${comp.value}: ${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); } }; //# sourceMappingURL=add.js.map