#!/usr/bin/env node /** * Validate Monorepo Best Practices * * Checks: * 1. No "turbo run" commands in child package.json files (prevents infinite loops) * 2. All internal packages have "private": true * 3. All internal dependencies use "workspace:*" protocol * 4. No prepublishOnly scripts in private packages */ import { readFileSync, readdirSync, statSync } from 'fs'; import { join, relative } from 'path'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const MONOREPO_ROOT = join(__dirname, '..'); const errors = []; const warnings = []; // Colors for terminal output const RED = '\x1b[31m'; const YELLOW = '\x1b[33m'; const GREEN = '\x1b[32m'; const RESET = '\x1b[0m'; /** * Find all package.json files in the monorepo */ function findPackageJsonFiles(dir, files = []) { const entries = readdirSync(dir); for (const entry of entries) { const fullPath = join(dir, entry); const stat = statSync(fullPath); // Skip node_modules and hidden directories if (entry === 'node_modules' || entry.startsWith('.')) { continue; } if (stat.isDirectory()) { findPackageJsonFiles(fullPath, files); } else if (entry === 'package.json') { files.push(fullPath); } } return files; } /** * Check if a package.json has turbo run commands (infinite loop risk) */ function checkTurboRecursion(packagePath, packageJson) { const relativePath = relative(MONOREPO_ROOT, packagePath); // Skip root package.json if (relativePath === 'package.json') { return; } // Check if package is in apps/, games/, packages/, or services/ const isChildPackage = relativePath.startsWith('apps/') || relativePath.startsWith('games/') || relativePath.startsWith('packages/') || relativePath.startsWith('services/'); if (!isChildPackage) { return; } const scripts = packageJson.scripts || {}; for (const [scriptName, scriptCommand] of Object.entries(scripts)) { if (typeof scriptCommand === 'string' && scriptCommand.includes('turbo run')) { // Exception: "dev" script with turbo run is sometimes OK if it filters correctly if (scriptName === 'dev' && scriptCommand.includes('--filter')) { warnings.push( `⚠️ ${relativePath}: "dev" script uses "turbo run" with --filter. Make sure it doesn't create infinite loops.` ); } else { errors.push( `❌ ${relativePath}: "${scriptName}" script contains "turbo run" which can cause infinite loops. Remove it and let root turbo.json handle orchestration.` ); } } } } /** * Check if all internal packages are marked private */ function checkPrivateFlag(packagePath, packageJson) { const relativePath = relative(MONOREPO_ROOT, packagePath); // Skip root if (relativePath === 'package.json') { return; } // Only check packages in packages/ and services/ const isInternalPackage = relativePath.startsWith('packages/') || relativePath.startsWith('services/'); if (!isInternalPackage) { return; } if (packageJson.private !== true) { errors.push( `❌ ${relativePath}: Missing "private": true. All internal packages should be private to prevent accidental npm publishing.` ); } } /** * Check if all internal dependencies use workspace protocol */ function checkWorkspaceProtocol(packagePath, packageJson) { const relativePath = relative(MONOREPO_ROOT, packagePath); // Skip root if (relativePath === 'package.json') { return; } const deps = { ...packageJson.dependencies, ...packageJson.devDependencies }; const internalScopes = [ '@manacore/', '@mana-core/', '@chat/', '@picture/', '@calendar/', '@contacts/', '@todo/', '@manadeck/', '@zitare/', '@voxel-lava/', '@mana-games/', '@figgos/', '@worldream/', '@context/', ]; for (const [depName, depVersion] of Object.entries(deps)) { // Check if dependency is internal const isInternal = internalScopes.some((scope) => depName.startsWith(scope)); if (isInternal && !depVersion.startsWith('workspace:')) { errors.push( `❌ ${relativePath}: Dependency "${depName}" should use "workspace:*" instead of "${depVersion}"` ); } } } /** * Check for prepublishOnly scripts in private packages */ function checkPrepublishOnlyScripts(packagePath, packageJson) { const relativePath = relative(MONOREPO_ROOT, packagePath); // Skip root if (relativePath === 'package.json') { return; } // Only check private packages if (packageJson.private !== true) { return; } const scripts = packageJson.scripts || {}; if (scripts.prepublishOnly) { warnings.push( `⚠️ ${relativePath}: Has "prepublishOnly" script but is marked private. This script won't execute. Consider removing it.` ); } } /** * Main validation function */ function validateMonorepo() { console.log('🔍 Validating Monorepo Best Practices...\n'); const packageJsonFiles = findPackageJsonFiles(MONOREPO_ROOT); for (const packagePath of packageJsonFiles) { try { const content = readFileSync(packagePath, 'utf-8'); const packageJson = JSON.parse(content); checkTurboRecursion(packagePath, packageJson); checkPrivateFlag(packagePath, packageJson); checkWorkspaceProtocol(packagePath, packageJson); checkPrepublishOnlyScripts(packagePath, packageJson); } catch (error) { errors.push(`❌ Failed to parse ${relative(MONOREPO_ROOT, packagePath)}: ${error.message}`); } } // Print results console.log(`\n📊 Validation Results:`); console.log(` Checked ${packageJsonFiles.length} package.json files\n`); if (errors.length === 0 && warnings.length === 0) { console.log(`${GREEN}✅ All checks passed! Monorepo follows best practices.${RESET}\n`); process.exit(0); } if (errors.length > 0) { console.log(`${RED}❌ Found ${errors.length} error(s):${RESET}\n`); errors.forEach((error) => console.log(` ${error}`)); console.log(''); } if (warnings.length > 0) { console.log(`${YELLOW}⚠️ Found ${warnings.length} warning(s):${RESET}\n`); warnings.forEach((warning) => console.log(` ${warning}`)); console.log(''); } if (errors.length > 0) { console.log(`${RED}❌ Validation failed. Please fix the errors above.${RESET}\n`); process.exit(1); } else { console.log(`${GREEN}✅ Validation passed with warnings. Consider fixing them.${RESET}\n`); process.exit(0); } } // Run validation validateMonorepo();