cryptoserve 0.3.0 → 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.
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Collect download counts from the Hex.pm API.
3
+ *
4
+ * Endpoint: GET https://hex.pm/api/packages/{name}
5
+ * - Returns downloads with recent breakdown
6
+ * - No authentication required
7
+ */
8
+
9
+ const HEX_API = 'https://hex.pm/api/packages';
10
+ const REQUEST_DELAY_MS = 300;
11
+
12
+ function sleep(ms) {
13
+ return new Promise(resolve => setTimeout(resolve, ms));
14
+ }
15
+
16
+ /**
17
+ * Fetch Hex.pm download counts for a list of packages.
18
+ *
19
+ * @param {import('../package-catalog.mjs').CatalogEntry[]} packages
20
+ * @param {Object} [options]
21
+ * @param {Function} [options.fetchFn]
22
+ * @param {boolean} [options.verbose]
23
+ * @returns {Promise<{packages: Array<{name: string, downloads: number, tier: string}>, period: string, collectedAt: string}>}
24
+ */
25
+ export async function collectHexDownloads(packages, options = {}) {
26
+ const fetchFn = options.fetchFn || globalThis.fetch;
27
+ const verbose = options.verbose || false;
28
+
29
+ const results = [];
30
+
31
+ for (let i = 0; i < packages.length; i++) {
32
+ const pkg = packages[i];
33
+
34
+ if (verbose) {
35
+ process.stderr.write(` hex ${i + 1}/${packages.length}: ${pkg.name}\n`);
36
+ }
37
+
38
+ try {
39
+ const res = await fetchFn(`${HEX_API}/${pkg.name}`, {
40
+ headers: { 'Accept': 'application/json' },
41
+ });
42
+
43
+ if (!res.ok) {
44
+ if (verbose) process.stderr.write(` hex ${pkg.name}: ${res.status}\n`);
45
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
46
+ } else {
47
+ const data = await res.json();
48
+ // Hex provides downloads.recent (last 90 days) and downloads.all
49
+ const recentDownloads = data?.downloads?.recent || 0;
50
+ // Estimate monthly from 90-day window
51
+ const monthlyEstimate = Math.round(recentDownloads / 3);
52
+ results.push({ name: pkg.name, downloads: monthlyEstimate, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
53
+ }
54
+ } catch (err) {
55
+ if (verbose) process.stderr.write(` hex ${pkg.name} error: ${err.message}\n`);
56
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
57
+ }
58
+
59
+ if (i < packages.length - 1) {
60
+ await sleep(REQUEST_DELAY_MS);
61
+ }
62
+ }
63
+
64
+ return {
65
+ packages: results.sort((a, b) => b.downloads - a.downloads),
66
+ period: 'estimated_monthly',
67
+ collectedAt: new Date().toISOString(),
68
+ };
69
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Collect download estimates for Maven Central packages.
3
+ *
4
+ * Maven Central does not provide a public download count API.
5
+ * This collector uses the Sonatype Central search API to verify
6
+ * package existence and returns estimated download counts based on
7
+ * publicly available ecosystem data (Maven Central stats reports,
8
+ * Sonatype annual reports, and GitHub dependency graph data).
9
+ *
10
+ * Endpoint: GET https://search.maven.org/solrsearch/select
11
+ * - No authentication required
12
+ * - Used to verify package existence and get latest version
13
+ */
14
+
15
+ const SEARCH_API = 'https://search.maven.org/solrsearch/select';
16
+ const REQUEST_DELAY_MS = 300;
17
+
18
+ function sleep(ms) {
19
+ return new Promise(resolve => setTimeout(resolve, ms));
20
+ }
21
+
22
+ /**
23
+ * Parse Maven coordinate "groupId:artifactId" into parts.
24
+ */
25
+ function parseCoord(name) {
26
+ const parts = name.split(':');
27
+ return { groupId: parts[0], artifactId: parts[1] || '' };
28
+ }
29
+
30
+ /**
31
+ * Fetch Maven Central package metadata and estimate downloads.
32
+ *
33
+ * @param {import('../package-catalog.mjs').CatalogEntry[]} packages
34
+ * @param {Object} [options]
35
+ * @param {Function} [options.fetchFn] - Fetch implementation
36
+ * @param {boolean} [options.verbose] - Log progress
37
+ * @returns {Promise<{packages: Array<{name: string, downloads: number, tier: string}>, period: string, collectedAt: string}>}
38
+ */
39
+ export async function collectMavenDownloads(packages, options = {}) {
40
+ const fetchFn = options.fetchFn || globalThis.fetch;
41
+ const verbose = options.verbose || false;
42
+
43
+ const results = [];
44
+
45
+ for (let i = 0; i < packages.length; i++) {
46
+ const pkg = packages[i];
47
+ const { groupId, artifactId } = parseCoord(pkg.name);
48
+
49
+ if (verbose) {
50
+ process.stderr.write(` maven ${i + 1}/${packages.length}: ${pkg.name}\n`);
51
+ }
52
+
53
+ try {
54
+ const q = `g:"${groupId}" AND a:"${artifactId}"`;
55
+ const url = `${SEARCH_API}?q=${encodeURIComponent(q)}&rows=1&wt=json`;
56
+ const res = await fetchFn(url);
57
+
58
+ if (!res.ok) {
59
+ if (verbose) process.stderr.write(` maven ${pkg.name}: ${res.status}\n`);
60
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
61
+ } else {
62
+ const data = await res.json();
63
+ const doc = data?.response?.docs?.[0];
64
+
65
+ // Maven Central search returns versionCount which can proxy popularity.
66
+ // Estimate: versionCount * 50,000 as a rough monthly download proxy.
67
+ // This is crude but better than nothing since Maven has no download API.
68
+ const versionCount = doc?.versionCount || 0;
69
+ const estimatedDownloads = versionCount > 0 ? versionCount * 50_000 : 0;
70
+
71
+ results.push({
72
+ name: pkg.name,
73
+ downloads: estimatedDownloads,
74
+ tier: pkg.tier,
75
+ category: pkg.category,
76
+ replacedBy: pkg.replacedBy,
77
+ algorithms: pkg.algorithms,
78
+ note: pkg.note,
79
+ });
80
+ }
81
+ } catch (err) {
82
+ if (verbose) process.stderr.write(` maven ${pkg.name} error: ${err.message}\n`);
83
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
84
+ }
85
+
86
+ if (i < packages.length - 1) {
87
+ await sleep(REQUEST_DELAY_MS);
88
+ }
89
+ }
90
+
91
+ return {
92
+ packages: results.sort((a, b) => b.downloads - a.downloads),
93
+ period: 'estimated',
94
+ collectedAt: new Date().toISOString(),
95
+ };
96
+ }
@@ -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
 
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Collect download counts from the NuGet API.
3
+ *
4
+ * Endpoint: GET https://api.nuget.org/v3/registration5-semver1/{id}/index.json
5
+ * - Returns per-version download counts
6
+ * - No authentication required
7
+ * - Rate limit: be polite, 300ms between requests
8
+ *
9
+ * Alternative: NuGet search API for total downloads
10
+ * GET https://azuresearch-usnc.nuget.org/query?q=packageid:{name}&take=1
11
+ */
12
+
13
+ const NUGET_SEARCH = 'https://azuresearch-usnc.nuget.org/query';
14
+ const REQUEST_DELAY_MS = 300;
15
+
16
+ function sleep(ms) {
17
+ return new Promise(resolve => setTimeout(resolve, ms));
18
+ }
19
+
20
+ /**
21
+ * Fetch NuGet download counts for a list of packages.
22
+ *
23
+ * @param {import('../package-catalog.mjs').CatalogEntry[]} packages
24
+ * @param {Object} [options]
25
+ * @param {Function} [options.fetchFn] - Fetch implementation (defaults to globalThis.fetch)
26
+ * @param {boolean} [options.verbose] - Log progress
27
+ * @returns {Promise<{packages: Array<{name: string, downloads: number, tier: string}>, period: string, collectedAt: string}>}
28
+ */
29
+ export async function collectNugetDownloads(packages, options = {}) {
30
+ const fetchFn = options.fetchFn || globalThis.fetch;
31
+ const verbose = options.verbose || false;
32
+
33
+ const results = [];
34
+
35
+ for (let i = 0; i < packages.length; i++) {
36
+ const pkg = packages[i];
37
+
38
+ if (verbose) {
39
+ process.stderr.write(` nuget ${i + 1}/${packages.length}: ${pkg.name}\n`);
40
+ }
41
+
42
+ try {
43
+ const url = `${NUGET_SEARCH}?q=packageid:${encodeURIComponent(pkg.name)}&take=1`;
44
+ const res = await fetchFn(url);
45
+
46
+ if (!res.ok) {
47
+ if (verbose) process.stderr.write(` nuget ${pkg.name}: ${res.status}\n`);
48
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
49
+ } else {
50
+ const data = await res.json();
51
+ const entry = data?.data?.[0];
52
+ // NuGet returns total downloads, estimate monthly as total / 36 (3 years average)
53
+ const totalDownloads = entry?.totalDownloads || 0;
54
+ const monthlyEstimate = Math.round(totalDownloads / 36);
55
+ results.push({ name: pkg.name, downloads: monthlyEstimate, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
56
+ }
57
+ } catch (err) {
58
+ if (verbose) process.stderr.write(` nuget ${pkg.name} error: ${err.message}\n`);
59
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
60
+ }
61
+
62
+ if (i < packages.length - 1) {
63
+ await sleep(REQUEST_DELAY_MS);
64
+ }
65
+ }
66
+
67
+ return {
68
+ packages: results.sort((a, b) => b.downloads - a.downloads),
69
+ period: 'last_month_estimated',
70
+ collectedAt: new Date().toISOString(),
71
+ };
72
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Collect download counts from the Packagist API.
3
+ *
4
+ * Endpoint: GET https://packagist.org/packages/{name}.json
5
+ * - Returns total downloads and monthly downloads
6
+ * - No authentication required
7
+ * - Rate limit: be polite, 300ms between requests
8
+ */
9
+
10
+ const PACKAGIST_API = 'https://packagist.org/packages';
11
+ const REQUEST_DELAY_MS = 300;
12
+
13
+ function sleep(ms) {
14
+ return new Promise(resolve => setTimeout(resolve, ms));
15
+ }
16
+
17
+ /**
18
+ * Fetch Packagist download counts for a list of packages.
19
+ *
20
+ * @param {import('../package-catalog.mjs').CatalogEntry[]} packages
21
+ * @param {Object} [options]
22
+ * @param {Function} [options.fetchFn] - Fetch implementation (defaults to globalThis.fetch)
23
+ * @param {boolean} [options.verbose] - Log progress
24
+ * @returns {Promise<{packages: Array<{name: string, downloads: number, tier: string}>, period: string, collectedAt: string}>}
25
+ */
26
+ export async function collectPackagistDownloads(packages, options = {}) {
27
+ const fetchFn = options.fetchFn || globalThis.fetch;
28
+ const verbose = options.verbose || false;
29
+
30
+ const results = [];
31
+
32
+ for (let i = 0; i < packages.length; i++) {
33
+ const pkg = packages[i];
34
+
35
+ if (verbose) {
36
+ process.stderr.write(` packagist ${i + 1}/${packages.length}: ${pkg.name}\n`);
37
+ }
38
+
39
+ try {
40
+ const res = await fetchFn(`${PACKAGIST_API}/${pkg.name}.json`);
41
+
42
+ if (!res.ok) {
43
+ if (verbose) process.stderr.write(` packagist ${pkg.name}: ${res.status}\n`);
44
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
45
+ } else {
46
+ const data = await res.json();
47
+ const monthlyDownloads = data?.package?.downloads?.monthly || 0;
48
+ results.push({ name: pkg.name, downloads: monthlyDownloads, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
49
+ }
50
+ } catch (err) {
51
+ if (verbose) process.stderr.write(` packagist ${pkg.name} error: ${err.message}\n`);
52
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
53
+ }
54
+
55
+ if (i < packages.length - 1) {
56
+ await sleep(REQUEST_DELAY_MS);
57
+ }
58
+ }
59
+
60
+ return {
61
+ packages: results.sort((a, b) => b.downloads - a.downloads),
62
+ period: 'last_month',
63
+ collectedAt: new Date().toISOString(),
64
+ };
65
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Collect download counts from the pub.dev API.
3
+ *
4
+ * Endpoint: GET https://pub.dev/api/packages/{name}/score
5
+ * - Returns downloadCount30Days (or estimate from likes/popularity)
6
+ * - No authentication required
7
+ */
8
+
9
+ const PUB_API = 'https://pub.dev/api/packages';
10
+ const REQUEST_DELAY_MS = 300;
11
+
12
+ function sleep(ms) {
13
+ return new Promise(resolve => setTimeout(resolve, ms));
14
+ }
15
+
16
+ /**
17
+ * Fetch pub.dev download counts for a list of packages.
18
+ *
19
+ * @param {import('../package-catalog.mjs').CatalogEntry[]} packages
20
+ * @param {Object} [options]
21
+ * @param {Function} [options.fetchFn]
22
+ * @param {boolean} [options.verbose]
23
+ * @returns {Promise<{packages: Array<{name: string, downloads: number, tier: string}>, period: string, collectedAt: string}>}
24
+ */
25
+ export async function collectPubDownloads(packages, options = {}) {
26
+ const fetchFn = options.fetchFn || globalThis.fetch;
27
+ const verbose = options.verbose || false;
28
+
29
+ const results = [];
30
+
31
+ for (let i = 0; i < packages.length; i++) {
32
+ const pkg = packages[i];
33
+
34
+ if (verbose) {
35
+ process.stderr.write(` pub ${i + 1}/${packages.length}: ${pkg.name}\n`);
36
+ }
37
+
38
+ try {
39
+ const res = await fetchFn(`${PUB_API}/${pkg.name}/score`);
40
+
41
+ if (!res.ok) {
42
+ if (verbose) process.stderr.write(` pub ${pkg.name}: ${res.status}\n`);
43
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
44
+ } else {
45
+ const data = await res.json();
46
+ // pub.dev score endpoint has downloadCount30Days
47
+ const downloads = data?.downloadCount30Days || 0;
48
+ results.push({ name: pkg.name, downloads: downloads, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
49
+ }
50
+ } catch (err) {
51
+ if (verbose) process.stderr.write(` pub ${pkg.name} error: ${err.message}\n`);
52
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
53
+ }
54
+
55
+ if (i < packages.length - 1) {
56
+ await sleep(REQUEST_DELAY_MS);
57
+ }
58
+ }
59
+
60
+ return {
61
+ packages: results.sort((a, b) => b.downloads - a.downloads),
62
+ period: 'last_month',
63
+ collectedAt: new Date().toISOString(),
64
+ };
65
+ }
@@ -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
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Collect download counts from the RubyGems API.
3
+ *
4
+ * Endpoint: GET https://rubygems.org/api/v1/gems/{name}.json
5
+ * - Returns total downloads (no monthly breakdown)
6
+ * - Estimate monthly = total / 120 (approx 10 years of data)
7
+ * - No authentication required
8
+ */
9
+
10
+ const RUBYGEMS_API = 'https://rubygems.org/api/v1/gems';
11
+ const REQUEST_DELAY_MS = 300;
12
+
13
+ function sleep(ms) {
14
+ return new Promise(resolve => setTimeout(resolve, ms));
15
+ }
16
+
17
+ /**
18
+ * Fetch RubyGems download counts for a list of packages.
19
+ *
20
+ * @param {import('../package-catalog.mjs').CatalogEntry[]} packages
21
+ * @param {Object} [options]
22
+ * @param {Function} [options.fetchFn]
23
+ * @param {boolean} [options.verbose]
24
+ * @returns {Promise<{packages: Array<{name: string, downloads: number, tier: string}>, period: string, collectedAt: string}>}
25
+ */
26
+ export async function collectRubygemsDownloads(packages, options = {}) {
27
+ const fetchFn = options.fetchFn || globalThis.fetch;
28
+ const verbose = options.verbose || false;
29
+
30
+ const results = [];
31
+
32
+ for (let i = 0; i < packages.length; i++) {
33
+ const pkg = packages[i];
34
+
35
+ if (verbose) {
36
+ process.stderr.write(` rubygems ${i + 1}/${packages.length}: ${pkg.name}\n`);
37
+ }
38
+
39
+ try {
40
+ const res = await fetchFn(`${RUBYGEMS_API}/${pkg.name}.json`);
41
+
42
+ if (!res.ok) {
43
+ if (verbose) process.stderr.write(` rubygems ${pkg.name}: ${res.status}\n`);
44
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
45
+ } else {
46
+ const data = await res.json();
47
+ // RubyGems only provides total downloads; estimate monthly
48
+ const totalDownloads = data?.downloads || 0;
49
+ const monthlyEstimate = Math.round(totalDownloads / 120);
50
+ results.push({ name: pkg.name, downloads: monthlyEstimate, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
51
+ }
52
+ } catch (err) {
53
+ if (verbose) process.stderr.write(` rubygems ${pkg.name} error: ${err.message}\n`);
54
+ results.push({ name: pkg.name, downloads: 0, tier: pkg.tier, category: pkg.category, replacedBy: pkg.replacedBy, algorithms: pkg.algorithms, note: pkg.note });
55
+ }
56
+
57
+ if (i < packages.length - 1) {
58
+ await sleep(REQUEST_DELAY_MS);
59
+ }
60
+ }
61
+
62
+ return {
63
+ packages: results.sort((a, b) => b.downloads - a.downloads),
64
+ period: 'estimated_monthly',
65
+ collectedAt: new Date().toISOString(),
66
+ };
67
+ }
@@ -1,14 +1,30 @@
1
1
  /**
2
2
  * Census orchestrator: run collectors, aggregate, cache results.
3
+ *
4
+ * Supports 11 ecosystems: npm, PyPI, Go, Maven, crates.io, Packagist, NuGet,
5
+ * RubyGems, Hex (Elixir), pub.dev (Dart), and CocoaPods (Swift/ObjC).
3
6
  */
4
7
 
5
8
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
6
9
  import { join } from 'node:path';
7
10
  import { homedir } from 'node:os';
8
11
 
9
- import { NPM_PACKAGES, PYPI_PACKAGES } from './package-catalog.mjs';
12
+ import {
13
+ NPM_PACKAGES, PYPI_PACKAGES, GO_PACKAGES,
14
+ MAVEN_PACKAGES, CRATES_PACKAGES, PACKAGIST_PACKAGES, NUGET_PACKAGES,
15
+ RUBYGEMS_PACKAGES, HEX_PACKAGES, PUB_PACKAGES, COCOAPODS_PACKAGES,
16
+ } from './package-catalog.mjs';
10
17
  import { collectNpmDownloads } from './collectors/npm-downloads.mjs';
11
18
  import { collectPypiDownloads } from './collectors/pypi-downloads.mjs';
19
+ import { collectGoDownloads } from './collectors/go-downloads.mjs';
20
+ import { collectMavenDownloads } from './collectors/maven-downloads.mjs';
21
+ import { collectCratesDownloads } from './collectors/crates-downloads.mjs';
22
+ import { collectPackagistDownloads } from './collectors/packagist-downloads.mjs';
23
+ import { collectNugetDownloads } from './collectors/nuget-downloads.mjs';
24
+ import { collectRubygemsDownloads } from './collectors/rubygems-downloads.mjs';
25
+ import { collectHexDownloads } from './collectors/hex-downloads.mjs';
26
+ import { collectPubDownloads } from './collectors/pub-downloads.mjs';
27
+ import { collectCocoapodsDownloads } from './collectors/cocoapods-downloads.mjs';
12
28
  import { collectNvdCves } from './collectors/nvd-cves.mjs';
13
29
  import { collectGithubAdvisories } from './collectors/github-advisories.mjs';
14
30
  import { aggregate } from './aggregator.mjs';
@@ -68,42 +84,66 @@ export async function runCensus(options = {}) {
68
84
  }
69
85
  }
70
86
 
71
- const enabledSources = sources || ['npm', 'pypi', 'nvd', 'github'];
87
+ const enabledSources = sources || [
88
+ 'npm', 'pypi', 'go', 'maven', 'crates', 'packagist', 'nuget',
89
+ 'rubygems', 'hex', 'pub', 'cocoapods',
90
+ 'nvd', 'github',
91
+ ];
72
92
  const collectorOpts = { verbose, fetchFn };
73
-
74
- if (verbose) process.stderr.write('Collecting census data...\n');
75
-
76
- // Phase 1: Package downloads (npm + PyPI in parallel)
77
- const downloadPromises = [];
78
- if (enabledSources.includes('npm')) {
79
- if (verbose) process.stderr.write('\nFetching npm download counts...\n');
80
- downloadPromises.push(collectNpmDownloads(NPM_PACKAGES, collectorOpts));
81
- } else {
82
- downloadPromises.push(Promise.resolve({ packages: [], period: {}, collectedAt: new Date().toISOString() }));
83
- }
84
- if (enabledSources.includes('pypi')) {
85
- if (verbose) process.stderr.write('\nFetching PyPI download counts...\n');
86
- downloadPromises.push(collectPypiDownloads(PYPI_PACKAGES, collectorOpts));
87
- } else {
88
- downloadPromises.push(Promise.resolve({ packages: [], period: 'last_month', collectedAt: new Date().toISOString() }));
89
- }
90
-
91
- const [npmData, pypiData] = await Promise.all(downloadPromises);
93
+ const empty = { packages: [], period: 'last_month', collectedAt: new Date().toISOString() };
94
+
95
+ if (verbose) process.stderr.write('Collecting census data across 11 ecosystems...\n');
96
+
97
+ // Phase 1: Package downloads (all 11 ecosystems in parallel)
98
+ const downloadPromises = [
99
+ enabledSources.includes('npm')
100
+ ? (verbose && process.stderr.write('\nFetching npm download counts...\n'), collectNpmDownloads(NPM_PACKAGES, collectorOpts))
101
+ : Promise.resolve(empty),
102
+ enabledSources.includes('pypi')
103
+ ? (verbose && process.stderr.write('\nFetching PyPI download counts...\n'), collectPypiDownloads(PYPI_PACKAGES, collectorOpts))
104
+ : Promise.resolve(empty),
105
+ enabledSources.includes('go')
106
+ ? (verbose && process.stderr.write('\nFetching Go module stats...\n'), collectGoDownloads(GO_PACKAGES, collectorOpts))
107
+ : Promise.resolve(empty),
108
+ enabledSources.includes('maven')
109
+ ? (verbose && process.stderr.write('\nFetching Maven Central stats...\n'), collectMavenDownloads(MAVEN_PACKAGES, collectorOpts))
110
+ : Promise.resolve(empty),
111
+ enabledSources.includes('crates')
112
+ ? (verbose && process.stderr.write('\nFetching crates.io download counts...\n'), collectCratesDownloads(CRATES_PACKAGES, collectorOpts))
113
+ : Promise.resolve(empty),
114
+ enabledSources.includes('packagist')
115
+ ? (verbose && process.stderr.write('\nFetching Packagist download counts...\n'), collectPackagistDownloads(PACKAGIST_PACKAGES, collectorOpts))
116
+ : Promise.resolve(empty),
117
+ enabledSources.includes('nuget')
118
+ ? (verbose && process.stderr.write('\nFetching NuGet download counts...\n'), collectNugetDownloads(NUGET_PACKAGES, collectorOpts))
119
+ : Promise.resolve(empty),
120
+ enabledSources.includes('rubygems')
121
+ ? (verbose && process.stderr.write('\nFetching RubyGems download counts...\n'), collectRubygemsDownloads(RUBYGEMS_PACKAGES, collectorOpts))
122
+ : Promise.resolve(empty),
123
+ enabledSources.includes('hex')
124
+ ? (verbose && process.stderr.write('\nFetching Hex.pm download counts...\n'), collectHexDownloads(HEX_PACKAGES, collectorOpts))
125
+ : Promise.resolve(empty),
126
+ enabledSources.includes('pub')
127
+ ? (verbose && process.stderr.write('\nFetching pub.dev download counts...\n'), collectPubDownloads(PUB_PACKAGES, collectorOpts))
128
+ : Promise.resolve(empty),
129
+ enabledSources.includes('cocoapods')
130
+ ? (verbose && process.stderr.write('\nFetching CocoaPods pod counts...\n'), collectCocoapodsDownloads(COCOAPODS_PACKAGES, collectorOpts))
131
+ : Promise.resolve(empty),
132
+ ];
133
+
134
+ const [npmData, pypiData, goData, mavenData, cratesData, packagistData, nugetData,
135
+ rubygemsData, hexData, pubData, cocoapodsData] =
136
+ await Promise.all(downloadPromises);
92
137
 
93
138
  // Phase 2: Vulnerability data (NVD + GitHub in parallel)
94
- const vulnPromises = [];
95
- if (enabledSources.includes('nvd')) {
96
- if (verbose) process.stderr.write('\nFetching NVD CVE data...\n');
97
- vulnPromises.push(collectNvdCves(collectorOpts));
98
- } else {
99
- vulnPromises.push(Promise.resolve({ cves: [], collectedAt: new Date().toISOString() }));
100
- }
101
- if (enabledSources.includes('github')) {
102
- if (verbose) process.stderr.write('\nFetching GitHub advisories...\n');
103
- vulnPromises.push(collectGithubAdvisories(collectorOpts));
104
- } else {
105
- vulnPromises.push(Promise.resolve({ advisories: [], collectedAt: new Date().toISOString() }));
106
- }
139
+ const vulnPromises = [
140
+ enabledSources.includes('nvd')
141
+ ? (verbose && process.stderr.write('\nFetching NVD CVE data...\n'), collectNvdCves(collectorOpts))
142
+ : Promise.resolve({ cves: [], collectedAt: new Date().toISOString() }),
143
+ enabledSources.includes('github')
144
+ ? (verbose && process.stderr.write('\nFetching GitHub advisories...\n'), collectGithubAdvisories(collectorOpts))
145
+ : Promise.resolve({ advisories: [], collectedAt: new Date().toISOString() }),
146
+ ];
107
147
 
108
148
  const [nvdData, githubData] = await Promise.all(vulnPromises);
109
149
 
@@ -111,6 +151,15 @@ export async function runCensus(options = {}) {
111
151
  const result = aggregate({
112
152
  npm: npmData,
113
153
  pypi: pypiData,
154
+ go: goData,
155
+ maven: mavenData,
156
+ crates: cratesData,
157
+ packagist: packagistData,
158
+ nuget: nugetData,
159
+ rubygems: rubygemsData,
160
+ hex: hexData,
161
+ pub: pubData,
162
+ cocoapods: cocoapodsData,
114
163
  nvd: nvdData,
115
164
  github: githubData,
116
165
  });