create-bluecopa-react-app
Version:
CLI tool to create bluecopa React applications
378 lines (324 loc) • 13.1 kB
JavaScript
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();