mirror of
https://github.com/umami-software/umami.git
synced 2025-12-06 01:18:00 +01:00
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
This commit is contained in:
parent
e706622db5
commit
ddc005625d
1 changed files with 352 additions and 0 deletions
352
scripts/setup-validator.js
Normal file
352
scripts/setup-validator.js
Normal file
|
|
@ -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<ValidationResult>}
|
||||
*/
|
||||
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<ValidationResult>}
|
||||
*/
|
||||
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<ValidationResult>}
|
||||
*/
|
||||
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<ValidationResult>}
|
||||
*/
|
||||
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<ValidationResult>}
|
||||
*/
|
||||
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<Object>}
|
||||
*/
|
||||
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,
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue