140 lines
3.7 KiB
TypeScript
140 lines
3.7 KiB
TypeScript
|
|
/**
|
||
|
|
* 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<any | null> {
|
||
|
|
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<string> {
|
||
|
|
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;
|
||
|
|
});
|
||
|
|
}
|