UNPKG

@gavbarosee/react-kickstart

Version:

A modern CLI tool for creating React applications with various frameworks

226 lines (190 loc) 6.8 kB
import chalk from "chalk"; import { execa } from "execa"; import open from "open"; import ora from "ora"; import path from "path"; import { createErrorHandler, ERROR_TYPES } from "../../errors/index.js"; import { getFrameworkDocumentation } from "../ui/project-summary.js"; let devProcess = null; let isShuttingDown = false; /** * Cleanup function that properly terminates the development server */ function cleanupDevServer() { if (!devProcess || isShuttingDown) return; isShuttingDown = true; console.log(chalk.yellow("\nStopping development server...")); try { // try graceful termination first with SIGTERM if (!devProcess.killed) { devProcess.kill("SIGTERM"); // give it a moment to terminate gracefully, then force if needed setTimeout(() => { if (devProcess && !devProcess.killed) { console.log(chalk.yellow("Forcing development server to stop...")); try { devProcess.kill("SIGKILL"); } catch { // last resort attempt by PID try { if (devProcess.pid) { process.kill(devProcess.pid, "SIGKILL"); } } catch (killError) { console.error( chalk.red(`Unable to terminate process: ${killError.message}`), ); } } } devProcess = null; isShuttingDown = false; }, 2000); } } catch (err) { console.error(chalk.red(`Error stopping server: ${err.message}`)); devProcess = null; isShuttingDown = false; } } /** * Legacy cleanup handlers - now replaced by centralized ErrorHandler * @deprecated Use ErrorHandler.setupGlobalHandlers() instead */ function registerCleanupHandlers() { // Only register dev server cleanup on process exit; signals handled globally process.on("exit", cleanupDevServer); } async function checkServerAndOpenBrowser(devUrl) { // function to check if server is responding const isServerReady = async () => { try { // using native http/https modules to avoid adding dependencies const http = devUrl.startsWith("https") ? await import("https") : await import("http"); return new Promise((resolve) => { const req = http.get(devUrl, (res) => { // any response means server is up (even 404, 500, etc.) resolve(true); req.destroy(); }); req.on("error", () => { resolve(false); }); // set a short timeout for each attempt req.setTimeout(500, () => { req.destroy(); resolve(false); }); }); } catch { return false; } }; // try to connect to the server for up to 30 seconds let attempts = 0; const maxAttempts = 30; while (attempts < maxAttempts) { if (await isServerReady()) { try { await open(devUrl); } catch { console.log( chalk.yellow( `\nCouldn't open your default browser. It might not be installed or properly configured.`, ), ); console.log(chalk.cyan(`Please visit ${devUrl} manually in your browser.`)); } return true; } // wait 1 second between attempts (no visual progress) await new Promise((resolve) => setTimeout(resolve, 1000)); attempts++; } console.log( chalk.yellow(`\nServer didn't respond in time. You may need to manually open:`), ); console.log(chalk.bold.underline(`${devUrl}`)); return false; } export async function startProject(projectPath, userChoices) { const errorHandler = createErrorHandler(); const userReporter = errorHandler.userReporter; errorHandler.setContext({ projectPath, framework: userChoices.framework, packageManager: userChoices.packageManager, }); // Global handlers are already set at app entry; do not re-register here const spinner = ora({ text: `Starting development server...`, color: "green", spinner: "dots", }).start(); return errorHandler.withErrorHandling( async () => { const frameworkInfo = getFrameworkDocumentation(userChoices.framework); const devUrl = `http://localhost:${frameworkInfo.port}`; const pmRun = userChoices.packageManager === "yarn" ? "yarn" : "npm run"; const devCommand = `${pmRun} dev`; const [cmd, ...args] = devCommand.split(" "); spinner.succeed(`Development server starting at ${chalk.cyan(devUrl)}`); console.log(`\n${chalk.cyan(">")} ${chalk.dim(`${cmd} ${args.join(" ")}`)}`); console.log(chalk.yellow("\nPress Ctrl+C to stop the development server\n")); await new Promise((resolve) => setTimeout(resolve, 500)); // start the process with improved tracking devProcess = execa(cmd, args, { cwd: projectPath, stdio: ["inherit", "pipe", "pipe"], killSignal: "SIGTERM", cleanup: true, // important: don't use detached mode to ensure process is killed with parent detached: false, }); // handle subprocess events with standardized error reporting devProcess.on("exit", (code, signal) => { const exitMessage = signal ? `Development server was terminated by signal: ${signal}` : `Development server exited with code: ${code}`; console.log(chalk.cyan(exitMessage)); // clear the reference to allow garbage collection devProcess = null; }); devProcess.on("error", (err) => { userReporter.reportProcessError(err); devProcess = null; }); // log any stderr output from the subprocess that might indicate problems devProcess.stderr.on("data", (data) => { const errorOutput = data.toString().trim(); if (errorOutput && errorOutput.toLowerCase().includes("error")) { userReporter.formatError("Development Server Error", errorOutput, [ "Check if the port is available", "Ensure all dependencies are installed", ]); } }); checkServerAndOpenBrowser(devUrl); return devProcess; }, { type: ERROR_TYPES.PROCESS, showRecovery: true, onError: async (error) => { spinner.fail(`Failed to start development server`); // Show manual start instructions const pmRun = userChoices.packageManager === "yarn" ? "yarn" : "npm run"; const devCommand = `${pmRun} dev`; userReporter.formatError("Development Server Failed", error.message || error, [ `cd ${path.basename(projectPath || ".")}`, devCommand, "Check that all dependencies are installed", "Ensure the port is not already in use", ]); return null; }, }, ); }