[READ-ONLY] a fast, modern browser for the npm registry
at main 169 lines 5.1 kB view raw
1import semver from 'semver' 2import type { 3 VersionDownloadPoint, 4 VersionGroupDownloads, 5 VersionGroupingMode, 6} from '#shared/types' 7 8/** 9 * Intermediate data structure for version processing 10 */ 11interface ProcessedVersion { 12 version: string 13 downloads: number 14 major: number 15 minor: number 16 parsed: semver.SemVer 17} 18 19/** 20 * Filter out versions below a usage threshold 21 * @param versions Array of version download points 22 * @param thresholdPercent Minimum percentage to include (default: 0.1%) 23 * @returns Filtered array of versions 24 */ 25export function filterLowUsageVersions( 26 versions: VersionDownloadPoint[], 27 thresholdPercent: number = 0.1, 28): VersionDownloadPoint[] { 29 return versions.filter(v => v.percentage >= thresholdPercent) 30} 31 32/** 33 * Parse and validate version strings, calculating total downloads 34 * @param rawDownloads Raw download data from npm API 35 * @returns Array of processed versions with parsed semver data 36 */ 37function parseVersions(rawDownloads: Record<string, number>): ProcessedVersion[] { 38 const processed: ProcessedVersion[] = [] 39 40 for (const [version, downloads] of Object.entries(rawDownloads)) { 41 const parsed = semver.parse(version) 42 if (!parsed) continue 43 44 processed.push({ 45 version, 46 downloads, 47 major: parsed.major, 48 minor: parsed.minor, 49 parsed, 50 }) 51 } 52 53 processed.sort((a, b) => semver.rcompare(a.version, b.version)) 54 55 return processed 56} 57 58/** 59 * Calculate percentage for each version 60 * @param versions Processed versions 61 * @param totalDownloads Total download count 62 * @returns Array of version download points with percentages 63 */ 64function addPercentages( 65 versions: ProcessedVersion[], 66 totalDownloads: number, 67): VersionDownloadPoint[] { 68 return versions.map(v => ({ 69 version: v.version, 70 downloads: v.downloads, 71 percentage: totalDownloads > 0 ? (v.downloads / totalDownloads) * 100 : 0, 72 })) 73} 74 75/** 76 * Group versions by major version (e.g., 1.x, 2.x) 77 * @param rawDownloads Raw download data from npm API 78 * @returns Array of version groups sorted by downloads descending 79 */ 80export function groupByMajor(rawDownloads: Record<string, number>): VersionGroupDownloads[] { 81 const processed = parseVersions(rawDownloads) 82 const totalDownloads = processed.reduce((sum, v) => sum + v.downloads, 0) 83 84 const groups = new Map<number, ProcessedVersion[]>() 85 for (const version of processed) { 86 const existing = groups.get(version.major) || [] 87 existing.push(version) 88 groups.set(version.major, existing) 89 } 90 91 const result: VersionGroupDownloads[] = [] 92 for (const [major, versions] of groups.entries()) { 93 const groupDownloads = versions.reduce((sum, v) => sum + v.downloads, 0) 94 const percentage = totalDownloads > 0 ? (groupDownloads / totalDownloads) * 100 : 0 95 96 result.push({ 97 groupKey: `${major}.x`, 98 label: `v${major}.x`, 99 downloads: groupDownloads, 100 percentage, 101 versions: addPercentages(versions, totalDownloads), 102 }) 103 } 104 105 result.sort((a, b) => b.downloads - a.downloads) 106 107 return result 108} 109 110/** 111 * Group versions by major.minor (e.g., 1.2.x, 1.3.x) 112 * Special handling for 0.x versions - treat them as separate majors 113 * @param rawDownloads Raw download data from npm API 114 * @returns Array of version groups sorted by downloads descending 115 */ 116export function groupByMinor(rawDownloads: Record<string, number>): VersionGroupDownloads[] { 117 const processed = parseVersions(rawDownloads) 118 const totalDownloads = processed.reduce((sum, v) => sum + v.downloads, 0) 119 120 // Group by major.minor 121 const groups = new Map<string, ProcessedVersion[]>() 122 for (const version of processed) { 123 // For 0.x versions, treat each minor as significant (0.9.x, 0.10.x are different) 124 // For 1.x+, group by major.minor normally 125 const groupKey = `${version.major}.${version.minor}` 126 const existing = groups.get(groupKey) || [] 127 existing.push(version) 128 groups.set(groupKey, existing) 129 } 130 131 // Convert to VersionGroupDownloads 132 const result: VersionGroupDownloads[] = [] 133 for (const [groupKey, versions] of groups.entries()) { 134 const groupDownloads = versions.reduce((sum, v) => sum + v.downloads, 0) 135 const percentage = totalDownloads > 0 ? (groupDownloads / totalDownloads) * 100 : 0 136 137 result.push({ 138 groupKey: `${groupKey}.x`, 139 label: `v${groupKey}.x`, 140 downloads: groupDownloads, 141 percentage, 142 versions: addPercentages(versions, totalDownloads), 143 }) 144 } 145 146 result.sort((a, b) => b.downloads - a.downloads) 147 148 return result 149} 150 151/** 152 * Group versions by the specified mode 153 * @param rawDownloads Raw download data from npm API 154 * @param mode Grouping mode ('major' or 'minor') 155 * @returns Array of version groups sorted by downloads descending 156 */ 157export function groupVersionDownloads( 158 rawDownloads: Record<string, number>, 159 mode: VersionGroupingMode, 160): VersionGroupDownloads[] { 161 switch (mode) { 162 case 'major': 163 return groupByMajor(rawDownloads) 164 case 'minor': 165 return groupByMinor(rawDownloads) 166 default: 167 throw new Error(`Invalid grouping mode: ${mode}`) 168 } 169}