UNPKG

create-bluecopa-react-app

Version:

CLI tool to create bluecopa React applications

378 lines (324 loc) 13.1 kB
#!/usr/bin/env node import { Command } from 'commander'; import path from 'path'; import fs from 'fs-extra'; import chalk from 'chalk'; import inquirer from 'inquirer'; import ora from 'ora'; import { execSync } from 'child_process'; import validatePackageName from 'validate-npm-package-name'; import { fileURLToPath } from 'url'; // ES module equivalent of __dirname const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const program = new Command(); program .version('1.0.5') .name('create-bluecopa-react-app') .description('Create a new Bluecopa React application') .argument('[project-name]', 'Name of the project') .option('-t, --template <template>', 'Template to use (latest)', 'latest') .option('--typescript', 'Use TypeScript template', true) .option('--no-typescript', 'Use JavaScript template') .option('--skip-install', 'Skip package installation') .option('--package-manager <manager>', 'Package manager to use (npm, yarn, pnpm)', 'auto') .option('--git', 'Initialize git repository', true) .option('--no-git', 'Skip git initialization') .option('--yes', 'Skip all prompts and use defaults') .addHelpText('after', ` Examples: $ create-bluecopa-react-app my-dashboard $ create-bluecopa-react-app my-app --template latest $ create-bluecopa-react-app my-app --package-manager pnpm $ create-bluecopa-react-app my-app --skip-install $ create-bluecopa-react-app my-app --no-git $ create-bluecopa-react-app my-app --yes Templates: latest Basic React Router setup with essential components For more information, visit: https://github.com/bluecopa/blui/tree/main/packages/boilerplate/react `) .action(async (projectName, options) => { try { await createApp(projectName, options); } catch (error) { console.error(chalk.red('Error creating app:'), error.message); process.exit(1); } }); async function createApp(projectName, options) { let appName = projectName; let selectedTemplate = options.template; let initGit = options.git; let packageManager = options.packageManager; // If not using --yes flag, prompt for all options if (!options.yes) { const prompts = []; // 1. Project Name prompt if (!appName) { // Generate a random default project name const adjectives = ['awesome', 'stellar', 'brilliant', 'dynamic', 'epic', 'fantastic', 'incredible', 'magnificent', 'outstanding', 'spectacular']; const nouns = ['app', 'dashboard', 'project', 'platform', 'tool', 'system', 'solution', 'interface', 'portal', 'workspace']; const randomAdjective = adjectives[Math.floor(Math.random() * adjectives.length)]; const randomNoun = nouns[Math.floor(Math.random() * nouns.length)]; const defaultProjectName = `my-${randomAdjective}-${randomNoun}`; prompts.push({ type: 'input', name: 'projectName', message: 'What is your project name?', default: defaultProjectName, validate: (input) => { if (!input) return 'Project name is required'; const validation = validatePackageName(input); if (!validation.validForNewPackages) { return validation.errors?.[0] || validation.warnings?.[0] || 'Invalid package name'; } return true; } }); } // 2. Template selection prompt prompts.push({ type: 'list', name: 'template', message: 'Select a template:', choices: [ { name: 'Latest - Basic React Router setup with essential components', value: 'latest' } ], default: selectedTemplate }); // 3. Git initialization prompt prompts.push({ type: 'confirm', name: 'initGit', message: 'Initialize git repository?', default: initGit !== false }); // 4. Package manager prompt prompts.push({ type: 'list', name: 'packageManager', message: 'Select package manager:', choices: [ { name: 'pnpm (recommended)', value: 'pnpm' }, { name: 'npm', value: 'npm' }, { name: 'Auto-detect', value: 'auto' } ], default: packageManager === 'auto' ? 'auto' : packageManager }); // 5. Install dependencies prompt prompts.push({ type: 'confirm', name: 'runInstall', message: 'Do you want to install dependencies now?', default: !options.skipInstall }); // Execute all prompts const answers = await inquirer.prompt(prompts); // Update values from prompts if (answers.projectName) appName = answers.projectName; selectedTemplate = answers.template; initGit = answers.initGit; packageManager = answers.packageManager; options.skipInstall = !answers.runInstall; } else { // When using --yes flag, validate required options if (!appName) { console.error(chalk.red('Project name is required when using --yes flag')); process.exit(1); } } // Validate template const validTemplates = ['latest']; if (!validTemplates.includes(selectedTemplate)) { console.error(chalk.red(`Invalid template: ${selectedTemplate}`)); console.error(chalk.yellow(`Available templates: ${validTemplates.join(', ')}`)); process.exit(1); } // Validate project name const validation = validatePackageName(appName); if (!validation.validForNewPackages) { console.error(chalk.red('Invalid project name:'), validation.errors?.[0] || validation.warnings?.[0]); console.error(chalk.yellow('Project names must be lowercase, contain no spaces, and follow npm naming conventions.')); process.exit(1); } const targetDir = path.resolve(process.cwd(), appName); // Check if directory already exists if (fs.existsSync(targetDir)) { if (options.yes) { console.log(chalk.yellow(`Directory ${appName} already exists. Overwriting...`)); await fs.remove(targetDir); } else { const answers = await inquirer.prompt([ { type: 'confirm', name: 'overwrite', message: `Directory ${appName} already exists. Overwrite?`, default: false } ]); if (!answers.overwrite) { console.log(chalk.yellow('Operation cancelled.')); return; } await fs.remove(targetDir); } } console.log(chalk.blue(`Creating a new Bluecopa React app in ${chalk.green(targetDir)}`)); console.log(chalk.gray(`Template: ${selectedTemplate}`)); console.log(); // Create app const spinner = ora('Creating project structure...').start(); try { await createProjectStructure(targetDir, appName, { ...options, template: selectedTemplate }); spinner.succeed('Project structure created'); if (!options.skipInstall) { const detectedPackageManager = await detectPackageManager(packageManager); spinner.start(`Installing dependencies with ${detectedPackageManager}...`); await installDependencies(targetDir, detectedPackageManager); spinner.succeed('Dependencies installed'); } else { console.log(chalk.yellow('Skipping dependency installation. You can run it manually later.')); } // Post-installation setup if (initGit !== false) { spinner.start('Initializing git repository...'); await initializeGit(targetDir, appName); spinner.succeed('Git repository initialized'); } spinner.succeed(chalk.green('Success! Created ' + appName + ' at ' + targetDir)); console.log(); console.log('Inside that directory, you can run several commands:'); console.log(); // Show install command if dependencies weren't installed if (options.skipInstall) { const detectedPackageManager = await detectPackageManager(packageManager); const installCommand = detectedPackageManager === 'yarn' ? 'yarn install' : detectedPackageManager === 'pnpm' ? 'pnpm install' : 'npm install'; console.log(chalk.cyan(` ${installCommand}`)); console.log(' Install dependencies first.'); console.log(); } const runCommand = packageManager === 'pnpm' ? 'pnpm run' : packageManager === 'yarn' ? 'yarn' : 'npm run'; console.log(chalk.cyan(` ${runCommand} dev`)); console.log(' Starts the development server.'); console.log(); console.log(chalk.cyan(` ${runCommand} build`)); console.log(' Bundles the app into static files for production.'); console.log(); console.log(chalk.cyan(` ${runCommand} start`)); console.log(' Start the production build locally.'); console.log(); console.log(chalk.cyan(` ${runCommand} preview`)); console.log(' Preview the production build locally.'); console.log(); console.log('We suggest that you begin by typing:'); console.log(); console.log(chalk.cyan(' cd'), appName); if (options.skipInstall) { const detectedPackageManager = await detectPackageManager(packageManager); const installCommand = detectedPackageManager === 'yarn' ? 'yarn install' : detectedPackageManager === 'pnpm' ? 'pnpm install' : 'npm install'; console.log(chalk.cyan(` ${installCommand}`)); } console.log(chalk.cyan(` ${runCommand} dev`)); console.log(); console.log('Happy coding! 🚀'); console.log(); console.log(chalk.gray('Need help? Check out the documentation:')); console.log(chalk.gray(' https://github.com/bluecopa/blui/tree/main/packages/boilerplate/react')); } catch (error) { spinner.fail('Failed to create project'); // Cleanup on failure if (fs.existsSync(targetDir)) { console.log(chalk.yellow('Cleaning up...')); await fs.remove(targetDir); } throw error; } } async function createProjectStructure(targetDir, appName, options) { const templateDir = path.join(__dirname, '../templates', options.template); // Validate template directory exists if (!await fs.pathExists(templateDir)) { throw new Error(`Template '${options.template}' not found at ${templateDir}`); } // Create base directory await fs.ensureDir(targetDir); // Copy template files await fs.copy(templateDir, targetDir); // Update package.json with project name const packageJsonPath = path.join(targetDir, 'package.json'); if (await fs.pathExists(packageJsonPath)) { const packageJson = await fs.readJson(packageJsonPath); packageJson.name = appName; await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); } // Update index.html title if it exists const indexHtmlPath = path.join(targetDir, 'public/index.html'); if (await fs.pathExists(indexHtmlPath)) { let indexHtml = await fs.readFile(indexHtmlPath, 'utf8'); indexHtml = indexHtml.replace(/{{APP_NAME}}/g, appName); await fs.writeFile(indexHtmlPath, indexHtml); } // Update root index.html if it exists const rootIndexHtmlPath = path.join(targetDir, 'index.html'); if (await fs.pathExists(rootIndexHtmlPath)) { let indexHtml = await fs.readFile(rootIndexHtmlPath, 'utf8'); indexHtml = indexHtml.replace(/{{APP_NAME}}/g, appName); await fs.writeFile(rootIndexHtmlPath, indexHtml); } } async function detectPackageManager(preferredManager) { if (preferredManager !== 'auto') { // Validate the preferred manager is available try { execSync(`${preferredManager} --version`, { stdio: 'ignore' }); return preferredManager; } catch { console.log(chalk.yellow(`Warning: ${preferredManager} not found, falling back to auto-detection`)); } } // Auto-detect with preference order: pnpm > yarn > npm const managers = ['pnpm', 'yarn', 'npm']; for (const manager of managers) { try { execSync(`${manager} --version`, { stdio: 'ignore' }); return manager; } catch { // Manager not available, try next } } // Fallback to npm (should always be available) return 'npm'; } async function installDependencies(targetDir, packageManager) { try { // Change to target directory and install process.chdir(targetDir); const installCommand = packageManager === 'yarn' ? 'yarn install' : packageManager === 'pnpm' ? 'pnpm install' : 'npm install'; execSync(installCommand, { stdio: 'inherit' }); } catch (error) { throw new Error(`Failed to install dependencies with ${packageManager}: ${error.message}`); } } async function initializeGit(targetDir, appName) { try { process.chdir(targetDir); // Initialize git repository execSync('git init', { stdio: 'ignore' }); // Create initial commit execSync('git add .', { stdio: 'ignore' }); execSync(`git commit -m "Initial commit: ${appName}"`, { stdio: 'ignore' }); } catch (error) { // Git initialization is optional, don't fail the whole process console.log(chalk.yellow('Warning: Failed to initialize git repository')); } } program.parse();