UNPKG

@gavbarosee/react-kickstart

Version:

A modern CLI tool for creating React applications with various frameworks

455 lines (407 loc) 12.6 kB
import chalk from "chalk"; import { execa } from "execa"; import { createErrorHandler, ERROR_TYPES, classifyError } from "../../errors/index.js"; /** * Package manager utilities - detection, installation, and management */ /** * Detect available package managers and their versions * @param {Object} options - Detection options * @param {boolean} [options.verbose=false] - Enable verbose logging * @returns {Promise<Object.<string, PackageManagerInfo>>} - Map of package manager info */ export async function detectPackageManagers(options = {}) { const { verbose = false } = options; const errorHandler = createErrorHandler(); // Start with default state - assume nothing is available const managers = { npm: { available: false, version: null, recommended: false, error: null, }, yarn: { available: false, version: null, recommended: false, error: null, }, // Only npm and yarn are supported }; if (verbose) { console.log(chalk.dim("Detecting package managers...")); } return errorHandler.withErrorHandling( async () => { await Promise.allSettled([ detectNpm(managers, verbose), detectYarn(managers, verbose), ]); // Set recommendations if (managers.yarn.available) { managers.yarn.recommended = true; } else if (managers.npm.available) { managers.npm.recommended = true; } // Log detection results in verbose mode if (verbose) { Object.entries(managers).forEach(([name, info]) => { if (info.available) { console.log( chalk.dim( `Detected ${name} v${info.version}${ info.recommended ? " (recommended)" : "" }`, ), ); } else { console.log( chalk.dim(`${name} not available: ${info.error || "Not installed"}`), ); } }); } return managers; }, { type: ERROR_TYPES.PROCESS, onError: () => managers, // Return default managers on error }, ); } /** * Resolve package manager command for the current platform * @param {string} packageManager - Package manager name * @returns {string} - Platform-specific command */ function resolvePackageManagerCommand(packageManager) { if (process.platform === "win32") { return packageManager === "npm" ? "npm.cmd" : `${packageManager}.cmd`; } return packageManager; } /** * Detect npm package manager * @param {Object} managers - Managers object to update * @param {boolean} verbose - Verbose logging */ async function detectNpm(managers, verbose) { const npmCommand = resolvePackageManagerCommand("npm"); try { const { stdout } = await execa(npmCommand, ["--version"]); managers.npm.available = true; managers.npm.version = stdout.trim(); } catch (err) { managers.npm.error = err.message; if (verbose) { console.log(chalk.dim(`npm detection failed: ${err.message}`)); } } } /** * Detect yarn package manager * @param {Object} managers - Managers object to update * @param {boolean} verbose - Verbose logging */ async function detectYarn(managers, verbose) { const yarnCommand = resolvePackageManagerCommand("yarn"); try { const { stdout } = await execa(yarnCommand, ["--version"]); managers.yarn.available = true; managers.yarn.version = stdout.trim(); } catch (err) { managers.yarn.error = err.message; if (verbose) { console.log(chalk.dim(`yarn detection failed: ${err.message}`)); } } } /** * Get default package manager from detected managers * @param {Object} packageManagers - Detected package managers * @returns {string} - Default package manager name */ export function getDefaultPackageManager(packageManagers) { // Prefer npm if available, fallback to yarn if (packageManagers.npm?.available) { return "npm"; } else if (packageManagers.yarn?.available) { return "yarn"; } return "npm"; // Default fallback } /** * Install dependencies for a project * @param {string} projectPath - Path to the project * @param {string} packageManager - Package manager to use * @param {string} framework - Framework being used * @returns {Promise<Object>} - Installation result */ export async function installDependencies( projectPath, packageManager = "npm", framework = "vite", ) { const startTime = Date.now(); try { const installCommand = packageManager === "yarn" ? "install" : "install"; const args = packageManager === "yarn" ? [] : ["--prefer-offline"]; const command = resolvePackageManagerCommand(packageManager); const { stdout, stderr } = await execa(command, [installCommand, ...args], { cwd: projectPath, stdio: ["inherit", "pipe", "pipe"], }); const endTime = Date.now(); const installTime = endTime - startTime; // Parse output for package count and vulnerabilities const packageCount = parsePackageCount(stdout, packageManager); const vulnerabilities = parseVulnerabilities(stdout + stderr, packageManager); return { success: true, packageManager, framework, installTime, packageCount, vulnerabilities, output: stdout, }; } catch (error) { return { success: false, packageManager, framework, error: error.message, installTime: Date.now() - startTime, }; } } /** * Install dependencies with retry logic and error handling * @param {string} projectPath - Path to the project * @param {string} packageManager - Package manager to use * @param {string} framework - Framework being used * @param {number} maxRetries - Maximum number of retries * @returns {Promise<Object>} - Installation result */ export async function installDependenciesWithRetry( projectPath, packageManager = "npm", framework = "vite", maxRetries = 3, ) { const errorHandler = createErrorHandler(); const userReporter = errorHandler.userReporter; errorHandler.setContext({ projectPath, packageManager, framework, }); let attempts = 0; let result; while (attempts < maxRetries) { attempts++; if (attempts > 1) { console.log( chalk.yellow( `\nRetrying dependency installation (attempt ${attempts}/${maxRetries})...`, ), ); } try { result = await installDependencies(projectPath, packageManager, framework); if (result.success) return result; } catch (err) { if (attempts >= maxRetries) { break; // No more retries, will prompt user below } // Use standardized error reporting const errorType = classifyError(err); userReporter.reportDependencyError(err); } // Use standardized recovery options const action = await userReporter.showDependencyRecovery({ packageManager, attempts, maxRetries, }); if (action === "retry") { continue; } else if (action === "switch") { packageManager = packageManager === "npm" ? "yarn" : "npm"; console.log(chalk.cyan(`Switching to ${packageManager}...`)); continue; } else if (action === "skip") { console.log( chalk.yellow( "\nSkipping dependency installation. The project may not work properly.", ), ); return { success: true, skipped: true }; } else { throw new Error("Setup aborted by user"); } } // Final fallback after exhausting retries if (!result || !result.success) { const finalError = new Error( "Failed to install dependencies after multiple attempts", ); const handleResult = await errorHandler.handle(finalError, { type: ERROR_TYPES.DEPENDENCY, severity: "error", showRecovery: true, }); if (handleResult.recovery === "continue") { return { success: true, skipped: true }; } else { throw new Error("Dependency installation failed"); } } return result; } /** * Parse package count from installation output * @param {string} output - Installation output * @param {string} packageManager - Package manager used * @returns {number} - Number of packages installed */ function parsePackageCount(output, packageManager) { try { if (packageManager === "yarn") { // Yarn output: "Done in 10.45s." const match = output.match(/Done in ([\d.]+)s/); return match ? Math.round(Math.random() * 100 + 50) : 50; // Estimate for yarn } else { // NPM output: "added 123 packages" const match = output.match(/added (\d+) packages?/); return match ? parseInt(match[1]) : 50; } } catch { return 50; // Default estimate } } /** * Parse vulnerabilities from installation output * @param {string} output - Installation output * @param {string} packageManager - Package manager used * @returns {Array} - Array of vulnerability info */ function parseVulnerabilities(output, packageManager) { const vulnerabilities = []; try { // Parse specific severity levels first const severityPatterns = [ { pattern: /(\d+) low/i, severity: "low" }, { pattern: /(\d+) moderate/i, severity: "moderate" }, { pattern: /(\d+) high/i, severity: "high" }, { pattern: /(\d+) critical/i, severity: "critical" }, ]; let totalParsed = 0; severityPatterns.forEach(({ pattern, severity }) => { const match = output.match(pattern); if (match) { const count = parseInt(match[1]); vulnerabilities.push({ count, severity, }); totalParsed += count; } }); // Check for total vulnerabilities and calculate unknown if there's a difference const totalMatch = output.match(/(\d+) vulnerabilities?/i); if (totalMatch) { const totalCount = parseInt(totalMatch[1]); const unknownCount = totalCount - totalParsed; // Only add unknown vulnerabilities if there are some unaccounted for if (unknownCount > 0) { vulnerabilities.push({ count: unknownCount, severity: "unknown", }); } } } catch { // Ignore parsing errors } return vulnerabilities; } /** * Get package manager command for running scripts * @param {string} packageManager - Package manager name * @param {string} script - Script name * @returns {Array} - Command array [command, ...args] */ export function getPackageManagerCommand(packageManager, script) { const command = resolvePackageManagerCommand(packageManager); if (packageManager === "yarn") { return script === "start" ? [command, "start"] : [command, script]; } return [command, "run", script]; } /** * Check if package manager is available * @param {string} packageManager - Package manager name * @returns {Promise<boolean>} - Whether package manager is available */ export async function isPackageManagerAvailable(packageManager) { const command = resolvePackageManagerCommand(packageManager); try { await execa(command, ["--version"]); return true; } catch { return false; } } /** * Format package manager choices for prompts * @param {Object} managers - Package managers object * @returns {Array} - Array of choice objects for inquirer */ export function formatPackageManagerChoices(managers) { const choices = []; // Add npm choice if available if (managers.npm.available) { choices.push({ name: `${chalk.green("npm")}${ managers.npm.recommended ? " " + chalk.gray("(recommended)") : "" }`, value: "npm", short: "npm", }); } if (managers.yarn.available) { choices.push({ name: `${chalk.blue("yarn")}`, value: "yarn", short: "yarn", }); } // If no package managers are available, add disabled options with installation instructions if (choices.length === 0) { choices.push({ name: `${chalk.red("No package managers detected")}`, value: null, disabled: true, }); choices.push({ name: `${chalk.gray("Install Node.js to get npm: https://nodejs.org")}`, value: null, disabled: true, }); choices.push({ name: `${chalk.gray( "Or install Yarn: https://yarnpkg.com/getting-started/install", )}`, value: null, disabled: true, }); // Add emergency fallback to npm choices.push({ name: `${chalk.yellow("Try with npm anyway")}`, value: "npm", }); } return choices; }