🔧 chore: enforce monorepo best practices with automated validation

Fix critical issues and add validation to prevent future violations:

**Fixes:**
- Remove turbo recursion in 5 app packages (infinite loop risk)
- Add "private": true to 11 packages (prevent accidental publishing)
- Rename @mana-core/nestjs-integration → @manacore/nestjs-integration
- Remove prepublishOnly scripts from 3 private packages

**New:**
- Add scripts/validate-monorepo.mjs with 4 critical checks
- Add validate:monorepo command to package.json
- Integrate validation into CI pipeline (.github/workflows/ci.yml)
- Document validation in CLAUDE.md

All 80 package.json files now pass validation 

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Wuesteon 2025-12-25 17:57:00 +01:00
parent 079b55a796
commit 5b7d3c649b
29 changed files with 304 additions and 25 deletions

241
scripts/validate-monorepo.mjs Executable file
View file

@ -0,0 +1,241 @@
#!/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();