#!/usr/bin/env node /** * This script validates that all @hcengineering packages use the same versions * across all dependencies (including devDependencies). * * This is similar to `rush check` but also checks for transitive dependencies * and only validates @hcengineering packages. * * The script will fail with exit code 1 if any version mismatches are found. */ const fs = require('fs') const path = require('path') const execSync = require('child_process').execSync const SCOPE = '@hcengineering' /** * Find the repository root by looking for rush.json * @returns {string} Path to repository root */ function findRepoRoot() { let currentDir = __dirname while (currentDir !== '/') { const rushJsonPath = path.join(currentDir, 'rush.json') if (fs.existsSync(rushJsonPath)) { return currentDir } currentDir = path.dirname(currentDir) } throw new Error('Could not find repository root (rush.json not found)') } /** * Parse pnpm-lock.yaml to extract resolved @hcengineering package versions * and track which packages depend on which versions * @returns {Object} Map of package names to version -> dependents mapping */ function parseLockfile() { const repoRoot = findRepoRoot() const lockfilePath = path.join(repoRoot, 'common/config/rush/pnpm-lock.yaml') if (!fs.existsSync(lockfilePath)) { console.warn('⚠️ pnpm-lock.yaml not found, skipping lockfile validation') return {} } const lockfileContent = fs.readFileSync(lockfilePath, 'utf-8') const lines = lockfileContent.split('\n') const lockfileVersions = {} // Track current package in the packages section let currentPackage = null let currentPackageName = null let currentPackageVersion = null let inPackagesSection = false let inDependenciesSection = false for (let i = 0; i < lines.length; i++) { const line = lines[i] // Check if we're in the packages section if (line.match(/^packages:/)) { inPackagesSection = true continue } // Parse @hcengineering package entries like ' @hcengineering/platform@0.7.3:' or '@hcengineering/analytics@0.7.4:' if (inPackagesSection && line.match(new RegExp(`^ '?(${SCOPE}/[^@']+)@([^':()]+)(?:\\([^)]*\\))*'?:`))) { const packageMatch = line.match(new RegExp(`^ '?(${SCOPE}/[^@']+)@([^':()]+)(?:\\([^)]*\\))*'?:`)) if (packageMatch) { currentPackageName = packageMatch[1] currentPackageVersion = packageMatch[2] currentPackage = `${currentPackageName}@${currentPackageVersion}` inDependenciesSection = false // Track this version exists if (!lockfileVersions[currentPackageName]) { lockfileVersions[currentPackageName] = {} } if (!lockfileVersions[currentPackageName][currentPackageVersion]) { lockfileVersions[currentPackageName][currentPackageVersion] = new Set() } } continue } // Check if we're entering dependencies section if (currentPackage && line.trim() === 'dependencies:') { inDependenciesSection = true continue } // Exit dependencies section if (inDependenciesSection && line.match(/^ [a-zA-Z]/)) { const nextSectionMatch = line.match(/^ ([a-zA-Z]+):/) if (nextSectionMatch && nextSectionMatch[1] !== 'dependencies' && nextSectionMatch[1] !== 'devDependencies') { inDependenciesSection = false } } // Exit current package when we hit another package definition at the same level if (currentPackage && line.match(/^ ['"]?[@a-zA-Z]/)) { currentPackage = null currentPackageName = null currentPackageVersion = null inDependenciesSection = false } // Parse dependency lines like " '@hcengineering/platform': 0.7.3" // This tells us that currentPackageName@currentPackageVersion depends on @hcengineering/platform@0.7.3 if (inDependenciesSection && currentPackageName && currentPackageVersion) { const depMatch = line.match(/^\s{6}'?(@hcengineering\/[^']+)'?:\s+([0-9.]+)/) if (depMatch) { const depPackageName = depMatch[1] const depVersion = depMatch[2] // Record that depPackageName@depVersion is required by currentPackageName@currentPackageVersion if (!lockfileVersions[depPackageName]) { lockfileVersions[depPackageName] = {} } if (!lockfileVersions[depPackageName][depVersion]) { lockfileVersions[depPackageName][depVersion] = new Set() } lockfileVersions[depPackageName][depVersion].add(currentPackage) } } } return lockfileVersions } /** * Get all projects from rush * @returns {Array} List of projects with name, version, and path */ function getProjects() { console.log('📦 Loading Rush projects...') try { const repoRoot = findRepoRoot() const output = execSync('node common/scripts/install-run-rush.js list -p --json', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], cwd: repoRoot }) // Parse the JSON output (skip any warnings/logs before the JSON) const lines = output.split('\n') let jsonStart = -1 for (let i = 0; i < lines.length; i++) { if (lines[i].trim().startsWith('{')) { jsonStart = i break } } if (jsonStart === -1) { throw new Error('Could not find JSON output from rush list') } const jsonOutput = lines.slice(jsonStart).join('\n') const config = JSON.parse(jsonOutput) return config.projects } catch (error) { console.error('❌ Error loading Rush projects:', error.message) process.exit(1) } } /** * Read package.json for a project * @param {string} projectPath - Path to the project * @returns {Object} Parsed package.json */ function readPackageJson(projectPath) { const repoRoot = findRepoRoot() const fullPath = path.join(repoRoot, projectPath) const packageJsonPath = path.join(fullPath, 'package.json') try { const content = fs.readFileSync(packageJsonPath, 'utf-8') return JSON.parse(content) } catch (error) { console.warn(`⚠️ Could not read ${packageJsonPath}: ${error.message}`) return null } } /** * Extract @hcengineering dependencies from package.json * @param {Object} packageJson - Parsed package.json * @param {string} packageName - Name of the package * @returns {Object} Map of dependency name to version */ function extractHceDependencies(packageJson, packageName) { const dependencies = {} // Check both dependencies and devDependencies const depTypes = ['dependencies', 'devDependencies'] for (const depType of depTypes) { const deps = packageJson[depType] if (!deps) continue for (const [depName, version] of Object.entries(deps)) { if (depName.startsWith(SCOPE)) { dependencies[depName] = { version: version, type: depType, usedBy: packageName } } } } return dependencies } /** * Build a map of all dependency versions across all packages * @param {Array} projects - List of projects * @returns {Object} Map of dependency name to list of versions and users */ function buildDependencyMap(projects) { console.log('🔍 Scanning all packages for @hcengineering dependencies...\n') const dependencyMap = {} let totalPackages = 0 let totalDependencies = 0 for (const project of projects) { const packageJson = readPackageJson(project.path) if (!packageJson) continue totalPackages++ const dependencies = extractHceDependencies(packageJson, project.name) for (const [depName, info] of Object.entries(dependencies)) { if (!dependencyMap[depName]) { dependencyMap[depName] = {} } if (!dependencyMap[depName][info.version]) { dependencyMap[depName][info.version] = [] } dependencyMap[depName][info.version].push({ package: project.name, type: info.type }) totalDependencies++ } } console.log(`✅ Scanned ${totalPackages} packages`) console.log(`✅ Found ${totalDependencies} @hcengineering dependencies\n`) return dependencyMap } /** * Find version mismatches in the dependency map * @param {Object} dependencyMap - Map of dependencies to versions * @returns {Object} Object with mismatches array and error flag */ function findMismatches(dependencyMap) { const mismatches = [] for (const [depName, versions] of Object.entries(dependencyMap)) { const versionList = Object.keys(versions) if (versionList.length > 1) { mismatches.push({ dependency: depName, versions: versions }) } } return mismatches } /** * Format and display mismatches * @param {Array} mismatches - List of mismatched dependencies */ function displayMismatches(mismatches) { console.log('━'.repeat(80)) console.log('❌ VERSION MISMATCHES FOUND') console.log('━'.repeat(80)) console.log() for (const mismatch of mismatches) { console.log(`📦 ${mismatch.dependency}`) console.log() const versionList = Object.keys(mismatch.versions).sort() for (const version of versionList) { const users = mismatch.versions[version] console.log(` Version: ${version}`) console.log(` Used by ${users.length} package(s):`) // Sort users for consistent output users.sort((a, b) => a.package.localeCompare(b.package)) // Show first 10 users, then summarize if more const displayUsers = users.slice(0, 10) for (const user of displayUsers) { console.log(` - ${user.package} (${user.type})`) } if (users.length > 10) { console.log(` ... and ${users.length - 10} more`) } console.log() } console.log('─'.repeat(80)) console.log() } } /** * Find mismatches in the lockfile * @param {Object} lockfileVersions - Map of package names to version -> dependents mapping * @returns {Array} Array of packages with multiple versions in lockfile */ function findLockfileMismatches(lockfileVersions) { const mismatches = [] for (const [packageName, versionMap] of Object.entries(lockfileVersions)) { const versions = Object.keys(versionMap) if (versions.length > 1) { mismatches.push({ package: packageName, versionMap: versionMap }) } } return mismatches } /** * Display lockfile mismatches * @param {Array} mismatches - Array of lockfile mismatches */ function displayLockfileMismatches(mismatches) { console.log('━'.repeat(80)) console.log('❌ LOCKFILE VERSION MISMATCHES FOUND') console.log('━'.repeat(80)) console.log() console.log('The following @hcengineering packages have multiple resolved versions') console.log('in pnpm-lock.yaml (transitive dependencies):') console.log() for (const mismatch of mismatches) { console.log(`📦 ${mismatch.package}`) console.log() const versions = Object.keys(mismatch.versionMap).sort() for (const version of versions) { const dependents = Array.from(mismatch.versionMap[version]) console.log(` Version ${version}:`) console.log(` Required by ${dependents.length} package(s):`) // Sort dependents for consistent output dependents.sort() // Show first 10 dependents, then summarize if more const displayDependents = dependents.slice(0, 10) for (const dependent of displayDependents) { // Clean up the path for better readability const cleanPath = dependent.replace(/^\.\.\/\.\.\//, '') console.log(` - ${cleanPath}`) } if (dependents.length > 10) { console.log(` ... and ${dependents.length - 10} more`) } console.log() } console.log('─'.repeat(80)) console.log() } } /** * Main function */ function main() { console.log('🚀 Checking @hcengineering dependency versions...\n') // Get all projects from rush const projects = getProjects() // Build dependency map from package.json files const dependencyMap = buildDependencyMap(projects) // Find mismatches in package.json files const packageJsonMismatches = findMismatches(dependencyMap) // Parse lockfile and find mismatches there console.log('🔒 Checking pnpm-lock.yaml for resolved version mismatches...\n') const lockfileVersions = parseLockfile() const lockfileMismatches = findLockfileMismatches(lockfileVersions) console.log(`✅ Found ${Object.keys(lockfileVersions).length} unique @hcengineering packages in lockfile`) console.log(`${lockfileMismatches.length > 0 ? '❌' : '✅'} Found ${lockfileMismatches.length} packages with multiple resolved versions\n`) const hasErrors = packageJsonMismatches.length > 0 || lockfileMismatches.length > 0 if (!hasErrors) { console.log('━'.repeat(80)) console.log('✅ SUCCESS - All @hcengineering dependencies use consistent versions!') console.log('━'.repeat(80)) console.log() console.log('✓ All package.json dependencies are consistent') console.log('✓ All resolved versions in pnpm-lock.yaml are consistent') console.log() process.exit(0) } else { if (packageJsonMismatches.length > 0) { displayMismatches(packageJsonMismatches) console.log(`❌ PACKAGE.JSON ISSUES: Found ${packageJsonMismatches.length} dependencies with mismatched versions`) console.log() } if (lockfileMismatches.length > 0) { displayLockfileMismatches(lockfileMismatches) console.log(`❌ LOCKFILE ISSUES: Found ${lockfileMismatches.length} packages with multiple resolved versions`) console.log() } console.log('To fix these issues:') console.log(' 1. Update package.json files to use consistent versions') console.log(' 2. Run: rush update') console.log(' 3. Run: rush rebuild') console.log(' 4. Run this script again to verify') console.log() process.exit(1) } } // Run the script if (require.main === module) { main() } module.exports = { getProjects, buildDependencyMap, findMismatches, parseLockfile, findLockfileMismatches }