@adonisjs/content 1.3.0 → 1.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/README.md CHANGED
@@ -15,7 +15,8 @@ We use this package for all official AdonisJS websites to manage docs, blog post
15
15
  **Key Features:**
16
16
 
17
17
  - **Type-safe collections** with VineJS schema validation
18
- - **GitHub loaders** for sponsors, releases, and contributors
18
+ - **GitHub loaders** for sponsors, releases, contributors, and aggregated OSS statistics
19
+ - **npm package statistics** aggregation with download counts
19
20
  - **Custom query methods** for filtering and transforming data
20
21
  - **JSON file loading** with validation
21
22
  - **Vite integration** for asset path resolution
@@ -178,6 +179,52 @@ const contributors = new Collection({
178
179
  })
179
180
  ```
180
181
 
182
+ ### OSS Stats Loader
183
+
184
+ Aggregate open source statistics from multiple sources including GitHub stars and npm package downloads:
185
+
186
+ ```ts
187
+ import vine from '@vinejs/vine'
188
+ import app from '@adonisjs/core/services/app'
189
+ import { Collection } from '@adonisjs/content'
190
+ import { loaders } from '@adonisjs/content/loaders'
191
+
192
+ const statsSchema = vine.object({
193
+ stars: vine.number(),
194
+ installs: vine.number(),
195
+ })
196
+
197
+ const ossStatsLoader = loaders.ossStats({
198
+ outputPath: app.makePath('cache/oss-stats.json'),
199
+ refresh: 'daily',
200
+ sources: [
201
+ {
202
+ type: 'github',
203
+ org: 'adonisjs',
204
+ ghToken: process.env.GITHUB_TOKEN!,
205
+ },
206
+ {
207
+ type: 'npm',
208
+ packages: [
209
+ { name: '@adonisjs/core', startDate: '2020-01-01' },
210
+ { name: '@adonisjs/lucid', startDate: '2020-01-01' },
211
+ ],
212
+ },
213
+ ],
214
+ })
215
+
216
+ const ossStats = new Collection({
217
+ schema: statsSchema,
218
+ loader: ossStatsLoader,
219
+ cache: true,
220
+ })
221
+
222
+ const query = await ossStats.load()
223
+ const stats = query.all()
224
+ console.log(`Total GitHub stars: ${stats.stars}`)
225
+ console.log(`Total npm downloads: ${stats.installs}`)
226
+ ```
227
+
181
228
  ### Custom Views
182
229
 
183
230
  Views allow you to define reusable query methods with full type safety:
@@ -0,0 +1,331 @@
1
+ import {
2
+ debug_default
3
+ } from "./chunk-LB6JFRVG.js";
4
+
5
+ // src/utils.ts
6
+ import { Octokit } from "@octokit/rest";
7
+ import { graphql } from "@octokit/graphql";
8
+ import { mkdir, readFile, writeFile } from "fs/promises";
9
+ import dayjs from "dayjs";
10
+ import { dirname } from "path";
11
+ async function fetchAllSponsors({
12
+ login,
13
+ isOrg,
14
+ ghToken
15
+ }) {
16
+ let hasNext = true;
17
+ let cursor = null;
18
+ const allSponsors = [];
19
+ const query = `
20
+ query($login: String!, $cursor: String) {
21
+ ${isOrg ? `organization(login: $login)` : `user(login: $login)`} {
22
+ sponsorshipsAsMaintainer(first: 100, after: $cursor) {
23
+ nodes {
24
+ id
25
+ createdAt
26
+ privacyLevel
27
+ isActive
28
+ tier {
29
+ name
30
+ isOneTime
31
+ monthlyPriceInCents
32
+ }
33
+ sponsorEntity {
34
+ __typename
35
+ ... on User {
36
+ login
37
+ name
38
+ avatarUrl
39
+ url
40
+ }
41
+ ... on Organization {
42
+ login
43
+ name
44
+ avatarUrl
45
+ url
46
+ }
47
+ }
48
+ }
49
+ pageInfo {
50
+ hasNextPage
51
+ endCursor
52
+ }
53
+ }
54
+ }
55
+ }
56
+ `;
57
+ while (hasNext) {
58
+ const data = await graphql(query, {
59
+ headers: {
60
+ authorization: `token ${ghToken}`
61
+ },
62
+ login,
63
+ cursor
64
+ });
65
+ const root = isOrg ? data.organization : data.user;
66
+ if (!root) {
67
+ break;
68
+ }
69
+ const conn = root.sponsorshipsAsMaintainer;
70
+ if (!conn || !conn.nodes) {
71
+ break;
72
+ }
73
+ for (const node of conn.nodes) {
74
+ const sponsorEntity = node.sponsorEntity;
75
+ allSponsors.push({
76
+ id: node.id,
77
+ createdAt: node.createdAt,
78
+ privacyLevel: node.privacyLevel,
79
+ tierName: node.tier?.name ?? null,
80
+ tierMonthlyPriceInCents: node.tier?.monthlyPriceInCents ?? null,
81
+ sponsorType: sponsorEntity.__typename,
82
+ sponsorLogin: sponsorEntity.login,
83
+ sponsorName: sponsorEntity.name ?? null,
84
+ sponsorAvatarUrl: sponsorEntity.avatarUrl ?? null,
85
+ sponsorUrl: sponsorEntity.url ?? null
86
+ });
87
+ }
88
+ hasNext = conn.pageInfo.hasNextPage;
89
+ cursor = conn.pageInfo.endCursor;
90
+ }
91
+ return allSponsors;
92
+ }
93
+ async function fetchReleases({
94
+ org,
95
+ ghToken,
96
+ filters
97
+ }) {
98
+ let hasMoreRepos = true;
99
+ let orgCursor = null;
100
+ const allReleases = [];
101
+ while (hasMoreRepos) {
102
+ const query = `
103
+ query($cursor: String) {
104
+ organization(login: "${org}") {
105
+ repositories(
106
+ first: 10
107
+ after: $cursor
108
+ privacy: PUBLIC
109
+ isArchived: false
110
+ ) {
111
+ nodes {
112
+ name
113
+ releases(first: 50, orderBy: {field: CREATED_AT, direction: DESC}) {
114
+ nodes {
115
+ name
116
+ tagName
117
+ publishedAt
118
+ url
119
+ description
120
+ }
121
+ pageInfo {
122
+ endCursor
123
+ hasNextPage
124
+ }
125
+ }
126
+ }
127
+ pageInfo {
128
+ endCursor
129
+ hasNextPage
130
+ }
131
+ }
132
+ }
133
+ }
134
+ `;
135
+ const data = await graphql(query, {
136
+ headers: {
137
+ authorization: `token ${ghToken}`
138
+ },
139
+ cursor: orgCursor
140
+ });
141
+ for (const repo of data.organization.repositories.nodes) {
142
+ const filtered = repo.releases.nodes.filter((r) => {
143
+ if (!filters) {
144
+ return true;
145
+ }
146
+ let pickRelease = true;
147
+ if (filters.nameDoesntInclude) {
148
+ pickRelease = !filters.nameDoesntInclude.some((substr) => r.name.includes(substr));
149
+ }
150
+ if (pickRelease && filters.nameIncludes) {
151
+ pickRelease = filters.nameIncludes.some((substr) => r.name.includes(substr));
152
+ }
153
+ return pickRelease;
154
+ }).map((r) => ({
155
+ repo: repo.name,
156
+ ...r
157
+ }));
158
+ allReleases.push(...filtered);
159
+ }
160
+ hasMoreRepos = data.organization.repositories.pageInfo.hasNextPage;
161
+ orgCursor = data.organization.repositories.pageInfo.endCursor;
162
+ }
163
+ return allReleases;
164
+ }
165
+ async function fetchContributorsForOrg({
166
+ org,
167
+ ghToken
168
+ }) {
169
+ const REPO_PAGE_SIZE = 100;
170
+ const CONTRIB_PAGE_SIZE = 100;
171
+ const octokit = new Octokit({ auth: ghToken });
172
+ const repos = await octokit.paginate(octokit.repos.listForOrg, {
173
+ org,
174
+ type: "public",
175
+ per_page: REPO_PAGE_SIZE
176
+ });
177
+ const activeRepos = repos.filter((r) => !r.archived);
178
+ const result = [];
179
+ for (const repo of activeRepos) {
180
+ const repoName = repo.name;
181
+ try {
182
+ const contributors = await octokit.paginate(
183
+ octokit.repos.listContributors,
184
+ {
185
+ owner: org,
186
+ repo: repoName,
187
+ per_page: CONTRIB_PAGE_SIZE
188
+ },
189
+ (response) => response.data
190
+ );
191
+ contributors.forEach((c) => {
192
+ if (c.login) {
193
+ result.push({
194
+ login: c.login,
195
+ id: c.id,
196
+ avatar_url: c.avatar_url ?? null,
197
+ html_url: c.html_url ?? null,
198
+ contributions: c.contributions ?? 0
199
+ });
200
+ }
201
+ });
202
+ } catch (err) {
203
+ console.warn(
204
+ `Warning: failed to fetch contributors for ${org}/${repoName}: ${err?.message ?? err}`
205
+ );
206
+ }
207
+ }
208
+ return result;
209
+ }
210
+ function mergeArrays(existing, fresh, key) {
211
+ const seen = /* @__PURE__ */ new Set();
212
+ const deduped = [];
213
+ for (const r of [...existing, ...fresh]) {
214
+ if (!seen.has(r[key])) {
215
+ seen.add(r[key]);
216
+ deduped.push(r);
217
+ }
218
+ }
219
+ return deduped;
220
+ }
221
+ async function aggregateInstalls(packages) {
222
+ let total = 0;
223
+ for (let pkg of packages) {
224
+ const startDate = pkg.startDate;
225
+ const endDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
226
+ const res = await fetch(
227
+ `https://api.npmjs.org/downloads/point/${startDate}:${endDate}/${pkg.name}`
228
+ );
229
+ const data = await res.json();
230
+ total += data.downloads;
231
+ }
232
+ return total;
233
+ }
234
+ async function aggregateStars({
235
+ org,
236
+ ghToken
237
+ }) {
238
+ let totalStars = 0;
239
+ const octokit = new Octokit({ auth: ghToken });
240
+ await octokit.paginate(
241
+ octokit.repos.listForOrg,
242
+ {
243
+ org,
244
+ type: "public",
245
+ per_page: 100
246
+ },
247
+ (response) => {
248
+ for (const repo of response.data) {
249
+ if (!repo.archived && repo.stargazers_count) {
250
+ totalStars += repo.stargazers_count;
251
+ }
252
+ }
253
+ return response.data;
254
+ }
255
+ );
256
+ return totalStars;
257
+ }
258
+ function createCache({
259
+ key,
260
+ outputPath,
261
+ refresh
262
+ }) {
263
+ function isExpired(fetchDate) {
264
+ switch (refresh) {
265
+ case "daily":
266
+ return dayjs().isAfter(fetchDate, "day");
267
+ case "weekly":
268
+ return dayjs().isAfter(fetchDate, "week");
269
+ case "monthly":
270
+ return dayjs().isAfter(fetchDate, "month");
271
+ }
272
+ }
273
+ return {
274
+ /**
275
+ * Retrieves cached data from disk if it exists and hasn't expired.
276
+ * Returns null if the cache doesn't exist, is expired, or the file is not found.
277
+ *
278
+ * @example
279
+ * ```ts
280
+ * const data = await cache.get()
281
+ * if (data === null) {
282
+ * console.log('Cache expired or not found')
283
+ * }
284
+ * ```
285
+ */
286
+ async get() {
287
+ try {
288
+ debug_default('loading %s from file "%s"', key, outputPath);
289
+ const cachedContents = JSON.parse(await readFile(outputPath, "utf-8"));
290
+ if (!cachedContents || isExpired(new Date(cachedContents.lastFetched))) {
291
+ return null;
292
+ }
293
+ return cachedContents[key];
294
+ } catch (error) {
295
+ if (error.code !== "ENOENT") {
296
+ throw error;
297
+ }
298
+ }
299
+ return null;
300
+ },
301
+ /**
302
+ * Saves data to the cache file with the current timestamp.
303
+ * Creates the directory structure if it doesn't exist.
304
+ *
305
+ * @param contents - The data to cache
306
+ *
307
+ * @example
308
+ * ```ts
309
+ * const freshData = await fetchDataFromAPI()
310
+ * await cache.put(freshData)
311
+ * ```
312
+ */
313
+ async put(contents) {
314
+ debug_default('caching %s "%s"', key, outputPath);
315
+ const fileContents = { lastFetched: (/* @__PURE__ */ new Date()).toISOString(), [key]: contents };
316
+ await mkdir(dirname(outputPath), { recursive: true });
317
+ await writeFile(outputPath, JSON.stringify(fileContents));
318
+ return contents;
319
+ }
320
+ };
321
+ }
322
+
323
+ export {
324
+ fetchAllSponsors,
325
+ fetchReleases,
326
+ fetchContributorsForOrg,
327
+ mergeArrays,
328
+ aggregateInstalls,
329
+ aggregateStars,
330
+ createCache
331
+ };
package/build/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export { Collection } from './src/collection.ts';
2
2
  export { configure } from './configure.ts';
