Skip to content

Commit 687ba47

Browse files
committed
feat: refactor docs implementation to use deno vercel microservice instead of subprocess
1 parent 095a758 commit 687ba47

13 files changed

Lines changed: 442 additions & 116 deletions

File tree

PLAN_MICRO.md

Lines changed: 38 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -48,74 +48,62 @@ docs-api/
4848
```typescript
4949
#!/usr/bin/env deno run --allow-net --allow-env
5050

51-
import { doc } from "jsr:@deno/doc";
51+
import { doc } from 'jsr:@deno/doc'
5252

5353
interface GenerateRequest {
54-
package: string;
55-
version: string;
54+
package: string
55+
version: string
5656
}
5757

5858
function validateAuth(req: Request): boolean {
59-
const authHeader = req.headers.get("Authorization");
60-
const expectedToken = Deno.env.get("API_SECRET");
61-
if (!expectedToken) return true;
62-
return authHeader === `Bearer ${expectedToken}`;
59+
const authHeader = req.headers.get('Authorization')
60+
const expectedToken = Deno.env.get('API_SECRET')
61+
if (!expectedToken) return true
62+
return authHeader === `Bearer ${expectedToken}`
6363
}
6464

6565
export default async function handler(req: Request): Promise<Response> {
6666
const headers = {
67-
"Access-Control-Allow-Origin": "https://npmx.dev",
68-
"Access-Control-Allow-Methods": "POST, OPTIONS",
69-
"Access-Control-Allow-Headers": "Content-Type, Authorization",
70-
"Content-Type": "application/json",
71-
};
72-
73-
if (req.method === "OPTIONS") {
74-
return new Response(null, { status: 204, headers });
67+
'Access-Control-Allow-Origin': 'https://npmx.dev',
68+
'Access-Control-Allow-Methods': 'POST, OPTIONS',
69+
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
70+
'Content-Type': 'application/json',
71+
}
72+
73+
if (req.method === 'OPTIONS') {
74+
return new Response(null, { status: 204, headers })
7575
}
7676

77-
if (req.method !== "POST") {
78-
return new Response(
79-
JSON.stringify({ error: "method_not_allowed" }),
80-
{ status: 405, headers }
81-
);
77+
if (req.method !== 'POST') {
78+
return new Response(JSON.stringify({ error: 'method_not_allowed' }), { status: 405, headers })
8279
}
8380

8481
if (!validateAuth(req)) {
85-
return new Response(
86-
JSON.stringify({ error: "unauthorized" }),
87-
{ status: 401, headers }
88-
);
82+
return new Response(JSON.stringify({ error: 'unauthorized' }), { status: 401, headers })
8983
}
9084

9185
try {
92-
const body: GenerateRequest = await req.json();
93-
86+
const body: GenerateRequest = await req.json()
87+
9488
if (!body.package || !body.version) {
95-
return new Response(
96-
JSON.stringify({ error: "bad_request" }),
97-
{ status: 400, headers }
98-
);
89+
return new Response(JSON.stringify({ error: 'bad_request' }), { status: 400, headers })
9990
}
10091

101-
const specifier = `https://esm.sh/${body.package}@${body.version}?target=deno`;
102-
const nodes = await doc(specifier);
103-
104-
return new Response(JSON.stringify({ nodes }), { status: 200, headers });
92+
const specifier = `https://esm.sh/${body.package}@${body.version}?target=deno`
93+
const nodes = await doc(specifier)
94+
95+
return new Response(JSON.stringify({ nodes }), { status: 200, headers })
10596
} catch (error) {
106-
const message = error instanceof Error ? error.message : "Unknown error";
107-
108-
if (message.includes("Could not find")) {
109-
return new Response(
110-
JSON.stringify({ error: "not_found" }),
111-
{ status: 404, headers }
112-
);
97+
const message = error instanceof Error ? error.message : 'Unknown error'
98+
99+
if (message.includes('Could not find')) {
100+
return new Response(JSON.stringify({ error: 'not_found' }), { status: 404, headers })
113101
}
114-
115-
return new Response(
116-
JSON.stringify({ error: "generation_failed", message }),
117-
{ status: 500, headers }
118-
);
102+
103+
return new Response(JSON.stringify({ error: 'generation_failed', message }), {
104+
status: 500,
105+
headers,
106+
})
119107
}
120108
}
121109
```
@@ -139,7 +127,7 @@ async function runDenoDoc(packageName: string, version: string): Promise<DenoDoc
139127
const headers: Record<string, string> = {
140128
'Content-Type': 'application/json',
141129
}
142-
130+
143131
if (DOCS_API_SECRET) {
144132
headers['Authorization'] = `Bearer ${DOCS_API_SECRET}`
145133
}
@@ -158,7 +146,7 @@ async function runDenoDoc(packageName: string, version: string): Promise<DenoDoc
158146
throw new Error(`Docs API error: ${error.message}`)
159147
}
160148

161-
return await response.json() as DenoDocResult
149+
return (await response.json()) as DenoDocResult
162150
}
163151

164152
export async function generateDocsWithDeno(
@@ -186,6 +174,7 @@ export async function generateDocsWithDeno(
186174
#### Remove Unused Code
187175

188176
Delete from `server/utils/docs.ts`:
177+
189178
- `execFileAsync` import
190179
- `DENO_DOC_TIMEOUT_MS`, `DENO_DOC_MAX_BUFFER` constants
191180
- `denoCheckPromise`, `isDenoInstalled()`, `verifyDenoInstalled()`
@@ -198,7 +187,7 @@ Keep subprocess as fallback for local dev:
198187

199188
```typescript
200189
async function runDenoDoc(packageName: string, version: string): Promise<DenoDocResult> {
201-
if (process.dev && await isDenoInstalled()) {
190+
if (process.dev && (await isDenoInstalled())) {
202191
return runLocalDenoDoc(packageName, version)
203192
}
204193
return runRemoteDenoDoc(packageName, version)

PLAN_WASM.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,14 +74,14 @@ export async function generateDocsWithWasm(
7474
version: string,
7575
): Promise<DocsGenerationResult | null> {
7676
const typesUrl = await getTypesUrl(packageName, version)
77-
77+
7878
if (!typesUrl) {
7979
return null
8080
}
8181

82-
const nodes = await doc(typesUrl, {
82+
const nodes = (await doc(typesUrl, {
8383
resolve: createResolver(),
84-
}) as DenoDocNode[]
84+
})) as DenoDocNode[]
8585

8686
if (!nodes || nodes.length === 0) {
8787
return null

app/components/AppHeader.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ withDefaults(
1212
</script>
1313

1414
<template>
15-
<header aria-label="Site header" class="sticky top-0 z-50 bg-bg/80 backdrop-blur-md border-b border-border">
15+
<header
16+
aria-label="Site header"
17+
class="sticky top-0 z-50 bg-bg/80 backdrop-blur-md border-b border-border"
18+
>
1619
<nav aria-label="Main navigation" class="container h-14 flex items-center justify-between">
1720
<NuxtLink
1821
v-if="showLogo"

app/components/DocsVersionSelector.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,11 @@ function handleKeydown(event: KeyboardEvent) {
112112
v-for="tag in tags"
113113
:key="tag"
114114
class="text-[10px] px-1.5 py-0.5 rounded font-sans font-medium"
115-
:class="tag === 'latest' ? 'bg-emerald-500/10 text-emerald-400' : 'bg-bg-muted text-fg-subtle'"
115+
:class="
116+
tag === 'latest'
117+
? 'bg-emerald-500/10 text-emerald-400'
118+
: 'bg-bg-muted text-fg-subtle'
119+
"
116120
>
117121
{{ tag }}
118122
</span>

app/pages/docs/[...path].vue

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,10 @@ const showEmptyState = computed(() => docsData.value?.status !== 'ok')
9393
<h1 class="sr-only">{{ packageName }} API Documentation</h1>
9494

9595
<!-- Sticky header - positioned below AppHeader -->
96-
<header aria-label="Package documentation header" class="docs-header sticky z-10 bg-bg/95 backdrop-blur border-b border-border">
96+
<header
97+
aria-label="Package documentation header"
98+
class="docs-header sticky z-10 bg-bg/95 backdrop-blur border-b border-border"
99+
>
97100
<div class="px-4 sm:px-6 lg:px-8 py-4">
98101
<div class="flex items-center justify-between gap-4">
99102
<div class="flex items-center gap-3 min-w-0">
@@ -116,7 +119,9 @@ const showEmptyState = computed(() => docsData.value?.status !== 'ok')
116119
</span>
117120
</div>
118121
<div class="flex items-center gap-3 shrink-0">
119-
<span class="text-xs px-2 py-1 rounded bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">
122+
<span
123+
class="text-xs px-2 py-1 rounded bg-emerald-500/10 text-emerald-400 border border-emerald-500/20"
124+
>
120125
API Docs
121126
</span>
122127
</div>
@@ -257,14 +262,30 @@ const showEmptyState = computed(() => docsData.value?.status !== 'ok')
257262
@apply text-xs px-2 py-0.5 rounded-full font-medium;
258263
}
259264
260-
.docs-content .docs-badge--function { @apply bg-blue-500/15 text-blue-400; }
261-
.docs-content .docs-badge--class { @apply bg-amber-500/15 text-amber-400; }
262-
.docs-content .docs-badge--interface { @apply bg-emerald-500/15 text-emerald-400; }
263-
.docs-content .docs-badge--typeAlias { @apply bg-violet-500/15 text-violet-400; }
264-
.docs-content .docs-badge--variable { @apply bg-orange-500/15 text-orange-400; }
265-
.docs-content .docs-badge--enum { @apply bg-pink-500/15 text-pink-400; }
266-
.docs-content .docs-badge--namespace { @apply bg-cyan-500/15 text-cyan-400; }
267-
.docs-content .docs-badge--async { @apply bg-purple-500/15 text-purple-400; }
265+
.docs-content .docs-badge--function {
266+
@apply bg-blue-500/15 text-blue-400;
267+
}
268+
.docs-content .docs-badge--class {
269+
@apply bg-amber-500/15 text-amber-400;
270+
}
271+
.docs-content .docs-badge--interface {
272+
@apply bg-emerald-500/15 text-emerald-400;
273+
}
274+
.docs-content .docs-badge--typeAlias {
275+
@apply bg-violet-500/15 text-violet-400;
276+
}
277+
.docs-content .docs-badge--variable {
278+
@apply bg-orange-500/15 text-orange-400;
279+
}
280+
.docs-content .docs-badge--enum {
281+
@apply bg-pink-500/15 text-pink-400;
282+
}
283+
.docs-content .docs-badge--namespace {
284+
@apply bg-cyan-500/15 text-cyan-400;
285+
}
286+
.docs-content .docs-badge--async {
287+
@apply bg-purple-500/15 text-purple-400;
288+
}
268289
269290
/* Signature code block - now uses Shiki */
270291
.docs-content .docs-signature {

docs-api/README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# docs-api
2+
3+
A Deno-based microservice for generating API documentation from npm packages.
4+
5+
## Overview
6+
7+
This service uses `@deno/doc` to generate documentation nodes from npm package types via esm.sh. It's designed to run on Vercel using the `vercel-deno` community runtime.
8+
9+
## Deployment
10+
11+
1. Deploy as a separate Vercel project
12+
2. Configure custom domain (e.g., `docs-api.npmx.dev`)
13+
3. Optionally set `API_SECRET` environment variable for authentication
14+
15+
## API
16+
17+
### POST /api/generate
18+
19+
Generate documentation for an npm package.
20+
21+
**Request:**
22+
23+
```json
24+
{
25+
"package": "ufo",
26+
"version": "1.5.0"
27+
}
28+
```
29+
30+
**Response (success):**
31+
32+
```json
33+
{
34+
"nodes": [...]
35+
}
36+
```
37+
38+
**Response (error):**
39+
40+
```json
41+
{
42+
"error": "not_found",
43+
"message": "Package types not found"
44+
}
45+
```
46+
47+
## Local Development
48+
49+
```bash
50+
cd docs-api
51+
deno run --allow-net --allow-env api/generate.ts
52+
```
53+
54+
## Environment Variables
55+
56+
- `API_SECRET` (optional): Bearer token for authentication

0 commit comments

Comments
 (0)