UNPKG

@gavbarosee/react-kickstart

Version:

A modern CLI tool for creating React applications with various frameworks

247 lines (213 loc) 8.41 kB
import chalk from "chalk"; import fs from "fs-extra"; import path from "path"; import { createErrorHandler, ERROR_TYPES } from "./errors/index.js"; import generateProject from "./generators/index.js"; import { promptUser, getFrameworkDefaults } from "./prompts/index.js"; import { keyboardNavManager } from "./prompts/navigation/navigation-manager.js"; import { CORE_UTILS, UI_UTILS, PROCESS_UTILS } from "./utils/index.js"; /** * Build userChoices object from CLI options when --yes flag is used */ function buildUserChoicesFromOptions(options) { const framework = options.framework || "vite"; const baseDefaults = getFrameworkDefaults(framework); return { ...baseDefaults, // Override with explicit CLI options framework, typescript: options.typescript || false, styling: options.styling || "tailwind", stateManagement: options.state || "none", api: options.api || "none", testing: options.testing || "none", deployment: options.deployment || "none", routing: options.routing || (framework === "vite" ? "none" : undefined), nextRouting: options.nextRouting || (framework === "nextjs" ? "app" : undefined), packageManager: options.packageManager || "npm", linting: options.linting !== false, // Default true unless --no-linting initGit: options.git !== false, // Default true unless --no-git openEditor: false, // Always false in CLI mode editor: "vscode", autoStart: options.autostart !== false, // Respect --no-autostart }; } export async function createApp(projectDirectory, options = {}) { // Initialize error handler const errorHandler = createErrorHandler(); errorHandler.setupGlobalHandlers(); // Setup cleanup on process exit const cleanup = () => { keyboardNavManager.cleanup(); }; process.on("exit", cleanup); process.on("SIGINT", cleanup); process.on("SIGTERM", cleanup); let projectPath = null; let shouldCleanup = false; return errorHandler.withErrorHandling( async () => { // Validate project directory input for security const dirValidation = CORE_UTILS.validateProjectDirectory(projectDirectory); if (!dirValidation.valid) { throw new Error(`Invalid project directory: ${dirValidation.error}`); } projectPath = dirValidation.sanitized ? path.resolve(process.cwd(), dirValidation.sanitized) : process.cwd(); const projectName = dirValidation.sanitized || path.basename(projectPath); // Set error handler context errorHandler.setContext({ projectPath, projectName, options, }); // Validate project name const validationResult = CORE_UTILS.validateProjectName(projectName); if (!validationResult.valid) { const validationErrors = [ ...(validationResult.errors || []), ...(validationResult.warnings || []), ]; const errorMessage = `Invalid project name: ${projectName}\n${validationErrors .map((msg) => ` - ${msg}`) .join("\n")}`; throw new Error(errorMessage); } // More atomic directory handling to prevent race conditions try { // Ensure directory exists (creates if missing) await fs.ensureDir(projectPath); // Check directory contents AFTER ensuring it exists const files = await fs.readdir(projectPath); if (files.length > 0) { // Do not mark for cleanup if directory is not empty throw new Error(`The directory ${projectPath} is not empty.`); } // Directory exists and is empty → safe to mark for cleanup shouldCleanup = true; errorHandler.setContext({ shouldCleanup }); // Write a temporary marker so cleanup is safe and scoped to this run only const markerPath = path.join(projectPath, ".react-kickstart.tmp"); await fs.writeFile(markerPath, "temporary setup marker\n"); } catch (error) { // If it's our "not empty" error, re-throw it if (error.message.includes("is not empty")) { throw error; } // For other errors (permission, etc), throw a more descriptive error throw new Error( `Cannot create or access directory ${projectPath}: ${error.message}`, ); } // Get user choices const userChoices = options.yes ? buildUserChoicesFromOptions(options) : await promptUser({ verbose: options.verbose }); // Show summary and get confirmation if (!options.yes && options.summary !== false) { UI_UTILS.divider(); const confirmed = await UI_UTILS.showSummaryPrompt( projectPath, projectName, userChoices, ); if (!confirmed) { await errorHandler.handle(new Error("User cancelled setup"), { type: ERROR_TYPES.USER_CANCELLED, shouldCleanup, }); return; } } // Start project generation with single progress bar UI_UTILS.divider(); // Start single continuous progress bar UI_UTILS.startProgress("Setting up your project", { size: 30 }); // Generate project files await generateProject(projectPath, projectName, userChoices); // Install dependencies (skippable via --skip-install) let installResult = { success: true, skipped: false }; if (options.skipInstall) { console.log(chalk.yellow("Skipping dependency installation (--skip-install).")); installResult = { success: true, skipped: true }; } else { installResult = await errorHandler.withErrorHandling( () => PROCESS_UTILS.installDependencies( projectPath, userChoices.packageManager, userChoices.framework, ), { type: ERROR_TYPES.DEPENDENCY, shouldCleanup, showRecovery: true, }, ); if (installResult.skipped) { console.log( chalk.yellow( "[!] Dependency installation was skipped. Some features may not work properly.", ), ); } else if (!installResult.success) { throw new Error("Failed to install dependencies"); } } // Additional setups if (userChoices.initGit) { await errorHandler.withErrorHandling( () => PROCESS_UTILS.initGit(projectPath, userChoices), { type: ERROR_TYPES.PROCESS }, ); } if (userChoices.openEditor) { await errorHandler.withErrorHandling( () => PROCESS_UTILS.openEditor(projectPath, userChoices.editor, userChoices), { type: ERROR_TYPES.PROCESS }, ); } // Complete the progress bar UI_UTILS.stopProgress(true); const successIcon = chalk.green("✅"); const successText = chalk.bold.white("Project successfully set up"); const sparkles = chalk.yellow("✨🎉✨"); console.log(` ${successIcon} ${successText} ${sparkles}`); console.log(); UI_UTILS.divider(); const completionSummary = UI_UTILS.generateCompletionSummary( projectPath, projectName, userChoices, installResult.vulnerabilities, installResult.packageCount, ); console.log(completionSummary); // Cleanup temporary marker after successful setup to prevent accidental deletions try { const markerPath = path.join(projectPath, ".react-kickstart.tmp"); if (await fs.pathExists(markerPath)) { await fs.remove(markerPath); } // Mark project as completed - no longer needs cleanup on cancellation errorHandler.setContext({ shouldCleanup: false }); } catch {} // Only start the project if autostart is enabled if (options.autostart !== false && userChoices.autoStart !== false) { console.log(); // Add spacing before server startup await errorHandler.withErrorHandling( () => PROCESS_UTILS.startProject(projectPath, userChoices), { type: ERROR_TYPES.PROCESS }, ); } }, { type: ERROR_TYPES.GENERAL, // Only clean up when we explicitly mark shouldCleanup during this run shouldCleanup: false, verbose: options.verbose, showRecovery: false, }, ); }