From ddc005625dd2ef2470b764d2e42e2ef55805f558 Mon Sep 17 00:00:00 2001 From: Ayush3603 Date: Mon, 10 Nov 2025 22:05:50 +0530 Subject: [PATCH] feat: add comprehensive setup validation script - Implement Node.js version validation (>= 18.18) - Add pnpm package manager check - Validate .env file existence and DATABASE_URL - Check dependencies installation - Color-coded output with solutions and documentation links --- scripts/setup-validator.js | 352 +++++++++++++++++++++++++++++++++++++ 1 file changed, 352 insertions(+) create mode 100644 scripts/setup-validator.js diff --git a/scripts/setup-validator.js b/scripts/setup-validator.js new file mode 100644 index 00000000..a017200b --- /dev/null +++ b/scripts/setup-validator.js @@ -0,0 +1,352 @@ +/* eslint-disable no-console */ +import { execSync } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { readFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; +import chalk from 'chalk'; +import semver from 'semver'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const projectRoot = join(__dirname, '..'); + +const MIN_NODE_VERSION = '18.18.0'; + +/** + * Validation result structure + * @typedef {Object} ValidationResult + * @property {string} check - Name of the validation check + * @property {'pass'|'fail'|'warning'} status - Status of the check + * @property {string} message - Human-readable message + * @property {string} [solution] - Suggested fix for failures + * @property {string} [documentation] - Link to relevant docs + */ + +/** + * Check Node.js version + * @returns {Promise} + */ +async function checkNodeVersion() { + try { + const currentVersion = process.version; + const version = semver.clean(currentVersion); + + if (semver.gte(version, MIN_NODE_VERSION)) { + return { + check: 'Node.js Version', + status: 'pass', + message: `Node.js ${version} is installed (minimum: ${MIN_NODE_VERSION})`, + }; + } else { + return { + check: 'Node.js Version', + status: 'fail', + message: `Node.js ${version} is installed, but ${MIN_NODE_VERSION} or newer is required`, + solution: `Please upgrade Node.js to version ${MIN_NODE_VERSION} or newer`, + documentation: 'https://nodejs.org/en/download/', + }; + } + } catch { + return { + check: 'Node.js Version', + status: 'fail', + message: 'Unable to determine Node.js version', + solution: 'Ensure Node.js is properly installed', + documentation: 'https://nodejs.org/en/download/', + }; + } +} + +/** + * Check if pnpm is installed + * @returns {Promise} + */ +async function checkPackageManager() { + try { + const version = execSync('pnpm --version', { encoding: 'utf-8' }).trim(); + + return { + check: 'Package Manager (pnpm)', + status: 'pass', + message: `pnpm ${version} is installed`, + }; + } catch { + return { + check: 'Package Manager (pnpm)', + status: 'fail', + message: 'pnpm is not installed or not in PATH', + solution: 'Install pnpm using: npm install -g pnpm', + documentation: 'https://pnpm.io/installation', + }; + } +} + +/** + * Check if .env file exists and has required variables + * @returns {Promise} + */ +async function checkEnvFile() { + const envPath = join(projectRoot, '.env'); + + if (!existsSync(envPath)) { + return { + check: 'Environment Configuration', + status: 'fail', + message: '.env file not found', + solution: 'Copy .env.example to .env and configure your database connection', + documentation: 'See .env.example for template', + }; + } + + try { + const envContent = await readFile(envPath, 'utf-8'); + + // Check for DATABASE_URL + const hasDatabaseUrl = /^DATABASE_URL=/m.test(envContent); + + if (!hasDatabaseUrl) { + return { + check: 'Environment Configuration', + status: 'fail', + message: '.env file exists but DATABASE_URL is not defined', + solution: 'Add DATABASE_URL to your .env file (see .env.example)', + }; + } + + return { + check: 'Environment Configuration', + status: 'pass', + message: '.env file exists with required variables', + }; + } catch { + return { + check: 'Environment Configuration', + status: 'fail', + message: 'Unable to read .env file', + solution: 'Ensure .env file has proper read permissions', + }; + } +} + +/** + * Validate DATABASE_URL format + * @returns {Promise} + */ +async function checkDatabaseUrl() { + const databaseUrl = process.env.DATABASE_URL; + + if (!databaseUrl) { + return { + check: 'Database URL Format', + status: 'fail', + message: 'DATABASE_URL environment variable is not set', + solution: 'Set DATABASE_URL in your .env file', + }; + } + + // Validate PostgreSQL URL format + const postgresUrlPattern = /^postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)$/; + + if (!postgresUrlPattern.test(databaseUrl)) { + return { + check: 'Database URL Format', + status: 'fail', + message: 'DATABASE_URL format is invalid', + solution: + 'Use format: postgresql://username:password@host:port/database\nExample: postgresql://umami:mypassword@localhost:5432/umami', + }; + } + + return { + check: 'Database URL Format', + status: 'pass', + message: 'DATABASE_URL format is valid', + }; +} + +/** + * Check if dependencies are installed + * @returns {Promise} + */ +async function checkDependencies() { + const nodeModulesPath = join(projectRoot, 'node_modules'); + + if (!existsSync(nodeModulesPath)) { + return { + check: 'Dependencies', + status: 'fail', + message: 'node_modules directory not found', + solution: 'Run: pnpm install', + }; + } + + // Check for key dependencies + const keyDependencies = ['next', 'react', 'prisma', '@prisma/client']; + const missingDeps = []; + + for (const dep of keyDependencies) { + const depPath = join(nodeModulesPath, dep); + if (!existsSync(depPath)) { + missingDeps.push(dep); + } + } + + if (missingDeps.length > 0) { + return { + check: 'Dependencies', + status: 'fail', + message: `Missing key dependencies: ${missingDeps.join(', ')}`, + solution: 'Run: pnpm install', + }; + } + + return { + check: 'Dependencies', + status: 'pass', + message: 'All key dependencies are installed', + }; +} + +/** + * Format a single validation result for display + * @param {ValidationResult} result + * @returns {string} + */ +function formatResult(result) { + const statusIcon = { + pass: chalk.green('āœ“'), + fail: chalk.red('āœ—'), + warning: chalk.yellow('⚠'), + }[result.status]; + + let output = `${statusIcon} ${chalk.bold(result.check)}: ${result.message}`; + + if (result.solution) { + output += `\n ${chalk.yellow('šŸ’” Solution:')} ${result.solution}`; + } + + if (result.documentation) { + output += `\n ${chalk.blue('šŸ“– Documentation:')} ${result.documentation}`; + } + + return output; +} + +/** + * Format all validation results + * @param {ValidationResult[]} results + * @returns {Object} + */ +function formatResults(results) { + const passed = results.filter(r => r.status === 'pass').length; + const failed = results.filter(r => r.status === 'fail').length; + const warnings = results.filter(r => r.status === 'warning').length; + + const overall = failed > 0 ? 'error' : warnings > 0 ? 'incomplete' : 'ready'; + + return { + overall, + passed, + failed, + warnings, + results, + }; +} + +/** + * Main validation function + * @returns {Promise} + */ +async function validateSetup() { + console.log(chalk.bold.cyan('\nšŸ” Validating Umami Setup...\n')); + + // Load environment variables + try { + const dotenv = await import('dotenv'); + dotenv.config({ path: join(projectRoot, '.env') }); + } catch { + // dotenv might not be available yet + } + + const checks = [ + { name: 'Node.js Version', fn: checkNodeVersion }, + { name: 'Package Manager', fn: checkPackageManager }, + { name: 'Environment File', fn: checkEnvFile }, + { name: 'Database URL', fn: checkDatabaseUrl }, + { name: 'Dependencies', fn: checkDependencies }, + ]; + + const results = []; + + for (const check of checks) { + try { + const result = await check.fn(); + results.push(result); + console.log(formatResult(result)); + + // Early exit on critical failures + if (result.status === 'fail' && ['Node.js Version', 'Package Manager'].includes(check.name)) { + console.log( + chalk.red.bold( + '\nāŒ Critical check failed. Please fix the above issue before continuing.\n', + ), + ); + break; + } + } catch (error) { + results.push({ + check: check.name, + status: 'fail', + message: `Unexpected error: ${error.message}`, + }); + console.log(formatResult(results[results.length - 1])); + } + } + + const summary = formatResults(results); + + console.log(chalk.bold('\n' + '='.repeat(60))); + console.log(chalk.bold('Summary:')); + console.log(` ${chalk.green('Passed:')} ${summary.passed}`); + console.log(` ${chalk.red('Failed:')} ${summary.failed}`); + console.log(` ${chalk.yellow('Warnings:')} ${summary.warnings}`); + console.log('='.repeat(60)); + + if (summary.overall === 'ready') { + console.log( + chalk.green.bold('\nāœ… Setup validation passed! You are ready to build and run Umami.\n'), + ); + console.log(chalk.cyan('Next steps:')); + console.log(' 1. Run: pnpm run build'); + console.log(' 2. Run: pnpm run dev (for development) or pnpm run start (for production)\n'); + } else if (summary.overall === 'incomplete') { + console.log(chalk.yellow.bold('\nāš ļø Setup validation completed with warnings.\n')); + console.log(chalk.cyan('Please review the warnings above and fix them if necessary.\n')); + } else { + console.log(chalk.red.bold('\nāŒ Setup validation failed!\n')); + console.log(chalk.cyan('Please fix the errors above and run validation again.\n')); + process.exit(1); + } + + return summary; +} + +// Run validation if this script is executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + validateSetup().catch(error => { + console.error(chalk.red.bold('\nāŒ Validation error:'), error.message); + process.exit(1); + }); +} + +export { + checkNodeVersion, + checkPackageManager, + checkEnvFile, + checkDatabaseUrl, + checkDependencies, + validateSetup, + formatResult, + formatResults, +};