diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000000..41583e36ca --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +@jsr:registry=https://npm.jsr.io diff --git a/PLAN_MICRO.md b/PLAN_MICRO.md new file mode 100644 index 0000000000..825dd75742 --- /dev/null +++ b/PLAN_MICRO.md @@ -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 { + 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 { + const headers: Record = { + '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 { + 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 { + 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 diff --git a/PLAN_WASM.md b/PLAN_WASM.md new file mode 100644 index 0000000000..2240b3e8f2 --- /dev/null +++ b/PLAN_WASM.md @@ -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 diff --git a/app/components/AppHeader.vue b/app/components/AppHeader.vue index d3ab19aa5d..ca95211937 100644 --- a/app/components/AppHeader.vue +++ b/app/components/AppHeader.vue @@ -12,7 +12,10 @@ withDefaults(