UNPKG

alepm

Version:

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

597 lines (492 loc) 16.9 kB
const path = require('path'); const fs = require('fs-extra'); const crypto = require('crypto'); const semver = require('semver'); class LockManager { constructor() { this.lockFileName = 'alepm.lock'; this.lockVersion = '1.0.0'; } async init() { const lockPath = this.getLockFilePath(); if (!fs.existsSync(lockPath)) { const lockData = { lockfileVersion: this.lockVersion, name: this.getProjectName(), version: this.getProjectVersion(), requires: true, packages: {}, dependencies: {}, integrity: {}, resolved: {}, metadata: { generated: new Date().toISOString(), generator: 'alepm', generatorVersion: require('../../package.json').version, nodejs: process.version, platform: process.platform, arch: process.arch } }; await this.saveLockFile(lockData); return lockData; } return await this.loadLockFile(); } async update(resolvedPackages, options = {}) { let lockData; try { // Try to load existing lock file lockData = await this.loadLockFile(); } catch (error) { // If lock file doesn't exist, create a new one if (error.message.includes('alepm.lock file not found')) { await this.init(); lockData = await this.loadLockFile(); } else { throw error; } } // Update timestamp lockData.metadata.lastModified = new Date().toISOString(); lockData.metadata.modifiedBy = options.user || 'alepm'; // Update packages for (const pkg of resolvedPackages) { const key = this.generatePackageKey(pkg.name, pkg.version); // Update packages section lockData.packages[key] = { version: pkg.version, resolved: pkg.resolved || pkg.tarball, integrity: pkg.integrity, requires: pkg.dependencies || {}, dev: options.saveDev || false, optional: pkg.optional || false, bundled: pkg.bundledDependencies || false, engines: pkg.engines || {}, os: pkg.os || [], cpu: pkg.cpu || [], deprecated: pkg.deprecated || false, license: pkg.license, funding: pkg.funding, homepage: pkg.homepage, repository: pkg.repository, bugs: pkg.bugs, keywords: pkg.keywords || [], maintainers: pkg.maintainers || [], contributors: pkg.contributors || [], time: { created: pkg.time?.created, modified: pkg.time?.modified || new Date().toISOString() } }; // Update dependencies section (flattened view) lockData.dependencies[pkg.name] = { version: pkg.version, from: `${pkg.name}@${pkg.requestedVersion || pkg.version}`, resolved: pkg.resolved || pkg.tarball, integrity: pkg.integrity, dev: options.saveDev || false, optional: pkg.optional || false }; // Store integrity information lockData.integrity[key] = { algorithm: 'sha512', hash: pkg.integrity, size: pkg.size || 0, fileCount: pkg.fileCount || 0, unpackedSize: pkg.unpackedSize || 0 }; // Store resolved information lockData.resolved[pkg.name] = { version: pkg.version, tarball: pkg.resolved || pkg.tarball, shasum: pkg.shasum, integrity: pkg.integrity, registry: pkg.registry || 'https://registry.npmjs.org', lastResolved: new Date().toISOString() }; } // Update dependency tree await this.updateDependencyTree(lockData, resolvedPackages); // Generate lock file hash for integrity lockData.metadata.hash = this.generateLockHash(lockData); await this.saveLockFile(lockData); return lockData; } async remove(packageNames, _options = {}) { const lockData = await this.loadLockFile(); for (const packageName of packageNames) { // Remove from dependencies delete lockData.dependencies[packageName]; // Remove from packages (all versions) const keysToRemove = Object.keys(lockData.packages) .filter(key => key.startsWith(`${packageName}@`)); for (const key of keysToRemove) { delete lockData.packages[key]; delete lockData.integrity[key]; } // Remove from resolved delete lockData.resolved[packageName]; } // Update metadata lockData.metadata.lastModified = new Date().toISOString(); lockData.metadata.hash = this.generateLockHash(lockData); await this.saveLockFile(lockData); return lockData; } async verify() { try { const lockData = await this.loadLockFile(); // Check lock file version compatibility if (!semver.satisfies(lockData.lockfileVersion, '^1.0.0')) { return { valid: false, errors: ['Incompatible lock file version'] }; } const errors = []; const warnings = []; // Verify integrity hash const currentHash = this.generateLockHash(lockData); if (lockData.metadata.hash && lockData.metadata.hash !== currentHash) { errors.push('Lock file integrity hash mismatch'); } // Verify package integrity for (const [key, pkg] of Object.entries(lockData.packages)) { if (!pkg.integrity) { warnings.push(`Package ${key} missing integrity information`); continue; } // Check if package exists in dependencies const [name] = key.split('@'); if (!lockData.dependencies[name]) { warnings.push(`Package ${name} in packages but not in dependencies`); } } // Verify dependency tree consistency for (const [name, dep] of Object.entries(lockData.dependencies)) { const key = this.generatePackageKey(name, dep.version); if (!lockData.packages[key]) { errors.push(`Dependency ${name}@${dep.version} missing from packages`); } } // Check for circular dependencies const circularDeps = this.detectCircularDependencies(lockData); if (circularDeps.length > 0) { warnings.push(`Circular dependencies detected: ${circularDeps.join(', ')}`); } return { valid: errors.length === 0, errors, warnings, stats: { packages: Object.keys(lockData.packages).length, dependencies: Object.keys(lockData.dependencies).length, size: JSON.stringify(lockData).length } }; } catch (error) { return { valid: false, errors: [`Lock file verification failed: ${error.message}`] }; } } async loadLockFile() { const lockPath = this.getLockFilePath(); if (!fs.existsSync(lockPath)) { throw new Error('alepm.lock file not found'); } try { const data = await fs.readJson(lockPath); // Migrate old lock file versions if needed return await this.migrateLockFile(data); } catch (error) { throw new Error(`Failed to parse alepm.lock: ${error.message}`); } } async saveLockFile(lockData) { const lockPath = this.getLockFilePath(); // Sort keys for consistent output const sortedLockData = this.sortLockData(lockData); // Save with proper formatting await fs.writeJson(lockPath, sortedLockData, { spaces: 2, EOL: '\n' }); } getLockFilePath() { const projectRoot = this.findProjectRoot(); return path.join(projectRoot, this.lockFileName); } findProjectRoot() { let current = process.cwd(); while (current !== path.dirname(current)) { if (fs.existsSync(path.join(current, 'package.json'))) { return current; } current = path.dirname(current); } return process.cwd(); } getProjectName() { try { const packageJsonPath = path.join(this.findProjectRoot(), 'package.json'); if (fs.existsSync(packageJsonPath)) { const packageJson = fs.readJsonSync(packageJsonPath); return packageJson.name; } } catch (error) { // Ignore errors } return path.basename(this.findProjectRoot()); } getProjectVersion() { try { const packageJsonPath = path.join(this.findProjectRoot(), 'package.json'); if (fs.existsSync(packageJsonPath)) { const packageJson = fs.readJsonSync(packageJsonPath); return packageJson.version; } } catch (error) { // Ignore errors } return '1.0.0'; } generatePackageKey(name, version) { return `${name}@${version}`; } generateLockHash(lockData) { // Create a stable hash by excluding metadata.hash field const dataForHash = { ...lockData }; if (dataForHash.metadata) { const metadata = { ...dataForHash.metadata }; delete metadata.hash; dataForHash.metadata = metadata; } const sortedData = this.sortLockData(dataForHash); const dataString = JSON.stringify(sortedData); return crypto.createHash('sha256') .update(dataString) .digest('hex'); } sortLockData(lockData) { const sorted = {}; // Sort top-level keys const topLevelKeys = Object.keys(lockData).sort(); for (const key of topLevelKeys) { if (typeof lockData[key] === 'object' && lockData[key] !== null && !Array.isArray(lockData[key])) { // Sort object keys const sortedObj = {}; const objKeys = Object.keys(lockData[key]).sort(); for (const objKey of objKeys) { sortedObj[objKey] = lockData[key][objKey]; } sorted[key] = sortedObj; } else { sorted[key] = lockData[key]; } } return sorted; } async updateDependencyTree(lockData, resolvedPackages) { // Build dependency tree structure if (!lockData.dependencyTree) { lockData.dependencyTree = {}; } for (const pkg of resolvedPackages) { lockData.dependencyTree[pkg.name] = { version: pkg.version, dependencies: this.buildPackageDependencyTree(pkg, resolvedPackages), depth: this.calculatePackageDepth(pkg, resolvedPackages), path: this.getPackagePath(pkg, resolvedPackages) }; } } buildPackageDependencyTree(pkg, allPackages) { const tree = {}; if (pkg.dependencies) { for (const [depName, depVersion] of Object.entries(pkg.dependencies)) { const resolvedDep = allPackages.find(p => p.name === depName && semver.satisfies(p.version, depVersion) ); if (resolvedDep) { tree[depName] = { version: resolvedDep.version, resolved: resolvedDep.resolved, integrity: resolvedDep.integrity }; } } } return tree; } calculatePackageDepth(pkg, allPackages, visited = new Set(), depth = 0) { if (visited.has(pkg.name)) { return depth; // Circular dependency } visited.add(pkg.name); let maxDepth = depth; if (pkg.dependencies) { for (const depName of Object.keys(pkg.dependencies)) { const dep = allPackages.find(p => p.name === depName); if (dep) { const depDepth = this.calculatePackageDepth(dep, allPackages, new Set(visited), depth + 1); maxDepth = Math.max(maxDepth, depDepth); } } } return maxDepth; } getPackagePath(pkg, allPackages, path = []) { if (path.includes(pkg.name)) { return path; // Circular dependency } return [...path, pkg.name]; } detectCircularDependencies(lockData) { const circular = []; const visiting = new Set(); const visited = new Set(); const visit = (packageName, path = []) => { if (visiting.has(packageName)) { // Found circular dependency const cycleStart = path.indexOf(packageName); const cycle = path.slice(cycleStart).concat(packageName); circular.push(cycle.join(' -> ')); return; } if (visited.has(packageName)) { return; } visiting.add(packageName); const pkg = lockData.dependencies[packageName]; if (pkg && lockData.packages[this.generatePackageKey(packageName, pkg.version)]) { const packageData = lockData.packages[this.generatePackageKey(packageName, pkg.version)]; if (packageData.requires) { for (const depName of Object.keys(packageData.requires)) { visit(depName, [...path, packageName]); } } } visiting.delete(packageName); visited.add(packageName); }; for (const packageName of Object.keys(lockData.dependencies)) { if (!visited.has(packageName)) { visit(packageName); } } return circular; } async migrateLockFile(lockData) { // Handle migration from older lock file versions if (!lockData.lockfileVersion || semver.lt(lockData.lockfileVersion, this.lockVersion)) { // Perform migration lockData.lockfileVersion = this.lockVersion; // Add missing metadata if (!lockData.metadata) { lockData.metadata = { generated: new Date().toISOString(), generator: 'alepm', generatorVersion: require('../../package.json').version, migrated: true, originalVersion: lockData.lockfileVersion }; } // Ensure all required sections exist lockData.packages = lockData.packages || {}; lockData.dependencies = lockData.dependencies || {}; lockData.integrity = lockData.integrity || {}; lockData.resolved = lockData.resolved || {}; // Save migrated version await this.saveLockFile(lockData); } return lockData; } async getDependencyGraph() { const lockData = await this.loadLockFile(); const graph = { nodes: [], edges: [], stats: { totalPackages: 0, totalDependencies: 0, maxDepth: 0, circularDependencies: [] } }; // Build nodes for (const [name, dep] of Object.entries(lockData.dependencies)) { graph.nodes.push({ id: name, name, version: dep.version, dev: dep.dev || false, optional: dep.optional || false }); } // Build edges for (const [key, pkg] of Object.entries(lockData.packages)) { const [name] = key.split('@'); if (pkg.requires) { for (const depName of Object.keys(pkg.requires)) { graph.edges.push({ from: name, to: depName, version: pkg.requires[depName] }); } } } // Calculate stats graph.stats.totalPackages = graph.nodes.length; graph.stats.totalDependencies = graph.edges.length; graph.stats.circularDependencies = this.detectCircularDependencies(lockData); // Calculate max depth for (const node of graph.nodes) { const depth = this.calculateNodeDepth(node.name, graph.edges); graph.stats.maxDepth = Math.max(graph.stats.maxDepth, depth); } return graph; } calculateNodeDepth(nodeName, edges, visited = new Set()) { if (visited.has(nodeName)) { return 0; // Circular dependency } visited.add(nodeName); const dependencies = edges.filter(edge => edge.from === nodeName); if (dependencies.length === 0) { return 0; } let maxDepth = 0; for (const dep of dependencies) { const depth = 1 + this.calculateNodeDepth(dep.to, edges, new Set(visited)); maxDepth = Math.max(maxDepth, depth); } return maxDepth; } async exportLockFile(format = 'json') { const lockData = await this.loadLockFile(); switch (format.toLowerCase()) { case 'json': return JSON.stringify(lockData, null, 2); case 'yaml': // Would need yaml library throw new Error('YAML export not implemented'); case 'csv': return this.exportToCsv(lockData); default: throw new Error(`Unsupported export format: ${format}`); } } exportToCsv(lockData) { const lines = ['Name,Version,Type,Integrity,Resolved']; for (const [name, dep] of Object.entries(lockData.dependencies)) { const type = dep.dev ? 'dev' : dep.optional ? 'optional' : 'prod'; lines.push(`${name},${dep.version},${type},${dep.integrity || ''},${dep.resolved || ''}`); } return lines.join('\n'); } } module.exports = LockManager;