cryptoserve 0.3.2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/census/aggregator.mjs +51 -1
- package/lib/census/collectors/cocoapods-downloads.mjs +3 -3
- package/lib/census/collectors/crates-downloads.mjs +3 -3
- package/lib/census/collectors/github-enrichment.mjs +160 -0
- package/lib/census/collectors/go-downloads.mjs +4 -4
- package/lib/census/collectors/hex-downloads.mjs +3 -3
- package/lib/census/collectors/maven-downloads.mjs +6 -2
- package/lib/census/collectors/npm-downloads.mjs +6 -6
- package/lib/census/collectors/nuget-downloads.mjs +3 -3
- package/lib/census/collectors/packagist-downloads.mjs +3 -3
- package/lib/census/collectors/pub-downloads.mjs +3 -3
- package/lib/census/collectors/pypi-downloads.mjs +3 -3
- package/lib/census/collectors/rubygems-downloads.mjs +3 -3
- package/lib/census/package-catalog.mjs +374 -367
- package/lib/census/report-html.mjs +20 -7
- package/lib/census/report-terminal.mjs +12 -2
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Includes project-level transparency stats when available.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { TIERS, getCatalogSize } from './package-catalog.mjs';
|
|
9
|
+
import { TIERS, CATEGORIES, getCatalogSize } from './package-catalog.mjs';
|
|
10
10
|
|
|
11
11
|
// NIST Post-Quantum Cryptography deadlines
|
|
12
12
|
const NIST_2030 = new Date('2030-01-01T00:00:00Z');
|
|
@@ -78,6 +78,50 @@ function buildEcosystemBreakdown(pkgs, period) {
|
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Build per-category breakdown across all packages.
|
|
83
|
+
* Groups packages by category and computes weak/modern/pqc totals + weak percentage.
|
|
84
|
+
*/
|
|
85
|
+
function buildCategoryBreakdown(allPkgs) {
|
|
86
|
+
const categoryMap = {};
|
|
87
|
+
for (const cat of CATEGORIES) {
|
|
88
|
+
categoryMap[cat] = { category: cat, weak: 0, modern: 0, pqc: 0, total: 0, weakPercentage: 0, topPackages: [] };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const pkg of allPkgs) {
|
|
92
|
+
const cat = pkg.category || 'general';
|
|
93
|
+
const entry = categoryMap[cat];
|
|
94
|
+
if (!entry) continue;
|
|
95
|
+
|
|
96
|
+
const dl = pkg.downloads || 0;
|
|
97
|
+
if (pkg.tier === TIERS.WEAK) entry.weak += dl;
|
|
98
|
+
else if (pkg.tier === TIERS.PQC) entry.pqc += dl;
|
|
99
|
+
else entry.modern += dl;
|
|
100
|
+
|
|
101
|
+
entry.total += dl;
|
|
102
|
+
entry.topPackages.push(pkg);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Compute weak percentage and sort top packages
|
|
106
|
+
const result = [];
|
|
107
|
+
for (const cat of CATEGORIES) {
|
|
108
|
+
const entry = categoryMap[cat];
|
|
109
|
+
entry.weakPercentage = entry.total > 0
|
|
110
|
+
? Math.round((entry.weak / entry.total) * 1000) / 10
|
|
111
|
+
: 0;
|
|
112
|
+
entry.topPackages = entry.topPackages
|
|
113
|
+
.sort((a, b) => b.downloads - a.downloads)
|
|
114
|
+
.slice(0, 10);
|
|
115
|
+
if (entry.total > 0) {
|
|
116
|
+
result.push(entry);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Sort by total downloads descending
|
|
121
|
+
result.sort((a, b) => b.total - a.total);
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
|
|
81
125
|
/**
|
|
82
126
|
* Aggregate all census data into headline metrics.
|
|
83
127
|
*
|
|
@@ -178,6 +222,9 @@ export function aggregate(data) {
|
|
|
178
222
|
return d?.packages?.length > 0;
|
|
179
223
|
});
|
|
180
224
|
|
|
225
|
+
// Category breakdown
|
|
226
|
+
const categoryBreakdown = buildCategoryBreakdown(allPkgs);
|
|
227
|
+
|
|
181
228
|
return {
|
|
182
229
|
// Headline numbers
|
|
183
230
|
totalDownloads,
|
|
@@ -189,6 +236,9 @@ export function aggregate(data) {
|
|
|
189
236
|
pqcPercentage,
|
|
190
237
|
weakToPqcRatio,
|
|
191
238
|
|
|
239
|
+
// Category breakdown
|
|
240
|
+
categoryBreakdown,
|
|
241
|
+
|
|
192
242
|
// Per-ecosystem
|
|
193
243
|
npm,
|
|
194
244
|
pypi,
|
|
@@ -46,17 +46,17 @@ export async function collectCocoapodsDownloads(packages, options = {}) {
|
|
|
46
46
|
|
|
47
47
|
if (!res.ok) {
|
|
48
48
|
if (verbose) process.stderr.write(` cocoapods ${pkg.name}: ${res.status}\n`);
|
|
49
|
-
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
|
|
49
|
+
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
|
|
50
50
|
} else {
|
|
51
51
|
// Trunk API confirms pod exists but has no download stats.
|
|
52
52
|
// Use conservative estimate: CocoaPods ecosystem is smaller,
|
|
53
53
|
// most crypto pods get 1K-50K installs/month based on GitHub activity.
|
|
54
54
|
// We set 0 and rely on scanner data if available.
|
|
55
|
-
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
|
|
55
|
+
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
|
|
56
56
|
}
|
|
57
57
|
} catch (err) {
|
|
58
58
|
if (verbose) process.stderr.write(` cocoapods ${pkg.name} error: ${err.message}\n`);
|
|
59
|
-
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
|
|
59
|
+
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
if (i < packages.length - 1) {
|
|
@@ -45,17 +45,17 @@ export async function collectCratesDownloads(packages, options = {}) {
|
|
|
45
45
|
|
|
46
46
|
if (!res.ok) {
|
|
47
47
|
if (verbose) process.stderr.write(` crates ${pkg.name}: ${res.status}\n`);
|
|
48
|
-
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
|
|
48
|
+
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
|
|
49
49
|
} else {
|
|
50
50
|
const data = await res.json();
|
|
51
51
|
// recent_downloads = last 90 days, divide by 3 for monthly estimate
|
|
52
52
|
const recentDownloads = data?.crate?.recent_downloads || 0;
|
|
53
53
|
const monthlyEstimate = Math.round(recentDownloads / 3);
|
|
54
|
-
results.push({ name: pkg.name, downloads: monthlyEstimate, tier: pkg.tier });
|
|
54
|
+
results.push({ name: pkg.name, downloads: monthlyEstimate, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
|
|
55
55
|
}
|
|
56
56
|
} catch (err) {
|
|
57
57
|
if (verbose) process.stderr.write(` crates ${pkg.name} error: ${err.message}\n`);
|
|
58
|
-
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
|
|
58
|
+
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
if (i < packages.length - 1) {
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enrich top packages with GitHub repository metadata.
|
|
3
|
+
*
|
|
4
|
+
* Fetches stars, forks, last push date, and archived status from the GitHub API
|
|
5
|
+
* for the top packages by download count. Uses unauthenticated requests
|
|
6
|
+
* (60 req/hr limit), so we limit to 30 packages with 1s delays.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const REQUEST_DELAY_MS = 1000;
|
|
10
|
+
const MAX_ENRICHMENTS = 30;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Static mapping of package name to GitHub owner/repo.
|
|
14
|
+
* Many registries don't expose repo URLs in download APIs,
|
|
15
|
+
* so we maintain this mapping for top packages.
|
|
16
|
+
*/
|
|
17
|
+
const KNOWN_REPOS = {
|
|
18
|
+
// npm
|
|
19
|
+
'crypto-js': 'brix/crypto-js',
|
|
20
|
+
'@noble/hashes': 'paulmillr/noble-hashes',
|
|
21
|
+
'@noble/curves': 'paulmillr/noble-curves',
|
|
22
|
+
'@noble/ciphers': 'paulmillr/noble-ciphers',
|
|
23
|
+
'@noble/post-quantum': 'paulmillr/noble-post-quantum',
|
|
24
|
+
'node-forge': 'digitalbazaar/forge',
|
|
25
|
+
'jose': 'panva/jose',
|
|
26
|
+
'elliptic': 'indutny/elliptic',
|
|
27
|
+
'hash.js': 'indutny/hash.js',
|
|
28
|
+
'tweetnacl': 'nicola/tweetnacl-js',
|
|
29
|
+
'bcryptjs': 'nicola/bcrypt.js',
|
|
30
|
+
'jsonwebtoken': 'auth0/node-jsonwebtoken',
|
|
31
|
+
'sodium-native': 'nicola/sodium-native',
|
|
32
|
+
'md5': 'pvorb/node-md5',
|
|
33
|
+
'scrypt-js': 'nicola/scrypt-js',
|
|
34
|
+
|
|
35
|
+
// PyPI
|
|
36
|
+
'cryptography': 'pyca/cryptography',
|
|
37
|
+
'pycryptodome': 'Legrandin/pycryptodome',
|
|
38
|
+
'bcrypt': 'pyca/bcrypt',
|
|
39
|
+
'pynacl': 'pyca/pynacl',
|
|
40
|
+
'argon2-cffi': 'hynek/argon2-cffi',
|
|
41
|
+
'PyJWT': 'jpadilla/pyjwt',
|
|
42
|
+
'liboqs-python': 'open-quantum-safe/liboqs-python',
|
|
43
|
+
|
|
44
|
+
// Rust crates
|
|
45
|
+
'ring': 'briansmith/ring',
|
|
46
|
+
'rustls': 'rustls/rustls',
|
|
47
|
+
'ed25519-dalek': 'dalek-cryptography/curve25519-dalek',
|
|
48
|
+
'sha2': 'RustCrypto/hashes',
|
|
49
|
+
'aes-gcm': 'RustCrypto/AEADs',
|
|
50
|
+
'chacha20poly1305': 'RustCrypto/AEADs',
|
|
51
|
+
'argon2': 'RustCrypto/password-hashes',
|
|
52
|
+
|
|
53
|
+
// Go
|
|
54
|
+
'github.com/cloudflare/circl': 'cloudflare/circl',
|
|
55
|
+
'golang-jwt/jwt/v5': 'golang-jwt/jwt',
|
|
56
|
+
|
|
57
|
+
// Maven
|
|
58
|
+
'org.bouncycastle:bcprov-jdk18on': 'bcgit/bc-java',
|
|
59
|
+
'com.google.crypto.tink:tink': 'google/tink',
|
|
60
|
+
'io.jsonwebtoken:jjwt-api': 'jwtk/jjwt',
|
|
61
|
+
|
|
62
|
+
// PHP
|
|
63
|
+
'phpseclib/phpseclib': 'phpseclib/phpseclib',
|
|
64
|
+
'defuse/php-encryption': 'defuse/php-encryption',
|
|
65
|
+
'firebase/php-jwt': 'firebase/php-jwt',
|
|
66
|
+
|
|
67
|
+
// Ruby
|
|
68
|
+
'rbnacl': 'crypto-rb/rbnacl',
|
|
69
|
+
'jwt': 'jwt/ruby-jwt',
|
|
70
|
+
|
|
71
|
+
// Dart
|
|
72
|
+
'pointycastle': 'nicola/pc-dart',
|
|
73
|
+
|
|
74
|
+
// Swift
|
|
75
|
+
'CryptoSwift': 'nicola/CryptoSwift',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function sleep(ms) {
|
|
79
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Enrich packages with GitHub metadata.
|
|
84
|
+
*
|
|
85
|
+
* @param {Array} allPackages - All package entries (with name + downloads)
|
|
86
|
+
* @param {Object} [options]
|
|
87
|
+
* @param {Function} [options.fetchFn] - Fetch implementation
|
|
88
|
+
* @param {boolean} [options.verbose] - Log progress
|
|
89
|
+
* @returns {Promise<Map<string, {stars: number, forks: number, lastPush: string, archived: boolean}>>}
|
|
90
|
+
*/
|
|
91
|
+
export async function collectGithubEnrichment(allPackages, options = {}) {
|
|
92
|
+
const fetchFn = options.fetchFn || globalThis.fetch;
|
|
93
|
+
const verbose = options.verbose || false;
|
|
94
|
+
|
|
95
|
+
// Sort by downloads descending and pick top packages that have known repos
|
|
96
|
+
const sorted = [...allPackages]
|
|
97
|
+
.sort((a, b) => b.downloads - a.downloads);
|
|
98
|
+
|
|
99
|
+
const toEnrich = [];
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
for (const pkg of sorted) {
|
|
102
|
+
const repo = KNOWN_REPOS[pkg.name];
|
|
103
|
+
if (!repo || seen.has(repo)) continue;
|
|
104
|
+
seen.add(repo);
|
|
105
|
+
toEnrich.push({ name: pkg.name, repo });
|
|
106
|
+
if (toEnrich.length >= MAX_ENRICHMENTS) break;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (verbose) {
|
|
110
|
+
process.stderr.write(` github enrichment: ${toEnrich.length} packages to enrich\n`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const results = new Map();
|
|
114
|
+
|
|
115
|
+
for (const { name, repo } of toEnrich) {
|
|
116
|
+
try {
|
|
117
|
+
const res = await fetchFn(`https://api.github.com/repos/${repo}`, {
|
|
118
|
+
headers: { 'Accept': 'application/vnd.github+json' },
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
if (!res.ok) {
|
|
122
|
+
if (verbose) process.stderr.write(` github ${repo}: HTTP ${res.status}\n`);
|
|
123
|
+
// Check rate limit
|
|
124
|
+
const remaining = res.headers?.get?.('x-ratelimit-remaining');
|
|
125
|
+
if (remaining === '0') {
|
|
126
|
+
if (verbose) process.stderr.write(' github rate limit hit, stopping\n');
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
await sleep(REQUEST_DELAY_MS);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const data = await res.json();
|
|
134
|
+
results.set(name, {
|
|
135
|
+
stars: data.stargazers_count || 0,
|
|
136
|
+
forks: data.forks_count || 0,
|
|
137
|
+
lastPush: data.pushed_at || null,
|
|
138
|
+
archived: data.archived || false,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
if (verbose) {
|
|
142
|
+
process.stderr.write(` github ${repo}: ${data.stargazers_count} stars\n`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
await sleep(REQUEST_DELAY_MS);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
if (verbose) process.stderr.write(` github ${repo} error: ${err.message}\n`);
|
|
148
|
+
await sleep(REQUEST_DELAY_MS);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (verbose) {
|
|
153
|
+
process.stderr.write(` github enrichment: ${results.size} packages enriched\n`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
enrichments: Object.fromEntries(results),
|
|
158
|
+
collectedAt: new Date().toISOString(),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -73,7 +73,7 @@ export async function collectGoDownloads(packages, options = {}) {
|
|
|
73
73
|
// Stdlib packages: use hardcoded estimates
|
|
74
74
|
if (pkg.name.startsWith('crypto/')) {
|
|
75
75
|
const estimate = STDLIB_ESTIMATES[pkg.name] || 10_000;
|
|
76
|
-
results.push({ name: pkg.name, downloads: estimate, tier: pkg.tier });
|
|
76
|
+
results.push({ name: pkg.name, downloads: estimate, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
|
|
77
77
|
continue;
|
|
78
78
|
}
|
|
79
79
|
|
|
@@ -84,7 +84,7 @@ export async function collectGoDownloads(packages, options = {}) {
|
|
|
84
84
|
|
|
85
85
|
if (!res.ok) {
|
|
86
86
|
if (verbose) process.stderr.write(` go ${pkg.name}: proxy ${res.status}\n`);
|
|
87
|
-
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
|
|
87
|
+
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
|
|
88
88
|
continue;
|
|
89
89
|
}
|
|
90
90
|
|
|
@@ -115,10 +115,10 @@ export async function collectGoDownloads(packages, options = {}) {
|
|
|
115
115
|
: Math.max(downloads, 500_000);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
results.push({ name: pkg.name, downloads, tier: pkg.tier });
|
|
118
|
+
results.push({ name: pkg.name, downloads, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
|
|
119
119
|
} catch (err) {
|
|
120
120
|
if (verbose) process.stderr.write(` go ${pkg.name} error: ${err.message}\n`);
|
|
121
|
-
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
|
|
121
|
+
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
if (i < packages.length - 1) await sleep(REQUEST_DELAY_MS);
|
|
@@ -42,18 +42,18 @@ export async function collectHexDownloads(packages, options = {}) {
|
|
|
42
42
|
|
|
43
43
|
if (!res.ok) {
|
|
44
44
|
if (verbose) process.stderr.write(` hex ${pkg.name}: ${res.status}\n`);
|
|
45
|
-
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
|
|
45
|
+
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
|
|
46
46
|
} else {
|
|
47
47
|
const data = await res.json();
|
|
48
48
|
// Hex provides downloads.recent (last 90 days) and downloads.all
|
|
49
49
|
const recentDownloads = data?.downloads?.recent || 0;
|
|
50
50
|
// Estimate monthly from 90-day window
|
|
51
51
|
const monthlyEstimate = Math.round(recentDownloads / 3);
|
|
52
|
-
results.push({ name: pkg.name, downloads: monthlyEstimate, tier: pkg.tier });
|
|
52
|
+
results.push({ name: pkg.name, downloads: monthlyEstimate, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
|
|
53
53
|
}
|
|
54
54
|
} catch (err) {
|
|
55
55
|
if (verbose) process.stderr.write(` hex ${pkg.name} error: ${err.message}\n`);
|
|
56
|
-
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
|
|
56
|
+
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
if (i < packages.length - 1) {
|
|
@@ -57,7 +57,7 @@ export async function collectMavenDownloads(packages, options = {}) {
|
|
|
57
57
|
|
|
58
58
|
if (!res.ok) {
|
|
59
59
|
if (verbose) process.stderr.write(` maven ${pkg.name}: ${res.status}\n`);
|
|
60
|
-
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
|
|
60
|
+
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
|
|
61
61
|
} else {
|
|
62
62
|
const data = await res.json();
|
|
63
63
|
const doc = data?.response?.docs?.[0];
|
|
@@ -72,11 +72,15 @@ export async function collectMavenDownloads(packages, options = {}) {
|
|
|
72
72
|
name: pkg.name,
|
|
73
73
|
downloads: estimatedDownloads,
|
|
74
74
|
tier: pkg.tier,
|
|
75
|
+
category: pkg.category,
|
|
76
|
+
replacedBy: pkg.replacedBy,
|
|
77
|
+
algorithms: pkg.algorithms,
|
|
78
|
+
note: pkg.note,
|
|
75
79
|
});
|
|
76
80
|
}
|
|
77
81
|
} catch (err) {
|
|
78
82
|
if (verbose) process.stderr.write(` maven ${pkg.name} error: ${err.message}\n`);
|
|
79
|
-
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
|
|
83
|
+
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
if (i < packages.length - 1) {
|
|
@@ -28,13 +28,13 @@ async function fetchSingle(pkg, fetchFn, period, verbose) {
|
|
|
28
28
|
period.start = data.start;
|
|
29
29
|
period.end = data.end;
|
|
30
30
|
}
|
|
31
|
-
return { name: pkg.name, downloads: data.downloads || 0, tier: pkg.tier };
|
|
31
|
+
return { name: pkg.name, downloads: data.downloads || 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note };
|
|
32
32
|
}
|
|
33
33
|
if (verbose) process.stderr.write(` npm ${pkg.name}: HTTP ${res.status}\n`);
|
|
34
34
|
} catch (err) {
|
|
35
35
|
if (verbose) process.stderr.write(` npm ${pkg.name} error: ${err.message}\n`);
|
|
36
36
|
}
|
|
37
|
-
return { name: pkg.name, downloads: 0, tier: pkg.tier };
|
|
37
|
+
return { name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note };
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
@@ -90,7 +90,7 @@ export async function collectNpmDownloads(packages, options = {}) {
|
|
|
90
90
|
period.start = data.start;
|
|
91
91
|
period.end = data.end;
|
|
92
92
|
}
|
|
93
|
-
results.push({ name: batch[0].name, downloads: data.downloads || 0, tier: batch[0].tier });
|
|
93
|
+
results.push({ name: batch[0].name, downloads: data.downloads || 0, tier: batch[0].tier, category: batch[0].category, replacedBy: batch[0].replacedBy, algorithms: batch[0].algorithms, note: batch[0].note });
|
|
94
94
|
} else {
|
|
95
95
|
for (const pkg of batch) {
|
|
96
96
|
const entry = data[pkg.name];
|
|
@@ -99,16 +99,16 @@ export async function collectNpmDownloads(packages, options = {}) {
|
|
|
99
99
|
period.start = entry.start;
|
|
100
100
|
period.end = entry.end;
|
|
101
101
|
}
|
|
102
|
-
results.push({ name: pkg.name, downloads: entry.downloads || 0, tier: pkg.tier });
|
|
102
|
+
results.push({ name: pkg.name, downloads: entry.downloads || 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
|
|
103
103
|
} else {
|
|
104
|
-
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
|
|
104
|
+
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
} catch (err) {
|
|
109
109
|
if (verbose) process.stderr.write(` npm batch ${i + 1} error: ${err.message}\n`);
|
|
110
110
|
for (const pkg of batch) {
|
|
111
|
-
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
|
|
111
|
+
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
|
|
@@ -45,18 +45,18 @@ export async function collectNugetDownloads(packages, options = {}) {
|
|
|
45
45
|
|
|
46
46
|
if (!res.ok) {
|
|
47
47
|
if (verbose) process.stderr.write(` nuget ${pkg.name}: ${res.status}\n`);
|
|
48
|
-
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
|
|
48
|
+
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
|
|
49
49
|
} else {
|
|
50
50
|
const data = await res.json();
|
|
51
51
|
const entry = data?.data?.[0];
|
|
52
52
|
// NuGet returns total downloads, estimate monthly as total / 36 (3 years average)
|
|
53
53
|
const totalDownloads = entry?.totalDownloads || 0;
|
|
54
54
|
const monthlyEstimate = Math.round(totalDownloads / 36);
|
|
55
|
-
results.push({ name: pkg.name, downloads: monthlyEstimate, tier: pkg.tier });
|
|
55
|
+
results.push({ name: pkg.name, downloads: monthlyEstimate, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
|
|
56
56
|
}
|
|
57
57
|
} catch (err) {
|
|
58
58
|
if (verbose) process.stderr.write(` nuget ${pkg.name} error: ${err.message}\n`);
|
|
59
|
-
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
|
|
59
|
+
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
if (i < packages.length - 1) {
|
|
@@ -41,15 +41,15 @@ export async function collectPackagistDownloads(packages, options = {}) {
|
|
|
41
41
|
|
|
42
42
|
if (!res.ok) {
|
|
43
43
|
if (verbose) process.stderr.write(` packagist ${pkg.name}: ${res.status}\n`);
|
|
44
|
-
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
|
|
44
|
+
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
|
|
45
45
|
} else {
|
|
46
46
|
const data = await res.json();
|
|
47
47
|
const monthlyDownloads = data?.package?.downloads?.monthly || 0;
|
|
48
|
-
results.push({ name: pkg.name, downloads: monthlyDownloads, tier: pkg.tier });
|
|
48
|
+
results.push({ name: pkg.name, downloads: monthlyDownloads, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
|
|
49
49
|
}
|
|
50
50
|
} catch (err) {
|
|
51
51
|
if (verbose) process.stderr.write(` packagist ${pkg.name} error: ${err.message}\n`);
|
|
52
|
-
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
|
|
52
|
+
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
if (i < packages.length - 1) {
|
|
@@ -40,16 +40,16 @@ export async function collectPubDownloads(packages, options = {}) {
|
|
|
40
40
|
|
|
41
41
|
if (!res.ok) {
|
|
42
42
|
if (verbose) process.stderr.write(` pub ${pkg.name}: ${res.status}\n`);
|
|
43
|
-
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
|
|
43
|
+
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
|
|
44
44
|
} else {
|
|
45
45
|
const data = await res.json();
|
|
46
46
|
// pub.dev score endpoint has downloadCount30Days
|
|
47
47
|
const downloads = data?.downloadCount30Days || 0;
|
|
48
|
-
results.push({ name: pkg.name, downloads: downloads, tier: pkg.tier });
|
|
48
|
+
results.push({ name: pkg.name, downloads: downloads, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
|
|
49
49
|
}
|
|
50
50
|
} catch (err) {
|
|
51
51
|
if (verbose) process.stderr.write(` pub ${pkg.name} error: ${err.message}\n`);
|
|
52
|
-
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
|
|
52
|
+
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
if (i < packages.length - 1) {
|
|
@@ -41,16 +41,16 @@ export async function collectPypiDownloads(packages, options = {}) {
|
|
|
41
41
|
const res = await fetchFn(url);
|
|
42
42
|
if (!res.ok) {
|
|
43
43
|
if (verbose) process.stderr.write(` pypi ${pkg.name}: ${res.status}\n`);
|
|
44
|
-
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
|
|
44
|
+
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
|
|
45
45
|
} else {
|
|
46
46
|
const data = await res.json();
|
|
47
47
|
// Response: { data: { last_month: N, last_week: N, last_day: N }, ... }
|
|
48
48
|
const downloads = data?.data?.last_month || 0;
|
|
49
|
-
results.push({ name: pkg.name, downloads, tier: pkg.tier });
|
|
49
|
+
results.push({ name: pkg.name, downloads, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
|
|
50
50
|
}
|
|
51
51
|
} catch (err) {
|
|
52
52
|
if (verbose) process.stderr.write(` pypi ${pkg.name} error: ${err.message}\n`);
|
|
53
|
-
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
|
|
53
|
+
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
// Delay between requests
|
|
@@ -41,17 +41,17 @@ export async function collectRubygemsDownloads(packages, options = {}) {
|
|
|
41
41
|
|
|
42
42
|
if (!res.ok) {
|
|
43
43
|
if (verbose) process.stderr.write(` rubygems ${pkg.name}: ${res.status}\n`);
|
|
44
|
-
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
|
|
44
|
+
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
|
|
45
45
|
} else {
|
|
46
46
|
const data = await res.json();
|
|
47
47
|
// RubyGems only provides total downloads; estimate monthly
|
|
48
48
|
const totalDownloads = data?.downloads || 0;
|
|
49
49
|
const monthlyEstimate = Math.round(totalDownloads / 120);
|
|
50
|
-
results.push({ name: pkg.name, downloads: monthlyEstimate, tier: pkg.tier });
|
|
50
|
+
results.push({ name: pkg.name, downloads: monthlyEstimate, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
|
|
51
51
|
}
|
|
52
52
|
} catch (err) {
|
|
53
53
|
if (verbose) process.stderr.write(` rubygems ${pkg.name} error: ${err.message}\n`);
|
|
54
|
-
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier });
|
|
54
|
+
results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
if (i < packages.length - 1) {
|