Last active
June 18, 2025 10:19
-
-
Save bernardobelchior/c683818ee91ffee97f7bab512b3bf46b to your computer and use it in GitHub Desktop.
NPM download breakdown by version
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env node | |
interface DownloadsResponse { | |
downloads: Record<string, number>; | |
} | |
interface MajorVersionData { | |
majorVersion: string; | |
totalDownloads: number; | |
versions: string[]; | |
} | |
async function fetchPackageDownloads(packageName: string): Promise<DownloadsResponse> { | |
const url = `https://api.npmjs.org/versions/${packageName}/last-week`; | |
try { | |
const response = await fetch(url); | |
if (!response.ok) { | |
throw new Error(`HTTP error! status: ${response.status}`); | |
} | |
const data = await response.json(); | |
return data; | |
} catch (error) { | |
throw new Error(`Failed to fetch downloads for ${packageName}: ${error}`); | |
} | |
} | |
function extractMajorVersion(version: string): string { | |
const majorMatch = version.match(/^(\d+)/); | |
return majorMatch ? majorMatch[1] : 'unknown'; | |
} | |
function aggregateByMajorVersion(downloads: Record<string, number>): Map<string, MajorVersionData> { | |
const majorVersionMap = new Map<string, MajorVersionData>(); | |
for (const [version, count] of Object.entries(downloads)) { | |
const majorVersion = extractMajorVersion(version); | |
if (majorVersionMap.has(majorVersion)) { | |
const existing = majorVersionMap.get(majorVersion)!; | |
existing.totalDownloads += count; | |
existing.versions.push(version); | |
} else { | |
majorVersionMap.set(majorVersion, { | |
majorVersion, | |
totalDownloads: count, | |
versions: [version] | |
}); | |
} | |
} | |
return majorVersionMap; | |
} | |
function formatNumber(num: number): string { | |
return num.toLocaleString(); | |
} | |
function displayTable(majorVersionData: Map<string, MajorVersionData>, packageName: string): void { | |
// Sort by major version number | |
const sortedData = Array.from(majorVersionData.values()).sort((a, b) => { | |
const aNum = parseInt(a.majorVersion); | |
const bNum = parseInt(b.majorVersion); | |
// Handle 'unknown' versions | |
if (a.majorVersion === 'unknown') return 1; | |
if (b.majorVersion === 'unknown') return -1; | |
return aNum - bNum; | |
}); | |
// Calculate total downloads | |
const totalDownloads = sortedData.reduce((sum, data) => sum + data.totalDownloads, 0); | |
console.log(`\nπ¦ NPM Downloads Report for: ${packageName}`); | |
console.log(`π Period: Last Week`); | |
console.log(`π Total Downloads: ${formatNumber(totalDownloads)}`); | |
console.log('\n' + '='.repeat(60)); | |
// Table header | |
console.log(`| ${'Major Version'.padEnd(15)} | ${'Downloads'.padStart(12)} | ${'Percentage'.padStart(10)} |`); | |
console.log('|' + '-'.repeat(17) + '|' + '-'.repeat(14) + '|' + '-'.repeat(12) + '|'); | |
// Table rows | |
for (const data of sortedData) { | |
const percentage = ((data.totalDownloads / totalDownloads) * 100).toFixed(1); | |
const versionDisplay = data.majorVersion === 'unknown' ? 'Unknown' : `v${data.majorVersion}.x`; | |
console.log( | |
`| ${versionDisplay.padEnd(15)} | ${formatNumber(data.totalDownloads).padStart(12)} | ${percentage.padStart(9)}% |` | |
); | |
} | |
console.log('='.repeat(60)); | |
} | |
async function main(): Promise<void> { | |
const args = process.argv.slice(2); | |
const packageName = args.find(arg => !arg.startsWith('--') && !arg.startsWith('-')); | |
if (!packageName) { | |
console.error('β Error: Please provide a package name'); | |
console.log('\nUsage: node --experimental-strip-types ./npm-downloads.ts <package-name>'); | |
console.log('Example: node --experimental-strip-types ./npm-downloads.ts react'); | |
process.exit(1); | |
} | |
try { | |
console.log(`π Fetching download data for: ${packageName}...`); | |
const data = await fetchPackageDownloads(packageName); | |
if (!data.downloads || Object.keys(data.downloads).length === 0) { | |
console.log(`β No download data found for package: ${packageName}`); | |
process.exit(1); | |
} | |
// Create version to download count map | |
const versionDownloads = new Map<string, number>(); | |
for (const [version, count] of Object.entries(data.downloads)) { | |
versionDownloads.set(version, count); | |
} | |
// Aggregate by major version | |
const majorVersionData = aggregateByMajorVersion(data.downloads); | |
// Display results | |
displayTable(majorVersionData, packageName); | |
} catch (error) { | |
console.error('β Error:', error instanceof Error ? error.message : String(error)); | |
process.exit(1); | |
} | |
} | |
// Run the script | |
if (require.main === module) { | |
main().catch(console.error); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uses last week data, as that's the only data available in npm's API.
Example: