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
JavaScript
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;