Skip to content

Commit 66bc1af

Browse files
committed
chore: some planning, remove old typedoc ref
1 parent ab87991 commit 66bc1af

4 files changed

Lines changed: 325 additions & 74 deletions

File tree

PLAN_MICRO.md

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
# Plan: Deno Microservice for Documentation Generation
2+
3+
> **Status**: Recommended approach. Uses official `@deno/doc` with full feature support.
4+
5+
Deploy a separate Vercel project using `vercel-deno` runtime that exposes a docs generation API.
6+
7+
## Architecture
8+
9+
```
10+
npmx.dev (Nuxt/Node.js) docs-api.npmx.dev (Deno)
11+
┌─────────────────────┐ ┌─────────────────────┐
12+
│ /api/registry/docs │ HTTP │ /api/generate │
13+
│ │ │ ──────> │ │ │
14+
│ ▼ │ │ ▼ │
15+
│ generateDocsWithDeno│ │ @deno/doc │
16+
└─────────────────────┘ └─────────────────────┘
17+
```
18+
19+
## Implementation
20+
21+
### Part 1: Deno Microservice
22+
23+
#### Project Structure
24+
25+
```
26+
docs-api/
27+
├── api/
28+
│ └── generate.ts
29+
├── vercel.json
30+
└── README.md
31+
```
32+
33+
#### `vercel.json`
34+
35+
```json
36+
{
37+
"$schema": "https://openapi.vercel.sh/vercel.json",
38+
"functions": {
39+
"api/**/*.[jt]s": {
40+
"runtime": "vercel-deno@3.1.0"
41+
}
42+
}
43+
}
44+
```
45+
46+
#### `api/generate.ts`
47+
48+
```typescript
49+
#!/usr/bin/env deno run --allow-net --allow-env
50+
51+
import { doc } from "jsr:@deno/doc";
52+
53+
interface GenerateRequest {
54+
package: string;
55+
version: string;
56+
}
57+
58+
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}`;
63+
}
64+
65+
export default async function handler(req: Request): Promise<Response> {
66+
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 });
75+
}
76+
77+
if (req.method !== "POST") {
78+
return new Response(
79+
JSON.stringify({ error: "method_not_allowed" }),
80+
{ status: 405, headers }
81+
);
82+
}
83+
84+
if (!validateAuth(req)) {
85+
return new Response(
86+
JSON.stringify({ error: "unauthorized" }),
87+
{ status: 401, headers }
88+
);
89+
}
90+
91+
try {
92+
const body: GenerateRequest = await req.json();
93+
94+
if (!body.package || !body.version) {
95+
return new Response(
96+
JSON.stringify({ error: "bad_request" }),
97+
{ status: 400, headers }
98+
);
99+
}
100+
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 });
105+
} 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+
);
113+
}
114+
115+
return new Response(
116+
JSON.stringify({ error: "generation_failed", message }),
117+
{ status: 500, headers }
118+
);
119+
}
120+
}
121+
```
122+
123+
### Part 2: Update Main App
124+
125+
#### Environment Variables
126+
127+
```bash
128+
DOCS_API_URL=https://docs-api.npmx.dev/api/generate
129+
DOCS_API_SECRET=your-secret-token
130+
```
131+
132+
#### Update `server/utils/docs.ts`
133+
134+
```typescript
135+
const DOCS_API_URL = process.env.DOCS_API_URL || 'https://docs-api.npmx.dev/api/generate'
136+
const DOCS_API_SECRET = process.env.DOCS_API_SECRET
137+
138+
async function runDenoDoc(packageName: string, version: string): Promise<DenoDocResult> {
139+
const headers: Record<string, string> = {
140+
'Content-Type': 'application/json',
141+
}
142+
143+
if (DOCS_API_SECRET) {
144+
headers['Authorization'] = `Bearer ${DOCS_API_SECRET}`
145+
}
146+
147+
const response = await fetch(DOCS_API_URL, {
148+
method: 'POST',
149+
headers,
150+
body: JSON.stringify({ package: packageName, version }),
151+
})
152+
153+
if (!response.ok) {
154+
const error = await response.json().catch(() => ({ message: 'Unknown error' }))
155+
if (response.status === 404) {
156+
return { nodes: [] }
157+
}
158+
throw new Error(`Docs API error: ${error.message}`)
159+
}
160+
161+
return await response.json() as DenoDocResult
162+
}
163+
164+
export async function generateDocsWithDeno(
165+
packageName: string,
166+
version: string,
167+
): Promise<DocsGenerationResult | null> {
168+
const result = await runDenoDoc(packageName, version)
169+
170+
if (!result.nodes || result.nodes.length === 0) {
171+
return null
172+
}
173+
174+
// Rest remains the same
175+
const flattenedNodes = flattenNamespaces(result.nodes)
176+
const mergedSymbols = mergeOverloads(flattenedNodes)
177+
const symbolLookup = buildSymbolLookup(flattenedNodes)
178+
179+
const html = await renderDocNodes(mergedSymbols, symbolLookup)
180+
const toc = renderToc(mergedSymbols)
181+
182+
return { html, toc, nodes: flattenedNodes }
183+
}
184+
```
185+
186+
#### Remove Unused Code
187+
188+
Delete from `server/utils/docs.ts`:
189+
- `execFileAsync` import
190+
- `DENO_DOC_TIMEOUT_MS`, `DENO_DOC_MAX_BUFFER` constants
191+
- `denoCheckPromise`, `isDenoInstalled()`, `verifyDenoInstalled()`
192+
- `buildEsmShUrl()` (moved to microservice)
193+
- Old `runDenoDoc()` implementation
194+
195+
### Local Development
196+
197+
Keep subprocess as fallback for local dev:
198+
199+
```typescript
200+
async function runDenoDoc(packageName: string, version: string): Promise<DenoDocResult> {
201+
if (process.dev && await isDenoInstalled()) {
202+
return runLocalDenoDoc(packageName, version)
203+
}
204+
return runRemoteDenoDoc(packageName, version)
205+
}
206+
```
207+
208+
## Pros/Cons
209+
210+
**Pros**: Uses official `@deno/doc`, exact parity with `deno doc` CLI, actively maintained, clean separation
211+
212+
**Cons**: Two deployments, +100-200ms latency, CORS/auth setup, more complex local dev

PLAN_WASM.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Plan: WASM-based Documentation Generation
2+
3+
> **Status**: Alternative approach. See PLAN_MICRO.md for recommended approach.
4+
5+
Replace the `deno doc` subprocess with `tsdoc-extractor`, a WASM build of `deno_doc` for Node.js.
6+
7+
## Package
8+
9+
- **npm**: `tsdoc-extractor`
10+
- **Size**: 2.8MB WASM + ~25KB JS
11+
- **Last updated**: July 2023
12+
- **Output**: Same JSON format as `deno doc --json`
13+
14+
## Test Results
15+
16+
```
17+
ufo@1.5.0: 58 nodes (works)
18+
vue@3.5.0: 4 nodes (re-exports not followed - WASM limitation)
19+
```
20+
21+
## Key Finding
22+
23+
esm.sh serves types URL in the `x-typescript-types` header, not at the main URL:
24+
25+
```bash
26+
$ curl -sI 'https://esm.sh/ufo@1.5.0' | grep x-typescript-types
27+
x-typescript-types: https://esm.sh/ufo@1.5.0/dist/index.d.ts
28+
```
29+
30+
## Architecture
31+
32+
```
33+
Current: [Request] -> [subprocess: deno doc --json] -> [parse JSON]
34+
New: [Request] -> [fetch types] -> [tsdoc-extractor WASM] -> [parse JSON]
35+
```
36+
37+
## Implementation
38+
39+
### 1. Install
40+
41+
```bash
42+
pnpm add tsdoc-extractor
43+
```
44+
45+
### 2. Create `server/utils/docs-wasm.ts`
46+
47+
```typescript
48+
import { doc, defaultResolver } from 'tsdoc-extractor'
49+
import type { DenoDocNode, DocsGenerationResult } from '#shared/types/deno-doc'
50+
51+
async function getTypesUrl(packageName: string, version: string): Promise<string | null> {
52+
const url = `https://esm.sh/${packageName}@${version}`
53+
const response = await fetch(url, { method: 'HEAD' })
54+
return response.headers.get('x-typescript-types')
55+
}
56+
57+
function createResolver() {
58+
return (specifier: string, referrer: string): string => {
59+
if (specifier.startsWith('.')) {
60+
return new URL(specifier, referrer).toString()
61+
}
62+
if (specifier.startsWith('https://esm.sh/')) {
63+
return specifier
64+
}
65+
if (!specifier.startsWith('http')) {
66+
return `https://esm.sh/${specifier}`
67+
}
68+
return defaultResolver(specifier, referrer)
69+
}
70+
}
71+
72+
export async function generateDocsWithWasm(
73+
packageName: string,
74+
version: string,
75+
): Promise<DocsGenerationResult | null> {
76+
const typesUrl = await getTypesUrl(packageName, version)
77+
78+
if (!typesUrl) {
79+
return null
80+
}
81+
82+
const nodes = await doc(typesUrl, {
83+
resolve: createResolver(),
84+
}) as DenoDocNode[]
85+
86+
if (!nodes || nodes.length === 0) {
87+
return null
88+
}
89+
90+
const flattenedNodes = flattenNamespaces(nodes)
91+
const mergedSymbols = mergeOverloads(flattenedNodes)
92+
const symbolLookup = buildSymbolLookup(flattenedNodes)
93+
94+
const html = await renderDocNodes(mergedSymbols, symbolLookup)
95+
const toc = renderToc(mergedSymbols)
96+
97+
return { html, toc, nodes: flattenedNodes }
98+
}
99+
```
100+
101+
## Limitations
102+
103+
- **WASM from 2023**: Known issues with `export *` re-exports
104+
- **Rebuild requires Rust**: Need wasm-pack and deno_doc source to update
105+
106+
## Pros/Cons
107+
108+
**Pros**: No external dependency, single deployment, faster cold starts
109+
110+
**Cons**: Old WASM with known issues, complex packages may fail, 2.8MB bundle increase

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,7 @@
4848
"validate-npm-package-name": "^7.0.2",
4949
"virtua": "^0.48.3",
5050
"vue": "3.5.27",
51-
"vue-data-ui": "^3.13.2",
52-
"typedoc": "^0.28.16"
51+
"vue-data-ui": "^3.13.2"
5352
},
5453
"devDependencies": {
5554
"@iconify-json/carbon": "1.2.18",

0 commit comments

Comments
 (0)