Skip to content

feat: generate package docs using @deno/doc#135

Merged
danielroe merged 25 commits intonpmx-dev:mainfrom
devdumpling:dwells-feat-docs-wasm-attempt
Jan 27, 2026
Merged

feat: generate package docs using @deno/doc#135
danielroe merged 25 commits intonpmx-dev:mainfrom
devdumpling:dwells-feat-docs-wasm-attempt

Conversation

@devdumpling
Copy link
Copy Markdown
Contributor

@devdumpling devdumpling commented Jan 26, 2026

Overview

This PR is one of two variants adding autogenerated documentation pages, accessible via a nice little link and routing to /docs/{package}/v/{version}.

If a package provides a homepage in its manifest, the link will default to that. Otherwise, we attempt to generate docs via deno docs using a WASM binary around @deno/docs. More on this below.

Note: in the other variant, we use a microservice instead of WASM. This has tradeoffs. See this PR.

Problem

Okay, so npmx.dev currently shows all kinds of fun package data (metadata, READMEs, file contents, etc). But it lacks a feature I love from JSR/crates/other modern package browsers... autogenerated docs off type definitions.

Generating these kind of docs is challenging for npm packages because:

  • npm pkgs don't ship in a standard doc format
  • TS types may be inline, in .d.ts, @types/*, jsdocs, or any combination
  • extracting and parsing that requires complex ASTs and compute overhead

Solution

Luckily, there's tons of prior art here. In particular, Deno and the JSR folks basically already solved this problem with deno doc.

Note: we should make sure they're cool with us using this if we end up going this direction... since we're sort of competing in a sense.

Architecture

Uses @deno/doc (official Deno doc generator package) compiled to WASM, running directly in the Node.js/Vercel serverless env. No subprocess shelling or microservice required.

It's a bit more complex than the microservice under the hood though, and has some thorns. It works like this:

  1. Fetches the types URL from esh.sh with a x-typescript-types header
  2. Loads the TS definitions using custom fetch-based loader
  3. Parses them with @deno/doc WASM to extract DocNode[]
  4. Render as HTML with syntax highlighting

Parts of this are the same as the microservice one, just calling it out here.

Arch Diagram

Screenshot 2026-01-26 at 12 36 31 PM

Implementation

This was quite a ride for me, as much of it is transforming the format from deno docs --json, but there's some key points.

Most of the heavy lifting is done in /server/utils/docs/ and is broken down into a few submodules:

  • index | entry point, calls generateDocsWithDeno(pkg, version)
  • client | WASM integration
  • processing | flattens namespaces, merge overloads, build symbol lookup, etc
  • render | generate HTML with Shiki syntax highlighting server side to not bloat the JS payload
  • text | escape HTML, parse JSDocs links, render markdown
  • format | format typescript signatures
  • types | internal types

WASM integration

The WASM integration required a Node.js compatability patch since @deno/doc is designed for Deno. This is a bit concerning since we'd have to maintain this or upstream it to the deno repo (possible, but not sure what they'd think). The issues are:

  1. It has .d.ts exports in JS, which node won't natively resolve (see export * from './types.d.ts' lines in it)
  2. WASM loader doesn't support Node.js fs, it tries to use the deno native one, so we needed to monkeypatch it to use node:fs and node:url for file:// URLs

I managed the patch with pnpm and is auto-applied on install.

Frontend implementation

The frontend implementation simply fetches from the api endpoint (/api/registry/docs/{pkg}/v/{version}) and renders the pre-generated HTML in v-html. I'm still learning vue/nuxt, so let me know if I'm doing anything dumb here.

The UI could probably be improved / iterated on. I left that out of scope for this POC / initial implementation.

A note on esm.sh

I needed to use esm.sh here instead of our existing calls to jsDeliver. Why?

In my testing, I couldn't get jsDeliver to resolve @types/* packages for untyped packages properly and kept hitting module resolution issues with varying .js|.ts extension 😢. I'm betting there's a way to make it work, but esm.sh worked on the first try, so I went with that. It also serves the typescript types natively with a x-typescript-types header which I use here.

If we're concerned about having two underlying providers here (jsDeliver and esm.sh) I can take this back to the drawing board / iterate.

Note on bundle size

This does add some WASM weight to our bundle. It should load once and then cache and only impact server-side, but worth calling out as downside.

Alternative approaches

I personally prefer this to the microservice as it works without more infra, but the caveats in the WASM bindings are worth exploring fixes / alternatives to.

As in the microservice approach, here are the alternatives:

  1. A microservice variant -- run deno doc --json out of a microservice
  2. Build our own parsing / AST solution natively, likely in Rust (I tried doing this with TypeDoc and it was clumsy and slow). We don't currently have any Rust stuff in the codebase so big decision there. Might be better solve long term.

Option 2 may be interesting, but also comes with the steep cost of diverging from the incredible work the JSR team already put into this. If we want to build out other rust stuff here though, might make sense. We could also theoretically do more interesting stuff than is possible with just the json data we get from deno doc --json.

closes #134

@vercel
Copy link
Copy Markdown

vercel Bot commented Jan 26, 2026

@devdumpling is attempting to deploy a commit to the danielroe Team on Vercel.

A member of the Team first needs to authorize it.

@vercel
Copy link
Copy Markdown

vercel Bot commented Jan 26, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
npmx-dev-docs Ready Ready Preview, Comment Jan 27, 2026 10:19pm
npmx.dev Ready Ready Preview, Comment Jan 27, 2026 10:19pm

Request Review

@danielroe
Copy link
Copy Markdown
Member

deployed, if you want to test ✅

@devdumpling
Copy link
Copy Markdown
Contributor Author

deployed, if you want to test ✅

Thanks! Just finished writing up the description in the microservice variant. #134

Comment thread app/pages/[...package].vue Outdated
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i hate this but... it does work

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

although i realize now this isn't going to work in serverless envs 😭

back to the drawing board with this part

swr: true,
getKey: event => {
const pkg = getRouterParam(event, 'pkg') ?? ''
return `docs:v5:${pkg}`
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was rolling multiple versions of this locally during rapid prototyping -- needs to be dropped back before moving from poc

})

// Shiki doesn't encode > in text content (e.g., arrow functions)
html = escapeRawGt(html)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this also fixes a lot of random html validation warnings we're getting

Comment thread shared/utils/npm.ts
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if this is better defined / housed elsewhere (i didn't see anything for it at first glance flipping through the repo)

@devdumpling devdumpling force-pushed the dwells-feat-docs-wasm-attempt branch 3 times, most recently from 87ee5b2 to 2987c0c Compare January 27, 2026 11:47
@devdumpling
Copy link
Copy Markdown
Contributor Author

devdumpling commented Jan 27, 2026

Still trying to debug this, but tough as whatever is failing seems to be specific to the vercel env. I'm guessing I need to configure something for nitro / vercel to make it happy w wasm.

Test urls are here:

I ran the vercel build locally and peeked through the build artifacts. I can see the wasm in the vercel function bundle. It's 4.7mb at the correct path. The patch is there as well and module importing / doc gen works in it. It's under the 25mb vercel limit too so...?

Only things I can think of without having access to the logs are:

  • cold start time is too high for this
  • function timeout default is too low for this (maybe it's taking longer than 10s or something?)
  • memory? i can't imagine this would hit a memory limit but maybe

Or that there's just something completely different wrong I'm missing here (likely).

@devdumpling devdumpling force-pushed the dwells-feat-docs-wasm-attempt branch 5 times, most recently from 4fcc2ee to 3d0121c Compare January 27, 2026 13:09
Comment on lines +327 to +334
* Fenced code blocks in descriptions use a subtle left-border style.
*
* Design rationale: We use two visual styles for code examples:
* 1. Boxed style (bg + border + padding) - for formal @example JSDoc tags
* and function signatures. These are intentional, structured sections.
* 2. Left-border style (blockquote-like) - for inline code in descriptions.
* These are illustrative/casual and shouldn't compete with the signature.
*/
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LMK if anyone has questions on this. My rationale was that the examples should feel more prominent. For a comparison look at the svelte docs page vs the react one.

