@shiinasaku/github-card 4.2.0 → 5.0.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 +40 -13
- package/dist/card.d.ts +2 -15
- package/dist/lib.js +4363 -930
- package/dist/types.d.ts +8 -3
- package/dist/utils/index.d.ts +4 -5
- package/dist/utils/languages.d.ts +0 -1
- package/package.json +14 -16
- package/src/card.ts +114 -92
- package/src/gallery.ts +37 -0
- package/src/github.ts +86 -42
- package/src/index.ts +89 -166
- package/src/types.ts +8 -3
- package/src/utils/format.ts +22 -20
- package/src/utils/github-client.ts +43 -138
- package/src/utils/icons.ts +5 -1
- package/src/utils/index.ts +4 -5
- package/src/utils/languages.ts +1 -1
- package/src/env.d.ts +0 -4
- package/src/input.css +0 -1
- package/src/tailwind.css +0 -3837
package/src/utils/format.ts
CHANGED
|
@@ -16,7 +16,7 @@ export function kFormat(num: number): string {
|
|
|
16
16
|
*/
|
|
17
17
|
export function measureText(text: string, size: number, weight = 400): number {
|
|
18
18
|
if (!text) return 0;
|
|
19
|
-
const factor = size
|
|
19
|
+
const factor = size;
|
|
20
20
|
const weightFactor = weight >= 700 ? 1.08 : weight >= 600 ? 1.04 : 1;
|
|
21
21
|
let width = 0;
|
|
22
22
|
for (const ch of text) {
|
|
@@ -45,6 +45,7 @@ export function measureText(text: string, size: number, weight = 400): number {
|
|
|
45
45
|
|
|
46
46
|
export function escapeXml(str: string): string {
|
|
47
47
|
return str
|
|
48
|
+
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\ufffe\uffff\ud800-\udfff]/gu, "")
|
|
48
49
|
.replace(/&/g, "&")
|
|
49
50
|
.replace(/</g, "<")
|
|
50
51
|
.replace(/>/g, ">")
|
|
@@ -53,28 +54,29 @@ export function escapeXml(str: string): string {
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
export function wrapText(input: string, maxLen: number, maxLines: number): string[] {
|
|
56
|
-
|
|
57
|
+
if (maxLen < 1 || maxLines < 1) return [];
|
|
58
|
+
let remaining = Array.from(input.trim().replace(/\s+/g, " "));
|
|
57
59
|
const lines: string[] = [];
|
|
58
|
-
let current = "";
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
continue;
|
|
61
|
+
while (remaining.length && lines.length < maxLines) {
|
|
62
|
+
if (remaining.length <= maxLen) {
|
|
63
|
+
lines.push(remaining.join(""));
|
|
64
|
+
break;
|
|
65
65
|
}
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
66
|
+
if (lines.length === maxLines - 1) {
|
|
67
|
+
lines.push(
|
|
68
|
+
remaining
|
|
69
|
+
.slice(0, maxLen - 1)
|
|
70
|
+
.join("")
|
|
71
|
+
.trimEnd() + "…",
|
|
72
|
+
);
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
const space = remaining.lastIndexOf(" ", maxLen);
|
|
76
|
+
const end = space > 0 ? space : maxLen;
|
|
77
|
+
lines.push(remaining.slice(0, end).join(""));
|
|
78
|
+
remaining = remaining.slice(end);
|
|
79
|
+
if (remaining[0] === " ") remaining.shift();
|
|
78
80
|
}
|
|
79
81
|
return lines;
|
|
80
82
|
}
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
export class GitHubError extends Error {
|
|
2
|
-
constructor(
|
|
3
|
-
message: string,
|
|
4
|
-
public httpStatus?: number,
|
|
5
|
-
public errorCode?: string,
|
|
6
|
-
) {
|
|
2
|
+
constructor(message: string) {
|
|
7
3
|
super(message);
|
|
8
4
|
this.name = "GitHubError";
|
|
9
5
|
}
|
|
@@ -11,93 +7,48 @@ export class GitHubError extends Error {
|
|
|
11
7
|
|
|
12
8
|
export class NotFoundError extends GitHubError {
|
|
13
9
|
constructor(message = "Resource not found") {
|
|
14
|
-
super(message
|
|
10
|
+
super(message);
|
|
15
11
|
this.name = "NotFoundError";
|
|
16
12
|
}
|
|
17
13
|
}
|
|
18
14
|
|
|
19
15
|
export class RateLimitError extends GitHubError {
|
|
20
|
-
constructor(
|
|
21
|
-
message
|
|
22
|
-
public resetAt?: Date,
|
|
23
|
-
) {
|
|
24
|
-
super(message, 429, "RATE_LIMITED");
|
|
16
|
+
constructor(message = "GitHub API rate limit exceeded") {
|
|
17
|
+
super(message);
|
|
25
18
|
this.name = "RateLimitError";
|
|
26
19
|
}
|
|
27
20
|
}
|
|
28
21
|
|
|
29
22
|
export class AuthError extends GitHubError {
|
|
30
23
|
constructor(message = "GitHub authentication failed") {
|
|
31
|
-
super(message
|
|
24
|
+
super(message);
|
|
32
25
|
this.name = "AuthError";
|
|
33
26
|
}
|
|
34
27
|
}
|
|
35
28
|
|
|
36
|
-
|
|
37
|
-
constructor(
|
|
38
|
-
super(
|
|
29
|
+
class TimeoutError extends GitHubError {
|
|
30
|
+
constructor() {
|
|
31
|
+
super("GitHub API request timed out");
|
|
39
32
|
this.name = "TimeoutError";
|
|
40
33
|
}
|
|
41
34
|
}
|
|
42
35
|
|
|
43
|
-
export interface GitHubClientOptions {
|
|
44
|
-
token?: string;
|
|
45
|
-
userAgent?: string;
|
|
46
|
-
timeoutMs?: number;
|
|
47
|
-
baseUrl?: string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface RequestOptions extends Omit<RequestInit, "body"> {
|
|
51
|
-
variables?: Record<string, unknown>;
|
|
52
|
-
timeoutMs?: number;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* A next-generation, generic GitHub GraphQL client.
|
|
57
|
-
* Designed to be robust, strongly typed, and publishable.
|
|
58
|
-
*/
|
|
59
36
|
export class GitHubClient {
|
|
60
|
-
private readonly token: string;
|
|
61
|
-
private readonly userAgent: string;
|
|
62
|
-
private readonly timeoutMs: number;
|
|
63
|
-
private readonly baseUrl: string;
|
|
64
|
-
|
|
65
|
-
constructor(options: GitHubClientOptions = {}) {
|
|
66
|
-
const token =
|
|
67
|
-
options.token || (typeof process !== "undefined" ? process.env?.GITHUB_TOKEN : "") || "";
|
|
68
|
-
if (!token) {
|
|
69
|
-
console.warn(
|
|
70
|
-
"GitHubClient initialized without a token. Requests requiring authentication will fail.",
|
|
71
|
-
);
|
|
72
|
-
}
|
|
37
|
+
constructor(private readonly options: { token: string; userAgent: string }) {}
|
|
73
38
|
|
|
74
|
-
|
|
75
|
-
this.userAgent = options.userAgent || "nextgen-github-client/1.0";
|
|
76
|
-
this.timeoutMs = options.timeoutMs || 10000;
|
|
77
|
-
this.baseUrl = options.baseUrl || "https://api.github.com/graphql";
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Classifies fetch or API errors into distinct custom error classes.
|
|
82
|
-
*/
|
|
83
|
-
private classifyError(message: string, status: number, headers?: Headers): never {
|
|
39
|
+
private classifyError(message: string, status: number): never {
|
|
84
40
|
const lower = message.toLowerCase();
|
|
85
|
-
|
|
86
41
|
if (status === 404 || lower.includes("not resolve to a user") || lower.includes("not found")) {
|
|
87
42
|
throw new NotFoundError(message);
|
|
88
43
|
}
|
|
89
|
-
|
|
90
44
|
if (
|
|
91
45
|
status === 429 ||
|
|
92
46
|
status === 403 ||
|
|
93
47
|
lower.includes("rate limit") ||
|
|
94
48
|
lower.includes("abuse")
|
|
95
49
|
) {
|
|
96
|
-
|
|
97
|
-
const resetAt = resetHeader ? new Date(parseInt(resetHeader, 10) * 1000) : undefined;
|
|
98
|
-
throw new RateLimitError(message, resetAt);
|
|
50
|
+
throw new RateLimitError(message);
|
|
99
51
|
}
|
|
100
|
-
|
|
101
52
|
if (
|
|
102
53
|
status === 401 ||
|
|
103
54
|
lower.includes("bad credentials") ||
|
|
@@ -105,94 +56,48 @@ export class GitHubClient {
|
|
|
105
56
|
) {
|
|
106
57
|
throw new AuthError(message);
|
|
107
58
|
}
|
|
108
|
-
|
|
109
|
-
throw new GitHubError(message || `GitHub API error (${status})`, status);
|
|
59
|
+
throw new GitHubError(message || `GitHub API error (${status})`);
|
|
110
60
|
}
|
|
111
61
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
* @param options Request options including variables and fetch options.
|
|
117
|
-
* @returns Strongly typed response data.
|
|
118
|
-
*/
|
|
119
|
-
public async request<T = unknown>(query: string, options: RequestOptions = {}): Promise<T> {
|
|
120
|
-
const { variables, timeoutMs = this.timeoutMs, signal: userSignal, ...fetchOptions } = options;
|
|
121
|
-
|
|
122
|
-
const headers = new Headers(fetchOptions.headers);
|
|
123
|
-
if (this.token && !headers.has("Authorization")) {
|
|
124
|
-
headers.set("Authorization", `bearer ${this.token}`);
|
|
125
|
-
}
|
|
126
|
-
if (!headers.has("Content-Type")) {
|
|
127
|
-
headers.set("Content-Type", "application/json");
|
|
128
|
-
}
|
|
129
|
-
if (!headers.has("User-Agent")) {
|
|
130
|
-
headers.set("User-Agent", this.userAgent);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const abortController = new AbortController();
|
|
134
|
-
const timeoutId =
|
|
135
|
-
timeoutMs > 0
|
|
136
|
-
? setTimeout(() => abortController.abort(new TimeoutError()), timeoutMs)
|
|
137
|
-
: undefined;
|
|
138
|
-
|
|
139
|
-
// Link user-provided signal if present
|
|
140
|
-
const onAbort = userSignal ? () => abortController.abort(userSignal.reason) : undefined;
|
|
141
|
-
if (userSignal) {
|
|
142
|
-
userSignal.addEventListener("abort", onAbort!);
|
|
143
|
-
if (userSignal.aborted) abortController.abort(userSignal.reason);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
let response: Response;
|
|
147
|
-
|
|
62
|
+
async request<T>(
|
|
63
|
+
query: string,
|
|
64
|
+
{ variables, timeoutMs }: { variables: Record<string, unknown>; timeoutMs: number },
|
|
65
|
+
): Promise<T> {
|
|
148
66
|
try {
|
|
149
|
-
response = await fetch(
|
|
150
|
-
...fetchOptions,
|
|
67
|
+
const response = await fetch("https://api.github.com/graphql", {
|
|
151
68
|
method: "POST",
|
|
152
|
-
headers
|
|
69
|
+
headers: {
|
|
70
|
+
Authorization: `bearer ${this.options.token}`,
|
|
71
|
+
"Content-Type": "application/json",
|
|
72
|
+
"User-Agent": this.options.userAgent,
|
|
73
|
+
},
|
|
153
74
|
body: JSON.stringify({ query, variables }),
|
|
154
|
-
signal:
|
|
75
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
155
76
|
});
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (name === "AbortError" || error instanceof TimeoutError) {
|
|
160
|
-
throw new TimeoutError(message || "Request timed out");
|
|
161
|
-
}
|
|
162
|
-
throw new GitHubError(`Network error: ${message || "Unknown error"}`);
|
|
163
|
-
} finally {
|
|
164
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
165
|
-
if (userSignal) {
|
|
166
|
-
userSignal.removeEventListener("abort", onAbort!);
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
const message = await response.text();
|
|
79
|
+
this.classifyError(message || response.statusText, response.status);
|
|
167
80
|
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (!response.ok) {
|
|
171
|
-
const errorText = await response.text().catch(() => "");
|
|
172
|
-
this.classifyError(errorText || response.statusText, response.status, response.headers);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const body = (await response.json().catch(() => ({}))) as {
|
|
176
|
-
data?: T;
|
|
177
|
-
errors?: { message: string }[];
|
|
178
|
-
};
|
|
179
81
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
82
|
+
const body = (await response.json()) as { data?: T; errors?: { message: string }[] };
|
|
83
|
+
if (body.errors?.length) {
|
|
84
|
+
this.classifyError(body.errors[0]?.message || "GraphQL error", response.status);
|
|
85
|
+
}
|
|
86
|
+
if (body.data == null) {
|
|
87
|
+
throw new GitHubError("Invalid response: No data returned from GraphQL endpoint");
|
|
88
|
+
}
|
|
89
|
+
return body.data;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (error instanceof GitHubError) throw error;
|
|
92
|
+
if (
|
|
93
|
+
error instanceof Error &&
|
|
94
|
+
(error.name === "AbortError" || error.name === "TimeoutError")
|
|
95
|
+
) {
|
|
96
|
+
throw new TimeoutError();
|
|
97
|
+
}
|
|
187
98
|
throw new GitHubError(
|
|
188
|
-
|
|
189
|
-
response.status,
|
|
99
|
+
`Network error: ${error instanceof Error ? error.message : String(error)}`,
|
|
190
100
|
);
|
|
191
101
|
}
|
|
192
|
-
|
|
193
|
-
return body.data as T;
|
|
194
102
|
}
|
|
195
103
|
}
|
|
196
|
-
|
|
197
|
-
// Export a default singleton instance for convenience
|
|
198
|
-
export const githubClient = new GitHubClient();
|
package/src/utils/icons.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { escapeXml } from "./format";
|
|
2
|
+
|
|
1
3
|
export const icons = {
|
|
2
4
|
star: `<path d="M10.92 2.868a1.25 1.25 0 0 1 2.16 0l2.795 4.798l5.428 1.176a1.25 1.25 0 0 1 .667 2.054l-3.7 4.141l.56 5.525a1.25 1.25 0 0 1-1.748 1.27L12 19.592l-5.082 2.24a1.25 1.25 0 0 1-1.748-1.27l.56-5.525l-3.7-4.14a1.25 1.25 0 0 1 .667-2.055l5.428-1.176z"/>`,
|
|
3
5
|
commit: `<path fill-rule="evenodd" d="M10.5 7.75a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0zm1.43.75a4.002 4.002 0 01-7.86 0H.75a.75.75 0 110-1.5h3.32a4.001 4.001 0 017.86 0h3.32a.75.75 0 110 1.5h-3.32z"/>`,
|
|
@@ -26,7 +28,9 @@ export function icon(name: IconName, color: string, size = 16): string {
|
|
|
26
28
|
if (!path) return "";
|
|
27
29
|
const meta = iconMeta[name];
|
|
28
30
|
const viewBox = meta?.viewBox ?? "0 0 16 16";
|
|
29
|
-
const colorStr =
|
|
31
|
+
const colorStr = escapeXml(
|
|
32
|
+
/^(?:[\da-f]{3}|[\da-f]{4}|[\da-f]{6}|[\da-f]{8})$/i.test(color) ? `#${color}` : color,
|
|
33
|
+
);
|
|
30
34
|
const attrs =
|
|
31
35
|
meta?.kind === "stroke"
|
|
32
36
|
? `fill="none" stroke="${colorStr}" stroke-width="${meta.strokeWidth ?? "1.8"}" stroke-linecap="round" stroke-linejoin="round"`
|
package/src/utils/index.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
export { resolveColors, themes
|
|
1
|
+
export { resolveColors, themes } from "./themes";
|
|
2
2
|
export { kFormat, escapeXml, wrapText, measureText } from "./format";
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
6
|
-
export * from "./colors";
|
|
3
|
+
export { icon } from "./icons";
|
|
4
|
+
export { FONT_FAMILY } from "./font";
|
|
5
|
+
export { resolveTw } from "./colors";
|
package/src/utils/languages.ts
CHANGED
package/src/env.d.ts
DELETED
package/src/input.css
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@import "tailwindcss";
|