anvy
Version:
A CLI tool for integrating Anvy UI components into your React.js and Next.js projects.
472 lines • 19.6 kB
JavaScript
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