@@ -7,17 +7,23 @@ import type {
77 ResolveResult ,
88 ExtractError ,
99 InternalResolveResult ,
10+ ExportSummary ,
1011} from '../core' ;
1112import { createExtractError , resolveError , isResolveError , isExtractError } from '../core' ;
1213import { loadTsConfig , resolveModule , resolveNodeBuiltin } from './module-resolver' ;
1314import { findSymbolInJavaScriptFile , getSymbolInfo } from './symbol-finder' ;
14- import { formatOutput } from './formatters ' ;
15+ import { getSymbolKind , getFileOverview } from './utils ' ;
1516
1617// Parse module path into extraction request
1718function parseModulePath ( modulePath : string ) : ExtractionRequest | ExtractError {
19+ // Handle empty or invalid paths
20+ if ( ! modulePath || modulePath . trim ( ) === '' ) {
21+ return createExtractError ( 'INVALID_REQUEST' , 'Module path is required' ) ;
22+ }
23+
1824 // Handle node: prefix specially
1925 let module : string ;
20- let symbolPath : string ;
26+ let symbolPath : string | undefined ;
2127
2228 if ( modulePath . startsWith ( 'node:' ) ) {
2329 // For node:Math -> module='node:Math', symbol='Math'
@@ -27,16 +33,31 @@ function parseModulePath(modulePath: string): ExtractionRequest | ExtractError {
2733 module = `node:${ baseSymbol } ` ; // 'node:Math'
2834 symbolPath = nodeSymbol ; // 'Math' or 'Math.min'
2935 } else {
30- // Regular module:symbol format
31- const [ mod , symPath ] = modulePath . split ( ':' ) ;
32- if ( ! mod || ! symPath ) {
33- return createExtractError (
34- 'INVALID_REQUEST' ,
35- `Invalid format. Expected 'module:symbol', got '${ modulePath } '` ,
36- ) ;
36+ // Regular module[:symbol] format
37+ const colonIndex = modulePath . indexOf ( ':' ) ;
38+
39+ if ( colonIndex === - 1 ) {
40+ // No colon found - file-level request
41+ module = modulePath ;
42+ symbolPath = undefined ;
43+ } else if ( colonIndex === 0 ) {
44+ // Only colon at start
45+ return createExtractError ( 'INVALID_REQUEST' , 'Module path is required before ":"' ) ;
46+ } else {
47+ // Split on first colon
48+ module = modulePath . substring ( 0 , colonIndex ) ;
49+ symbolPath = modulePath . substring ( colonIndex + 1 ) ;
50+
51+ // If symbol part is empty after colon, return error
52+ if ( ! symbolPath || symbolPath . trim ( ) === '' ) {
53+ return createExtractError ( 'INVALID_REQUEST' , 'Symbol name is required after ":"' ) ;
54+ }
3755 }
38- module = mod ;
39- symbolPath = symPath ;
56+ }
57+
58+ // If no symbol path, return file-level request
59+ if ( ! symbolPath ) {
60+ return { module, symbol : undefined , member : undefined , isStatic : false } ;
4061 }
4162
4263 // Check for instance member (Constructor#instanceMember)
@@ -66,6 +87,51 @@ function parseModulePath(modulePath: string): ExtractionRequest | ExtractError {
6687 return { module, symbol : symbolPath , member : undefined , isStatic : false } ;
6788}
6889
90+ // Get all exported symbols with their summaries
91+ function getExportSummaries (
92+ moduleSymbol : ts . Symbol | undefined ,
93+ sourceFile : ts . SourceFile ,
94+ checker : ts . TypeChecker ,
95+ ) : ExportSummary [ ] {
96+ if ( ! moduleSymbol ) {
97+ return [ ] ;
98+ }
99+
100+ try {
101+ const exports = checker . getExportsOfModule ( moduleSymbol ) ;
102+ const summaries : ExportSummary [ ] = [ ] ;
103+
104+ for ( const exp of exports ) {
105+ const name = exp . getName ( ) ;
106+ const kind = getSymbolKind ( exp ) ;
107+
108+ // Get line number from declaration
109+ const decl = exp . valueDeclaration ?? exp . declarations ?. [ 0 ] ;
110+ let line = 0 ;
111+ if ( decl ) {
112+ const pos = sourceFile . getLineAndCharacterOfPosition ( decl . getStart ( ) ) ;
113+ line = pos . line + 1 ; // 1-indexed
114+ }
115+
116+ // Get brief documentation (first line)
117+ const fullDoc = ts . displayPartsToString ( exp . getDocumentationComment ( checker ) ) ;
118+ const briefDoc = fullDoc . split ( '\n' ) [ 0 ] ?. trim ( ) ;
119+
120+ summaries . push ( {
121+ name,
122+ kind,
123+ line,
124+ documentation : briefDoc || undefined ,
125+ } ) ;
126+ }
127+
128+ // Sort by line number
129+ return summaries . sort ( ( a , b ) => a . line - b . line ) ;
130+ } catch {
131+ return [ ] ;
132+ }
133+ }
134+
69135// Extract documentation for a module:symbol path
70136export async function extractDocs ( modulePath : string ) : Promise < ExtractResult > {
71137 const options : ExtractorOptions = { prefix : process . cwd ( ) } ;
@@ -95,6 +161,28 @@ export async function extractDocs(modulePath: string): Promise<ExtractResult> {
95161
96162 const { sourceFile, program } = resolvedModule ;
97163 const checker = program . getTypeChecker ( ) ;
164+
165+ // Handle file-level request (no symbol specified)
166+ if ( symbol === undefined ) {
167+ const overview = getFileOverview ( sourceFile ) ;
168+ const moduleSymbol = checker . getSymbolAtLocation ( sourceFile ) ;
169+ const exports = getExportSummaries ( moduleSymbol , sourceFile , checker ) ;
170+
171+ // Format path as relative (consistent with SymbolInfo.location)
172+ let relativePath = sourceFile . fileName ;
173+ const cwd = process . cwd ( ) ;
174+ if ( relativePath . startsWith ( cwd ) ) {
175+ relativePath = path . relative ( cwd , relativePath ) ;
176+ }
177+
178+ return {
179+ path : relativePath ,
180+ overview,
181+ exportCount : exports . length ,
182+ exports,
183+ } ;
184+ }
185+
98186 let targetSymbol : ts . Symbol | undefined ;
99187
100188 if ( isGlobalModule ) {
@@ -196,6 +284,20 @@ export async function getSourceLocation(reference: string): Promise<ResolveResul
196284 }
197285
198286 const { module, symbol, member, isStatic } = parseResult ;
287+
288+ // In this branch, we know reference contains ':', so symbol should be defined
289+ // If symbol is undefined here, it means parseModulePath returned successfully for a file-level request
290+ // which shouldn't happen in this code path
291+ if ( ! symbol ) {
292+ return {
293+ success : false ,
294+ error : {
295+ code : 'INVALID_SPECIFIER' ,
296+ message : 'Symbol reference required for source location lookup' ,
297+ } ,
298+ } ;
299+ }
300+
199301 const config = loadTsConfig ( options . prefix ) ;
200302
201303 // Try to resolve as a regular module first
@@ -365,6 +467,8 @@ export async function extractSymbol(
365467 } ;
366468 }
367469
470+ // After the check above, we know symbol is defined
471+ const { symbol } = request ;
368472 const config = loadTsConfig ( options . prefix ) ;
369473
370474 // Resolve module with dedicated program
@@ -388,27 +492,27 @@ export async function extractSymbol(
388492 if ( moduleSymbol ) {
389493 // TypeScript module with proper exports
390494 const exports = checker . getExportsOfModule ( moduleSymbol ) ;
391- targetSymbol = exports . find ( ( exp : ts . Symbol ) => exp . getName ( ) === request . symbol ) ;
495+ targetSymbol = exports . find ( ( exp : ts . Symbol ) => exp . getName ( ) === symbol ) ;
392496
393497 if ( ! targetSymbol ) {
394498 return {
395499 error : {
396500 code : 'SYMBOL_NOT_FOUND' ,
397- message : `Symbol '${ request . symbol } ' not found in module '${ request . module } '. Available exports: ${ exports
501+ message : `Symbol '${ symbol } ' not found in module '${ request . module } '. Available exports: ${ exports
398502 . map ( ( e : ts . Symbol ) => e . getName ( ) )
399503 . join ( ', ' ) } `,
400504 } ,
401505 } ;
402506 }
403507 } else {
404508 // JavaScript file - look for symbols in statements
405- targetSymbol = findSymbolInJavaScriptFile ( sourceFile , checker , request . symbol ) ;
509+ targetSymbol = findSymbolInJavaScriptFile ( sourceFile , checker , symbol ) ;
406510
407511 if ( ! targetSymbol ) {
408512 return {
409513 error : {
410514 code : 'SYMBOL_NOT_FOUND' ,
411- message : `Symbol '${ request . symbol } ' not found in JavaScript module '${ request . module } '` ,
515+ message : `Symbol '${ symbol } ' not found in JavaScript module '${ request . module } '` ,
412516 } ,
413517 } ;
414518 }
@@ -426,5 +530,5 @@ export async function extractSymbol(
426530 }
427531}
428532
429- // Re-export formatOutput
430- export { formatOutput } ;
533+ // Re-export formatters
534+ export { formatOutput } from './formatters' ;
0 commit comments