|
1 | 1 | # Plan: WASM-based Documentation Generation |
2 | 2 |
|
3 | | -> **Status**: Alternative approach. See PLAN_MICRO.md for recommended approach. |
| 3 | +> **Status**: ✅ Working! Uses official `@deno/doc` v0.189.1 with patches for Node.js compatibility. |
4 | 4 |
|
5 | | -Replace the `deno doc` subprocess with `tsdoc-extractor`, a WASM build of `deno_doc` for Node.js. |
| 5 | +## Summary |
6 | 6 |
|
7 | | -## Package |
| 7 | +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. |
8 | 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` |
| 9 | +## Package Used |
13 | 10 |
|
14 | | -## Test Results |
| 11 | +- **Package**: `@deno/doc` (JSR - official Deno package) |
| 12 | +- **Version**: 0.189.1 (latest, Jan 2026) |
| 13 | +- **WASM size**: 4.7MB |
| 14 | +- **Install**: `pnpm install jsr:@deno/doc` |
15 | 15 |
|
16 | | -``` |
17 | | -ufo@1.5.0: 58 nodes (works) |
18 | | -vue@3.5.0: 4 nodes (re-exports not followed - WASM limitation) |
| 16 | +## Compatibility Issues & Fixes |
| 17 | + |
| 18 | +We encountered three issues when running `@deno/doc` in Node.js. All were resolved with a single patch file. |
| 19 | + |
| 20 | +### Issue 1: Invalid JavaScript exports ✅ Fixed |
| 21 | + |
| 22 | +```js |
| 23 | +// In mod.js - this is invalid JS, only works in Deno |
| 24 | +export * from './types.d.ts' |
| 25 | +export * from './html_types.d.ts' |
19 | 26 | ``` |
20 | 27 |
|
21 | | -## Key Finding |
| 28 | +**Fix**: Patch removes these lines. |
22 | 29 |
|
23 | | -esm.sh serves types URL in the `x-typescript-types` header, not at the main URL: |
| 30 | +### Issue 2: WASM loader doesn't support Node.js ✅ Fixed |
24 | 31 |
|
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 |
| 32 | +```js |
| 33 | +// Original code throws error for Node.js + file:// URLs |
| 34 | +if (isFile && typeof Deno !== 'object') { |
| 35 | + throw new Error('Loading local files are not supported in this environment') |
| 36 | +} |
28 | 37 | ``` |
29 | 38 |
|
30 | | -## Architecture |
| 39 | +**Fix**: Patch adds Node.js fs support: |
31 | 40 |
|
| 41 | +```js |
| 42 | +if (isNode && isFile) { |
| 43 | + const { readFileSync } = await import('node:fs') |
| 44 | + const { fileURLToPath } = await import('node:url') |
| 45 | + const wasmCode = readFileSync(fileURLToPath(url)) |
| 46 | + return WebAssembly.instantiate(decompress ? decompress(wasmCode) : wasmCode, imports) |
| 47 | +} |
32 | 48 | ``` |
33 | | -Current: [Request] -> [subprocess: deno doc --json] -> [parse JSON] |
34 | | -New: [Request] -> [fetch types] -> [tsdoc-extractor WASM] -> [parse JSON] |
| 49 | + |
| 50 | +### Issue 3: Bundler rewrites WASM paths ✅ Fixed |
| 51 | + |
| 52 | +When Nitro inlines the package, `import.meta.url` gets rewritten and WASM path becomes invalid. |
| 53 | + |
| 54 | +**Fix**: Don't inline `@deno/doc` in Nitro config - let it load from `node_modules/`: |
| 55 | + |
| 56 | +```ts |
| 57 | +// nuxt.config.ts - do NOT add @deno/doc to nitro.externals.inline |
35 | 58 | ``` |
36 | 59 |
|
37 | | -## Implementation |
| 60 | +## Patch File |
38 | 61 |
|
39 | | -### 1. Install |
| 62 | +All fixes are in `patches/@jsr__deno__doc@0.189.1.patch` (managed by pnpm). |
| 63 | + |
| 64 | +## Key Finding |
| 65 | + |
| 66 | +esm.sh serves types URL in the `x-typescript-types` header: |
40 | 67 |
|
41 | 68 | ```bash |
42 | | -pnpm add tsdoc-extractor |
| 69 | +$ curl -sI 'https://esm.sh/ufo@1.5.0' | grep x-typescript-types |
| 70 | +x-typescript-types: https://esm.sh/ufo@1.5.0/dist/index.d.ts |
43 | 71 | ``` |
44 | 72 |
|
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' |
| 73 | +## Architecture |
50 | 74 |
|
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 | | -} |
| 75 | +``` |
| 76 | +[Request] -> [fetch x-typescript-types header] -> [@deno/doc WASM] -> [HTML] |
| 77 | +``` |
56 | 78 |
|
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 | | -} |
| 79 | +## Files Changed |
71 | 80 |
|
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 | +- `server/utils/docs.ts` - Uses `@deno/doc` with custom loader/resolver |
| 82 | +- `server/utils/docs-text.ts` - Extracted text utilities |
| 83 | +- `patches/@jsr__deno__doc@0.189.1.patch` - Node.js compatibility patch |
81 | 84 |
|
82 | | - const nodes = await doc(typesUrl, { |
83 | | - resolve: createResolver(), |
84 | | - }) as DenoDocNode[] |
| 85 | +## Verified Working |
85 | 86 |
|
86 | | - if (!nodes || nodes.length === 0) { |
87 | | - return null |
88 | | - } |
| 87 | +- ✅ `ufo@1.5.0` - Generates full docs |
| 88 | +- ✅ `react@19.0.0` - Large package, generates full docs |
| 89 | +- ✅ All 361 tests pass |
| 90 | +- ✅ Dev server runs without errors |
89 | 91 |
|
90 | | - const flattenedNodes = flattenNamespaces(nodes) |
91 | | - const mergedSymbols = mergeOverloads(flattenedNodes) |
92 | | - const symbolLookup = buildSymbolLookup(flattenedNodes) |
| 92 | +## Pros/Cons |
93 | 93 |
|
94 | | - const html = await renderDocNodes(mergedSymbols, symbolLookup) |
95 | | - const toc = renderToc(mergedSymbols) |
| 94 | +**Pros**: |
96 | 95 |
|
97 | | - return { html, toc, nodes: flattenedNodes } |
98 | | -} |
99 | | -``` |
| 96 | +- Single deployment (no microservice) |
| 97 | +- Uses official, maintained `@deno/doc` (latest version) |
| 98 | +- In-process, no network latency |
| 99 | +- 4.7MB WASM loaded once, cached |
100 | 100 |
|
101 | | -## Limitations |
| 101 | +**Cons**: |
102 | 102 |
|
103 | | -- **WASM from 2023**: Known issues with `export *` re-exports |
104 | | -- **Rebuild requires Rust**: Need wasm-pack and deno_doc source to update |
| 103 | +- Requires patch for Node.js compatibility (may need updates per version) |
| 104 | +- 4.7MB added to server bundle |
| 105 | +- Patch maintenance burden |
105 | 106 |
|
106 | | -## Pros/Cons |
| 107 | +## Alternative: Microservice Approach |
107 | 108 |
|
108 | | -**Pros**: No external dependency, single deployment, faster cold starts |
| 109 | +See `PLAN_MICRO.md` for the microservice approach that runs actual Deno in a separate Vercel project. That approach: |
109 | 110 |
|
110 | | -**Cons**: Old WASM with known issues, complex packages may fail, 2.8MB bundle increase |
| 111 | +- Requires no patches |
| 112 | +- Has network latency |
| 113 | +- Requires managing two deployments |
0 commit comments