forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
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}