UNPKG

alepm

Version:

Advanced and secure Node.js package manager with binary storage, intelligent caching, and comprehensive security features

525 lines (434 loc) 15.4 kB
const semver = require('semver'); const chalk = require('chalk'); class DependencyResolver { constructor() { this.registry = null; this.lockManager = null; this.resolved = new Map(); this.resolving = new Set(); this.conflicts = new Map(); } setRegistry(registry) { this.registry = registry; } setLockManager(lockManager) { this.lockManager = lockManager; } async resolve(packageSpecs, options = {}) { this.resolved.clear(); this.resolving.clear(); this.conflicts.clear(); const resolved = []; // Load existing lock file if available let lockData = null; if (this.lockManager) { try { lockData = await this.lockManager.loadLockFile(); } catch (error) { // No lock file exists, continue without it } } // Resolve each package spec for (const spec of packageSpecs) { const packageResolution = await this.resolvePackage(spec, { ...options, lockData, depth: 0 }); resolved.push(...packageResolution); } // Check for conflicts and resolve them const conflictResolution = await this.resolveConflicts(); resolved.push(...conflictResolution); // Remove duplicates and return flattened result return this.deduplicateResolved(resolved); } async resolvePackage(spec, options = {}) { const { name, version } = spec; const key = `${name}@${version}`; // Check if registry is configured if (!this.registry) { throw new Error('Registry not configured. Please set up the dependency resolver properly.'); } // Check if already resolved if (this.resolved.has(key)) { return [this.resolved.get(key)]; } // Check for circular dependencies if (this.resolving.has(key)) { console.warn(chalk.yellow(`Warning: Circular dependency detected for ${key}`)); return []; } this.resolving.add(key); try { // Try to resolve from lock file first if (options.lockData && options.lockData.packages[key]) { const lockedPackage = options.lockData.packages[key]; const resolvedPackage = { name, version: lockedPackage.version, resolved: lockedPackage.resolved, integrity: lockedPackage.integrity, dependencies: lockedPackage.requires || {}, devDependencies: {}, optional: lockedPackage.optional || false, dev: lockedPackage.dev || false, source: 'lockfile', depth: options.depth || 0 }; this.resolved.set(key, resolvedPackage); // Resolve dependencies recursively const dependencies = await this.resolveDependencies( resolvedPackage.dependencies, { ...options, depth: (options.depth || 0) + 1 } ); this.resolving.delete(key); return [resolvedPackage, ...dependencies]; } // Resolve version if needed const resolvedVersion = await this.registry.resolveVersion(name, version); const resolvedKey = `${name}@${resolvedVersion}`; // Check if we already resolved this exact version if (this.resolved.has(resolvedKey)) { this.resolving.delete(key); return [this.resolved.get(resolvedKey)]; } // Get package metadata const metadata = await this.registry.getMetadata(name, resolvedVersion); // Create resolved package object const resolvedPackage = { name: metadata.name, version: metadata.version, resolved: metadata.dist.tarball, integrity: metadata.dist.integrity, dependencies: metadata.dependencies || {}, devDependencies: metadata.devDependencies || {}, peerDependencies: metadata.peerDependencies || {}, optionalDependencies: metadata.optionalDependencies || {}, bundledDependencies: metadata.bundledDependencies || [], engines: metadata.engines || {}, os: metadata.os || [], cpu: metadata.cpu || [], deprecated: metadata.deprecated, license: metadata.license, homepage: metadata.homepage, repository: metadata.repository, bugs: metadata.bugs, keywords: metadata.keywords || [], maintainers: metadata.maintainers || [], time: metadata.time, bin: metadata.bin || {}, scripts: metadata.scripts || {}, optional: false, dev: options.dev || false, source: 'registry', depth: options.depth || 0, requestedVersion: version, shasum: metadata.dist.shasum, size: metadata.dist.unpackedSize, fileCount: metadata.dist.fileCount }; this.resolved.set(resolvedKey, resolvedPackage); // Resolve dependencies recursively const allDependencies = { ...resolvedPackage.dependencies, ...(options.includeDevDependencies ? resolvedPackage.devDependencies : {}), ...(options.includeOptionalDependencies ? resolvedPackage.optionalDependencies : {}) }; const dependencies = await this.resolveDependencies( allDependencies, { ...options, depth: (options.depth || 0) + 1 } ); this.resolving.delete(key); return [resolvedPackage, ...dependencies]; } catch (error) { this.resolving.delete(key); throw new Error(`Failed to resolve ${key}: ${error.message}`); } } async resolveDependencies(dependencies, options = {}) { const resolved = []; for (const [name, versionSpec] of Object.entries(dependencies)) { try { const spec = { name, version: versionSpec }; const packageResolution = await this.resolvePackage(spec, options); resolved.push(...packageResolution); } catch (error) { if (options.optional) { console.warn(chalk.yellow(`Warning: Optional dependency ${name}@${versionSpec} could not be resolved: ${error.message}`)); } else { throw error; } } } return resolved; } async resolveConflicts() { const resolved = []; for (const [packageName, conflictVersions] of this.conflicts.entries()) { // Simple conflict resolution: choose the highest version that satisfies all requirements const versions = Array.from(conflictVersions); const chosenVersion = this.chooseVersion(versions); if (chosenVersion) { console.warn(chalk.yellow(`Resolved conflict for ${packageName}: using version ${chosenVersion}`)); const spec = { name: packageName, version: chosenVersion }; const packageResolution = await this.resolvePackage(spec, { source: 'conflict-resolution' }); resolved.push(...packageResolution); } else { throw new Error(`Cannot resolve version conflict for ${packageName}: ${versions.join(', ')}`); } } return resolved; } chooseVersion(versionSpecs) { // Find a version that satisfies all specs // Get all possible versions from registry for this package // For now, use a simplified approach const sortedSpecs = versionSpecs.sort(semver.rcompare); // Try to find a version that satisfies all requirements for (const spec of sortedSpecs) { let satisfiesAll = true; for (const otherSpec of versionSpecs) { if (!semver.satisfies(spec, otherSpec)) { satisfiesAll = false; break; } } if (satisfiesAll) { return spec; } } // If no single version satisfies all, return the highest return sortedSpecs[0]; } deduplicateResolved(resolved) { const deduplicated = new Map(); for (const pkg of resolved) { const key = `${pkg.name}@${pkg.version}`; if (!deduplicated.has(key)) { deduplicated.set(key, pkg); } else { // Merge information if needed const existing = deduplicated.get(key); deduplicated.set(key, { ...existing, ...pkg, // Keep the minimum depth depth: Math.min(existing.depth, pkg.depth) }); } } return Array.from(deduplicated.values()); } async buildDependencyTree(packages) { const tree = new Map(); for (const pkg of packages) { tree.set(pkg.name, { package: pkg, dependencies: new Map(), dependents: new Set(), depth: pkg.depth }); } // Build relationships for (const pkg of packages) { const node = tree.get(pkg.name); for (const depName of Object.keys(pkg.dependencies || {})) { const depNode = tree.get(depName); if (depNode) { node.dependencies.set(depName, depNode); depNode.dependents.add(pkg.name); } } } return tree; } async analyzeImpact(packageName, newVersion, currentPackages) { const impact = { directDependents: new Set(), indirectDependents: new Set(), breakingChanges: [], warnings: [] }; const tree = await this.buildDependencyTree(currentPackages); const targetNode = tree.get(packageName); if (!targetNode) { return impact; } // Find all dependents const visited = new Set(); const findDependents = (nodeName, isIndirect = false) => { if (visited.has(nodeName)) return; visited.add(nodeName); const node = tree.get(nodeName); if (!node) return; for (const dependent of node.dependents) { if (isIndirect) { impact.indirectDependents.add(dependent); } else { impact.directDependents.add(dependent); } findDependents(dependent, true); } }; findDependents(packageName); // Check for breaking changes const currentVersion = targetNode.package.version; if (semver.major(newVersion) > semver.major(currentVersion)) { impact.breakingChanges.push(`Major version change: ${currentVersion} -> ${newVersion}`); } return impact; } async validateResolution(resolved) { const validation = { valid: true, errors: [], warnings: [], stats: { totalPackages: resolved.length, duplicates: 0, conflicts: 0, circular: [] } }; // Check for duplicates const seen = new Map(); for (const pkg of resolved) { const key = pkg.name; if (seen.has(key)) { const existing = seen.get(key); if (existing.version !== pkg.version) { validation.stats.conflicts++; validation.warnings.push(`Version conflict for ${key}: ${existing.version} vs ${pkg.version}`); } else { validation.stats.duplicates++; } } else { seen.set(key, pkg); } } // Check for circular dependencies const circular = this.detectCircularDependencies(resolved); validation.stats.circular = circular; if (circular.length > 0) { validation.warnings.push(`Circular dependencies detected: ${circular.join(', ')}`); } // Check platform compatibility for (const pkg of resolved) { if (pkg.engines && pkg.engines.node) { if (!semver.satisfies(process.version, pkg.engines.node)) { validation.warnings.push(`${pkg.name}@${pkg.version} requires Node.js ${pkg.engines.node}, current: ${process.version}`); } } if (pkg.os && pkg.os.length > 0) { const currentOs = process.platform; const supportedOs = pkg.os.filter(os => !os.startsWith('!')); const blockedOs = pkg.os.filter(os => os.startsWith('!')).map(os => os.substring(1)); if (supportedOs.length > 0 && !supportedOs.includes(currentOs)) { validation.warnings.push(`${pkg.name}@${pkg.version} is not supported on ${currentOs}`); } if (blockedOs.includes(currentOs)) { validation.warnings.push(`${pkg.name}@${pkg.version} is blocked on ${currentOs}`); } } if (pkg.cpu && pkg.cpu.length > 0) { const currentCpu = process.arch; const supportedCpu = pkg.cpu.filter(cpu => !cpu.startsWith('!')); const blockedCpu = pkg.cpu.filter(cpu => cpu.startsWith('!')).map(cpu => cpu.substring(1)); if (supportedCpu.length > 0 && !supportedCpu.includes(currentCpu)) { validation.warnings.push(`${pkg.name}@${pkg.version} is not supported on ${currentCpu} architecture`); } if (blockedCpu.includes(currentCpu)) { validation.warnings.push(`${pkg.name}@${pkg.version} is blocked on ${currentCpu} architecture`); } } } return validation; } detectCircularDependencies(packages) { const graph = new Map(); const circular = []; // Build graph for (const pkg of packages) { graph.set(pkg.name, Object.keys(pkg.dependencies || {})); } // Detect cycles using DFS const visited = new Set(); const visiting = new Set(); const visit = (node, path = []) => { if (visiting.has(node)) { const cycleStart = path.indexOf(node); const cycle = path.slice(cycleStart); circular.push(cycle.join(' -> ') + ' -> ' + node); return; } if (visited.has(node)) { return; } visiting.add(node); const dependencies = graph.get(node) || []; for (const dep of dependencies) { if (graph.has(dep)) { visit(dep, [...path, node]); } } visiting.delete(node); visited.add(node); }; for (const node of graph.keys()) { if (!visited.has(node)) { visit(node); } } return circular; } async optimizeResolution(resolved) { // Implement resolution optimization strategies const optimized = [...resolved]; // Remove unnecessary duplicates const nameCounts = new Map(); for (const pkg of resolved) { nameCounts.set(pkg.name, (nameCounts.get(pkg.name) || 0) + 1); } // Hoist dependencies when possible const hoisted = new Map(); for (const pkg of optimized) { if (pkg.depth > 0 && !hoisted.has(pkg.name)) { // Check if this package can be hoisted const canHoist = this.canHoistPackage(pkg, optimized); if (canHoist) { pkg.depth = 0; pkg.hoisted = true; hoisted.set(pkg.name, pkg); } } } return optimized; } canHoistPackage(pkg, allPackages) { // Check if hoisting this package would cause conflicts const topLevelPackages = allPackages.filter(p => p.depth === 0 && p.name !== pkg.name); for (const topPkg of topLevelPackages) { if (topPkg.dependencies && topPkg.dependencies[pkg.name]) { const requiredVersion = topPkg.dependencies[pkg.name]; if (!semver.satisfies(pkg.version, requiredVersion)) { return false; } } } return true; } getResolutionStats() { return { resolved: this.resolved.size, resolving: this.resolving.size, conflicts: this.conflicts.size }; } clearCache() { this.resolved.clear(); this.resolving.clear(); this.conflicts.clear(); } } module.exports = DependencyResolver;