@mcptoolshop/registry-stats 0.3.1 → 0.4.0
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/dist/cli.js +216 -15
- package/dist/index.cjs +154 -15
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +154 -15
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -19,22 +19,64 @@ var RegistryError = class extends Error {
|
|
|
19
19
|
var RETRYABLE = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
20
20
|
var MAX_RETRIES = 3;
|
|
21
21
|
var BASE_DELAY = 1e3;
|
|
22
|
+
var registryLocks = /* @__PURE__ */ new Map();
|
|
23
|
+
var REGISTRY_DELAYS = {
|
|
24
|
+
npm: 400,
|
|
25
|
+
// ~2.5 req/s — safe for 54+ scoped packages
|
|
26
|
+
pypi: 2200,
|
|
27
|
+
// 30 req/60s = 1 per 2s, with headroom
|
|
28
|
+
docker: 4e3
|
|
29
|
+
// 10 req/3600s — very tight
|
|
30
|
+
};
|
|
31
|
+
var DEFAULT_DELAY = 100;
|
|
32
|
+
function acquireSlot(registry) {
|
|
33
|
+
const minDelay = REGISTRY_DELAYS[registry] ?? DEFAULT_DELAY;
|
|
34
|
+
const prev = registryLocks.get(registry) ?? Promise.resolve();
|
|
35
|
+
const slot = prev.then(() => new Promise((r) => setTimeout(r, minDelay)));
|
|
36
|
+
registryLocks.set(registry, slot);
|
|
37
|
+
return prev;
|
|
38
|
+
}
|
|
22
39
|
async function fetchWithRetry(url, registry, init) {
|
|
40
|
+
let lastError;
|
|
41
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
42
|
+
await acquireSlot(registry);
|
|
43
|
+
const res = await fetch(url, init);
|
|
44
|
+
if (res.status === 404) return null;
|
|
45
|
+
if (res.ok) return res.json();
|
|
46
|
+
const retryAfter = res.headers.get("retry-after");
|
|
47
|
+
const retryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : void 0;
|
|
48
|
+
lastError = new RegistryError(
|
|
49
|
+
registry,
|
|
50
|
+
res.status,
|
|
51
|
+
`${res.statusText}: ${url}`,
|
|
52
|
+
retryAfterSeconds
|
|
53
|
+
);
|
|
54
|
+
if (!RETRYABLE.has(res.status) || attempt === MAX_RETRIES) break;
|
|
55
|
+
const backoff = BASE_DELAY * Math.pow(2, attempt);
|
|
56
|
+
const retryAfterMs = retryAfterSeconds ? retryAfterSeconds * 1e3 : 0;
|
|
57
|
+
const delay = Math.max(backoff, retryAfterMs);
|
|
58
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
59
|
+
}
|
|
60
|
+
throw lastError;
|
|
61
|
+
}
|
|
62
|
+
async function fetchDirect(url, registry, init) {
|
|
23
63
|
let lastError;
|
|
24
64
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
25
65
|
const res = await fetch(url, init);
|
|
26
66
|
if (res.status === 404) return null;
|
|
27
67
|
if (res.ok) return res.json();
|
|
28
68
|
const retryAfter = res.headers.get("retry-after");
|
|
29
|
-
const
|
|
69
|
+
const retryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : void 0;
|
|
30
70
|
lastError = new RegistryError(
|
|
31
71
|
registry,
|
|
32
72
|
res.status,
|
|
33
73
|
`${res.statusText}: ${url}`,
|
|
34
|
-
|
|
74
|
+
retryAfterSeconds
|
|
35
75
|
);
|
|
36
76
|
if (!RETRYABLE.has(res.status) || attempt === MAX_RETRIES) break;
|
|
37
|
-
const
|
|
77
|
+
const backoff = BASE_DELAY * Math.pow(2, attempt);
|
|
78
|
+
const retryAfterMs = retryAfterSeconds ? retryAfterSeconds * 1e3 : 0;
|
|
79
|
+
const delay = Math.max(backoff, retryAfterMs);
|
|
38
80
|
await new Promise((r) => setTimeout(r, delay));
|
|
39
81
|
}
|
|
40
82
|
throw lastError;
|
|
@@ -45,20 +87,22 @@ var API = "https://api.npmjs.org/downloads";
|
|
|
45
87
|
var npm = {
|
|
46
88
|
name: "npm",
|
|
47
89
|
async getStats(pkg) {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
90
|
+
const end = /* @__PURE__ */ new Date();
|
|
91
|
+
const start = new Date(end);
|
|
92
|
+
start.setDate(start.getDate() - 30);
|
|
93
|
+
const data = await fetchWithRetry(
|
|
94
|
+
`${API}/range/${fmt(start)}:${fmt(end)}/${pkg}`,
|
|
95
|
+
"npm"
|
|
96
|
+
);
|
|
97
|
+
if (!data || !data.downloads || data.downloads.length === 0) return null;
|
|
98
|
+
const days = data.downloads;
|
|
99
|
+
const lastDay = days[days.length - 1]?.downloads ?? 0;
|
|
100
|
+
const lastWeek = days.slice(-7).reduce((s, d) => s + d.downloads, 0);
|
|
101
|
+
const lastMonth = days.reduce((s, d) => s + d.downloads, 0);
|
|
54
102
|
return {
|
|
55
103
|
registry: "npm",
|
|
56
104
|
package: pkg,
|
|
57
|
-
downloads: {
|
|
58
|
-
lastDay: day?.downloads,
|
|
59
|
-
lastWeek: week?.downloads,
|
|
60
|
-
lastMonth: month?.downloads
|
|
61
|
-
},
|
|
105
|
+
downloads: { lastDay, lastWeek, lastMonth },
|
|
62
106
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
63
107
|
};
|
|
64
108
|
},
|
|
@@ -86,6 +130,27 @@ var npm = {
|
|
|
86
130
|
return chunks;
|
|
87
131
|
}
|
|
88
132
|
};
|
|
133
|
+
async function npmBulkPoint(packages, period = "last-month") {
|
|
134
|
+
const result = /* @__PURE__ */ new Map();
|
|
135
|
+
if (packages.length === 0) return result;
|
|
136
|
+
const BATCH_SIZE = 128;
|
|
137
|
+
for (let i = 0; i < packages.length; i += BATCH_SIZE) {
|
|
138
|
+
const batch = packages.slice(i, i + BATCH_SIZE);
|
|
139
|
+
const joined = batch.join(",");
|
|
140
|
+
const data = await fetchDirect(
|
|
141
|
+
`${API}/point/${period}/${joined}`,
|
|
142
|
+
"npm"
|
|
143
|
+
);
|
|
144
|
+
if (data) {
|
|
145
|
+
for (const [name, entry] of Object.entries(data)) {
|
|
146
|
+
if (entry && typeof entry.downloads === "number") {
|
|
147
|
+
result.set(name, entry.downloads);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
89
154
|
function fmt(d) {
|
|
90
155
|
return d.toISOString().slice(0, 10);
|
|
91
156
|
}
|
|
@@ -470,7 +535,12 @@ function createHandler(opts) {
|
|
|
470
535
|
}
|
|
471
536
|
error(res, "Not found", 404);
|
|
472
537
|
} catch (e) {
|
|
473
|
-
|
|
538
|
+
if (e instanceof RegistryError) {
|
|
539
|
+
const status = e.statusCode === 429 ? 429 : e.statusCode === 404 ? 404 : e.statusCode >= 500 ? 502 : 500;
|
|
540
|
+
error(res, e.message, status);
|
|
541
|
+
} else {
|
|
542
|
+
error(res, e.message, 500);
|
|
543
|
+
}
|
|
474
544
|
}
|
|
475
545
|
};
|
|
476
546
|
}
|
|
@@ -566,10 +636,54 @@ stats.bulk = async function bulk(registry, packages, options) {
|
|
|
566
636
|
if (!provider) {
|
|
567
637
|
throw new RegistryError(registry, 0, `Unknown registry "${registry}".`);
|
|
568
638
|
}
|
|
639
|
+
if (registry === "npm" && packages.length > 1) {
|
|
640
|
+
return npmBulkStats(packages, options);
|
|
641
|
+
}
|
|
569
642
|
const concurrency = options?.concurrency ?? 5;
|
|
570
643
|
const limit = pLimit(concurrency);
|
|
571
644
|
return Promise.all(packages.map((pkg) => limit(() => stats(registry, pkg, options))));
|
|
572
645
|
};
|
|
646
|
+
async function npmBulkStats(packages, options) {
|
|
647
|
+
const scoped = [];
|
|
648
|
+
const unscoped = [];
|
|
649
|
+
for (const pkg of packages) {
|
|
650
|
+
if (pkg.startsWith("@")) {
|
|
651
|
+
scoped.push(pkg);
|
|
652
|
+
} else {
|
|
653
|
+
unscoped.push(pkg);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
const bulkMonth = unscoped.length > 0 ? await npmBulkPoint(unscoped, "last-month") : /* @__PURE__ */ new Map();
|
|
657
|
+
const bulkWeek = unscoped.length > 0 ? await npmBulkPoint(unscoped, "last-week") : /* @__PURE__ */ new Map();
|
|
658
|
+
const bulkDay = unscoped.length > 0 ? await npmBulkPoint(unscoped, "last-day") : /* @__PURE__ */ new Map();
|
|
659
|
+
const unscopedResults = /* @__PURE__ */ new Map();
|
|
660
|
+
for (const pkg of unscoped) {
|
|
661
|
+
const month = bulkMonth.get(pkg);
|
|
662
|
+
const week = bulkWeek.get(pkg);
|
|
663
|
+
const day = bulkDay.get(pkg);
|
|
664
|
+
if (month === void 0 && week === void 0 && day === void 0) {
|
|
665
|
+
unscopedResults.set(pkg, null);
|
|
666
|
+
} else {
|
|
667
|
+
unscopedResults.set(pkg, {
|
|
668
|
+
registry: "npm",
|
|
669
|
+
package: pkg,
|
|
670
|
+
downloads: {
|
|
671
|
+
lastDay: day,
|
|
672
|
+
lastWeek: week,
|
|
673
|
+
lastMonth: month
|
|
674
|
+
},
|
|
675
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
const limit = pLimit(1);
|
|
680
|
+
const scopedResults = await Promise.all(
|
|
681
|
+
scoped.map((pkg) => limit(() => stats("npm", pkg, options)))
|
|
682
|
+
);
|
|
683
|
+
const scopedMap = /* @__PURE__ */ new Map();
|
|
684
|
+
scoped.forEach((pkg, i) => scopedMap.set(pkg, scopedResults[i]));
|
|
685
|
+
return packages.map((pkg) => unscopedResults.get(pkg) ?? scopedMap.get(pkg) ?? null);
|
|
686
|
+
}
|
|
573
687
|
stats.range = async function range(registry, pkg, start, end, options) {
|
|
574
688
|
const provider = providers[registry];
|
|
575
689
|
if (!provider) {
|
|
@@ -613,6 +727,31 @@ stats.compare = async function compare(pkg, registries, options) {
|
|
|
613
727
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
614
728
|
};
|
|
615
729
|
};
|
|
730
|
+
stats.mine = async function mine(maintainer, options) {
|
|
731
|
+
const packages = [];
|
|
732
|
+
const PAGE_SIZE = 250;
|
|
733
|
+
let offset = 0;
|
|
734
|
+
while (true) {
|
|
735
|
+
const url = `https://registry.npmjs.org/-/v1/search?text=maintainer:${encodeURIComponent(maintainer)}&size=${PAGE_SIZE}&from=${offset}`;
|
|
736
|
+
const data = await fetchDirect(url, "npm");
|
|
737
|
+
if (!data || data.objects.length === 0) break;
|
|
738
|
+
for (const obj of data.objects) {
|
|
739
|
+
packages.push(obj.package.name);
|
|
740
|
+
}
|
|
741
|
+
offset += data.objects.length;
|
|
742
|
+
if (offset >= data.total) break;
|
|
743
|
+
}
|
|
744
|
+
if (packages.length === 0) return [];
|
|
745
|
+
const results = [];
|
|
746
|
+
const bulkResults = await stats.bulk("npm", packages, options);
|
|
747
|
+
for (let i = 0; i < packages.length; i++) {
|
|
748
|
+
const r = bulkResults[i];
|
|
749
|
+
if (r) results.push(r);
|
|
750
|
+
options?.onProgress?.(i + 1, packages.length, packages[i]);
|
|
751
|
+
}
|
|
752
|
+
results.sort((a, b) => (b.downloads.lastMonth ?? 0) - (a.downloads.lastMonth ?? 0));
|
|
753
|
+
return results;
|
|
754
|
+
};
|
|
616
755
|
|
|
617
756
|
// src/cli.ts
|
|
618
757
|
function usage() {
|
|
@@ -625,6 +764,8 @@ Usage: registry-stats [package] [options]
|
|
|
625
764
|
Options:
|
|
626
765
|
--registry, -r Registry to query (npm, pypi, nuget, vscode, docker)
|
|
627
766
|
Omit to query all registries
|
|
767
|
+
--mine Discover and show stats for all npm packages by a maintainer
|
|
768
|
+
e.g. registry-stats --mine mikefrilot
|
|
628
769
|
--range Date range for time series (e.g. 2025-01-01:2025-06-30)
|
|
629
770
|
Only npm and pypi support this
|
|
630
771
|
--compare Compare package across registries side-by-side
|
|
@@ -640,6 +781,8 @@ Examples:
|
|
|
640
781
|
registry-stats express
|
|
641
782
|
registry-stats express -r npm
|
|
642
783
|
registry-stats express --compare
|
|
784
|
+
registry-stats --mine mikefrilot
|
|
785
|
+
registry-stats --mine mikefrilot --format json
|
|
643
786
|
registry-stats express -r npm --range 2025-01-01:2025-06-30 --format csv
|
|
644
787
|
registry-stats serve --port 8080
|
|
645
788
|
registry-stats --init
|
|
@@ -702,6 +845,37 @@ function printComparison(result) {
|
|
|
702
845
|
}
|
|
703
846
|
console.log();
|
|
704
847
|
}
|
|
848
|
+
function printMineTable(results, maintainer) {
|
|
849
|
+
const withDownloads = results.filter((r) => (r.downloads.lastMonth ?? 0) > 0);
|
|
850
|
+
const noData = results.filter((r) => (r.downloads.lastMonth ?? 0) === 0);
|
|
851
|
+
const totalMonth = results.reduce((s, r) => s + (r.downloads.lastMonth ?? 0), 0);
|
|
852
|
+
const totalWeek = results.reduce((s, r) => s + (r.downloads.lastWeek ?? 0), 0);
|
|
853
|
+
const totalDay = results.reduce((s, r) => s + (r.downloads.lastDay ?? 0), 0);
|
|
854
|
+
const nameWidth = Math.max(7, ...results.map((r) => r.package.length)) + 2;
|
|
855
|
+
const numWidth = 10;
|
|
856
|
+
console.log(`
|
|
857
|
+
${maintainer} \u2014 ${results.length} npm packages
|
|
858
|
+
`);
|
|
859
|
+
console.log(
|
|
860
|
+
` ${"Package".padEnd(nameWidth)}${"Month".padStart(numWidth)}${"Week".padStart(numWidth)}${"Day".padStart(numWidth)}`
|
|
861
|
+
);
|
|
862
|
+
console.log(` ${"\u2500".repeat(nameWidth + numWidth * 3)}`);
|
|
863
|
+
for (const r of withDownloads) {
|
|
864
|
+
console.log(
|
|
865
|
+
` ${r.package.padEnd(nameWidth)}${formatNumber(r.downloads.lastMonth).padStart(numWidth)}${formatNumber(r.downloads.lastWeek).padStart(numWidth)}${formatNumber(r.downloads.lastDay).padStart(numWidth)}`
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
console.log(` ${"\u2500".repeat(nameWidth + numWidth * 3)}`);
|
|
869
|
+
console.log(
|
|
870
|
+
` ${"TOTAL".padEnd(nameWidth)}${formatNumber(totalMonth).padStart(numWidth)}${formatNumber(totalWeek).padStart(numWidth)}${formatNumber(totalDay).padStart(numWidth)}`
|
|
871
|
+
);
|
|
872
|
+
if (noData.length > 0) {
|
|
873
|
+
console.log(`
|
|
874
|
+
${noData.length} package(s) with no download data yet:`);
|
|
875
|
+
console.log(` ${noData.map((r) => r.package).join(", ")}`);
|
|
876
|
+
}
|
|
877
|
+
console.log();
|
|
878
|
+
}
|
|
705
879
|
function buildOptions(config) {
|
|
706
880
|
const opts = {};
|
|
707
881
|
if (!config) return opts;
|
|
@@ -751,6 +925,26 @@ async function runConfigPackages(config, format) {
|
|
|
751
925
|
}
|
|
752
926
|
console.log();
|
|
753
927
|
}
|
|
928
|
+
async function runMine(maintainer, format, config) {
|
|
929
|
+
const opts = buildOptions(config);
|
|
930
|
+
process.stderr.write(` Discovering packages for ${maintainer}...`);
|
|
931
|
+
const results = await stats.mine(maintainer, {
|
|
932
|
+
...opts,
|
|
933
|
+
onProgress(done, total, pkg) {
|
|
934
|
+
process.stderr.write(`\r Fetching stats... ${done}/${total} (${pkg})${"".padEnd(20)}`);
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
process.stderr.write("\r" + " ".repeat(80) + "\r");
|
|
938
|
+
if (results.length === 0) {
|
|
939
|
+
console.error(`No packages found for maintainer "${maintainer}".`);
|
|
940
|
+
process.exit(1);
|
|
941
|
+
}
|
|
942
|
+
if (format === "json") {
|
|
943
|
+
console.log(JSON.stringify(results, null, 2));
|
|
944
|
+
} else {
|
|
945
|
+
printMineTable(results, maintainer);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
754
948
|
async function main() {
|
|
755
949
|
const args = process.argv.slice(2);
|
|
756
950
|
if (args.includes("--help") || args.includes("-h")) {
|
|
@@ -782,6 +976,7 @@ async function main() {
|
|
|
782
976
|
let range2;
|
|
783
977
|
let format = "table";
|
|
784
978
|
let compare2 = false;
|
|
979
|
+
let mineUser;
|
|
785
980
|
for (let i = 0; i < args.length; i++) {
|
|
786
981
|
if ((args[i] === "--registry" || args[i] === "-r") && args[i + 1]) {
|
|
787
982
|
registry = args[++i];
|
|
@@ -793,11 +988,17 @@ async function main() {
|
|
|
793
988
|
format = "json";
|
|
794
989
|
} else if (args[i] === "--compare") {
|
|
795
990
|
compare2 = true;
|
|
991
|
+
} else if (args[i] === "--mine" && args[i + 1]) {
|
|
992
|
+
mineUser = args[++i];
|
|
796
993
|
} else if (!args[i].startsWith("-") && !pkg) {
|
|
797
994
|
pkg = args[i];
|
|
798
995
|
}
|
|
799
996
|
}
|
|
800
997
|
const config = loadConfig();
|
|
998
|
+
if (mineUser) {
|
|
999
|
+
await runMine(mineUser, format, config);
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
801
1002
|
if (!pkg) {
|
|
802
1003
|
if (!config) {
|
|
803
1004
|
usage();
|
package/dist/index.cjs
CHANGED
|
@@ -48,22 +48,64 @@ var RegistryError = class extends Error {
|
|
|
48
48
|
var RETRYABLE = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
49
49
|
var MAX_RETRIES = 3;
|
|
50
50
|
var BASE_DELAY = 1e3;
|
|
51
|
+
var registryLocks = /* @__PURE__ */ new Map();
|
|
52
|
+
var REGISTRY_DELAYS = {
|
|
53
|
+
npm: 400,
|
|
54
|
+
// ~2.5 req/s — safe for 54+ scoped packages
|
|
55
|
+
pypi: 2200,
|
|
56
|
+
// 30 req/60s = 1 per 2s, with headroom
|
|
57
|
+
docker: 4e3
|
|
58
|
+
// 10 req/3600s — very tight
|
|
59
|
+
};
|
|
60
|
+
var DEFAULT_DELAY = 100;
|
|
61
|
+
function acquireSlot(registry) {
|
|
62
|
+
const minDelay = REGISTRY_DELAYS[registry] ?? DEFAULT_DELAY;
|
|
63
|
+
const prev = registryLocks.get(registry) ?? Promise.resolve();
|
|
64
|
+
const slot = prev.then(() => new Promise((r) => setTimeout(r, minDelay)));
|
|
65
|
+
registryLocks.set(registry, slot);
|
|
66
|
+
return prev;
|
|
67
|
+
}
|
|
51
68
|
async function fetchWithRetry(url, registry, init) {
|
|
69
|
+
let lastError;
|
|
70
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
71
|
+
await acquireSlot(registry);
|
|
72
|
+
const res = await fetch(url, init);
|
|
73
|
+
if (res.status === 404) return null;
|
|
74
|
+
if (res.ok) return res.json();
|
|
75
|
+
const retryAfter = res.headers.get("retry-after");
|
|
76
|
+
const retryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : void 0;
|
|
77
|
+
lastError = new RegistryError(
|
|
78
|
+
registry,
|
|
79
|
+
res.status,
|
|
80
|
+
`${res.statusText}: ${url}`,
|
|
81
|
+
retryAfterSeconds
|
|
82
|
+
);
|
|
83
|
+
if (!RETRYABLE.has(res.status) || attempt === MAX_RETRIES) break;
|
|
84
|
+
const backoff = BASE_DELAY * Math.pow(2, attempt);
|
|
85
|
+
const retryAfterMs = retryAfterSeconds ? retryAfterSeconds * 1e3 : 0;
|
|
86
|
+
const delay = Math.max(backoff, retryAfterMs);
|
|
87
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
88
|
+
}
|
|
89
|
+
throw lastError;
|
|
90
|
+
}
|
|
91
|
+
async function fetchDirect(url, registry, init) {
|
|
52
92
|
let lastError;
|
|
53
93
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
54
94
|
const res = await fetch(url, init);
|
|
55
95
|
if (res.status === 404) return null;
|
|
56
96
|
if (res.ok) return res.json();
|
|
57
97
|
const retryAfter = res.headers.get("retry-after");
|
|
58
|
-
const
|
|
98
|
+
const retryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : void 0;
|
|
59
99
|
lastError = new RegistryError(
|
|
60
100
|
registry,
|
|
61
101
|
res.status,
|
|
62
102
|
`${res.statusText}: ${url}`,
|
|
63
|
-
|
|
103
|
+
retryAfterSeconds
|
|
64
104
|
);
|
|
65
105
|
if (!RETRYABLE.has(res.status) || attempt === MAX_RETRIES) break;
|
|
66
|
-
const
|
|
106
|
+
const backoff = BASE_DELAY * Math.pow(2, attempt);
|
|
107
|
+
const retryAfterMs = retryAfterSeconds ? retryAfterSeconds * 1e3 : 0;
|
|
108
|
+
const delay = Math.max(backoff, retryAfterMs);
|
|
67
109
|
await new Promise((r) => setTimeout(r, delay));
|
|
68
110
|
}
|
|
69
111
|
throw lastError;
|
|
@@ -74,20 +116,22 @@ var API = "https://api.npmjs.org/downloads";
|
|
|
74
116
|
var npm = {
|
|
75
117
|
name: "npm",
|
|
76
118
|
async getStats(pkg) {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
119
|
+
const end = /* @__PURE__ */ new Date();
|
|
120
|
+
const start = new Date(end);
|
|
121
|
+
start.setDate(start.getDate() - 30);
|
|
122
|
+
const data = await fetchWithRetry(
|
|
123
|
+
`${API}/range/${fmt(start)}:${fmt(end)}/${pkg}`,
|
|
124
|
+
"npm"
|
|
125
|
+
);
|
|
126
|
+
if (!data || !data.downloads || data.downloads.length === 0) return null;
|
|
127
|
+
const days = data.downloads;
|
|
128
|
+
const lastDay = days[days.length - 1]?.downloads ?? 0;
|
|
129
|
+
const lastWeek = days.slice(-7).reduce((s, d) => s + d.downloads, 0);
|
|
130
|
+
const lastMonth = days.reduce((s, d) => s + d.downloads, 0);
|
|
83
131
|
return {
|
|
84
132
|
registry: "npm",
|
|
85
133
|
package: pkg,
|
|
86
|
-
downloads: {
|
|
87
|
-
lastDay: day?.downloads,
|
|
88
|
-
lastWeek: week?.downloads,
|
|
89
|
-
lastMonth: month?.downloads
|
|
90
|
-
},
|
|
134
|
+
downloads: { lastDay, lastWeek, lastMonth },
|
|
91
135
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
92
136
|
};
|
|
93
137
|
},
|
|
@@ -115,6 +159,27 @@ var npm = {
|
|
|
115
159
|
return chunks;
|
|
116
160
|
}
|
|
117
161
|
};
|
|
162
|
+
async function npmBulkPoint(packages, period = "last-month") {
|
|
163
|
+
const result = /* @__PURE__ */ new Map();
|
|
164
|
+
if (packages.length === 0) return result;
|
|
165
|
+
const BATCH_SIZE = 128;
|
|
166
|
+
for (let i = 0; i < packages.length; i += BATCH_SIZE) {
|
|
167
|
+
const batch = packages.slice(i, i + BATCH_SIZE);
|
|
168
|
+
const joined = batch.join(",");
|
|
169
|
+
const data = await fetchDirect(
|
|
170
|
+
`${API}/point/${period}/${joined}`,
|
|
171
|
+
"npm"
|
|
172
|
+
);
|
|
173
|
+
if (data) {
|
|
174
|
+
for (const [name, entry] of Object.entries(data)) {
|
|
175
|
+
if (entry && typeof entry.downloads === "number") {
|
|
176
|
+
result.set(name, entry.downloads);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
118
183
|
function fmt(d) {
|
|
119
184
|
return d.toISOString().slice(0, 10);
|
|
120
185
|
}
|
|
@@ -508,7 +573,12 @@ function createHandler(opts) {
|
|
|
508
573
|
}
|
|
509
574
|
error(res, "Not found", 404);
|
|
510
575
|
} catch (e) {
|
|
511
|
-
|
|
576
|
+
if (e instanceof RegistryError) {
|
|
577
|
+
const status = e.statusCode === 429 ? 429 : e.statusCode === 404 ? 404 : e.statusCode >= 500 ? 502 : 500;
|
|
578
|
+
error(res, e.message, status);
|
|
579
|
+
} else {
|
|
580
|
+
error(res, e.message, 500);
|
|
581
|
+
}
|
|
512
582
|
}
|
|
513
583
|
};
|
|
514
584
|
}
|
|
@@ -607,10 +677,54 @@ stats.bulk = async function bulk(registry, packages, options) {
|
|
|
607
677
|
if (!provider) {
|
|
608
678
|
throw new RegistryError(registry, 0, `Unknown registry "${registry}".`);
|
|
609
679
|
}
|
|
680
|
+
if (registry === "npm" && packages.length > 1) {
|
|
681
|
+
return npmBulkStats(packages, options);
|
|
682
|
+
}
|
|
610
683
|
const concurrency = options?.concurrency ?? 5;
|
|
611
684
|
const limit = pLimit(concurrency);
|
|
612
685
|
return Promise.all(packages.map((pkg) => limit(() => stats(registry, pkg, options))));
|
|
613
686
|
};
|
|
687
|
+
async function npmBulkStats(packages, options) {
|
|
688
|
+
const scoped = [];
|
|
689
|
+
const unscoped = [];
|
|
690
|
+
for (const pkg of packages) {
|
|
691
|
+
if (pkg.startsWith("@")) {
|
|
692
|
+
scoped.push(pkg);
|
|
693
|
+
} else {
|
|
694
|
+
unscoped.push(pkg);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
const bulkMonth = unscoped.length > 0 ? await npmBulkPoint(unscoped, "last-month") : /* @__PURE__ */ new Map();
|
|
698
|
+
const bulkWeek = unscoped.length > 0 ? await npmBulkPoint(unscoped, "last-week") : /* @__PURE__ */ new Map();
|
|
699
|
+
const bulkDay = unscoped.length > 0 ? await npmBulkPoint(unscoped, "last-day") : /* @__PURE__ */ new Map();
|
|
700
|
+
const unscopedResults = /* @__PURE__ */ new Map();
|
|
701
|
+
for (const pkg of unscoped) {
|
|
702
|
+
const month = bulkMonth.get(pkg);
|
|
703
|
+
const week = bulkWeek.get(pkg);
|
|
704
|
+
const day = bulkDay.get(pkg);
|
|
705
|
+
if (month === void 0 && week === void 0 && day === void 0) {
|
|
706
|
+
unscopedResults.set(pkg, null);
|
|
707
|
+
} else {
|
|
708
|
+
unscopedResults.set(pkg, {
|
|
709
|
+
registry: "npm",
|
|
710
|
+
package: pkg,
|
|
711
|
+
downloads: {
|
|
712
|
+
lastDay: day,
|
|
713
|
+
lastWeek: week,
|
|
714
|
+
lastMonth: month
|
|
715
|
+
},
|
|
716
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
const limit = pLimit(1);
|
|
721
|
+
const scopedResults = await Promise.all(
|
|
722
|
+
scoped.map((pkg) => limit(() => stats("npm", pkg, options)))
|
|
723
|
+
);
|
|
724
|
+
const scopedMap = /* @__PURE__ */ new Map();
|
|
725
|
+
scoped.forEach((pkg, i) => scopedMap.set(pkg, scopedResults[i]));
|
|
726
|
+
return packages.map((pkg) => unscopedResults.get(pkg) ?? scopedMap.get(pkg) ?? null);
|
|
727
|
+
}
|
|
614
728
|
stats.range = async function range(registry, pkg, start, end, options) {
|
|
615
729
|
const provider = providers[registry];
|
|
616
730
|
if (!provider) {
|
|
@@ -654,6 +768,31 @@ stats.compare = async function compare(pkg, registries, options) {
|
|
|
654
768
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
655
769
|
};
|
|
656
770
|
};
|
|
771
|
+
stats.mine = async function mine(maintainer, options) {
|
|
772
|
+
const packages = [];
|
|
773
|
+
const PAGE_SIZE = 250;
|
|
774
|
+
let offset = 0;
|
|
775
|
+
while (true) {
|
|
776
|
+
const url = `https://registry.npmjs.org/-/v1/search?text=maintainer:${encodeURIComponent(maintainer)}&size=${PAGE_SIZE}&from=${offset}`;
|
|
777
|
+
const data = await fetchDirect(url, "npm");
|
|
778
|
+
if (!data || data.objects.length === 0) break;
|
|
779
|
+
for (const obj of data.objects) {
|
|
780
|
+
packages.push(obj.package.name);
|
|
781
|
+
}
|
|
782
|
+
offset += data.objects.length;
|
|
783
|
+
if (offset >= data.total) break;
|
|
784
|
+
}
|
|
785
|
+
if (packages.length === 0) return [];
|
|
786
|
+
const results = [];
|
|
787
|
+
const bulkResults = await stats.bulk("npm", packages, options);
|
|
788
|
+
for (let i = 0; i < packages.length; i++) {
|
|
789
|
+
const r = bulkResults[i];
|
|
790
|
+
if (r) results.push(r);
|
|
791
|
+
options?.onProgress?.(i + 1, packages.length, packages[i]);
|
|
792
|
+
}
|
|
793
|
+
results.sort((a, b) => (b.downloads.lastMonth ?? 0) - (a.downloads.lastMonth ?? 0));
|
|
794
|
+
return results;
|
|
795
|
+
};
|
|
657
796
|
// Annotate the CommonJS export names for ESM import in node:
|
|
658
797
|
0 && (module.exports = {
|
|
659
798
|
RegistryError,
|
package/dist/index.d.cts
CHANGED
|
@@ -125,6 +125,9 @@ declare namespace stats {
|
|
|
125
125
|
var bulk: (registry: string, packages: string[], options?: StatsOptions) => Promise<(PackageStats | null)[]>;
|
|
126
126
|
var range: (registry: string, pkg: string, start: string, end: string, options?: StatsOptions) => Promise<DailyDownloads[]>;
|
|
127
127
|
var compare: (pkg: string, registries?: string[], options?: StatsOptions) => Promise<ComparisonResult>;
|
|
128
|
+
var mine: (maintainer: string, options?: StatsOptions & {
|
|
129
|
+
onProgress?: (done: number, total: number, pkg: string) => void;
|
|
130
|
+
}) => Promise<PackageStats[]>;
|
|
128
131
|
}
|
|
129
132
|
|
|
130
133
|
export { type ChartData, type ComparisonResult, type Config, type DailyDownloads, type PackageConfig, type PackageStats, type RateLimitConfig, RegistryError, type RegistryName, type RegistryProvider, type ServerOptions, type StatsCache, type StatsOptions, calc, createCache, createHandler, defaultConfig, loadConfig, registerProvider, serve, starterConfig, stats };
|
package/dist/index.d.ts
CHANGED
|
@@ -125,6 +125,9 @@ declare namespace stats {
|
|
|
125
125
|
var bulk: (registry: string, packages: string[], options?: StatsOptions) => Promise<(PackageStats | null)[]>;
|
|
126
126
|
var range: (registry: string, pkg: string, start: string, end: string, options?: StatsOptions) => Promise<DailyDownloads[]>;
|
|
127
127
|
var compare: (pkg: string, registries?: string[], options?: StatsOptions) => Promise<ComparisonResult>;
|
|
128
|
+
var mine: (maintainer: string, options?: StatsOptions & {
|
|
129
|
+
onProgress?: (done: number, total: number, pkg: string) => void;
|
|
130
|
+
}) => Promise<PackageStats[]>;
|
|
128
131
|
}
|
|
129
132
|
|
|
130
133
|
export { type ChartData, type ComparisonResult, type Config, type DailyDownloads, type PackageConfig, type PackageStats, type RateLimitConfig, RegistryError, type RegistryName, type RegistryProvider, type ServerOptions, type StatsCache, type StatsOptions, calc, createCache, createHandler, defaultConfig, loadConfig, registerProvider, serve, starterConfig, stats };
|
package/dist/index.js
CHANGED
|
@@ -13,22 +13,64 @@ var RegistryError = class extends Error {
|
|
|
13
13
|
var RETRYABLE = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
14
14
|
var MAX_RETRIES = 3;
|
|
15
15
|
var BASE_DELAY = 1e3;
|
|
16
|
+
var registryLocks = /* @__PURE__ */ new Map();
|
|
17
|
+
var REGISTRY_DELAYS = {
|
|
18
|
+
npm: 400,
|
|
19
|
+
// ~2.5 req/s — safe for 54+ scoped packages
|
|
20
|
+
pypi: 2200,
|
|
21
|
+
// 30 req/60s = 1 per 2s, with headroom
|
|
22
|
+
docker: 4e3
|
|
23
|
+
// 10 req/3600s — very tight
|
|
24
|
+
};
|
|
25
|
+
var DEFAULT_DELAY = 100;
|
|
26
|
+
function acquireSlot(registry) {
|
|
27
|
+
const minDelay = REGISTRY_DELAYS[registry] ?? DEFAULT_DELAY;
|
|
28
|
+
const prev = registryLocks.get(registry) ?? Promise.resolve();
|
|
29
|
+
const slot = prev.then(() => new Promise((r) => setTimeout(r, minDelay)));
|
|
30
|
+
registryLocks.set(registry, slot);
|
|
31
|
+
return prev;
|
|
32
|
+
}
|
|
16
33
|
async function fetchWithRetry(url, registry, init) {
|
|
34
|
+
let lastError;
|
|
35
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
36
|
+
await acquireSlot(registry);
|
|
37
|
+
const res = await fetch(url, init);
|
|
38
|
+
if (res.status === 404) return null;
|
|
39
|
+
if (res.ok) return res.json();
|
|
40
|
+
const retryAfter = res.headers.get("retry-after");
|
|
41
|
+
const retryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : void 0;
|
|
42
|
+
lastError = new RegistryError(
|
|
43
|
+
registry,
|
|
44
|
+
res.status,
|
|
45
|
+
`${res.statusText}: ${url}`,
|
|
46
|
+
retryAfterSeconds
|
|
47
|
+
);
|
|
48
|
+
if (!RETRYABLE.has(res.status) || attempt === MAX_RETRIES) break;
|
|
49
|
+
const backoff = BASE_DELAY * Math.pow(2, attempt);
|
|
50
|
+
const retryAfterMs = retryAfterSeconds ? retryAfterSeconds * 1e3 : 0;
|
|
51
|
+
const delay = Math.max(backoff, retryAfterMs);
|
|
52
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
53
|
+
}
|
|
54
|
+
throw lastError;
|
|
55
|
+
}
|
|
56
|
+
async function fetchDirect(url, registry, init) {
|
|
17
57
|
let lastError;
|
|
18
58
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
19
59
|
const res = await fetch(url, init);
|
|
20
60
|
if (res.status === 404) return null;
|
|
21
61
|
if (res.ok) return res.json();
|
|
22
62
|
const retryAfter = res.headers.get("retry-after");
|
|
23
|
-
const
|
|
63
|
+
const retryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : void 0;
|
|
24
64
|
lastError = new RegistryError(
|
|
25
65
|
registry,
|
|
26
66
|
res.status,
|
|
27
67
|
`${res.statusText}: ${url}`,
|
|
28
|
-
|
|
68
|
+
retryAfterSeconds
|
|
29
69
|
);
|
|
30
70
|
if (!RETRYABLE.has(res.status) || attempt === MAX_RETRIES) break;
|
|
31
|
-
const
|
|
71
|
+
const backoff = BASE_DELAY * Math.pow(2, attempt);
|
|
72
|
+
const retryAfterMs = retryAfterSeconds ? retryAfterSeconds * 1e3 : 0;
|
|
73
|
+
const delay = Math.max(backoff, retryAfterMs);
|
|
32
74
|
await new Promise((r) => setTimeout(r, delay));
|
|
33
75
|
}
|
|
34
76
|
throw lastError;
|
|
@@ -39,20 +81,22 @@ var API = "https://api.npmjs.org/downloads";
|
|
|
39
81
|
var npm = {
|
|
40
82
|
name: "npm",
|
|
41
83
|
async getStats(pkg) {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
84
|
+
const end = /* @__PURE__ */ new Date();
|
|
85
|
+
const start = new Date(end);
|
|
86
|
+
start.setDate(start.getDate() - 30);
|
|
87
|
+
const data = await fetchWithRetry(
|
|
88
|
+
`${API}/range/${fmt(start)}:${fmt(end)}/${pkg}`,
|
|
89
|
+
"npm"
|
|
90
|
+
);
|
|
91
|
+
if (!data || !data.downloads || data.downloads.length === 0) return null;
|
|
92
|
+
const days = data.downloads;
|
|
93
|
+
const lastDay = days[days.length - 1]?.downloads ?? 0;
|
|
94
|
+
const lastWeek = days.slice(-7).reduce((s, d) => s + d.downloads, 0);
|
|
95
|
+
const lastMonth = days.reduce((s, d) => s + d.downloads, 0);
|
|
48
96
|
return {
|
|
49
97
|
registry: "npm",
|
|
50
98
|
package: pkg,
|
|
51
|
-
downloads: {
|
|
52
|
-
lastDay: day?.downloads,
|
|
53
|
-
lastWeek: week?.downloads,
|
|
54
|
-
lastMonth: month?.downloads
|
|
55
|
-
},
|
|
99
|
+
downloads: { lastDay, lastWeek, lastMonth },
|
|
56
100
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
57
101
|
};
|
|
58
102
|
},
|
|
@@ -80,6 +124,27 @@ var npm = {
|
|
|
80
124
|
return chunks;
|
|
81
125
|
}
|
|
82
126
|
};
|
|
127
|
+
async function npmBulkPoint(packages, period = "last-month") {
|
|
128
|
+
const result = /* @__PURE__ */ new Map();
|
|
129
|
+
if (packages.length === 0) return result;
|
|
130
|
+
const BATCH_SIZE = 128;
|
|
131
|
+
for (let i = 0; i < packages.length; i += BATCH_SIZE) {
|
|
132
|
+
const batch = packages.slice(i, i + BATCH_SIZE);
|
|
133
|
+
const joined = batch.join(",");
|
|
134
|
+
const data = await fetchDirect(
|
|
135
|
+
`${API}/point/${period}/${joined}`,
|
|
136
|
+
"npm"
|
|
137
|
+
);
|
|
138
|
+
if (data) {
|
|
139
|
+
for (const [name, entry] of Object.entries(data)) {
|
|
140
|
+
if (entry && typeof entry.downloads === "number") {
|
|
141
|
+
result.set(name, entry.downloads);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
83
148
|
function fmt(d) {
|
|
84
149
|
return d.toISOString().slice(0, 10);
|
|
85
150
|
}
|
|
@@ -473,7 +538,12 @@ function createHandler(opts) {
|
|
|
473
538
|
}
|
|
474
539
|
error(res, "Not found", 404);
|
|
475
540
|
} catch (e) {
|
|
476
|
-
|
|
541
|
+
if (e instanceof RegistryError) {
|
|
542
|
+
const status = e.statusCode === 429 ? 429 : e.statusCode === 404 ? 404 : e.statusCode >= 500 ? 502 : 500;
|
|
543
|
+
error(res, e.message, status);
|
|
544
|
+
} else {
|
|
545
|
+
error(res, e.message, 500);
|
|
546
|
+
}
|
|
477
547
|
}
|
|
478
548
|
};
|
|
479
549
|
}
|
|
@@ -572,10 +642,54 @@ stats.bulk = async function bulk(registry, packages, options) {
|
|
|
572
642
|
if (!provider) {
|
|
573
643
|
throw new RegistryError(registry, 0, `Unknown registry "${registry}".`);
|
|
574
644
|
}
|
|
645
|
+
if (registry === "npm" && packages.length > 1) {
|
|
646
|
+
return npmBulkStats(packages, options);
|
|
647
|
+
}
|
|
575
648
|
const concurrency = options?.concurrency ?? 5;
|
|
576
649
|
const limit = pLimit(concurrency);
|
|
577
650
|
return Promise.all(packages.map((pkg) => limit(() => stats(registry, pkg, options))));
|
|
578
651
|
};
|
|
652
|
+
async function npmBulkStats(packages, options) {
|
|
653
|
+
const scoped = [];
|
|
654
|
+
const unscoped = [];
|
|
655
|
+
for (const pkg of packages) {
|
|
656
|
+
if (pkg.startsWith("@")) {
|
|
657
|
+
scoped.push(pkg);
|
|
658
|
+
} else {
|
|
659
|
+
unscoped.push(pkg);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
const bulkMonth = unscoped.length > 0 ? await npmBulkPoint(unscoped, "last-month") : /* @__PURE__ */ new Map();
|
|
663
|
+
const bulkWeek = unscoped.length > 0 ? await npmBulkPoint(unscoped, "last-week") : /* @__PURE__ */ new Map();
|
|
664
|
+
const bulkDay = unscoped.length > 0 ? await npmBulkPoint(unscoped, "last-day") : /* @__PURE__ */ new Map();
|
|
665
|
+
const unscopedResults = /* @__PURE__ */ new Map();
|
|
666
|
+
for (const pkg of unscoped) {
|
|
667
|
+
const month = bulkMonth.get(pkg);
|
|
668
|
+
const week = bulkWeek.get(pkg);
|
|
669
|
+
const day = bulkDay.get(pkg);
|
|
670
|
+
if (month === void 0 && week === void 0 && day === void 0) {
|
|
671
|
+
unscopedResults.set(pkg, null);
|
|
672
|
+
} else {
|
|
673
|
+
unscopedResults.set(pkg, {
|
|
674
|
+
registry: "npm",
|
|
675
|
+
package: pkg,
|
|
676
|
+
downloads: {
|
|
677
|
+
lastDay: day,
|
|
678
|
+
lastWeek: week,
|
|
679
|
+
lastMonth: month
|
|
680
|
+
},
|
|
681
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
const limit = pLimit(1);
|
|
686
|
+
const scopedResults = await Promise.all(
|
|
687
|
+
scoped.map((pkg) => limit(() => stats("npm", pkg, options)))
|
|
688
|
+
);
|
|
689
|
+
const scopedMap = /* @__PURE__ */ new Map();
|
|
690
|
+
scoped.forEach((pkg, i) => scopedMap.set(pkg, scopedResults[i]));
|
|
691
|
+
return packages.map((pkg) => unscopedResults.get(pkg) ?? scopedMap.get(pkg) ?? null);
|
|
692
|
+
}
|
|
579
693
|
stats.range = async function range(registry, pkg, start, end, options) {
|
|
580
694
|
const provider = providers[registry];
|
|
581
695
|
if (!provider) {
|
|
@@ -619,6 +733,31 @@ stats.compare = async function compare(pkg, registries, options) {
|
|
|
619
733
|
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
620
734
|
};
|
|
621
735
|
};
|
|
736
|
+
stats.mine = async function mine(maintainer, options) {
|
|
737
|
+
const packages = [];
|
|
738
|
+
const PAGE_SIZE = 250;
|
|
739
|
+
let offset = 0;
|
|
740
|
+
while (true) {
|
|
741
|
+
const url = `https://registry.npmjs.org/-/v1/search?text=maintainer:${encodeURIComponent(maintainer)}&size=${PAGE_SIZE}&from=${offset}`;
|
|
742
|
+
const data = await fetchDirect(url, "npm");
|
|
743
|
+
if (!data || data.objects.length === 0) break;
|
|
744
|
+
for (const obj of data.objects) {
|
|
745
|
+
packages.push(obj.package.name);
|
|
746
|
+
}
|
|
747
|
+
offset += data.objects.length;
|
|
748
|
+
if (offset >= data.total) break;
|
|
749
|
+
}
|
|
750
|
+
if (packages.length === 0) return [];
|
|
751
|
+
const results = [];
|
|
752
|
+
const bulkResults = await stats.bulk("npm", packages, options);
|
|
753
|
+
for (let i = 0; i < packages.length; i++) {
|
|
754
|
+
const r = bulkResults[i];
|
|
755
|
+
if (r) results.push(r);
|
|
756
|
+
options?.onProgress?.(i + 1, packages.length, packages[i]);
|
|
757
|
+
}
|
|
758
|
+
results.sort((a, b) => (b.downloads.lastMonth ?? 0) - (a.downloads.lastMonth ?? 0));
|
|
759
|
+
return results;
|
|
760
|
+
};
|
|
622
761
|
export {
|
|
623
762
|
RegistryError,
|
|
624
763
|
calc,
|
package/package.json
CHANGED