Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@jsr:registry=https://npm.jsr.io
201 changes: 201 additions & 0 deletions PLAN_MICRO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
# Plan: Deno Microservice for Documentation Generation

> **Status**: Recommended approach. Uses official `@deno/doc` with full feature support.

Deploy a separate Vercel project using `vercel-deno` runtime that exposes a docs generation API.

## Architecture

```
npmx.dev (Nuxt/Node.js) docs-api.npmx.dev (Deno)
┌─────────────────────┐ ┌─────────────────────┐
│ /api/registry/docs │ HTTP │ /api/generate │
│ │ │ ──────> │ │ │
│ ▼ │ │ ▼ │
│ generateDocsWithDeno│ │ @deno/doc │
└─────────────────────┘ └─────────────────────┘
```

## Implementation

### Part 1: Deno Microservice

#### Project Structure

```
docs-api/
├── api/
│ └── generate.ts
├── vercel.json
└── README.md
```

#### `vercel.json`

```json
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"functions": {
"api/**/*.[jt]s": {
"runtime": "vercel-deno@3.1.0"
}
}
}
```

#### `api/generate.ts`

```typescript
#!/usr/bin/env deno run --allow-net --allow-env

import { doc } from 'jsr:@deno/doc'

interface GenerateRequest {
package: string
version: string
}

function validateAuth(req: Request): boolean {
const authHeader = req.headers.get('Authorization')
const expectedToken = Deno.env.get('API_SECRET')
if (!expectedToken) return true
return authHeader === `Bearer ${expectedToken}`
}

export default async function handler(req: Request): Promise<Response> {
const headers = {
'Access-Control-Allow-Origin': 'https://npmx.dev',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Content-Type': 'application/json',
}

if (req.method === 'OPTIONS') {
return new Response(null, { status: 204, headers })
}

if (req.method !== 'POST') {
return new Response(JSON.stringify({ error: 'method_not_allowed' }), { status: 405, headers })
}

if (!validateAuth(req)) {
return new Response(JSON.stringify({ error: 'unauthorized' }), { status: 401, headers })
}

try {
const body: GenerateRequest = await req.json()

if (!body.package || !body.version) {
return new Response(JSON.stringify({ error: 'bad_request' }), { status: 400, headers })
}

const specifier = `https://esm.sh/${body.package}@${body.version}?target=deno`
const nodes = await doc(specifier)

return new Response(JSON.stringify({ nodes }), { status: 200, headers })
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'

if (message.includes('Could not find')) {
return new Response(JSON.stringify({ error: 'not_found' }), { status: 404, headers })
}

return new Response(JSON.stringify({ error: 'generation_failed', message }), {
status: 500,
headers,
})
}
}
```

### Part 2: Update Main App

#### Environment Variables

```bash
DOCS_API_URL=https://docs-api.npmx.dev/api/generate
DOCS_API_SECRET=your-secret-token
```

#### Update `server/utils/docs.ts`

```typescript
const DOCS_API_URL = process.env.DOCS_API_URL || 'https://docs-api.npmx.dev/api/generate'
const DOCS_API_SECRET = process.env.DOCS_API_SECRET

async function runDenoDoc(packageName: string, version: string): Promise<DenoDocResult> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
}

if (DOCS_API_SECRET) {
headers['Authorization'] = `Bearer ${DOCS_API_SECRET}`
}

const response = await fetch(DOCS_API_URL, {
method: 'POST',
headers,
body: JSON.stringify({ package: packageName, version }),
})

if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Unknown error' }))
if (response.status === 404) {
return { nodes: [] }
}
throw new Error(`Docs API error: ${error.message}`)
}

return (await response.json()) as DenoDocResult
}

