/** * Composer Registry Helper Functions */ import type { IComposerPackage } from './interfaces.composer.js'; /** * Normalize version string to Composer format * Example: "1.0.0" -> "1.0.0.0", "v2.3.1" -> "2.3.1.0" */ export function normalizeVersion(version: string): string { // Remove 'v' prefix if present let normalized = version.replace(/^v/i, ''); // Handle special versions (dev, alpha, beta, rc) if (normalized.includes('dev') || normalized.includes('alpha') || normalized.includes('beta') || normalized.includes('RC')) { // For dev versions, just return as-is with .0 appended if needed const parts = normalized.split(/[-+]/)[0].split('.'); while (parts.length < 4) { parts.push('0'); } return parts.slice(0, 4).join('.'); } // Split by dots const parts = normalized.split('.'); // Ensure 4 parts (major.minor.patch.build) while (parts.length < 4) { parts.push('0'); } return parts.slice(0, 4).join('.'); } /** * Validate composer.json structure */ export function validateComposerJson(composerJson: any): boolean { return !!( composerJson && typeof composerJson.name === 'string' && composerJson.name.includes('/') && (composerJson.version || composerJson.require) ); } /** * Extract composer.json from ZIP buffer */ export async function extractComposerJsonFromZip(zipBuffer: Buffer): Promise { try { const AdmZip = (await import('adm-zip')).default; const zip = new AdmZip(zipBuffer); const entries = zip.getEntries(); // Look for composer.json in root or first-level directory for (const entry of entries) { if (entry.entryName.endsWith('composer.json')) { const parts = entry.entryName.split('/'); if (parts.length <= 2) { // Root or first-level dir const content = entry.getData().toString('utf-8'); return JSON.parse(content); } } } return null; } catch (error) { return null; } } /** * Calculate SHA-1 hash for ZIP file */ export async function calculateSha1(data: Buffer): Promise { const crypto = await import('crypto'); return crypto.createHash('sha1').update(data).digest('hex'); } /** * Parse vendor/package format */ export function parseVendorPackage(name: string): { vendor: string; package: string } | null { const parts = name.split('/'); if (parts.length !== 2) { return null; } return { vendor: parts[0], package: parts[1] }; } /** * Generate packages.json root repository file */ export function generatePackagesJson( registryUrl: string, availablePackages: string[] ): any { return { 'metadata-url': `${registryUrl}/p2/%package%.json`, 'available-packages': availablePackages, }; } /** * Sort versions in semantic version order */ export function sortVersions(versions: string[]): string[] { return versions.sort((a, b) => { const aParts = a.replace(/^v/i, '').split(/[.-]/).map(part => { const num = parseInt(part, 10); return isNaN(num) ? part : num; }); const bParts = b.replace(/^v/i, '').split(/[.-]/).map(part => { const num = parseInt(part, 10); return isNaN(num) ? part : num; }); for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) { const aPart = aParts[i] ?? 0; const bPart = bParts[i] ?? 0; // Compare numbers numerically, strings lexicographically if (typeof aPart === 'number' && typeof bPart === 'number') { if (aPart !== bPart) { return aPart - bPart; } } else { const aStr = String(aPart); const bStr = String(bPart); if (aStr !== bStr) { return aStr.localeCompare(bStr); } } } return 0; }); }