mirror of
https://github.com/hcengineering/platform.git
synced 2026-08-17 18:05:42 +02:00
465 lines
14 KiB
JavaScript
Executable File
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
|
|
}
|