React one uses a lot of @example while svelte has inline @description with casual examples.

Comment thread app/pages/[...package].vue Outdated
Comment on lines +680 to +703
<a
:href="homepageUrl"
target="_blank"
rel="noopener noreferrer"
class="link-subtle font-mono text-sm inline-flex items-center gap-1.5"
>
<span class="i-carbon-link w-4 h-4" aria-hidden="true" />
{{ t('package.links.homepage') }}
</a>
</li>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dropped the homepage url here in favor of the docs link, but as I'm looking back I wonder if maybe we just add both instead?

Comment thread i18n/locales/en.json Outdated
"code": "code"
"code": "code",
"docs": "docs",
"socket": "socket"
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addresses this on main

export function formatType(type?: TsType): string {
if (!type) return ''

// Strip ANSI codes from repr (deno doc may include terminal colors since it's built for that)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is hacky

Comment thread package.json Outdated
Comment on lines +86 to +99
"resolutions": {
"vite": "^8.0.0-beta.10"
},
"pnpm": {
"overrides": {
"sharp": "0.34.5"
},
"packageExtensions": {
"@nuxt/scripts": {
"dependencies": {
"estree-walker": "^3.0.3"
}
}
},
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did these get dropped since I started working on this? Taking a look now...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah i see 14ccc37

@danielroe danielroe changed the title feat: docs (wasm variant) feat: generate package docs using @deno/doc Jan 27, 2026
- Add synchronous highlightCodeSync() function to shiki.ts
- Update readme.ts to use shared function instead of duplicating logic
- Reduces code duplication between readme and docs rendering
@danielroe danielroe merged commit 8aae4f3 into npmx-dev:main Jan 27, 2026
9 checks passed
vinnymac pushed a commit to vinnymac/npmx.dev that referenced this pull request Jan 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants