@@ -17,23 +17,29 @@ import type { DenoDocNode, DenoDocResult } from '#shared/types/deno-doc'
1717/** Timeout for fetching modules in milliseconds */
1818const 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 */
2733export 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 */
156246async 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