Skip to content

Commit e19834d

Browse files
committed
fix(docgen): look for type entrypoints beyond just main entrypoint
1 parent 3476c47 commit e19834d

File tree

1 file changed

+114
-6
lines changed

1 file changed

+114
-6
lines changed

server/utils/docs/client.ts

Lines changed: 114 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,29 @@ import type { DenoDocNode, DenoDocResult } from '#shared/types/deno-doc'
1717
/** Timeout for fetching modules in milliseconds */
1818
const FETCH_TIMEOUT_MS = 30 * 1000
1919

20+
/** Maximum number of subpath exports to process (prevents runaway on huge packages) */
21+
const MAX_SUBPATH_EXPORTS = 10
22+
2023
// =============================================================================
2124
// Main Export
2225
// =============================================================================
2326

2427
/**
2528
* Get documentation nodes for a package using @deno/doc WASM.
29+
*
30+
* This function fetches types for all subpath exports (e.g., `nuxt`, `nuxt/app`, `nuxt/kit`)
31+
* to provide comprehensive documentation for packages with multiple entry points.
2632
*/
2733
export async function getDocNodes(packageName: string, version: string): Promise<DenoDocResult> {
28-
// Get types URL from esm.sh header
29-
const typesUrl = await getTypesUrl(packageName, version)
34+
// Get all types URLs from package exports
35+
const typesUrls = await getAllTypesUrls(packageName, version)
3036

31-
if (!typesUrl) {
37+
if (typesUrls.length === 0) {
3238
return { version: 1, nodes: [] }
3339
}
3440

35-
// Generate docs using @deno/doc WASM
36-
const result = await doc([typesUrl], {
41+
// Generate docs using @deno/doc WASM for all entry points
42+
const result = await doc(typesUrls, {
3743
load: createLoader(),
3844
resolve: createResolver(),
3945
})
@@ -47,6 +53,90 @@ export async function getDocNodes(packageName: string, version: string): Promise
4753
return { version: 1, nodes: allNodes }
4854
}
4955

56+
// =============================================================================
57+
// Types URL Discovery
58+
// =============================================================================
59+
60+
/**
61+
* Get all TypeScript types URLs for a package, including subpath exports.
62+
*
63+
* 1. Fetches package.json from npm registry to discover exports
64+
* 2. For each subpath with types, queries esm.sh for the types URL
65+
* 3. Returns all discovered types URLs
66+
*/
67+
async function getAllTypesUrls(packageName: string, version: string): Promise<string[]> {
68+
// First, try the main entry point
69+
const mainTypesUrl = await getTypesUrl(packageName, version)
70+
71+
// Fetch package exports to discover subpaths
72+
const subpathTypesUrls = await getSubpathTypesUrls(packageName, version)
73+
74+
// Combine and deduplicate
75+
const allUrls = new Set<string>()
76+
if (mainTypesUrl) allUrls.add(mainTypesUrl)
77+
for (const url of subpathTypesUrls) {
78+
allUrls.add(url)
79+
}
80+
81+
return [...allUrls]
82+
}
83+
84+
/**
85+
* Fetch package.json exports and get types URLs for each subpath.
86+
*/
87+
async function getSubpathTypesUrls(packageName: string, version: string): Promise<string[]> {
88+
const controller = new AbortController()
89+
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)
90+
91+
try {
92+
// Fetch package.json from npm registry
93+
const response = await fetch(`https://registry.npmjs.org/${packageName}/${version}`, {
94+
signal: controller.signal,
95+
})
96+
clearTimeout(timeoutId)
97+
98+
if (!response.ok) return []
99+
100+
const pkgJson = await response.json()
101+
const exports = pkgJson.exports
102+
103+
// No exports field or simple string export
104+
if (!exports || typeof exports !== 'object') return []
105+
106+
// Find subpaths with types
107+
const subpathsWithTypes: string[] = []
108+
for (const [subpath, config] of Object.entries(exports)) {
109+
// Skip the main entry (already handled) and non-object configs
110+
if (subpath === '.' || typeof config !== 'object' || config === null) continue
111+
// Skip package.json export
112+
if (subpath === './package.json') continue
113+
114+
const exportConfig = config as Record<string, unknown>
115+
if (exportConfig.types && typeof exportConfig.types === 'string') {
116+
subpathsWithTypes.push(subpath)
117+
}
118+
}
119+
120+
// Limit to prevent runaway on huge packages
121+
const limitedSubpaths = subpathsWithTypes.slice(0, MAX_SUBPATH_EXPORTS)
122+
123+
// Fetch types URLs for each subpath in parallel
124+
const typesUrls = await Promise.all(
125+
limitedSubpaths.map(async subpath => {
126+
// Convert ./app to /app for esm.sh URL
127+
// esm.sh format: https://esm.sh/nuxt@3.15.4/app (not nuxt/app@3.15.4)
128+
const esmSubpath = subpath.startsWith('./') ? subpath.slice(1) : subpath
129+
return getTypesUrlForSubpath(packageName, version, esmSubpath)
130+
}),
131+
)
132+
133+
return typesUrls.filter((url): url is string => url !== null)
134+
} catch {
135+
clearTimeout(timeoutId)
136+
return []
137+
}
138+
}
139+
50140
// =============================================================================
51141
// Module Loading
52142
// =============================================================================
@@ -154,8 +244,26 @@ function createResolver(): (specifier: string, referrer: string) => string {
154244
* x-typescript-types: https://esm.sh/ufo@1.5.0/dist/index.d.ts
155245
*/
156246
async function getTypesUrl(packageName: string, version: string): Promise<string | null> {
157-
const url = `https://esm.sh/${packageName}@${version}`
247+
return fetchTypesHeader(`https://esm.sh/${packageName}@${version}`)
248+
}
158249

250+
/**
251+
* Get types URL for a package subpath.
252+
* Example: getTypesUrlForSubpath('nuxt', '3.15.4', '/app')
253+
* → fetches https://esm.sh/nuxt@3.15.4/app
254+
*/
255+
async function getTypesUrlForSubpath(
256+
packageName: string,
257+
version: string,
258+
subpath: string,
259+
): Promise<string | null> {
260+
return fetchTypesHeader(`https://esm.sh/${packageName}@${version}${subpath}`)
261+
}
262+
263+
/**
264+
* Fetch the x-typescript-types header from an esm.sh URL.
265+
*/
266+
async function fetchTypesHeader(url: string): Promise<string | null> {
159267
const controller = new AbortController()
160268
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)
161269

0 commit comments

Comments
 (0)