-
Notifications
You must be signed in to change notification settings - Fork 88
fix: (docgen) get types from more than just main entrypoint, fail gracefully-er #240
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e19834d
51f9cfe
b59e173
f8439cf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,34 +17,119 @@ import type { DenoDocNode, DenoDocResult } from '#shared/types/deno-doc' | |
| /** Timeout for fetching modules in milliseconds */ | ||
| const FETCH_TIMEOUT_MS = 30 * 1000 | ||
|
|
||
| /** Maximum number of subpath exports to process (prevents runaway on huge packages) */ | ||
| const MAX_SUBPATH_EXPORTS = 10 | ||
|
|
||
| // ============================================================================= | ||
| // Main Export | ||
| // ============================================================================= | ||
|
|
||
| /** | ||
| * Get documentation nodes for a package using @deno/doc WASM. | ||
| * | ||
| * This function fetches types for all subpath exports (e.g., `nuxt`, `nuxt/app`, `nuxt/kit`) | ||
| * to provide comprehensive documentation for packages with multiple entry points. | ||
| * | ||
| * All errors are caught and result in empty nodes - docgen failures are graceful degradation | ||
| * and should never cause error logging or wake up a maintainer. | ||
| */ | ||
| export async function getDocNodes(packageName: string, version: string): Promise<DenoDocResult> { | ||
| // Get types URL from esm.sh header | ||
| const typesUrl = await getTypesUrl(packageName, version) | ||
| export async function getDocNodes( | ||
| packageName: string, | ||
| version: string, | ||
| exports?: Record<string, unknown>, | ||
| ): Promise<DenoDocResult> { | ||
| try { | ||
| // Get all types URLs from package exports (uses pre-fetched exports data) | ||
| const typesUrls = await getAllTypesUrls(packageName, version, exports) | ||
|
|
||
| if (typesUrls.length === 0) { | ||
| return { version: 1, nodes: [] } | ||
| } | ||
|
|
||
| // Generate docs using @deno/doc WASM for all entry points | ||
| const result = await doc(typesUrls, { | ||
| load: createLoader(), | ||
| resolve: createResolver(), | ||
| }) | ||
|
|
||
| // Collect all nodes from all specifiers | ||
| const allNodes: DenoDocNode[] = [] | ||
| for (const nodes of Object.values(result)) { | ||
| allNodes.push(...(nodes as DenoDocNode[])) | ||
| } | ||
|
|
||
| if (!typesUrl) { | ||
| return { version: 1, nodes: allNodes } | ||
| } catch { | ||
| // Silent failure - all docgen errors are graceful degradation | ||
| // In future should maybe consider debug mode / struct logging of some kind | ||
| return { version: 1, nodes: [] } | ||
| } | ||
| } | ||
|
|
||
| // Generate docs using @deno/doc WASM | ||
| const result = await doc([typesUrl], { | ||
| load: createLoader(), | ||
| resolve: createResolver(), | ||
| }) | ||
| // ============================================================================= | ||
| // Types URL Discovery | ||
| // ============================================================================= | ||
|
|
||
| // Collect all nodes from all specifiers | ||
| const allNodes: DenoDocNode[] = [] | ||
| for (const nodes of Object.values(result)) { | ||
| allNodes.push(...(nodes as DenoDocNode[])) | ||
| /** | ||
| * Get all TypeScript types URLs for a package, including subpath exports. | ||
| */ | ||
| async function getAllTypesUrls( | ||
| packageName: string, | ||
| version: string, | ||
| exports?: Record<string, unknown>, | ||
| ): Promise<string[]> { | ||
| const mainTypesUrl = await getTypesUrl(packageName, version) | ||
| const subpathTypesUrls = await getSubpathTypesUrlsFromExports(packageName, version, exports) | ||
|
|
||
| // Combine and deduplicate | ||
| const allUrls = new Set<string>() | ||
| if (mainTypesUrl) allUrls.add(mainTypesUrl) | ||
| for (const url of subpathTypesUrls) { | ||
| allUrls.add(url) | ||
| } | ||
|
|
||
| return { version: 1, nodes: allNodes } | ||
| return [...allUrls] | ||
| } | ||
|
|
||
| /** | ||
| * Extract types URLs from pre-fetched package exports. | ||
| */ | ||
| async function getSubpathTypesUrlsFromExports( | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a bottleneck. Going to think if there's a better way.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or rather, it's a bottleneck that we have to run deno doc potentially on a bunch of subpath exports. |
||
| packageName: string, | ||
| version: string, | ||
| exports?: Record<string, unknown>, | ||
| ): Promise<string[]> { | ||
| // No exports field or simple string export | ||
| if (!exports || typeof exports !== 'object') return [] | ||
|
|
||
| // Find subpaths with types | ||
| const subpathsWithTypes: string[] = [] | ||
| for (const [subpath, config] of Object.entries(exports)) { | ||
| // Skip the main entry (already handled) and non-object configs | ||
| if (subpath === '.' || typeof config !== 'object' || config === null) continue | ||
| // Skip package.json export | ||
| if (subpath === './package.json') continue | ||
|
|
||
| const exportConfig = config as Record<string, unknown> | ||
| if (exportConfig.types && typeof exportConfig.types === 'string') { | ||
| subpathsWithTypes.push(subpath) | ||
| } | ||
| } | ||
|
|
||
| // Limit to prevent runaway on huge packages | ||
| const limitedSubpaths = subpathsWithTypes.slice(0, MAX_SUBPATH_EXPORTS) | ||
|
|
||
| // Fetch types URLs for each subpath in parallel | ||
| const typesUrls = await Promise.all( | ||
| limitedSubpaths.map(async subpath => { | ||
| // Convert ./app to /app for esm.sh URL | ||
| // esm.sh format: https://esm.sh/nuxt@3.15.4/app (not nuxt/app@3.15.4) | ||
| const esmSubpath = subpath.startsWith('./') ? subpath.slice(1) : subpath | ||
| return getTypesUrlForSubpath(packageName, version, esmSubpath) | ||
| }), | ||
| ) | ||
|
|
||
| return typesUrls.filter((url): url is string => url !== null) | ||
| } | ||
|
|
||
| // ============================================================================= | ||
|
|
@@ -154,8 +239,26 @@ function createResolver(): (specifier: string, referrer: string) => string { | |
| * x-typescript-types: https://esm.sh/ufo@1.5.0/dist/index.d.ts | ||
| */ | ||
| async function getTypesUrl(packageName: string, version: string): Promise<string | null> { | ||
| const url = `https://esm.sh/${packageName}@${version}` | ||
| return fetchTypesHeader(`https://esm.sh/${packageName}@${version}`) | ||
| } | ||
|
|
||
| /** | ||
| * Get types URL for a package subpath. | ||
| * Example: getTypesUrlForSubpath('nuxt', '3.15.4', '/app') | ||
| * → fetches https://esm.sh/nuxt@3.15.4/app | ||
| */ | ||
| async function getTypesUrlForSubpath( | ||
| packageName: string, | ||
| version: string, | ||
| subpath: string, | ||
| ): Promise<string | null> { | ||
| return fetchTypesHeader(`https://esm.sh/${packageName}@${version}${subpath}`) | ||
| } | ||
|
|
||
| /** | ||
| * Fetch the x-typescript-types header from an esm.sh URL. | ||
| */ | ||
| async function fetchTypesHeader(url: string): Promise<string | null> { | ||
| const controller = new AbortController() | ||
| const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS) | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remember
exportscan also be astring(one entrypoint soexports: "./foo.js"is likeexports: {".": "./foo.js"}iirc)