3
+ export { createCache } from './src/utils.ts';
package/build/index.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import {
2
2
  Collection
3
3
  } from "./chunk-Q6QIMTOW.js";
4
+ import {
5
+ createCache
6
+ } from "./chunk-WO5VTK7D.js";
4
7
  import "./chunk-LB6JFRVG.js";
5
8
 
6
9
  // configure.ts
@@ -12,5 +15,6 @@ async function configure(command) {
12
15
  }
13
16
  export {
14
17
  Collection,
15
- configure
18
+ configure,
19
+ createCache
16
20
  };
@@ -7,5 +7,5 @@
7
7
  * debug('loading file "%s"', filePath)
8
8
  * ```
9
9
  */
10
- declare const _default: import("util").DebugLogger;
10
+ declare const _default: import("node:util").DebugLogger;
11
11
  export default _default;
@@ -1,8 +1,9 @@
1
- import { type GithubSponsorsOptions, type GithubReleasesOptions, type GithubContributorsOptions } from '../types.ts';
1
+ import { type GithubSponsorsOptions, type GithubReleasesOptions, type GithubContributorsOptions, type OssStatsOptions } from '../types.ts';
2
+ import { JsonLoader } from './json.ts';
3
+ import { OssStatsLoader } from './oss_stats.ts';
2
4
  import { GithubSponsorsLoader } from './gh_sponsors.ts';
3
5
  import { GithubReleasesLoader } from './gh_releases.ts';
4
6
  import { GithubContributorsLoader } from './gh_contributors.ts';
5
- import { JsonLoader } from './json.ts';
6
7
  /**
7
8
  * Factory functions for creating content loaders.
8
9
  * Provides convenient access to GitHub-based data loaders.
@@ -77,6 +78,33 @@ export declare const loaders: {
77
78
  * ```
78
79
  */
79
80
  ghReleases(options: GithubReleasesOptions): GithubReleasesLoader<import("@vinejs/vine/types").SchemaTypes>;
81
+ /**
82
+ * Creates an OSS statistics loader instance.
83
+ *
84
+ * @param options - Configuration options for the OSS stats loader
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * const loader = loaders.ossStats({
89
+ * outputPath: './cache/oss-stats.json',
90
+ * refresh: 'daily',
91
+ * sources: [
92
+ * {
93
+ * type: 'github',
94
+ * org: 'adonisjs',
95
+ * ghToken: process.env.GITHUB_TOKEN
96
+ * },
97
+ * {
98
+ * type: 'npm',
99
+ * packages: [
100
+ * { name: '@adonisjs/core', startDate: '2020-01-01' }
101
+ * ]
102
+ * }
103
+ * ]
104
+ * })
105
+ * ```
106
+ */
107
+ ossStats(options: OssStatsOptions): OssStatsLoader<import("@vinejs/vine/types").SchemaTypes>;
80
108
  /**
81
109
  * Creates a JSON file loader instance.
82
110
  *