packages.ts (4212B)
1 import { bundleHash, type BundleHash } from './bundle.js'; 2 export interface Release { name: string; version: string; hash: BundleHash; manifest: BundleHash; title: string; demo?: string; } 3 export interface EnvironmentProfile { hash: BundleHash; names: string[]; } 4 const component = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u; 5 function record(value: unknown): Record<string, unknown> { 6 if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Invalid metadata object'); 7 return value as Record<string, unknown>; 8 } 9 export function packageIndex(value: unknown): Release[] { 10 const input = record(value); 11 if (input['schema'] !== 1 || !Array.isArray(input['releases']) || input['releases'].length > 4096) throw new Error('Invalid package index'); 12 const seen = new Set<string>(); 13 return input['releases'].map((raw: unknown) => { 14 const r = record(raw); 15 if (typeof r['name'] !== 'string' || !component.test(r['name']) || typeof r['version'] !== 'string' || !component.test(r['version']) || 16 typeof r['title'] !== 'string' || r['title'].length > 256 || (r['demo'] !== undefined && (typeof r['demo'] !== 'string' || !component.test(r['demo'])))) throw new Error('Invalid release'); 17 const key = `${r['name']}@${r['version']}`; 18 if (seen.has(key)) throw new Error('Duplicate package version'); seen.add(key); 19 return { name: r['name'], version: r['version'], title: r['title'], hash: bundleHash(r['hash']), manifest: bundleHash(r['manifest']), 20 ...(r['demo'] === undefined ? {} : { demo: r['demo'] as string }) }; 21 }); 22 } 23 // SemVer precedence for conventional releases; other accepted labels use a 24 // deterministic numeric-aware ordering below conventional releases. Never infer 25 // recency from index order. 26 function compareLabel(a: string, b: string): number { 27 const left = a.match(/[0-9]+|[^0-9]+/gu) ?? [], right = b.match(/[0-9]+|[^0-9]+/gu) ?? []; 28 for (let i = 0; i < Math.min(left.length, right.length); i++) { 29 const x = left[i]!, y = right[i]!; 30 const order = /^\d+$/u.test(x) && /^\d+$/u.test(y) 31 ? (BigInt(x) < BigInt(y) ? -1 : BigInt(x) > BigInt(y) ? 1 : 0) 32 : (x < y ? -1 : x > y ? 1 : 0); 33 if (order) return order; 34 } 35 return left.length - right.length; 36 } 37 function compareVersion(a: string, b: string): number { 38 const semver = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/u; 39 const x = a.match(semver), y = b.match(semver); 40 if (!x || !y) return x ? 1 : y ? -1 : compareLabel(a, b); 41 for (let i = 1; i <= 3; i++) { 42 const order = compareLabel(x[i]!, y[i]!); if (order) return order; 43 } 44 if (x[4] === undefined || y[4] === undefined) return x[4] === y[4] ? 0 : x[4] === undefined ? 1 : -1; 45 const left = x[4].split('.'), right = y[4].split('.'); 46 for (let i = 0; i < Math.min(left.length, right.length); i++) { 47 const a = left[i]!, b = right[i]!, an = /^\d+$/u.test(a), bn = /^\d+$/u.test(b); 48 const order = an && bn ? compareLabel(a, b) : an !== bn ? (an ? -1 : 1) : (a < b ? -1 : a > b ? 1 : 0); 49 if (order) return order; 50 } 51 return left.length - right.length; 52 } 53 export function packageReleases(releases: readonly Release[], name: string): Release[] { 54 return releases.filter(r => r.name === name).sort((a, b) => compareVersion(b.version, a.version) || (a.version < b.version ? 1 : a.version > b.version ? -1 : 0)); 55 } 56 export function environmentProfile(value: unknown, name: string): { compiler: BundleHash; profile: EnvironmentProfile | null } { 57 const input = record(value), profiles = record(input['profiles']); 58 if (input['schema'] !== 1 || !Object.hasOwn(profiles, name)) throw new Error('Unknown demo environment'); 59 const compiler = bundleHash(input['compiler']), raw = profiles[name]; 60 if (raw === null) return { compiler, profile: null }; 61 const profile = record(raw), names = profile['names']; 62 if (!Array.isArray(names) || names.length > 128 || names.some(n => typeof n !== 'string' || !/^[A-Za-z_][A-Za-z0-9_?$%'.-]*$/u.test(n) || n.length > 128) || new Set(names).size !== names.length) throw new Error('Invalid environment bindings'); 63 return { compiler, profile: { hash: bundleHash(profile['hash']), names: names as string[] } }; 64 }