500 líneas
14 KiB
JavaScript
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;
|