From 2d10ebb51c5e02050d8b813838601bb7a3f432be Mon Sep 17 00:00:00 2001
From: dev wells
Date: Sun, 25 Jan 2026 12:31:15 -0500
Subject: [PATCH 1/9] feat: prototyping phase for doc gen
---
.npmrc | 1 +
app/pages/[...package].vue | 12 +
app/pages/docs/[...path].vue | 372 ++++++++++
package.json | 3 +-
pnpm-lock.yaml | 164 +++-
pnpm-workspace.yaml | 1 +
prototypes/deno-doc/README.md | 183 +++++
prototypes/deno-doc/package.json | 17 +
prototypes/deno-doc/src/test-deno-cli.js | 249 +++++++
prototypes/deno-doc/src/test-direct-wasm.js | 73 ++
prototypes/deno-doc/src/test.js | 209 ++++++
prototypes/deno-doc/src/test.ts | 143 ++++
server/api/registry/docs/[...pkg].get.ts | 89 +++
server/utils/docs.ts | 782 ++++++++++++++++++++
shared/types/docs.ts | 17 +
shared/types/index.ts | 1 +
16 files changed, 2312 insertions(+), 4 deletions(-)
create mode 100644 .npmrc
create mode 100644 app/pages/docs/[...path].vue
create mode 100644 prototypes/deno-doc/README.md
create mode 100644 prototypes/deno-doc/package.json
create mode 100644 prototypes/deno-doc/src/test-deno-cli.js
create mode 100644 prototypes/deno-doc/src/test-direct-wasm.js
create mode 100644 prototypes/deno-doc/src/test.js
create mode 100644 prototypes/deno-doc/src/test.ts
create mode 100644 server/api/registry/docs/[...pkg].get.ts
create mode 100644 server/utils/docs.ts
create mode 100644 shared/types/docs.ts
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/app/pages/[...package].vue b/app/pages/[...package].vue
index bc79a7c135..ab95eda0aa 100644
--- a/app/pages/[...package].vue
+++ b/app/pages/[...package].vue
@@ -672,6 +672,18 @@ defineOgImageComponent('Package', {
+
+
+
+ docs
+
+
+import type { DocsResponse } from '#shared/types'
+
+definePageMeta({
+ name: 'docs',
+})
+
+const route = useRoute('docs')
+const router = useRouter()
+
+const parsedRoute = computed(() => {
+ const segments = route.params.path || []
+ const vIndex = segments.indexOf('v')
+
+ if (vIndex === -1 || vIndex >= segments.length - 1) {
+ return {
+ packageName: segments.join('/'),
+ version: null as string | null,
+ }
+ }
+
+ return {
+ packageName: segments.slice(0, vIndex).join('/'),
+ version: segments.slice(vIndex + 1).join('/'),
+ }
+})
+
+const packageName = computed(() => parsedRoute.value.packageName)
+const requestedVersion = computed(() => parsedRoute.value.version)
+
+const { data: pkg } = usePackage(packageName)
+
+const latestVersion = computed(() => pkg.value?.['dist-tags']?.latest ?? null)
+
+watch(
+ [requestedVersion, latestVersion, packageName],
+ ([version, latest, name]) => {
+ if (!version && latest && name) {
+ router.replace(`/docs/${name}/v/${latest}`)
+ }
+ },
+ { immediate: true },
+)
+
+const resolvedVersion = computed(() => requestedVersion.value ?? latestVersion.value)
+
+const docsUrl = computed(() => {
+ if (!packageName.value || !resolvedVersion.value) return null
+ return `/api/registry/docs/${packageName.value}/v/${resolvedVersion.value}`
+})
+
+const { data: docsData, status: docsStatus } = useLazyFetch(() => docsUrl.value!, {
+ watch: [docsUrl],
+ immediate: computed(() => !!docsUrl.value).value,
+ default: () => ({
+ package: packageName.value,
+ version: resolvedVersion.value ?? '',
+ html: '',
+ toc: null,
+ breadcrumbs: null,
+ status: 'missing',
+ message: 'Docs are not available for this version.',
+ }),
+})
+
+const pageTitle = computed(() => {
+ if (!packageName.value) return 'API Docs - npmx'
+ if (!resolvedVersion.value) return `${packageName.value} docs - npmx`
+ return `${packageName.value}@${resolvedVersion.value} docs - npmx`
+})
+
+useSeoMeta({
+ title: () => pageTitle.value,
+})
+
+const showLoading = computed(() => docsStatus.value === 'pending')
+const showEmptyState = computed(() => docsData.value?.status !== 'ok')
+
+
+
+
+
+
+
+
+
+
+ {{ packageName }}
+
+
+ {{ resolvedVersion }}
+
+
+
+
+ API Docs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Docs not available
+
+ {{ docsData?.message ?? 'We could not generate docs for this version.' }}
+
+
+
+ View package
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/package.json b/package.json
index 4465fca258..452da922b9 100644
--- a/package.json
+++ b/package.json
@@ -48,7 +48,8 @@
"validate-npm-package-name": "^7.0.2",
"virtua": "^0.48.3",
"vue": "3.5.27",
- "vue-data-ui": "^3.13.2"
+ "vue-data-ui": "^3.13.2",
+ "typedoc": "^0.28.16"
},
"devDependencies": {
"@iconify-json/carbon": "1.2.18",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2e01ccde5a..4c66c45b19 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -62,6 +62,9 @@ importers:
shiki:
specifier: ^3.21.0
version: 3.21.0
+ typedoc:
+ specifier: ^0.28.16
+ version: 0.28.16(typescript@5.9.3)
ufo:
specifier: ^1.6.3
version: 1.6.3
@@ -203,6 +206,16 @@ importers:
specifier: 5.9.3
version: 5.9.3
+ prototypes/deno-doc:
+ dependencies:
+ '@deno/doc':
+ specifier: npm:@jsr/deno__doc@^0.189.1
+ version: '@jsr/deno__doc@0.189.1'
+ devDependencies:
+ '@types/node':
+ specifier: ^22.0.0
+ version: 22.19.7
+
packages:
'@acemir/cssom@0.9.31':
@@ -1215,6 +1228,9 @@ packages:
'@noble/hashes':
optional: true
+ '@gerrit0/mini-shiki@3.21.0':
+ resolution: {integrity: sha512-9PrsT5DjZA+w3lur/aOIx3FlDeHdyCEFlv9U+fmsVyjPZh61G5SYURQ/1ebe2U63KbDmI2V8IhIUegWb8hjOyg==}
+
'@html-validate/stylish@4.3.0':
resolution: {integrity: sha512-eUfvKpRJg5TvzSfTf2EovrQoTKjkRnPUOUnXVJ2cQ4GbC/bQw98oxN+DdSf+HxOBK00YOhsP52xWdJPV1o4n5w==}
engines: {node: '>= 18'}
@@ -1428,6 +1444,36 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+ '@jsr/deno__cache-dir@0.25.0':
+ resolution: {integrity: sha512-J5/kPR8sc+dsRyE8rnLPhqay7B4XgBmkv7QaeIV41sGibszQjIb8b599LssN1YWBbqPLakPRMVobg5u6LheWfg==, tarball: https://npm.jsr.io/~/11/@jsr/deno__cache-dir/0.25.0.tgz}
+
+ '@jsr/deno__doc@0.189.1':
+ resolution: {integrity: sha512-GtlJz3nGX1zF+XBG5pEPYdRbo5feyP3eM1+zsEqFqjfvT/nfRdjyGNogEs4yd+DW8cD7tQceRd2vypDhG1X7kA==, tarball: https://npm.jsr.io/~/11/@jsr/deno__doc/0.189.1.tgz}
+
+ '@jsr/deno__graph@0.100.1':
+ resolution: {integrity: sha512-mPemftpdwtz8fo+RNKujjXKpDQRcj5E0nOcxeNiDGLRzqQk/Q69IcGM4ruZGX+0xhTNNSLc8Uu3W4ynIDfTR6g==, tarball: https://npm.jsr.io/~/11/@jsr/deno__graph/0.100.1.tgz}
+
+ '@jsr/deno__graph@0.86.9':
+ resolution: {integrity: sha512-+qrrma5/bL+hcG20mfaEeC8SLopqoyd1RjcKFMRu++3SAXyrTKuvuIjBJCn/NyN7X+kV+QrJG67BCHX38Rzw+g==, tarball: https://npm.jsr.io/~/11/@jsr/deno__graph/0.86.9.tgz}
+
+ '@jsr/std__bytes@1.0.6':
+ resolution: {integrity: sha512-St6yKggjFGhxS52IFLJWvkchRFbAKg2Xh8UxA4S1EGz7GJ2Ui+ssDDldj/w2c8vCxvl6qgR0HaYbKeFJNqujmA==, tarball: https://npm.jsr.io/~/11/@jsr/std__bytes/1.0.6.tgz}
+
+ '@jsr/std__fmt@1.0.9':
+ resolution: {integrity: sha512-YFJJMozmORj2K91c5J9opWeh0VUwrd+Mwb7Pr0FkVCAKVLu2UhT4LyvJqWiyUT+eF+MdfqQ9F7RtQj4bXn9Smw==, tarball: https://npm.jsr.io/~/11/@jsr/std__fmt/1.0.9.tgz}
+
+ '@jsr/std__fs@1.0.22':
+ resolution: {integrity: sha512-PvDtgT25IqhFEX2LjQI0aTz/Wg61jCtJ8l19fE9MUSvSmtw57Kzr6sM7GcCsSrsZEdQ7wjLfXvvhy8irta4Zww==, tarball: https://npm.jsr.io/~/11/@jsr/std__fs/1.0.22.tgz}
+
+ '@jsr/std__internal@1.0.12':
+ resolution: {integrity: sha512-6xReMW9p+paJgqoFRpOE2nogJFvzPfaLHLIlyADYjKMUcwDyjKZxryIbgcU+gxiTygn8yCjld1HoI0ET4/iZeA==, tarball: https://npm.jsr.io/~/11/@jsr/std__internal/1.0.12.tgz}
+
+ '@jsr/std__io@0.225.2':
+ resolution: {integrity: sha512-QNImMbao6pKXvV4xpFkY2zviZi1r+1KpYzgMqaa2gHDPZhXQqlia/Og+VqMxxfAr8Pw6BF3tw/hSw3LrWWTRmA==, tarball: https://npm.jsr.io/~/11/@jsr/std__io/0.225.2.tgz}
+
+ '@jsr/std__path@1.1.4':
+ resolution: {integrity: sha512-SK4u9H6NVTfolhPdlvdYXfNFefy1W04AEHWJydryYbk+xqzNiVmr5o7TLJLJFqwHXuwMRhwrn+mcYeUfS0YFaA==, tarball: https://npm.jsr.io/~/11/@jsr/std__path/1.1.4.tgz}
+
'@kwsites/file-exists@1.1.1':
resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==}
@@ -2663,6 +2709,9 @@ packages:
'@types/mdast@4.0.4':
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
+ '@types/node@22.19.7':
+ resolution: {integrity: sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==}
+
'@types/node@24.10.9':
resolution: {integrity: sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==}
@@ -4798,6 +4847,9 @@ packages:
linebreak@1.1.0:
resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==}
+ linkify-it@5.0.0:
+ resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
+
lint-staged@16.2.7:
resolution: {integrity: sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==}
engines: {node: '>=20.17'}
@@ -4861,6 +4913,9 @@ packages:
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+ lunr@2.3.9:
+ resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==}
+
magic-regexp@0.10.0:
resolution: {integrity: sha512-Uly1Bu4lO1hwHUW0CQeSWuRtzCMNO00CmXtS8N6fyvB3B979GOEEeAkiTUDsmbYLAbvpUS/Kt5c4ibosAzVyVg==}
@@ -4881,6 +4936,10 @@ packages:
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
engines: {node: '>=10'}
+ markdown-it@14.1.0:
+ resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
+ hasBin: true
+
marked@17.0.1:
resolution: {integrity: sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==}
engines: {node: '>= 20'}
@@ -4902,6 +4961,9 @@ packages:
mdn-data@2.12.2:
resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==}
+ mdurl@2.0.0:
+ resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
+
merge-stream@2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
@@ -5557,6 +5619,10 @@ packages:
protocols@2.0.2:
resolution: {integrity: sha512-hHVTzba3wboROl0/aWRRG9dMytgH6ow//STBZh43l/wQgmMhYhOFi0EHWAPtoCz9IAUymsyP0TSBHkhgMEGNnQ==}
+ punycode.js@2.3.1:
+ resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
+ engines: {node: '>=6'}
+
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
@@ -6261,11 +6327,21 @@ packages:
resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
engines: {node: '>= 0.4'}
+ typedoc@0.28.16:
+ resolution: {integrity: sha512-x4xW77QC3i5DUFMBp0qjukOTnr/sSg+oEs86nB3LjDslvAmwe/PUGDWbe3GrIqt59oTqoXK5GRK9tAa0sYMiog==}
+ engines: {node: '>= 18', pnpm: '>= 10'}
+ hasBin: true
+ peerDependencies:
+ typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x
+
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
+ uc.micro@2.1.0:
+ resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
+
ufo@1.6.3:
resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==}
@@ -6288,6 +6364,9 @@ packages:
unctx@2.5.0:
resolution: {integrity: sha512-p+Rz9x0R7X+CYDkT+Xg8/GhpcShTlU8n+cf9OtOEf7zEQsNcCZO1dPKNRDqvUTaq+P32PMMkxWHwfrxkqfqAYg==}
+ undici-types@6.21.0:
+ resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
+
undici-types@7.16.0:
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
@@ -8008,6 +8087,14 @@ snapshots:
'@exodus/bytes@1.9.0':
optional: true
+ '@gerrit0/mini-shiki@3.21.0':
+ dependencies:
+ '@shikijs/engine-oniguruma': 3.21.0
+ '@shikijs/langs': 3.21.0
+ '@shikijs/themes': 3.21.0
+ '@shikijs/types': 3.21.0
+ '@shikijs/vscode-textmate': 10.0.2
+
'@html-validate/stylish@4.3.0':
dependencies:
kleur: 4.1.5
@@ -8192,6 +8279,42 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
+ '@jsr/deno__cache-dir@0.25.0':
+ dependencies:
+ '@jsr/deno__graph': 0.86.9
+ '@jsr/std__fmt': 1.0.9
+ '@jsr/std__fs': 1.0.22
+ '@jsr/std__io': 0.225.2
+ '@jsr/std__path': 1.1.4
+
+ '@jsr/deno__doc@0.189.1':
+ dependencies:
+ '@jsr/deno__cache-dir': 0.25.0
+ '@jsr/deno__graph': 0.100.1
+
+ '@jsr/deno__graph@0.100.1': {}
+
+ '@jsr/deno__graph@0.86.9': {}
+
+ '@jsr/std__bytes@1.0.6': {}
+
+ '@jsr/std__fmt@1.0.9': {}
+
+ '@jsr/std__fs@1.0.22':
+ dependencies:
+ '@jsr/std__internal': 1.0.12
+ '@jsr/std__path': 1.1.4
+
+ '@jsr/std__internal@1.0.12': {}
+
+ '@jsr/std__io@0.225.2':
+ dependencies:
+ '@jsr/std__bytes': 1.0.6
+
+ '@jsr/std__path@1.1.4':
+ dependencies:
+ '@jsr/std__internal': 1.0.12
+
'@kwsites/file-exists@1.1.1':
dependencies:
debug: 4.4.3
@@ -9447,6 +9570,10 @@ snapshots:
dependencies:
'@types/unist': 3.0.3
+ '@types/node@22.19.7':
+ dependencies:
+ undici-types: 6.21.0
+
'@types/node@24.10.9':
dependencies:
undici-types: 7.16.0
@@ -10169,8 +10296,7 @@ snapshots:
- bare-abort-controller
- react-native-b4a
- argparse@2.0.1:
- optional: true
+ argparse@2.0.1: {}
array-buffer-byte-length@1.0.2:
dependencies:
@@ -11827,7 +11953,7 @@ snapshots:
jest-worker@27.5.1:
dependencies:
- '@types/node': 24.10.9
+ '@types/node': 22.19.7
merge-stream: 2.0.0
supports-color: 8.1.1
@@ -12001,6 +12127,10 @@ snapshots:
base64-js: 0.0.8
unicode-trie: 2.0.0
+ linkify-it@5.0.0:
+ dependencies:
+ uc.micro: 2.1.0
+
lint-staged@16.2.7:
dependencies:
commander: 14.0.2
@@ -12087,6 +12217,8 @@ snapshots:
dependencies:
yallist: 3.1.1
+ lunr@2.3.9: {}
+
magic-regexp@0.10.0:
dependencies:
estree-walker: 3.0.3
@@ -12119,6 +12251,15 @@ snapshots:
dependencies:
semver: 7.7.3
+ markdown-it@14.1.0:
+ dependencies:
+ argparse: 2.0.1
+ entities: 4.5.0
+ linkify-it: 5.0.0
+ mdurl: 2.0.0
+ punycode.js: 2.3.1
+ uc.micro: 2.1.0
+
marked@17.0.1: {}
marky@1.3.0: {}
@@ -12141,6 +12282,8 @@ snapshots:
mdn-data@2.12.2: {}
+ mdurl@2.0.0: {}
+
merge-stream@2.0.0: {}
merge2@1.4.1: {}
@@ -13055,6 +13198,8 @@ snapshots:
protocols@2.0.2: {}
+ punycode.js@2.3.1: {}
+
punycode@2.3.1: {}
quansync@0.2.11: {}
@@ -13911,8 +14056,19 @@ snapshots:
possible-typed-array-names: 1.1.0
reflect.getprototypeof: 1.0.10
+ typedoc@0.28.16(typescript@5.9.3):
+ dependencies:
+ '@gerrit0/mini-shiki': 3.21.0
+ lunr: 2.3.9
+ markdown-it: 14.1.0
+ minimatch: 9.0.5
+ typescript: 5.9.3
+ yaml: 2.8.2
+
typescript@5.9.3: {}
+ uc.micro@2.1.0: {}
+
ufo@1.6.3: {}
ultrahtml@1.6.0: {}
@@ -13946,6 +14102,8 @@ snapshots:
magic-string: 0.30.21
unplugin: 2.3.11
+ undici-types@6.21.0: {}
+
undici-types@7.16.0: {}
unenv@2.0.0-rc.24:
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 19a167f135..1d92469d1f 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -1,6 +1,7 @@
packages:
- '.'
- 'cli'
+ - 'prototypes/*'
ignoredBuiltDependencies:
- '@parcel/watcher'
diff --git a/prototypes/deno-doc/README.md b/prototypes/deno-doc/README.md
new file mode 100644
index 0000000000..a2ee3da9de
--- /dev/null
+++ b/prototypes/deno-doc/README.md
@@ -0,0 +1,183 @@
+# @deno/doc Prototype Findings
+
+## Summary
+
+This prototype explored using `@deno/doc` to generate TypeScript documentation for npm packages.
+
+## Key Discovery: Use esm.sh Instead of jsDelivr!
+
+**esm.sh properly resolves module imports**, while jsDelivr serves files as-is without resolution.
+
+```javascript
+// WORKS - esm.sh resolves imports
+deno doc --json "https://esm.sh/zod@4?target=deno" // 380 nodes!
+
+// FAILS - jsDelivr doesn't resolve relative imports
+deno doc --json "https://cdn.jsdelivr.net/npm/zod@4/index.d.ts" // Error
+```
+
+## Approaches Tested
+
+### 1. Direct `@deno/doc` JSR Package in Node.js/Bun
+
+**Result: Does not work**
+
+- The JSR package (`@deno/doc`) can be installed via `npx jsr add @deno/doc`
+- However, the WASM loading mechanism requires `file://` URLs which are blocked outside Deno
+- The package throws: `"Loading local files are not supported in this environment"`
+
+### 2. `deno doc --json` CLI with esm.sh
+
+**Result: Works excellently!**
+
+```javascript
+import { spawn } from "node:child_process";
+
+async function denoDoc(packageName, version) {
+ const url = `https://esm.sh/${packageName}@${version}?target=deno`;
+ return new Promise((resolve, reject) => {
+ const proc = spawn("deno", ["doc", "--json", url]);
+ // ... collect stdout, parse JSON
+ });
+}
+
+const result = await denoDoc("zod", "4.3.6");
+// Returns: { version: 1, nodes: [...] } with 380 nodes!
+```
+
+## Test Results with esm.sh
+
+| Package | Nodes | Breakdown |
+|---------|-------|-----------|
+| **zod@4** | 380 | 112 functions, 82 interfaces, 87 variables, 11 type aliases |
+| **vue@3** | 408 | Full Vue API documented |
+| **date-fns@3** | 364 | All date functions |
+| **axios@1** | 80 | 4 classes, 36 interfaces, 8 functions |
+| **react@18** | 30 | 7 interfaces, 19 type aliases |
+| **ufo** | 63 | 52 functions, 5 interfaces |
+
+### Packages Using @types/* (DefinitelyTyped)
+
+| Package | Direct | @types/* | Notes |
+|---------|--------|----------|-------|
+| express | 4 nodes | 4 nodes + 30 in namespace | Uses `export default namespace` pattern |
+| lodash-es | 0 nodes | N/A | No bundled types |
+
+For packages like express that use namespace patterns, the documentation IS available - it's just nested inside the namespace node.
+
+## Limitations
+
+### 1. Packages Without Bundled Types
+- Packages relying solely on `@types/*` may have limited results
+- Can fall back to documenting the `@types/*` package directly
+- Namespace patterns need flattening in the UI
+
+### 2. Server Requirements
+- Requires Deno installed on the server
+- Subprocess spawning has overhead (~200-500ms per call)
+- Would need caching strategy (by package@version)
+
+### 3. esm.sh Dependency
+- Relies on esm.sh for module resolution
+- esm.sh is reliable but is a third-party service
+- Could self-host esm.sh if needed
+
+## Recommended Architecture
+
+```
+ ┌─────────────────────────┐
+ │ npmx.dev Server │
+ │ (Nitro) │
+ └───────────┬─────────────┘
+ │
+ ┌───────────▼─────────────┐
+ │ Doc Generation API │
+ │ GET /api/docs/[pkg] │
+ └───────────┬─────────────┘
+ │
+ ┌─────────────────────┼─────────────────────┐
+ │ │ │
+ ┌────────▼────────┐ ┌────────▼────────┐ ┌────────▼────────┐
+ │ Check Cache │ │ Build esm.sh │ │ Subprocess │
+ │ (KV/Redis) │ │ URL │ │ deno doc --json │
+ └─────────────────┘ └─────────────────┘ └─────────────────┘
+```
+
+### URL Strategy
+
+```javascript
+function getDocUrl(packageName, version) {
+ // esm.sh handles module resolution
+ return `https://esm.sh/${packageName}@${version}?target=deno`;
+}
+
+// For @types/* fallback
+function getTypesUrl(packageName, version) {
+ const typesName = packageName.startsWith("@")
+ ? `@types/${packageName.slice(1).replace("/", "__")}`
+ : `@types/${packageName}`;
+ return `https://esm.sh/${typesName}@${version}?target=deno`;
+}
+```
+
+### Caching Strategy
+
+1. Cache by `{package}@{version}` (immutable - versions don't change)
+2. Store in KV/Redis with 30-day TTL
+3. Generate on first request, return cached thereafter
+4. Pre-warm cache for popular packages
+
+### Handling Namespace Patterns
+
+For packages like express that use `export default namespace`:
+
+```javascript
+function flattenNamespaces(nodes) {
+ return nodes.flatMap(node => {
+ if (node.kind === "namespace" && node.namespaceDef?.elements) {
+ // Include both the namespace and its flattened elements
+ return [node, ...node.namespaceDef.elements.map(el => ({
+ ...el,
+ namespacePath: node.name
+ }))];
+ }
+ return [node];
+ });
+}
+```
+
+## Alternative: TypeDoc
+
+If the Deno dependency is undesirable, TypeDoc is the main alternative:
+
+```javascript
+import { Application } from "typedoc";
+
+const app = await Application.bootstrap({
+ entryPoints: ["./index.d.ts"],
+ skipErrorChecking: true,
+});
+
+const project = await app.convert();
+const json = app.serializer.projectToObject(project, cwd);
+```
+
+**Pros**: Native npm package, no Deno dependency
+**Cons**: Heavier, requires TypeScript compiler, slower
+
+## Files
+
+- `src/test-deno-cli.js` - Working Deno CLI integration test
+- `src/test.js` - Failed direct @deno/doc import test
+- `src/test-direct-wasm.js` - Failed direct WASM loading test
+
+## Conclusion
+
+**Recommendation**: Use `deno doc --json` CLI with esm.sh URLs
+
+- **Works reliably** - esm.sh resolves module imports correctly
+- **High quality output** - Same format as JSR documentation
+- **Good coverage** - Works for most packages with bundled types
+- **Fallback available** - Can use @types/* for packages without bundled types
+- **Server requirement** - Needs Deno installed (acceptable for Vercel)
+- **Caching essential** - Doc generation takes ~1-3 seconds, cache by version
diff --git a/prototypes/deno-doc/package.json b/prototypes/deno-doc/package.json
new file mode 100644
index 0000000000..7731eb2912
--- /dev/null
+++ b/prototypes/deno-doc/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "deno-doc-prototype",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "description": "Prototype for @deno/doc integration to generate TypeScript documentation",
+ "scripts": {
+ "test": "node --experimental-strip-types src/test.ts",
+ "test:bun": "bun src/test.ts"
+ },
+ "dependencies": {
+ "@deno/doc": "npm:@jsr/deno__doc@^0.189.1"
+ },
+ "devDependencies": {
+ "@types/node": "^22.0.0"
+ }
+}
diff --git a/prototypes/deno-doc/src/test-deno-cli.js b/prototypes/deno-doc/src/test-deno-cli.js
new file mode 100644
index 0000000000..a4810a4757
--- /dev/null
+++ b/prototypes/deno-doc/src/test-deno-cli.js
@@ -0,0 +1,249 @@
+/**
+ * Prototype test - Using `deno doc --json` CLI
+ *
+ * This is the officially recommended approach for non-Deno environments.
+ */
+
+import { spawn } from "node:child_process";
+import { writeFile, unlink, mkdir } from "node:fs/promises";
+import { join } from "node:path";
+import { tmpdir } from "node:os";
+
+console.log("=".repeat(60));
+console.log("@deno/doc Prototype - Using Deno CLI");
+console.log("=".repeat(60));
+
+/**
+ * Run `deno doc --json` on a URL or file
+ * @param {string} specifier - URL or file path to document
+ * @returns {Promise<{version: number, nodes: object[]}>} - Doc output with nodes
+ */
+async function denoDoc(specifier) {
+ return new Promise((resolve, reject) => {
+ const args = ["doc", "--json", specifier];
+ const proc = spawn("deno", args, {
+ stdio: ["ignore", "pipe", "pipe"],
+ });
+
+ let stdout = "";
+ let stderr = "";
+
+ proc.stdout.on("data", (data) => {
+ stdout += data.toString();
+ });
+
+ proc.stderr.on("data", (data) => {
+ stderr += data.toString();
+ });
+
+ proc.on("close", (code) => {
+ if (code !== 0) {
+ reject(new Error(`deno doc failed (code ${code}): ${stderr}`));
+ return;
+ }
+ try {
+ const result = JSON.parse(stdout);
+ // Handle both old format (array) and new format ({ version, nodes })
+ if (Array.isArray(result)) {
+ resolve({ version: 0, nodes: result });
+ } else {
+ resolve(result);
+ }
+ } catch (e) {
+ reject(new Error(`Failed to parse JSON: ${e.message}`));
+ }
+ });
+
+ proc.on("error", reject);
+ });
+}
+
+/**
+ * Create a temp file with TypeScript content and document it
+ * @param {string} content - TypeScript source code
+ * @returns {Promise
`)
+ }
+
+ // See also
+ if (see.length > 0) {
+ lines.push(``)
+ lines.push(`
See Also
`)
+ lines.push(`
`)
+ for (const s of see) {
+ if (s.doc) {
+ // Parse {@link} tags
+ lines.push(`- ${parseJsDocLinks(s.doc, symbolLookup)}
`)
+ }
+ }
+ lines.push(`
`)
+ lines.push(`
`)
+ }
+
+ return lines.join('\n')
+}
+
+function renderClassMembers(def: NonNullable): string {
+ const lines: string[] = []
+ const { constructors, properties, methods } = def
+
+ // Constructors
+ if (constructors && constructors.length > 0) {
+ lines.push(``)
+ lines.push(`
Constructor
`)
+ for (const ctor of constructors) {
+ const params = ctor.params?.map(p => formatParam(p)).join(', ') || ''
+ lines.push(`
constructor(${escapeHtml(params)})
`)
+ }
+ lines.push(`
`)
+ }
+
+ // Properties
+ if (properties && properties.length > 0) {
+ lines.push(``)
+ lines.push(`
Properties
`)
+ lines.push(`
`)
+ for (const prop of properties) {
+ const modifiers: string[] = []
+ if (prop.isStatic) modifiers.push('static')
+ if (prop.readonly) modifiers.push('readonly')
+ const modStr = modifiers.length > 0 ? `${modifiers.join(' ')} ` : ''
+ const type = formatType(prop.tsType)
+ const opt = prop.optional ? '?' : ''
+ lines.push(`${escapeHtml(modStr)}${escapeHtml(prop.name)}${opt}: ${escapeHtml(type)} `)
+ const propDoc = prop.jsDoc?.doc
+ if (propDoc) {
+ lines.push(`- ${escapeHtml(propDoc.split('\n')[0] ?? '')}
`)
+ }
+ }
+ lines.push(`
`)
+ lines.push(`
`)
+ }
+
+ // Methods
+ if (methods && methods.length > 0) {
+ lines.push(``)
+ lines.push(`
Methods
`)
+ lines.push(`
`)
+ for (const method of methods) {
+ const params = method.functionDef?.params?.map(p => formatParam(p)).join(', ') || ''
+ const ret = formatType(method.functionDef?.returnType) || 'void'
+ const staticStr = method.isStatic ? 'static ' : ''
+ lines.push(`${escapeHtml(staticStr)}${escapeHtml(method.name)}(${escapeHtml(params)}): ${escapeHtml(ret)} `)
+ const methodDoc = method.jsDoc?.doc
+ if (methodDoc) {
+ lines.push(`- ${escapeHtml(methodDoc.split('\n')[0] ?? '')}
`)
+ }
+ }
+ lines.push(`
`)
+ lines.push(`
`)
+ }
+
+ return lines.join('\n')
+}
+
+function renderInterfaceMembers(def: NonNullable): string {
+ const lines: string[] = []
+ const { properties, methods } = def
+
+ if (properties && properties.length > 0) {
+ lines.push(``)
+ lines.push(`
Properties
`)
+ lines.push(`
`)
+ for (const prop of properties) {
+ const type = formatType(prop.tsType)
+ const opt = prop.optional ? '?' : ''
+ const ro = prop.readonly ? 'readonly ' : ''
+ lines.push(`${escapeHtml(ro)}${escapeHtml(prop.name)}${opt}: ${escapeHtml(type)} `)
+ const propDoc = prop.jsDoc?.doc
+ if (propDoc) {
+ lines.push(`- ${escapeHtml(propDoc.split('\n')[0] ?? '')}
`)
+ }
+ }
+ lines.push(`
`)
+ lines.push(`
`)
+ }
+
+ if (methods && methods.length > 0) {
+ lines.push(``)
+ lines.push(`
Methods
`)
+ lines.push(`
`)
+ for (const method of methods) {
+ const params = method.params?.map(p => formatParam(p)).join(', ') || ''
+ const ret = formatType(method.returnType) || 'void'
+ lines.push(`${escapeHtml(method.name)}(${escapeHtml(params)}): ${escapeHtml(ret)} `)
+ const methodDoc = method.jsDoc?.doc
+ if (methodDoc) {
+ lines.push(`- ${escapeHtml(methodDoc.split('\n')[0] ?? '')}
`)
+ }
+ }
+ lines.push(`
`)
+ lines.push(`
`)
+ }
+
+ return lines.join('\n')
+}
+
+function renderEnumMembers(def: NonNullable): string {
+ const lines: string[] = []
+ const { members } = def
+
+ if (members && members.length > 0) {
+ lines.push(``)
+ lines.push(`
Members
`)
+ lines.push(`
`)
+ for (const member of members) {
+ lines.push(`${escapeHtml(member.name)} `)
+ }
+ lines.push(`
`)
+ lines.push(`
`)
+ }
+
+ return lines.join('\n')
+}
+
+function renderToc(nodes: DenoDocNode[]): string {
+ // Use merged symbols for TOC to avoid duplicates
+ const merged = mergeOverloads(nodes)
+ const grouped = groupMergedByKind(merged)
+ const lines: string[] = []
+
+ lines.push(``)
+
+ return lines.join('\n')
+}
+
+function renderMarkdown(text: string, symbolLookup: SymbolLookup): string {
+ // First parse {@link} tags (this also escapes HTML)
+ let result = parseJsDocLinks(text, symbolLookup)
+
+ // Then apply markdown formatting
+ result = result
+ .replace(/`([^`]+)`/g, '$1')
+ .replace(/\*\*([^*]+)\*\*/g, '$1')
+ .replace(/\n\n/g, '')
+ .replace(/\n/g, '
')
+
+ return result
+}
+
+function escapeHtml(text: string): string {
+ return text
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''')
+}
diff --git a/shared/types/docs.ts b/shared/types/docs.ts
new file mode 100644
index 0000000000..a59164f811
--- /dev/null
+++ b/shared/types/docs.ts
@@ -0,0 +1,17 @@
+export type DocsStatus = 'ok' | 'missing' | 'error'
+
+export interface DocsResponse {
+ package: string
+ version: string
+ html: string
+ toc: string | null
+ breadcrumbs?: string | null
+ status: DocsStatus
+ message?: string
+}
+
+export interface DocsSearchResponse {
+ package: string
+ version: string
+ index: Record
+}
diff --git a/shared/types/index.ts b/shared/types/index.ts
index 2414a4f37f..918f91143e 100644
--- a/shared/types/index.ts
+++ b/shared/types/index.ts
@@ -2,3 +2,4 @@ export * from './npm-registry'
export * from './jsr'
export * from './osv'
export * from './readme'
+export * from './docs'
From f6630ffd4b0624c06c58a265d92a451549618517 Mon Sep 17 00:00:00 2001
From: dev wells
Date: Sun, 25 Jan 2026 16:19:37 -0500
Subject: [PATCH 2/9] cleanup old prototype
---
app/pages/docs/[...path].vue | 31 +-
prototypes/deno-doc/README.md | 183 ------
prototypes/deno-doc/package.json | 17 -
prototypes/deno-doc/src/test-deno-cli.js | 249 --------
prototypes/deno-doc/src/test-direct-wasm.js | 73 ---
prototypes/deno-doc/src/test.js | 209 -------
prototypes/deno-doc/src/test.ts | 143 -----
server/utils/docs.ts | 652 ++++++++++++--------
8 files changed, 435 insertions(+), 1122 deletions(-)
delete mode 100644 prototypes/deno-doc/README.md
delete mode 100644 prototypes/deno-doc/package.json
delete mode 100644 prototypes/deno-doc/src/test-deno-cli.js
delete mode 100644 prototypes/deno-doc/src/test-direct-wasm.js
delete mode 100644 prototypes/deno-doc/src/test.js
delete mode 100644 prototypes/deno-doc/src/test.ts
diff --git a/app/pages/docs/[...path].vue b/app/pages/docs/[...path].vue
index ed6878868a..6a0c25644d 100644
--- a/app/pages/docs/[...path].vue
+++ b/app/pages/docs/[...path].vue
@@ -49,9 +49,11 @@ const docsUrl = computed(() => {
return `/api/registry/docs/${packageName.value}/v/${resolvedVersion.value}`
})
+const shouldFetch = computed(() => !!docsUrl.value)
+
const { data: docsData, status: docsStatus } = useLazyFetch(() => docsUrl.value!, {
watch: [docsUrl],
- immediate: computed(() => !!docsUrl.value).value,
+ immediate: shouldFetch.value,
default: () => ({
package: packageName.value,
version: resolvedVersion.value ?? '',
@@ -78,9 +80,9 @@ const showEmptyState = computed(() => docsData.value?.status !== 'ok')
-