export async function generateDocsWithDeno(
packageName: string,
version: string,
): Promise<DocsGenerationResult | null> {
const result = await runDenoDoc(packageName, version)

if (!result.nodes || result.nodes.length === 0) {
return null
}

// Rest remains the same
const flattenedNodes = flattenNamespaces(result.nodes)
const mergedSymbols = mergeOverloads(flattenedNodes)
const symbolLookup = buildSymbolLookup(flattenedNodes)

const html = await renderDocNodes(mergedSymbols, symbolLookup)
const toc = renderToc(mergedSymbols)

return { html, toc, nodes: flattenedNodes }
}
```

#### Remove Unused Code

Delete from `server/utils/docs.ts`:

- `execFileAsync` import
- `DENO_DOC_TIMEOUT_MS`, `DENO_DOC_MAX_BUFFER` constants
- `denoCheckPromise`, `isDenoInstalled()`, `verifyDenoInstalled()`
- `buildEsmShUrl()` (moved to microservice)
- Old `runDenoDoc()` implementation

### Local Development

Keep subprocess as fallback for local dev:

```typescript
async function runDenoDoc(packageName: string, version: string): Promise<DenoDocResult> {
if (process.dev && (await isDenoInstalled())) {
return runLocalDenoDoc(packageName, version)
}
return runRemoteDenoDoc(packageName, version)
}
```

## Pros/Cons

**Pros**: Uses official `@deno/doc`, exact parity with `deno doc` CLI, actively maintained, clean separation

**Cons**: Two deployments, +100-200ms latency, CORS/auth setup, more complex local dev
113 changes: 113 additions & 0 deletions PLAN_WASM.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Plan: WASM-based Documentation Generation

> **Status**: ✅ Working! Uses official `@deno/doc` v0.189.1 with patches for Node.js compatibility.

## Summary

Successfully replaced the `deno doc` subprocess with WASM-based documentation generation that runs directly in Node.js/Vercel serverless, using the official `@deno/doc` package from JSR.

## Package Used

- **Package**: `@deno/doc` (JSR - official Deno package)
- **Version**: 0.189.1 (latest, Jan 2026)
- **WASM size**: 4.7MB
- **Install**: `pnpm install jsr:@deno/doc`

## Compatibility Issues & Fixes

We encountered three issues when running `@deno/doc` in Node.js. All were resolved with a single patch file.

### Issue 1: Invalid JavaScript exports ✅ Fixed

```js
// In mod.js - this is invalid JS, only works in Deno
export * from './types.d.ts'
export * from './html_types.d.ts'
```

**Fix**: Patch removes these lines.

### Issue 2: WASM loader doesn't support Node.js ✅ Fixed

```js
// Original code throws error for Node.js + file:// URLs
if (isFile && typeof Deno !== 'object') {
throw new Error('Loading local files are not supported in this environment')
}
```

**Fix**: Patch adds Node.js fs support:

```js
if (isNode && isFile) {
const { readFileSync } = await import('node:fs')
const { fileURLToPath } = await import('node:url')
const wasmCode = readFileSync(fileURLToPath(url))
return WebAssembly.instantiate(decompress ? decompress(wasmCode) : wasmCode, imports)
}
```

### Issue 3: Bundler rewrites WASM paths ✅ Fixed

When Nitro inlines the package, `import.meta.url` gets rewritten and WASM path becomes invalid.

**Fix**: Don't inline `@deno/doc` in Nitro config - let it load from `node_modules/`:

```ts
// nuxt.config.ts - do NOT add @deno/doc to nitro.externals.inline
```

## Patch File

All fixes are in `patches/@jsr__deno__doc@0.189.1.patch` (managed by pnpm).

## Key Finding

esm.sh serves types URL in the `x-typescript-types` header:

```bash
$ curl -sI 'https://esm.sh/ufo@1.5.0' | grep x-typescript-types
x-typescript-types: https://esm.sh/ufo@1.5.0/dist/index.d.ts
```

## Architecture

```
[Request] -> [fetch x-typescript-types header] -> [@deno/doc WASM] -> [HTML]
```

## Files Changed

- `server/utils/docs.ts` - Uses `@deno/doc` with custom loader/resolver
- `server/utils/docs-text.ts` - Extracted text utilities
- `patches/@jsr__deno__doc@0.189.1.patch` - Node.js compatibility patch

## Verified Working

- ✅ `ufo@1.5.0` - Generates full docs
- ✅ `react@19.0.0` - Large package, generates full docs
- ✅ All 361 tests pass
- ✅ Dev server runs without errors

## Pros/Cons

**Pros**:

- Single deployment (no microservice)
- Uses official, maintained `@deno/doc` (latest version)
- In-process, no network latency
- 4.7MB WASM loaded once, cached

**Cons**:

- Requires patch for Node.js compatibility (may need updates per version)
- 4.7MB added to server bundle
- Patch maintenance burden

## Alternative: Microservice Approach

See `PLAN_MICRO.md` for the microservice approach that runs actual Deno in a separate Vercel project. That approach:

- Requires no patches
- Has network latency
- Requires managing two deployments
5 changes: 4 additions & 1 deletion app/components/AppHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ withDefaults(
</script>

<template>
<header class="sticky top-0 z-50 bg-bg/80 backdrop-blur-md border-b border-border">
<header
aria-label="Site header"
class="sticky top-0 z-50 bg-bg/80 backdrop-blur-md border-b border-border"
>
<nav aria-label="Main navigation" class="container h-14 flex items-center justify-between">
<NuxtLink
v-if="showLogo"
Expand Down
Loading