@doccov/api 0.2.0 → 0.2.1

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.
@@ -1,5 +1,5 @@
1
- import type { OpenPkg } from '@openpkg-ts/spec';
2
1
  import { Hono } from 'hono';
2
+ import { fetchSpecFromGitHub } from '../utils/github';
3
3
 
4
4
  export const badgeRoute = new Hono();
5
5
 
@@ -70,30 +70,6 @@ function generateBadgeSvg(options: BadgeOptions): string {
70
70
  </svg>`;
71
71
  }
72
72
 
73
- async function fetchSpecFromGitHub(
74
- owner: string,
75
- repo: string,
76
- branch = 'main',
77
- ): Promise<OpenPkg | null> {
78
- const urls = [
79
- `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/openpkg.json`,
80
- `https://raw.githubusercontent.com/${owner}/${repo}/master/openpkg.json`,
81
- ];
82
-
83
- for (const url of urls) {
84
- try {
85
- const response = await fetch(url);
86
- if (response.ok) {
87
- return (await response.json()) as OpenPkg;
88
- }
89
- } catch {
90
- // Try next URL
91
- }
92
- }
93
-
94
- return null;
95
- }
96
-
97
73
  // GET /badge/:owner/:repo
98
74
  badgeRoute.get('/:owner/:repo', async (c) => {
99
75
  const { owner, repo } = c.req.param();
@@ -126,7 +102,7 @@ badgeRoute.get('/:owner/:repo', async (c) => {
126
102
  'Content-Type': 'image/svg+xml',
127
103
  'Cache-Control': 'public, max-age=300', // Cache for 5 minutes
128
104
  });
129
- } catch (error) {
105
+ } catch {
130
106
  const svg = generateBadgeSvg({
131
107
  label: 'docs',
132
108
  message: 'error',
@@ -52,7 +52,7 @@ async function fetchSpecFromGitHub(owner: string, repo: string): Promise<OpenPkg
52
52
  return null;
53
53
  }
54
54
 
55
- async function buildLeaderboard(category?: string): Promise<LeaderboardEntry[]> {
55
+ async function buildLeaderboard(_category?: string): Promise<LeaderboardEntry[]> {
56
56
  const entries: LeaderboardEntry[] = [];
57
57
 
58
58
  // Fetch specs for all tracked repos
@@ -0,0 +1,178 @@
1
+ import type { OpenPkg } from '@openpkg-ts/spec';
2
+ import { Hono } from 'hono';
3
+ import { fetchSpecFromGitHub } from '../utils/github';
4
+
5
+ export const widgetRoute = new Hono();
6
+
7
+ type SignalCoverage = {
8
+ description: number;
9
+ params: number;
10
+ returns: number;
11
+ examples: number;
12
+ overall: number;
13
+ };
14
+
15
+ function computeSignalCoverage(spec: OpenPkg): SignalCoverage {
16
+ const exports = spec.exports ?? [];
17
+ const signals = {
18
+ description: { covered: 0, total: 0 },
19
+ params: { covered: 0, total: 0 },
20
+ returns: { covered: 0, total: 0 },
21
+ examples: { covered: 0, total: 0 },
22
+ };
23
+
24
+ for (const exp of exports) {
25
+ const missing = exp.docs?.missing ?? [];
26
+ for (const sig of ['description', 'params', 'returns', 'examples'] as const) {
27
+ signals[sig].total++;
28
+ if (!missing.includes(sig)) signals[sig].covered++;
29
+ }
30
+ }
31
+
32
+ const pct = (s: { covered: number; total: number }) =>
33
+ s.total ? Math.round((s.covered / s.total) * 100) : 0;
34
+
35
+ return {
36
+ description: pct(signals.description),
37
+ params: pct(signals.params),
38
+ returns: pct(signals.returns),
39
+ examples: pct(signals.examples),
40
+ overall: spec.docs?.coverageScore ?? 0,
41
+ };
42
+ }
43
+
44
+ function getColorForScore(score: number): string {
45
+ if (score >= 90) return '#4c1';
46
+ if (score >= 80) return '#97ca00';
47
+ if (score >= 70) return '#a4a61d';
48
+ if (score >= 60) return '#dfb317';
49
+ if (score >= 50) return '#fe7d37';
50
+ return '#e05d44';
51
+ }
52
+
53
+ type WidgetTheme = 'dark' | 'light';
54
+
55
+ interface WidgetOptions {
56
+ theme: WidgetTheme;
57
+ compact: boolean;
58
+ }
59
+
60
+ function generateWidgetSvg(stats: SignalCoverage, options: WidgetOptions): string {
61
+ const { theme, compact } = options;
62
+ const isDark = theme === 'dark';
63
+
64
+ const bg = isDark ? '#0d1117' : '#ffffff';
65
+ const fg = isDark ? '#c9d1d9' : '#24292f';
66
+ const border = isDark ? '#30363d' : '#d0d7de';
67
+ const barBg = isDark ? '#21262d' : '#eaeef2';
68
+ const accent = '#58a6ff';
69
+
70
+ const width = compact ? 160 : 200;
71
+ const rowHeight = 18;
72
+ const headerHeight = 28;
73
+ const padding = 8;
74
+ const barWidth = compact ? 60 : 80;
75
+ const labelWidth = compact ? 0 : 70;
76
+
77
+ const signals = [
78
+ { key: 'description', label: 'desc', pct: stats.description },
79
+ { key: 'params', label: 'params', pct: stats.params },
80
+ { key: 'returns', label: 'returns', pct: stats.returns },
81
+ { key: 'examples', label: 'examples', pct: stats.examples },
82
+ ];
83
+
84
+ const height = headerHeight + signals.length * rowHeight + padding * 2;
85
+
86
+ const rows = signals
87
+ .map((s, i) => {
88
+ const y = headerHeight + padding + i * rowHeight;
89
+ const barFill = (s.pct / 100) * barWidth;
90
+ const color = getColorForScore(s.pct);
91
+
92
+ if (compact) {
93
+ return `
94
+ <g transform="translate(${padding}, ${y})">
95
+ <rect x="0" y="2" width="${barWidth}" height="12" rx="2" fill="${barBg}"/>
96
+ <rect x="0" y="2" width="${barFill}" height="12" rx="2" fill="${color}"/>
97
+ <text x="${barWidth + 6}" y="12" font-size="10" fill="${fg}">${s.pct}%</text>
98
+ </g>`;
99
+ }
100
+
101
+ return `
102
+ <g transform="translate(${padding}, ${y})">
103
+ <text x="0" y="12" font-size="10" fill="${fg}">${s.label}</text>
104
+ <rect x="${labelWidth}" y="2" width="${barWidth}" height="12" rx="2" fill="${barBg}"/>
105
+ <rect x="${labelWidth}" y="2" width="${barFill}" height="12" rx="2" fill="${color}"/>
106
+ <text x="${labelWidth + barWidth + 6}" y="12" font-size="10" fill="${fg}">${s.pct}%</text>
107
+ </g>`;
108
+ })
109
+ .join('');
110
+
111
+ const overallColor = getColorForScore(stats.overall);
112
+
113
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
114
+ <rect width="${width}" height="${height}" rx="6" fill="${bg}" stroke="${border}" stroke-width="1"/>
115
+ <g transform="translate(${padding}, 0)">
116
+ <text x="0" y="18" font-size="12" font-weight="600" fill="${accent}">DocCov</text>
117
+ <text x="${width - padding * 2}" y="18" font-size="12" font-weight="600" fill="${overallColor}" text-anchor="end">${stats.overall}%</text>
118
+ </g>
119
+ <line x1="0" y1="${headerHeight}" x2="${width}" y2="${headerHeight}" stroke="${border}" stroke-width="1"/>
120
+ <style>text { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }</style>
121
+ ${rows}
122
+ </svg>`;
123
+ }
124
+
125
+ function generateErrorSvg(message: string, theme: WidgetTheme): string {
126
+ const isDark = theme === 'dark';
127
+ const bg = isDark ? '#0d1117' : '#ffffff';
128
+ const fg = isDark ? '#c9d1d9' : '#24292f';
129
+ const border = isDark ? '#30363d' : '#d0d7de';
130
+
131
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="160" height="50" viewBox="0 0 160 50">
132
+ <rect width="160" height="50" rx="6" fill="${bg}" stroke="${border}" stroke-width="1"/>
133
+ <text x="80" y="20" font-size="12" font-weight="600" fill="#58a6ff" text-anchor="middle">DocCov</text>
134
+ <text x="80" y="36" font-size="10" fill="${fg}" text-anchor="middle">${message}</text>
135
+ <style>text { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }</style>
136
+ </svg>`;
137
+ }
138
+
139
+ // GET /widget/:owner/:repo
140
+ widgetRoute.get('/:owner/:repo', async (c) => {
141
+ const { owner, repo } = c.req.param();
142
+ const branch = c.req.query('branch') ?? 'main';
143
+ const theme = (c.req.query('theme') ?? 'dark') as WidgetTheme;
144
+ const compact = c.req.query('compact') === 'true';
145
+
146
+ try {
147
+ const spec = await fetchSpecFromGitHub(owner, repo, branch);
148
+
149
+ if (!spec) {
150
+ const svg = generateErrorSvg('not found', theme);
151
+ return c.body(svg, 404, {
152
+ 'Content-Type': 'image/svg+xml',
153
+ 'Cache-Control': 'no-cache',
154
+ });
155
+ }
156
+
157
+ const stats = computeSignalCoverage(spec);
158
+ const svg = generateWidgetSvg(stats, { theme, compact });
159
+
160
+ return c.body(svg, 200, {
161
+ 'Content-Type': 'image/svg+xml',
162
+ 'Cache-Control': 'public, max-age=300',
163
+ });
164
+ } catch {
165
+ const svg = generateErrorSvg('error', theme);
166
+ return c.body(svg, 500, {
167
+ 'Content-Type': 'image/svg+xml',
168
+ 'Cache-Control': 'no-cache',
169
+ });
170
+ }
171
+ });
172
+
173
+ // GET /widget/:owner/:repo.svg (alias)
174
+ widgetRoute.get('/:owner/:repo.svg', async (c) => {
175
+ const { owner, repo } = c.req.param();
176
+ const repoName = repo.replace(/\.svg$/, '');
177
+ return c.redirect(`/widget/${owner}/${repoName}?${c.req.url.split('?')[1] ?? ''}`);
178
+ });
@@ -0,0 +1,25 @@
1
+ import type { OpenPkg } from '@openpkg-ts/spec';
2
+
3
+ export async function fetchSpecFromGitHub(
4
+ owner: string,
5
+ repo: string,
6
+ branch = 'main',
7
+ ): Promise<OpenPkg | null> {
8
+ const urls = [
9
+ `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/openpkg.json`,
10
+ `https://raw.githubusercontent.com/${owner}/${repo}/master/openpkg.json`,
11
+ ];
12
+
13
+ for (const url of urls) {
14
+ try {
15
+ const response = await fetch(url);
16
+ if (response.ok) {
17
+ return (await response.json()) as OpenPkg;
18
+ }
19
+ } catch {
20
+ // Try next URL
21
+ }
22
+ }
23
+
24
+ return null;
25
+ }
package/tsconfig.json CHANGED
@@ -3,17 +3,13 @@
3
3
  "target": "ES2022",
4
4
  "module": "ESNext",
5
5
  "moduleResolution": "bundler",
6
- "strict": true,
7
6
  "esModuleInterop": true,
7
+ "strict": true,
8
8
  "skipLibCheck": true,
9
- "forceConsistentCasingInFileNames": true,
10
9
  "resolveJsonModule": true,
11
- "declaration": true,
12
- "declarationMap": true,
13
- "outDir": "./dist",
14
- "rootDir": "./src"
10
+ "isolatedModules": true,
11
+ "noEmit": true
15
12
  },
16
- "include": ["src/**/*"],
17
- "exclude": ["node_modules", "dist"]
13
+ "include": ["api/**/*", "src/**/*"],
14
+ "exclude": ["node_modules"]
18
15
  }
19
-
package/vercel.json CHANGED
@@ -1,6 +1,10 @@
1
1
  {
2
- "buildCommand": "bun install",
3
- "installCommand": "bun install",
4
- "framework": null,
5
- "outputDirectory": "."
6
- }
2
+ "rewrites": [
3
+ { "source": "/badge/:path*", "destination": "/api" },
4
+ { "source": "/spec/:path*", "destination": "/api" },
5
+ { "source": "/scan-stream", "destination": "/api/scan-stream" },
6
+ { "source": "/scan/detect", "destination": "/api/scan/detect" },
7
+ { "source": "/scan", "destination": "/api/scan" },
8
+ { "source": "/(.*)", "destination": "/api" }
9
+ ]
10
+ }