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