known-ui
Version:
A CLI tool for integrating Known UI components into your Next.js projects.
281 lines (236 loc) ⢠8.44 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";
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;