UNPKG

sysrot-hub

Version:

CLI de nueva generación para proyectos Next.js 14+ con IA multi-modelo, Web3 integration, internacionalización completa y roadmap realista 2025-2026

735 lines (623 loc) 23.7 kB
const fs = require('fs-extra'); const path = require('path'); const chalk = require('chalk'); const { execSync } = require('child_process'); const ora = require('ora'); const logger = require('./utils/logger'); async function createProject(options) { const { projectName } = options; const projectPath = path.resolve(process.cwd(), projectName); // Verificar si la carpeta ya existe if (fs.existsSync(projectPath)) { logger.error(`La carpeta ${projectName} ya existe. Por favor elige otro nombre o elimina la carpeta existente.`); process.exit(1); } logger.info('Generando tu proyecto...'); logger.debug(`Ruta del proyecto: ${projectPath}`); try { // Crear directorio principal fs.mkdirSync(projectPath, { recursive: true }); // Copiar la plantilla base await copyTemplateFiles(projectPath); logger.templateStep(); // Asegurar compatibilidad con Pages Router await ensurePagesRouterOnly(projectPath); logger.structureStep(); // Configurar el proyecto según las opciones seleccionadas await customizeProject(projectPath, options); logger.configStep(); // Añadir ejemplos de integración completa si están seleccionados if (options.fullIntegrationExamples && options.fullIntegrationExamples.length > 0) { await addFullIntegrationExamples(projectPath, options.fullIntegrationExamples); logger.info('Ejemplos de integración completa añadidos'); } // Remover archivos no utilizados await removeUnusedFiles(projectPath, options); logger.customizeStep(); // Instalar dependencias await installDependencies(projectPath, options); // Generar archivo README personalizado await generateReadme(projectPath, options); return true; } catch (error) { console.log(chalk.red('× Error al instalar las dependencias')); console.log(chalk.red('× Error al crear la estructura de carpetas')); console.error(chalk.red(`Error: ${error.message}`)); // Limpiar directorio si algo falló try { if (fs.existsSync(projectPath)) { await fs.remove(projectPath); } } catch (cleanupError) { console.error(chalk.yellow('Advertencia: No se pudo limpiar el directorio del proyecto')); } process.exit(1); } } async function ensurePagesRouterOnly(projectPath) { const spinner = ora('Configurando compatibilidad con Pages Router...').start(); try { // Solo verificar que el archivo existe, no procesarlo const nextConfigPath = path.join(projectPath, 'next.config.js'); if (!fs.existsSync(nextConfigPath)) { spinner.fail('Error: next.config.js no encontrado'); throw new Error('Archivo de configuración Next.js no encontrado'); } spinner.succeed('Compatibilidad con Pages Router configurada correctamente'); } catch (error) { spinner.fail('Error al configurar compatibilidad con Pages Router'); throw error; } } async function copyTemplateFiles(targetPath) { const templatePath = path.resolve(__dirname, 'template'); const spinner = ora('Copiando archivos de la plantilla base...').start(); try { // Copiar todos los archivos del template al directorio destino await fs.copy(templatePath, targetPath); spinner.succeed('Archivos de plantilla copiados'); } catch (error) { spinner.fail('Error al copiar los archivos de la plantilla'); throw error; } } async function customizeProject(projectPath, options) { const { typescript, tailwindcss, eslint, database, auth, authProviders, roles, middleware, ai, aiModels, cloudinary, blog, mdxFeatures, forms, darkMode, uiComponents, framerMotion, notifications, examplePages, exampleTypes, fullIntegrationExamples, envExample, documentation } = options; const spinner = ora('Personalizando el proyecto según tus preferencias...').start(); try { // Actualizar package.json con las dependencias necesarias const packageJsonPath = path.join(projectPath, 'package.json'); const packageJson = await fs.readJson(packageJsonPath); // Dependencias base que siempre se incluyen const dependencies = { "next": "^14.2.17", "react": "^18.2.0", "react-dom": "^18.2.0" }; const devDependencies = {}; // TypeScript (opcional) if (typescript) { devDependencies["typescript"] = "^5.3.3"; devDependencies["@types/react"] = "^18.2.45"; devDependencies["@types/node"] = "^20.10.5"; devDependencies["@types/react-dom"] = "^18.2.18"; } // TailwindCSS (opcional) if (tailwindcss) { dependencies["tailwindcss"] = "^3.4.0"; dependencies["postcss"] = "^8.4.32"; dependencies["autoprefixer"] = "^10.4.16"; dependencies["clsx"] = "^2.0.0"; dependencies["tailwind-merge"] = "^2.2.0"; dependencies["critters"] = "^0.0.20"; // Para optimización CSS } // ESLint (opcional) if (eslint) { devDependencies["eslint"] = "^8.56.0"; devDependencies["eslint-config-next"] = "^14.0.4"; devDependencies["@typescript-eslint/parser"] = "^6.15.0"; devDependencies["@typescript-eslint/eslint-plugin"] = "^6.15.0"; } // Base de datos if (database === 'MongoDB') { dependencies["mongodb"] = "^6.3.0"; dependencies["mongoose"] = "^8.0.3"; } else if (database === 'Supabase') { dependencies["@supabase/supabase-js"] = "^2.39.1"; } else if (database === 'Firebase') { dependencies["firebase"] = "^10.7.1"; dependencies["firebase-admin"] = "^11.11.1"; } // Autenticación if (auth) { dependencies["next-auth"] = "^4.24.5"; // Si no está TypeScript habilitado, añadir tipos como dependencia normal if (!typescript) { dependencies["@types/next-auth"] = "^3.15.0"; } } // Cloudinary if (cloudinary) { dependencies["cloudinary"] = "^1.41.1"; dependencies["next-cloudinary"] = "^5.13.0"; dependencies["formidable"] = "^2.1.2"; devDependencies["@types/formidable"] = "^3.4.5"; } // Modelos de IA if (ai && aiModels && aiModels.length > 0) { // Añadir cada modelo según selección if (aiModels.includes('GPT-4o (OpenAI)')) { dependencies["openai"] = "^4.24.1"; } if (aiModels.includes('Claude 3.5 (Anthropic)')) { dependencies["@anthropic-ai/sdk"] = "^0.12.0"; } if (aiModels.includes('Gemini Flash Pro (Google)')) { dependencies["@google/generative-ai"] = "^0.2.0"; } // DeepSeek usa el SDK de OpenAI if (aiModels.includes('DeepSeek V3 Chat') || aiModels.includes('DeepSeek R1 Reasoner')) { if (!dependencies["openai"]) { dependencies["openai"] = "^4.24.1"; } } } // Blog con MDX if (blog) { dependencies["gray-matter"] = "^4.0.3"; dependencies["next-mdx-remote"] = "^4.4.1"; dependencies["date-fns"] = "^2.30.0"; dependencies["remark"] = "^15.0.1"; dependencies["remark-html"] = "^16.0.1"; if (mdxFeatures) { dependencies["rehype-highlight"] = "^7.0.0"; dependencies["rehype-slug"] = "^6.0.0"; dependencies["remark-gfm"] = "^4.0.0"; } if (typescript) { devDependencies["@types/mdx"] = "^2.0.10"; } } // Formularios if (forms) { dependencies["react-hook-form"] = "^7.49.2"; dependencies["zod"] = "^3.22.4"; dependencies["@hookform/resolvers"] = "^3.3.3"; } // Tema claro/oscuro if (darkMode) { dependencies["next-themes"] = "^0.2.1"; } // Animaciones if (framerMotion) { dependencies["framer-motion"] = "^10.16.16"; } // Notificaciones if (notifications) { dependencies["react-hot-toast"] = "^2.4.1"; } // Full Integration Examples Dependencies if (fullIntegrationExamples && fullIntegrationExamples.length > 0) { const EXAMPLE_DEPENDENCIES = { 'analytics-dashboard': { deps: ['chart.js', 'react-chartjs-2', 'd3'], devDeps: ['@types/d3'] }, 'landing-page': { deps: ['react-intersection-observer', 'react-parallax'], devDeps: [] }, 'ecommerce': { deps: ['zustand', 'react-currency-input-field'], devDeps: [] }, 'portfolio': { deps: ['typed.js', 'react-scroll'], devDeps: [] }, 'task-app': { deps: ['use-local-storage-state', 'react-beautiful-dnd'], devDeps: [] }, 'chat': { deps: ['socket.io-client', 'uuid'], devDeps: ['@types/uuid'] }, 'saas': { deps: ['@stripe/stripe-js', 'stripe'], devDeps: [] }, 'marketplace': { deps: ['react-rating-stars-component', 'react-image-gallery'], devDeps: [] } }; // Add dependencies for selected full integration examples fullIntegrationExamples.forEach(exampleKey => { const exampleDeps = EXAMPLE_DEPENDENCIES[exampleKey]; if (exampleDeps) { // Add regular dependencies exampleDeps.deps.forEach(dep => { if (dep === 'chart.js') dependencies[dep] = '^4.4.1'; else if (dep === 'react-chartjs-2') dependencies[dep] = '^5.2.0'; else if (dep === 'd3') dependencies[dep] = '^7.8.5'; else if (dep === 'react-intersection-observer') dependencies[dep] = '^9.5.3'; else if (dep === 'react-parallax') dependencies[dep] = '^3.5.1'; else if (dep === 'zustand') dependencies[dep] = '^4.4.7'; else if (dep === 'react-currency-input-field') dependencies[dep] = '^3.6.11'; else if (dep === 'typed.js') dependencies[dep] = '^2.1.0'; else if (dep === 'react-scroll') dependencies[dep] = '^1.9.0'; else if (dep === 'use-local-storage-state') dependencies[dep] = '^19.2.0'; else if (dep === 'react-beautiful-dnd') dependencies[dep] = '^13.1.1'; else if (dep === 'socket.io-client') dependencies[dep] = '^4.7.4'; else if (dep === 'uuid') dependencies[dep] = '^9.0.1'; else if (dep === '@stripe/stripe-js') dependencies[dep] = '^2.4.0'; else if (dep === 'stripe') dependencies[dep] = '^14.13.0'; else if (dep === 'react-rating-stars-component') dependencies[dep] = '^2.2.0'; else if (dep === 'react-image-gallery') dependencies[dep] = '^1.3.0'; else dependencies[dep] = 'latest'; }); // Add dev dependencies exampleDeps.devDeps.forEach(dep => { if (dep === '@types/d3') devDependencies[dep] = '^7.4.3'; else if (dep === '@types/uuid') devDependencies[dep] = '^9.0.7'; else devDependencies[dep] = 'latest'; }); } }); } // Actualizar package.json packageJson.dependencies = { ...packageJson.dependencies, ...dependencies }; packageJson.devDependencies = { ...packageJson.devDependencies, ...devDependencies }; await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); // Remover archivos/componentes según las opciones await removeUnusedFiles(projectPath, options); spinner.succeed('Proyecto personalizado según tus preferencias'); } catch (error) { spinner.fail('Error al personalizar el proyecto'); throw error; } } async function removeUnusedFiles(projectPath, options) { const { typescript, tailwindcss, eslint, auth, ai, cloudinary, blog, forms, darkMode, framerMotion, notifications, examplePages, exampleTypes } = options; try { // Remover archivos de TypeScript si no está habilitado if (!typescript) { const tsFiles = [ 'tsconfig.json', 'next-env.d.ts' ]; for (const file of tsFiles) { const filePath = path.join(projectPath, file); if (fs.existsSync(filePath)) { await fs.remove(filePath); } } } // Remover configuración de TailwindCSS si no está habilitado if (!tailwindcss) { const tailwindFiles = [ 'tailwind.config.js', 'postcss.config.js' ]; for (const file of tailwindFiles) { const filePath = path.join(projectPath, file); if (fs.existsSync(filePath)) { await fs.remove(filePath); } } } // Remover ESLint si no está habilitado if (!eslint) { const eslintPath = path.join(projectPath, '.eslintrc.json'); if (fs.existsSync(eslintPath)) { await fs.remove(eslintPath); } } // Remover componentes de autenticación si no está habilitado if (!auth) { const authFiles = [ 'components/auth', 'lib/auth.ts', 'lib/roles.ts', 'pages/api/auth', 'middleware.ts' ]; for (const file of authFiles) { const filePath = path.join(projectPath, file); if (fs.existsSync(filePath)) { await fs.remove(filePath); } } } // Remover componentes de IA si no está habilitado if (!ai) { const aiFiles = [ 'components/ai', 'pages/api/ai.ts', 'pages/api/openai.ts' ]; for (const file of aiFiles) { const filePath = path.join(projectPath, file); if (fs.existsSync(filePath)) { await fs.remove(filePath); } } } // Remover Cloudinary si no está habilitado if (!cloudinary) { const cloudinaryFiles = [ 'components/upload', 'pages/api/upload.ts' ]; for (const file of cloudinaryFiles) { const filePath = path.join(projectPath, file); if (fs.existsSync(filePath)) { await fs.remove(filePath); } } } // Remover blog si no está habilitado if (!blog) { const blogFiles = [ 'pages/blog', 'lib/mdx.ts', 'content' ]; for (const file of blogFiles) { const filePath = path.join(projectPath, file); if (fs.existsSync(filePath)) { await fs.remove(filePath); } } } // Remover páginas de ejemplo si no están habilitadas if (!examplePages) { const examplePath = path.join(projectPath, 'pages/ejemplos'); if (fs.existsSync(examplePath)) { await fs.remove(examplePath); } } else if (exampleTypes && exampleTypes.length > 0) { // Remover ejemplos específicos no seleccionados const examplePath = path.join(projectPath, 'pages/ejemplos'); if (!exampleTypes.includes('Ejemplo de Autenticación')) { const authExamplePath = path.join(examplePath, 'auth.tsx'); if (fs.existsSync(authExamplePath)) { await fs.remove(authExamplePath); } } if (!exampleTypes.includes('Ejemplo de IA (Multi-modelo)')) { const aiExamplePath = path.join(examplePath, 'ai.tsx'); if (fs.existsSync(aiExamplePath)) { await fs.remove(aiExamplePath); } } if (!exampleTypes.includes('Biblioteca de Componentes')) { const componentsExamplePath = path.join(examplePath, 'componentes.tsx'); if (fs.existsSync(componentsExamplePath)) { await fs.remove(componentsExamplePath); } } if (!exampleTypes.includes('Ejemplo de Carga de Imágenes')) { const uploadExamplePath = path.join(examplePath, 'upload.tsx'); if (fs.existsSync(uploadExamplePath)) { await fs.remove(uploadExamplePath); } } if (!exampleTypes.includes('Ejemplo de Formularios')) { const formsExamplePath = path.join(examplePath, 'formularios.tsx'); if (fs.existsSync(formsExamplePath)) { await fs.remove(formsExamplePath); } } if (!exampleTypes.includes('Ejemplo de Animaciones')) { const animationsExamplePath = path.join(examplePath, 'animaciones.tsx'); if (fs.existsSync(animationsExamplePath)) { await fs.remove(animationsExamplePath); } } if (!exampleTypes.includes('Ejemplo de Notificaciones')) { const notificationsExamplePath = path.join(examplePath, 'notificaciones.tsx'); if (fs.existsSync(notificationsExamplePath)) { await fs.remove(notificationsExamplePath); } } if (!exampleTypes.includes('Ejemplo de Base de Datos')) { const databaseExamplePath = path.join(examplePath, 'database.tsx'); if (fs.existsSync(databaseExamplePath)) { await fs.remove(databaseExamplePath); } // También remover las APIs de prueba de database const databaseApiPath = path.join(projectPath, 'pages/api/database'); if (fs.existsSync(databaseApiPath)) { await fs.remove(databaseApiPath); } } if (!exampleTypes.includes('Ejemplo de UI y Temas')) { const uiExamplePath = path.join(examplePath, 'ui-temas.tsx'); if (fs.existsSync(uiExamplePath)) { await fs.remove(uiExamplePath); } } if (!exampleTypes.includes('Ejemplo de TypeScript')) { const tsExamplePath = path.join(examplePath, 'typescript.tsx'); if (fs.existsSync(tsExamplePath)) { await fs.remove(tsExamplePath); } } if (!exampleTypes.includes('Páginas 404 personalizada')) { const notFoundPath = path.join(projectPath, 'pages/404.tsx'); if (fs.existsSync(notFoundPath)) { await fs.remove(notFoundPath); } } } } catch (error) { console.log(chalk.yellow('Advertencia: Error al remover algunos archivos no utilizados')); } } async function installDependencies(projectPath, options) { const spinner = ora('Instalando dependencias (esto puede tardar unos minutos)...').start(); try { const isWindows = process.platform === 'win32'; if (isWindows) { // Solución robusta para Windows usando spawn const { spawn } = require('child_process'); await new Promise((resolve, reject) => { const npmProcess = spawn('npm', ['install'], { cwd: projectPath, stdio: 'inherit', shell: true, env: process.env }); npmProcess.on('close', (code) => { if (code === 0) { resolve(); } else { reject(new Error(`npm install falló con código ${code}`)); } }); npmProcess.on('error', (error) => { reject(error); }); }); } else { // En Unix/Linux/macOS usar execSync execSync('npm install', { cwd: projectPath, stdio: 'inherit' }); } spinner.succeed('Dependencias instaladas correctamente'); } catch (error) { spinner.fail('Error al instalar las dependencias'); console.error(chalk.red(`Error: ${error.message}`)); // Fallback: Guía manual para el usuario console.error(chalk.yellow('\n🚨 SOLUCIÓN ALTERNATIVA:')); console.error(chalk.yellow('El CLI generó el proyecto exitosamente, pero falló la instalación automática.')); console.error(chalk.yellow('Ejecuta estos comandos manualmente:')); console.error(chalk.cyan(` cd ${path.basename(projectPath)}`)); console.error(chalk.cyan(' npm install')); console.error(chalk.cyan(' npm run dev')); console.error(chalk.yellow('\n💡 Si npm install también falla:')); console.error(chalk.yellow(' 1. Usa yarn: yarn install')); console.error(chalk.yellow(' 2. O bun: bun install')); console.error(chalk.yellow(' 3. Verifica Node.js versión: node --version (necesita >=18)')); // No lanzar error para que el proyecto se complete console.log(chalk.green('\n✅ Proyecto generado exitosamente. Instala las dependencias manualmente.')); } } async function addFullIntegrationExamples(projectPath, selectedExamples) { const spinner = ora('Añadiendo ejemplos de integración completa seleccionados...').start(); try { // Copy full integration example files from templates to main ejemplos directory const ejemplosPath = path.join(projectPath, 'pages/ejemplos'); const templateEjemplosPath = path.resolve(__dirname, 'template/pages/ejemplos'); for (const exampleKey of selectedExamples) { const exampleFilePath = path.join(templateEjemplosPath, `${exampleKey}.tsx`); const targetFilePath = path.join(ejemplosPath, `${exampleKey}.tsx`); if (fs.existsSync(exampleFilePath)) { await fs.copy(exampleFilePath, targetFilePath); } } spinner.succeed('Ejemplos de integración completa añadidos correctamente'); } catch (error) { spinner.fail('Error al añadir ejemplos de integración completa'); throw error; } } // Function removed - full integration examples are now included directly in main examples page async function generateReadme(projectPath, options) { const spinner = ora('Generando README personalizado...').start(); try { const readmePath = path.join(projectPath, 'README.md'); let readmeContent = `# ${options.projectName}\n\n`; readmeContent += `Proyecto Next.js 14+ con Pages Router y varias características modernas.\n\n`; readmeContent += `## Características\n\n`; // Listar características según opciones elegidas readmeContent += `- Next.js 14+ con Pages Router\n`; readmeContent += `- TypeScript\n`; readmeContent += `- TailwindCSS\n`; if (options.darkMode) { readmeContent += `- Modo claro/oscuro con next-themes\n`; } if (options.database !== 'Ninguna') { readmeContent += `- Integración con ${options.database}\n`; } if (options.auth) { readmeContent += `- Autenticación con NextAuth.js (`; readmeContent += options.authProviders.join(', '); readmeContent += `)\n`; if (options.roles) { readmeContent += `- Sistema de roles básico (admin/user)\n`; } } if (options.blog) { readmeContent += `- Blog con MDX\n`; } if (options.forms) { readmeContent += `- Formularios con react-hook-form y validaciones zod\n`; } if (options.cloudinary) { readmeContent += `- Integración con Cloudinary para gestión de imágenes\n`; } if (options.ai) { readmeContent += `- Integración con IA\n`; } if (options.framerMotion) { readmeContent += `- Animaciones con Framer Motion\n`; } readmeContent += `- Notificaciones con react-hot-toast\n\n`; readmeContent += `## Primeros pasos\n\n`; readmeContent += `\`\`\`bash\n`; readmeContent += `npm run dev\n`; readmeContent += `\`\`\`\n\n`; readmeContent += `Abre [http://localhost:3000](http://localhost:3000) en tu navegador para ver el resultado.\n\n`; readmeContent += `## Variables de entorno\n\n`; readmeContent += `Copia el archivo \`.env.example\` a \`.env.local\` y completa las variables necesarias.\n\n`; await fs.writeFile(readmePath, readmeContent); spinner.succeed('README generado correctamente'); } catch (error) { spinner.fail('Error al generar el README'); throw error; } } module.exports = { createProject };