@rmdes/indiekit-endpoint-github 1.2.6 → 1.2.8
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/index.js +25 -7
- package/lib/blocks.js +36 -0
- package/lib/controllers/activity.js +4 -4
- package/lib/controllers/changelog.js +1 -10
- package/lib/controllers/repos.js +57 -0
- package/lib/controllers/stars.js +4 -4
- package/lib/github-client.js +18 -30
- package/package.json +2 -1
package/index.js
CHANGED
|
@@ -2,6 +2,8 @@ import express from "express";
|
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
+
import { waitForReady } from "@rmdes/indiekit-startup-gate";
|
|
6
|
+
|
|
5
7
|
import { activityController } from "./lib/controllers/activity.js";
|
|
6
8
|
import { changelogController } from "./lib/controllers/changelog.js";
|
|
7
9
|
import { commitsController } from "./lib/controllers/commits.js";
|
|
@@ -10,6 +12,8 @@ import { dashboardController } from "./lib/controllers/dashboard.js";
|
|
|
10
12
|
import { featuredController } from "./lib/controllers/featured.js";
|
|
11
13
|
import { starsController } from "./lib/controllers/stars.js";
|
|
12
14
|
import { starredController } from "./lib/controllers/starred.js";
|
|
15
|
+
import { reposController } from "./lib/controllers/repos.js";
|
|
16
|
+
import { GITHUB_BLOCKS } from "./lib/blocks.js";
|
|
13
17
|
|
|
14
18
|
// Module-level routers (matching Indiekit's endpoint pattern)
|
|
15
19
|
const protectedRouter = express.Router();
|
|
@@ -64,6 +68,10 @@ export default class GitHubEndpoint {
|
|
|
64
68
|
};
|
|
65
69
|
}
|
|
66
70
|
|
|
71
|
+
get blocks() {
|
|
72
|
+
return GITHUB_BLOCKS;
|
|
73
|
+
}
|
|
74
|
+
|
|
67
75
|
/**
|
|
68
76
|
* Protected routes (require authentication)
|
|
69
77
|
* HTML pages for admin dashboard
|
|
@@ -97,6 +105,7 @@ export default class GitHubEndpoint {
|
|
|
97
105
|
publicRouter.get("/api/changelog", changelogController.api);
|
|
98
106
|
publicRouter.get("/api/starred/all", starredController.all);
|
|
99
107
|
publicRouter.get("/api/starred/recent", starredController.recent);
|
|
108
|
+
publicRouter.get("/api/repos", reposController.api);
|
|
100
109
|
|
|
101
110
|
return publicRouter;
|
|
102
111
|
}
|
|
@@ -115,13 +124,22 @@ export default class GitHubEndpoint {
|
|
|
115
124
|
|
|
116
125
|
// Start background sync for starred repos (if token + DB available)
|
|
117
126
|
if (this.options.token && Indiekit.database) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
127
|
+
this._stopGate = waitForReady(
|
|
128
|
+
() => {
|
|
129
|
+
import("./lib/starred-sync.js")
|
|
130
|
+
.then(({ startStarredSync }) => {
|
|
131
|
+
startStarredSync(Indiekit, this.options);
|
|
132
|
+
})
|
|
133
|
+
.catch((error) => {
|
|
134
|
+
console.error("[GitHub Stars] Sync scheduler failed to start:", error.message);
|
|
135
|
+
});
|
|
136
|
+
},
|
|
137
|
+
{ label: "GitHub" },
|
|
138
|
+
);
|
|
125
139
|
}
|
|
126
140
|
}
|
|
141
|
+
|
|
142
|
+
destroy() {
|
|
143
|
+
this._stopGate?.();
|
|
144
|
+
}
|
|
127
145
|
}
|
package/lib/blocks.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub v2 block declaration (Phase 7c — plugin block ownership).
|
|
3
|
+
*
|
|
4
|
+
* The `github-repos` sidebar widget was a site-config BUILTIN_BLOCKS seed
|
|
5
|
+
* (requiresPlugin null). Declaring it here makes site-config's scanPlugins stamp
|
|
6
|
+
* `sourcePlugin` → `requiresPlugin` ("GitHub activity endpoint"), so the block is
|
|
7
|
+
* properly plugin-gated (theme ENDPOINT_SLUGS maps it to the `github` loadout
|
|
8
|
+
* slug). scanPlugins precedence is `built-in < plugin blocks`, so this entry
|
|
9
|
+
* OVERWRITES the builtin seed where the plugin is loaded; the seed itself is
|
|
10
|
+
* removed from site-config in Phase 7d alongside the legacy-map bridge.
|
|
11
|
+
*
|
|
12
|
+
* `source:"api"` is honest: the widget fetches the plugin's JSON API LIVE
|
|
13
|
+
* client-side (commits/featured/contributions/repos) — no rebuild needed for
|
|
14
|
+
* fresh data. As of 7c the Repos tab also routes through this plugin
|
|
15
|
+
* (/api/repos) instead of hitting api.github.com directly from the browser, so
|
|
16
|
+
* the plugin is the single source of truth. Bespoke template: the theme owns
|
|
17
|
+
* `components/widgets/github-repos.njk` + `js/widgets/github-repos.js`.
|
|
18
|
+
*
|
|
19
|
+
* @module lib/blocks
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** @type {Array<object>} */
|
|
23
|
+
export const GITHUB_BLOCKS = [
|
|
24
|
+
{
|
|
25
|
+
id: "github-repos",
|
|
26
|
+
version: 1,
|
|
27
|
+
label: "GitHub Projects",
|
|
28
|
+
description: "GitHub repositories and activity",
|
|
29
|
+
icon: "github",
|
|
30
|
+
category: "social",
|
|
31
|
+
placement: { regions: ["sidebar"], surfaces: ["homepage"] },
|
|
32
|
+
multiple: false,
|
|
33
|
+
data: { source: "api" },
|
|
34
|
+
schema: { type: "object", additionalProperties: false, properties: {} },
|
|
35
|
+
},
|
|
36
|
+
];
|
|
@@ -109,16 +109,16 @@ export const activityController = {
|
|
|
109
109
|
activity = utils.extractRepoActivity(events, username);
|
|
110
110
|
}
|
|
111
111
|
} catch (apiError) {
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
return response
|
|
113
|
+
.status(apiError.status || 500)
|
|
114
|
+
.json({ error: apiError.message });
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
activity = activity.slice(0, limits.activity);
|
|
117
118
|
|
|
118
119
|
response.json({ activity });
|
|
119
120
|
} catch (error) {
|
|
120
|
-
|
|
121
|
-
response.json({ activity: [], error: "GitHub API temporarily unavailable" });
|
|
121
|
+
next(error);
|
|
122
122
|
}
|
|
123
123
|
},
|
|
124
124
|
};
|
|
@@ -206,16 +206,7 @@ export const changelogController = {
|
|
|
206
206
|
generatedAt: new Date().toISOString(),
|
|
207
207
|
});
|
|
208
208
|
} catch (error) {
|
|
209
|
-
|
|
210
|
-
response.json({
|
|
211
|
-
commits: [],
|
|
212
|
-
categories: {},
|
|
213
|
-
commitCategories: {},
|
|
214
|
-
totalCommits: 0,
|
|
215
|
-
days: daysValue || 30,
|
|
216
|
-
generatedAt: new Date().toISOString(),
|
|
217
|
-
error: "GitHub API temporarily unavailable",
|
|
218
|
-
});
|
|
209
|
+
next(error);
|
|
219
210
|
}
|
|
220
211
|
},
|
|
221
212
|
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { GitHubClient } from "../github-client.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Public JSON API for the user's own repositories (Phase 7c).
|
|
5
|
+
*
|
|
6
|
+
* The sidebar widget's "Repos" tab previously fetched api.github.com DIRECTLY
|
|
7
|
+
* from the browser (sort=updated, type=owner) and filtered fork/private
|
|
8
|
+
* client-side — bypassing this plugin, so the GitHub token never protected the
|
|
9
|
+
* call (rate limits, no auth). This endpoint moves that fetch server-side: the
|
|
10
|
+
* plugin (GitHubClient + token + 15-min cache) is now the single source of truth.
|
|
11
|
+
*
|
|
12
|
+
* Returns a SLIM shape using the RAW GitHub field names the widget template
|
|
13
|
+
* already reads (html_url/name/description/language/stargazers_count/updated_at),
|
|
14
|
+
* so github-repos.njk needs no change — only the widget's fetch URL moves to
|
|
15
|
+
* /api/repos.
|
|
16
|
+
* @type {import("express").RequestHandler}
|
|
17
|
+
*/
|
|
18
|
+
export const reposController = {
|
|
19
|
+
async api(request, response, next) {
|
|
20
|
+
try {
|
|
21
|
+
const { username, token, cacheTtl, limits } =
|
|
22
|
+
request.app.locals.application.githubConfig;
|
|
23
|
+
|
|
24
|
+
if (!username) {
|
|
25
|
+
return response.status(400).json({ error: "No username configured" });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const client = new GitHubClient({ token, cacheTtl });
|
|
29
|
+
|
|
30
|
+
let raw = [];
|
|
31
|
+
try {
|
|
32
|
+
// Fetch extra (×2) so the fork/private filter still yields enough.
|
|
33
|
+
raw = await client.getUserRepos(username, (limits.repos || 10) * 2, "updated");
|
|
34
|
+
} catch (apiError) {
|
|
35
|
+
return response
|
|
36
|
+
.status(apiError.status || 500)
|
|
37
|
+
.json({ error: apiError.message });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const repos = (Array.isArray(raw) ? raw : [])
|
|
41
|
+
.filter((r) => r && !r.fork && !r.private)
|
|
42
|
+
.slice(0, limits.repos || 10)
|
|
43
|
+
.map((r) => ({
|
|
44
|
+
name: r.name,
|
|
45
|
+
html_url: r.html_url,
|
|
46
|
+
description: r.description,
|
|
47
|
+
language: r.language,
|
|
48
|
+
stargazers_count: r.stargazers_count,
|
|
49
|
+
updated_at: r.updated_at,
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
response.json({ repos });
|
|
53
|
+
} catch (error) {
|
|
54
|
+
next(error);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
};
|
package/lib/controllers/stars.js
CHANGED
|
@@ -69,16 +69,16 @@ export const starsController = {
|
|
|
69
69
|
try {
|
|
70
70
|
starred = await client.getUserStarred(username, limits.stars);
|
|
71
71
|
} catch (apiError) {
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
return response
|
|
73
|
+
.status(apiError.status || 500)
|
|
74
|
+
.json({ error: apiError.message });
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
const stars = utils.formatStarred(starred);
|
|
77
78
|
|
|
78
79
|
response.json({ stars });
|
|
79
80
|
} catch (error) {
|
|
80
|
-
|
|
81
|
-
response.json({ stars: [], error: "GitHub API temporarily unavailable" });
|
|
81
|
+
next(error);
|
|
82
82
|
}
|
|
83
83
|
},
|
|
84
84
|
};
|
package/lib/github-client.js
CHANGED
|
@@ -22,7 +22,7 @@ export class GitHubClient {
|
|
|
22
22
|
async fetch(endpoint) {
|
|
23
23
|
const url = `${BASE_URL}${endpoint}`;
|
|
24
24
|
|
|
25
|
-
// Check cache first
|
|
25
|
+
// Check cache first
|
|
26
26
|
const cached = this.cache.get(url);
|
|
27
27
|
if (cached && Date.now() - cached.timestamp < this.cacheTtl) {
|
|
28
28
|
return cached.data;
|
|
@@ -37,40 +37,28 @@ export class GitHubClient {
|
|
|
37
37
|
headers.Authorization = `Bearer ${this.token}`;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
throw await IndiekitError.fromFetch(response);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
throw new IndiekitError(response.statusText, {
|
|
52
|
-
status: response.status,
|
|
53
|
-
code: response.statusText,
|
|
54
|
-
});
|
|
40
|
+
const response = await fetch(url, { headers });
|
|
41
|
+
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
// Only use fromFetch for JSON error responses; GitHub sometimes returns
|
|
44
|
+
// HTML error pages (e.g., 502 Bad Gateway) which cause SyntaxError noise
|
|
45
|
+
const contentType = response.headers.get("content-type") || "";
|
|
46
|
+
if (contentType.includes("json")) {
|
|
47
|
+
throw await IndiekitError.fromFetch(response);
|
|
55
48
|
}
|
|
56
49
|
|
|
57
|
-
|
|
50
|
+
throw new IndiekitError(response.statusText, {
|
|
51
|
+
status: response.status,
|
|
52
|
+
code: response.statusText,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
58
55
|
|
|
59
|
-
|
|
60
|
-
this.cache.set(url, { data, timestamp: Date.now() });
|
|
56
|
+
const data = await response.json();
|
|
61
57
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// Stale-while-error: if we have stale cached data, return it
|
|
65
|
-
if (cached) {
|
|
66
|
-
console.warn(
|
|
67
|
-
`[GitHub] API error for ${endpoint}: ${error.message}. Serving stale cache (age: ${Math.round((Date.now() - cached.timestamp) / 60_000)}min)`,
|
|
68
|
-
);
|
|
69
|
-
return cached.data;
|
|
70
|
-
}
|
|
58
|
+
// Cache result
|
|
59
|
+
this.cache.set(url, { data, timestamp: Date.now() });
|
|
71
60
|
|
|
72
|
-
|
|
73
|
-
}
|
|
61
|
+
return data;
|
|
74
62
|
}
|
|
75
63
|
|
|
76
64
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmdes/indiekit-endpoint-github",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.8",
|
|
4
4
|
"description": "GitHub activity endpoint for Indiekit. Display commits, stars, contributions, and featured repositories.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"indiekit",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@indiekit/error": "^1.0.0-beta.25",
|
|
43
|
+
"@rmdes/indiekit-startup-gate": "^1.0.0",
|
|
43
44
|
"express": "^5.0.0"
|
|
44
45
|
},
|
|
45
46
|
"peerDependencies": {
|