@@ -23,6 +23,7 @@ import * as TypeParser from "../core/TypeParser"
2323import * as TypeScriptApi from "../core/TypeScriptApi"
2424import * as TypeScriptUtils from "../core/TypeScriptUtils"
2525import { getFileNamesInTsConfig , TypeScriptContext } from "./utils"
26+ import * as ExportedSymbols from "./utils/ExportedSymbols"
2627import * as Spinner from "./utils/Spinner"
2728
2829/**
@@ -45,7 +46,7 @@ export class NoFilesToCheckError extends Data.TaggedError("NoFilesToCheckError")
4546 }
4647}
4748
48- interface ServiceInfo {
49+ export interface ServiceInfo {
4950 readonly name : string
5051 readonly filePath : string
5152 readonly line : number
@@ -54,7 +55,7 @@ interface ServiceInfo {
5455 readonly description : string | undefined
5556}
5657
57- interface LayerInfo {
58+ export interface LayerInfo {
5859 readonly name : string
5960 readonly filePath : string
6061 readonly line : number
@@ -63,7 +64,7 @@ interface LayerInfo {
6364 readonly description : string | undefined
6465}
6566
66- interface ErrorInfo {
67+ export interface ErrorInfo {
6768 readonly name : string
6869 readonly filePath : string
6970 readonly line : number
@@ -72,7 +73,13 @@ interface ErrorInfo {
7273 readonly description : string | undefined
7374}
7475
75- interface OverviewResult {
76+ export interface ExportedItemsResult {
77+ readonly services : Array < ServiceInfo >
78+ readonly layers : Array < LayerInfo >
79+ readonly errors : Array < ErrorInfo >
80+ }
81+
82+ export interface OverviewResult extends ExportedItemsResult {
7683 readonly services : Array < ServiceInfo >
7784 readonly layers : Array < LayerInfo >
7885 readonly errors : Array < ErrorInfo >
@@ -91,32 +98,22 @@ const typeToString = (
9198) : string => typeChecker . typeToString ( type , undefined , tsInstance . TypeFormatFlags . NoTruncation )
9299
93100/**
94- * Gets the location info from a declaration
95- */
96- const getLocationFromDeclaration = (
97- declaration : ts . Declaration ,
98- tsInstance : typeof ts
99- ) : { filePath : string ; line : number ; column : number } | undefined => {
100- const sourceFile = declaration . getSourceFile ( )
101- if ( ! sourceFile ) return undefined
102- const { character, line } = tsInstance . getLineAndCharacterOfPosition ( sourceFile , declaration . getStart ( ) )
103- return {
104- filePath : sourceFile . fileName ,
105- line : line + 1 ,
106- column : character + 1
107- }
108- }
109-
110- /**
111- * Collects all exported services, layers, and errors from a source file using getExportsOfModule
112- * Also traverses properties of exported symbols to find nested services/layers (e.g., Effect.Service.Default)
101+ * Collects all exported services, layers, and errors from a source file.
102+ * Uses ExportedSymbols to get all exported symbols with their names and locations,
103+ * then checks each symbol's type to categorize it.
104+ *
105+ * @param sourceFile - The source file to collect exports from
106+ * @param tsInstance - The TypeScript instance
107+ * @param typeChecker - The TypeScript type checker
108+ * @param maxSymbolDepth - Maximum depth to traverse nested properties (default: 3)
113109 */
114- const collectExportedItems = (
110+ export const collectExportedItems = (
115111 sourceFile : ts . SourceFile ,
116112 tsInstance : typeof ts ,
117- typeChecker : ts . TypeChecker
113+ typeChecker : ts . TypeChecker ,
114+ maxSymbolDepth : number = 3
118115) : Nano . Nano <
119- { services : Array < ServiceInfo > ; layers : Array < LayerInfo > ; errors : Array < ErrorInfo > } ,
116+ ExportedItemsResult ,
120117 never ,
121118 TypeParser . TypeParser
122119> =>
@@ -126,61 +123,19 @@ const collectExportedItems = (
126123 const layers : Array < LayerInfo > = [ ]
127124 const errors : Array < ErrorInfo > = [ ]
128125
129- // Get the module symbol for the source file
130- const moduleSymbol = typeChecker . getSymbolAtLocation ( sourceFile )
131- if ( ! moduleSymbol ) {
132- return { services, layers, errors }
133- }
134-
135- // Get all exports from the module
136- const exports = typeChecker . getExportsOfModule ( moduleSymbol )
137-
138- type CodeLocation = { filePath : string ; line : number ; column : number }
139-
140- // Work queue: [symbol, qualifiedName, codeLocation | undefined]
141- // Initialize with exported symbols using their names and declaration locations
142- const workQueue : Array < [ ts . Symbol , string , CodeLocation | undefined ] > = exports . map ( ( s ) => {
143- const declarations = s . getDeclarations ( )
144- const location = declarations && declarations . length > 0
145- ? getLocationFromDeclaration ( declarations [ 0 ] , tsInstance )
146- : undefined
147- return [ s , tsInstance . symbolName ( s ) , location ]
148- } )
149- // Track which symbols have been exploded (properties added to queue)
150- const exploded = new WeakSet < ts . Symbol > ( )
151-
152- while ( workQueue . length > 0 ) {
153- const [ symbol , name , location ] = workQueue . shift ( ) !
154-
155- if ( ! location ) continue
156-
157- const type = typeChecker . getTypeOfSymbol ( symbol )
158-
159- // Explode symbol: add its properties to the queue (only once per symbol)
160- // Child symbols inherit the parent's code location
161- if ( ! exploded . has ( symbol ) ) {
162- exploded . add ( symbol )
163-
164- const properties = typeChecker . getPropertiesOfType ( type )
165- for ( const propSymbol of properties ) {
166- const propName = tsInstance . symbolName ( propSymbol )
167- // Skip prototype property - it contains instance type, not a real export
168- if ( propName === "prototype" ) continue
169- const childName = `${ name } .${ propName } `
170- workQueue . push ( [ propSymbol , childName , location ] )
171- }
172- }
126+ // Get all exported symbols with their names and locations
127+ const exportedSymbols = ExportedSymbols . collectSourceFileExportedSymbols (
128+ sourceFile ,
129+ tsInstance ,
130+ typeChecker ,
131+ maxSymbolDepth
132+ )
173133
134+ for ( const { description, location, name, symbol, type } of exportedSymbols ) {
174135 // Get a declaration for type parsing context
175136 const declarations = symbol . getDeclarations ( )
176137 const declaration = declarations && declarations . length > 0 ? declarations [ 0 ] : sourceFile
177138
178- // Get JSDoc description if available
179- const docComment = symbol . getDocumentationComment ( typeChecker )
180- const description = docComment . length > 0
181- ? docComment . map ( ( part ) => part . text ) . join ( "" )
182- : undefined
183-
184139 // Check if it's a Context.Tag (has _Identifier and _Service variance)
185140 const contextTagResult = yield * pipe (
186141 typeParser . contextTag ( type , declaration ) ,
@@ -347,7 +302,7 @@ const renderError = (error: ErrorInfo, cwd: string): Doc.AnsiDoc => {
347302/**
348303 * Renders the overview result as a styled document
349304 */
350- const renderOverview = ( result : OverviewResult , cwd : string ) : Doc . AnsiDoc => {
305+ export const renderOverview = ( result : OverviewResult , cwd : string ) : Doc . AnsiDoc => {
351306 const lines : Array < Doc . AnsiDoc > = [ ]
352307
353308 // Errors section
@@ -391,8 +346,9 @@ const renderOverview = (result: OverviewResult, cwd: string): Doc.AnsiDoc => {
391346const collectAllItems = (
392347 filesToCheck : Set < string > ,
393348 tsInstance : typeof ts ,
349+ maxSymbolDepth : number ,
394350 onProgress : ( current : number , total : number ) => Effect . Effect < void >
395- ) : Effect . Effect < { services : Array < ServiceInfo > ; layers : Array < LayerInfo > ; errors : Array < ErrorInfo > } > =>
351+ ) : Effect . Effect < ExportedItemsResult > =>
396352 Effect . gen ( function * ( ) {
397353 const services : Array < ServiceInfo > = [ ]
398354 const layers : Array < LayerInfo > = [ ]
@@ -421,7 +377,7 @@ const collectAllItems = (
421377 if ( ! sourceFile ) continue
422378
423379 const result = pipe (
424- collectExportedItems ( sourceFile , tsInstance , program . getTypeChecker ( ) ) ,
380+ collectExportedItems ( sourceFile , tsInstance , program . getTypeChecker ( ) , maxSymbolDepth ) ,
425381 TypeParser . nanoLayer ,
426382 TypeCheckerUtils . nanoLayer ,
427383 TypeScriptUtils . nanoLayer ,
@@ -461,9 +417,15 @@ export const overview = Command.make(
461417 project : Options . file ( "project" ) . pipe (
462418 Options . optional ,
463419 Options . withDescription ( "The full path of the project tsconfig.json file to analyze." )
420+ ) ,
421+ maxSymbolDepth : Options . integer ( "max-symbol-depth" ) . pipe (
422+ Options . withDefault ( 3 ) ,
423+ Options . withDescription (
424+ "Maximum depth to traverse nested symbol properties. 0 = only root exports, 1 = root + one level, etc."
425+ )
464426 )
465427 } ,
466- Effect . fn ( "overview" ) ( function * ( { file, project } ) {
428+ Effect . fn ( "overview" ) ( function * ( { file, maxSymbolDepth , project } ) {
467429 const path = yield * Path . Path
468430 const cwd = path . resolve ( "." )
469431 const tsInstance = yield * TypeScriptContext
@@ -486,6 +448,7 @@ export const overview = Command.make(
486448 collectAllItems (
487449 filesToCheck ,
488450 tsInstance ,
451+ maxSymbolDepth ,
489452 ( current , total ) => handle . updateMessage ( `Processing file ${ current } /${ total } ...` )
490453 ) ,
491454 {
0 commit comments