Files
alepm/src/core/registry.js
2025-08-19 02:22:37 +02:00

500 líneas
14 KiB
JavaScript

const fetch = require('node-fetch');
const semver = require('semver');
const path = require('path');
const fs = require('fs-extra');
class Registry {
constructor() {
this.defaultRegistry = 'https://registry.npmjs.org';
this.registries = new Map();
this.cache = new Map();
this.config = this.loadConfig();
// Default registries
this.registries.set('npm', 'https://registry.npmjs.org');
this.registries.set('yarn', 'https://registry.yarnpkg.com');
}
loadConfig() {
return {
registry: this.defaultRegistry,
timeout: 30000,
retries: 3,
userAgent: 'alepm/1.0.0 node/' + process.version,
auth: {},
scopes: {},
cache: true,
offline: false
};
}
async getPackageInfo(packageName, version = 'latest') {
if (this.config.offline) {
throw new Error('Cannot fetch package info in offline mode');
}
const cacheKey = `info:${packageName}@${version}`;
if (this.config.cache && this.cache.has(cacheKey)) {
const cached = this.cache.get(cacheKey);
if (Date.now() - cached.timestamp < 300000) { // 5 minutes
return cached.data;
}
}
const registry = this.getRegistryForPackage(packageName);
const url = version === 'latest'
? `${registry}/${encodeURIComponent(packageName)}`
: `${registry}/${encodeURIComponent(packageName)}/${encodeURIComponent(version)}`;
const response = await this.fetchWithRetry(url);
if (!response.ok) {
if (response.status === 404) {
throw new Error(`Package "${packageName}" not found`);
}
throw new Error(`Failed to fetch package info: ${response.status} ${response.statusText}`);
}
const data = await response.json();
// Cache the result
if (this.config.cache) {
this.cache.set(cacheKey, {
data,
timestamp: Date.now()
});
}
return data;
}
async getLatestVersion(packageName) {
const info = await this.getPackageInfo(packageName);
return info['dist-tags'].latest;
}
async getVersions(packageName) {
const info = await this.getPackageInfo(packageName);
return Object.keys(info.versions || {}).sort(semver.rcompare);
}
async resolveVersion(packageName, versionSpec) {
if (versionSpec === 'latest') {
return await this.getLatestVersion(packageName);
}
const versions = await this.getVersions(packageName);
// Handle exact version
if (versions.includes(versionSpec)) {
return versionSpec;
}
// Handle semver range
const resolved = semver.maxSatisfying(versions, versionSpec);
if (!resolved) {
throw new Error(`No version of "${packageName}" satisfies "${versionSpec}"`);
}
return resolved;
}
async download(pkg) {
if (this.config.offline) {
throw new Error('Cannot download packages in offline mode');
}
const registry = this.getRegistryForPackage(pkg.name);
const packageInfo = await this.getPackageInfo(pkg.name, pkg.version);
if (!packageInfo.versions || !packageInfo.versions[pkg.version]) {
throw new Error(`Version ${pkg.version} of package ${pkg.name} not found`);
}
const versionInfo = packageInfo.versions[pkg.version];
const tarballUrl = versionInfo.dist.tarball;
if (!tarballUrl) {
throw new Error(`No tarball URL found for ${pkg.name}@${pkg.version}`);
}
const response = await this.fetchWithRetry(tarballUrl);
if (!response.ok) {
throw new Error(`Failed to download ${pkg.name}@${pkg.version}: ${response.status}`);
}
const buffer = await response.buffer();
// Verify integrity if available
if (versionInfo.dist.integrity) {
await this.verifyIntegrity(buffer, versionInfo.dist.integrity);
}
return {
data: buffer,
integrity: versionInfo.dist.integrity,
shasum: versionInfo.dist.shasum,
size: buffer.length,
tarball: tarballUrl,
resolved: tarballUrl,
packageInfo: versionInfo
};
}
async search(query, options = {}) {
if (this.config.offline) {
throw new Error('Cannot search packages in offline mode');
}
const registry = this.config.registry;
const limit = options.limit || 20;
const offset = options.offset || 0;
const searchUrl = `${registry}/-/v1/search?text=${encodeURIComponent(query)}&size=${limit}&from=${offset}`;
const response = await this.fetchWithRetry(searchUrl);
if (!response.ok) {
throw new Error(`Search failed: ${response.status} ${response.statusText}`);
}
const data = await response.json();
return data.objects.map(obj => ({
name: obj.package.name,
version: obj.package.version,
description: obj.package.description,
keywords: obj.package.keywords,
author: obj.package.author,
publisher: obj.package.publisher,
maintainers: obj.package.maintainers,
repository: obj.package.links?.repository,
homepage: obj.package.links?.homepage,
npm: obj.package.links?.npm,
downloadScore: obj.score?.detail?.downloads || 0,
popularityScore: obj.score?.detail?.popularity || 0,
qualityScore: obj.score?.detail?.quality || 0,
maintenanceScore: obj.score?.detail?.maintenance || 0,
finalScore: obj.score?.final || 0
}));
}
async getMetadata(packageName, version) {
const info = await this.getPackageInfo(packageName, version);
if (version === 'latest') {
version = info['dist-tags'].latest;
}
const versionInfo = info.versions[version];
if (!versionInfo) {
throw new Error(`Version ${version} not found for ${packageName}`);
}
return {
name: versionInfo.name,
version: versionInfo.version,
description: versionInfo.description,
keywords: versionInfo.keywords || [],
homepage: versionInfo.homepage,
repository: versionInfo.repository,
bugs: versionInfo.bugs,
license: versionInfo.license,
author: versionInfo.author,
contributors: versionInfo.contributors || [],
maintainers: versionInfo.maintainers || [],
dependencies: versionInfo.dependencies || {},
devDependencies: versionInfo.devDependencies || {},
peerDependencies: versionInfo.peerDependencies || {},
optionalDependencies: versionInfo.optionalDependencies || {},
bundledDependencies: versionInfo.bundledDependencies || [],
engines: versionInfo.engines || {},
os: versionInfo.os || [],
cpu: versionInfo.cpu || [],
scripts: versionInfo.scripts || {},
bin: versionInfo.bin || {},
man: versionInfo.man || [],
directories: versionInfo.directories || {},
files: versionInfo.files || [],
main: versionInfo.main,
browser: versionInfo.browser,
module: versionInfo.module,
types: versionInfo.types,
typings: versionInfo.typings,
exports: versionInfo.exports,
imports: versionInfo.imports,
funding: versionInfo.funding,
dist: {
tarball: versionInfo.dist.tarball,
shasum: versionInfo.dist.shasum,
integrity: versionInfo.dist.integrity,
fileCount: versionInfo.dist.fileCount,
unpackedSize: versionInfo.dist.unpackedSize
},
time: {
created: info.time.created,
modified: info.time.modified,
version: info.time[version]
},
readme: versionInfo.readme,
readmeFilename: versionInfo.readmeFilename,
deprecated: versionInfo.deprecated
};
}
async getDependencies(packageName, version) {
const metadata = await this.getMetadata(packageName, version);
return {
dependencies: metadata.dependencies,
devDependencies: metadata.devDependencies,
peerDependencies: metadata.peerDependencies,
optionalDependencies: metadata.optionalDependencies,
bundledDependencies: metadata.bundledDependencies
};
}
async getDownloadStats(packageName, period = 'last-week') {
const registry = 'https://api.npmjs.org';
const url = `${registry}/downloads/point/${period}/${encodeURIComponent(packageName)}`;
try {
const response = await this.fetchWithRetry(url);
if (!response.ok) {
return { downloads: 0, period };
}
const data = await response.json();
return data;
} catch (error) {
return { downloads: 0, period };
}
}
async getUserPackages(username) {
const registry = this.config.registry;
const url = `${registry}/-/user/${encodeURIComponent(username)}/package`;
const response = await this.fetchWithRetry(url);
if (!response.ok) {
throw new Error(`Failed to get user packages: ${response.status}`);
}
const data = await response.json();
return Object.keys(data);
}
async addRegistry(name, url, options = {}) {
this.registries.set(name, url);
if (options.auth) {
this.config.auth[url] = options.auth;
}
if (options.scope) {
this.config.scopes[options.scope] = url;
}
}
async removeRegistry(name) {
const url = this.registries.get(name);
if (url) {
this.registries.delete(name);
delete this.config.auth[url];
// Remove scope mappings
for (const [scope, registryUrl] of Object.entries(this.config.scopes)) {
if (registryUrl === url) {
delete this.config.scopes[scope];
}
}
}
}
getRegistryForPackage(packageName) {
// Check for scoped packages
if (packageName.startsWith('@')) {
const scope = packageName.split('/')[0];
if (this.config.scopes[scope]) {
return this.config.scopes[scope];
}
}
return this.config.registry;
}
async fetchWithRetry(url, options = {}) {
const requestOptions = {
timeout: this.config.timeout,
headers: {
'User-Agent': this.config.userAgent,
'Accept': 'application/json',
...options.headers
},
...options
};
// Add authentication if available
const registry = new URL(url).origin;
if (this.config.auth[registry]) {
const auth = this.config.auth[registry];
if (auth.token) {
requestOptions.headers['Authorization'] = `Bearer ${auth.token}`;
} else if (auth.username && auth.password) {
const credentials = Buffer.from(`${auth.username}:${auth.password}`).toString('base64');
requestOptions.headers['Authorization'] = `Basic ${credentials}`;
}
}
let lastError;
for (let attempt = 0; attempt < this.config.retries; attempt++) {
try {
const response = await fetch(url, requestOptions);
return response;
} catch (error) {
lastError = error;
if (attempt < this.config.retries - 1) {
// Exponential backoff
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw lastError;
}
async verifyIntegrity(data, integrity) {
const crypto = require('crypto');
// Parse integrity string (algorithm-hash)
const match = integrity.match(/^(sha\d+)-(.+)$/);
if (!match) {
throw new Error(`Invalid integrity format: ${integrity}`);
}
const [, algorithm, expectedHash] = match;
const actualHash = crypto.createHash(algorithm.replace('sha', 'sha')).update(data).digest('base64');
if (actualHash !== expectedHash) {
throw new Error('Package integrity verification failed');
}
}
async publishPackage(packagePath, options = {}) {
// This would implement package publishing
throw new Error('Package publishing not yet implemented');
}
async unpublishPackage(packageName, version, options = {}) {
// This would implement package unpublishing
throw new Error('Package unpublishing not yet implemented');
}
async deprecatePackage(packageName, version, message, options = {}) {
// This would implement package deprecation
throw new Error('Package deprecation not yet implemented');
}
async login(username, password, email, registry) {
// This would implement user authentication
throw new Error('Login not yet implemented');
}
async logout(registry) {
// This would implement logout
const registryUrl = registry || this.config.registry;
delete this.config.auth[registryUrl];
}
async whoami(registry) {
// This would return current user info
throw new Error('whoami not yet implemented');
}
// Utility methods
async ping(registry) {
const registryUrl = registry || this.config.registry;
try {
const response = await this.fetchWithRetry(`${registryUrl}/-/ping`);
return {
registry: registryUrl,
ok: response.ok,
status: response.status,
time: Date.now()
};
} catch (error) {
return {
registry: registryUrl,
ok: false,
error: error.message,
time: Date.now()
};
}
}
async getRegistryInfo(registry) {
const registryUrl = registry || this.config.registry;
try {
const response = await this.fetchWithRetry(registryUrl);
if (!response.ok) {
throw new Error(`Registry not accessible: ${response.status}`);
}
const data = await response.json();
return {
registry: registryUrl,
db_name: data.db_name,
doc_count: data.doc_count,
doc_del_count: data.doc_del_count,
update_seq: data.update_seq,
purge_seq: data.purge_seq,
compact_running: data.compact_running,
disk_size: data.disk_size,
data_size: data.data_size,
instance_start_time: data.instance_start_time,
disk_format_version: data.disk_format_version,
committed_update_seq: data.committed_update_seq
};
} catch (error) {
throw new Error(`Failed to get registry info: ${error.message}`);
}
}
clearCache() {
this.cache.clear();
}
getCacheStats() {
const entries = Array.from(this.cache.values());
const totalSize = JSON.stringify(entries).length;
return {
entries: this.cache.size,
totalSize,
oldestEntry: entries.length > 0 ? Math.min(...entries.map(e => e.timestamp)) : null,
newestEntry: entries.length > 0 ? Math.max(...entries.map(e => e.timestamp)) : null
};
}
setOfflineMode(offline = true) {
this.config.offline = offline;
}
isOffline() {
return this.config.offline;
}
}
module.exports = Registry;