Skip to content

Instantly share code, notes, and snippets.

@bernardobelchior
Last active June 18, 2025 10:19
Show Gist options
  • Save bernardobelchior/c683818ee91ffee97f7bab512b3bf46b to your computer and use it in GitHub Desktop.
Save bernardobelchior/c683818ee91ffee97f7bab512b3bf46b to your computer and use it in GitHub Desktop.
NPM download breakdown by version
#!/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);
}
@bernardobelchior
Copy link
Author

bernardobelchior commented Jun 18, 2025

Uses last week data, as that's the only data available in npm's API.

Example:

❯ node --experimental-strip-types ./npm-downloads.ts react
πŸ” Fetching download data for: react...
(node:34471) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)

πŸ“¦ NPM Downloads Report for: react
πŸ“… Period: Last Week
πŸ“Š Total Downloads: 44,535,251

============================================================
| Major Version   |    Downloads | Percentage |
|-----------------|--------------|------------|
| v0.x            |      168,321 |       0.4% |
| v15.x           |      281,977 |       0.6% |
| v16.x           |    4,663,225 |      10.5% |
| v17.x           |    5,041,286 |      11.3% |
| v18.x           |   24,478,510 |      55.0% |
| v19.x           |    9,901,932 |      22.2% |
============================================================

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment