Files

465 lines
14 KiB
JavaScript
Executable File

#!/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
}