import * as ts from 'typescript' ; export interface DocEntry { name : string ; documentation ? : string ; type : string ; optional ? : boolean ; nested ? : DocEntry [ ] ; } /** Generate documentation for all schema definitions in a set of .ts files */ export function extractDocumentation ( fileNames , options = { target : ts . ScriptTarget . ES2015 , module : ts . ModuleKind . CommonJS , } ) { const program = ts . createProgram ( fileNames , options ) ; const checker = program . getTypeChecker ( ) ; const result = new Map < string , DocEntry [ ] > ( ) ; for ( const sourceFile of program . getSourceFiles ( ) ) { if ( ! sourceFile . isDeclarationFile ) { ts . forEachChild ( sourceFile , visit ) ; } } return result ; /** visit nodes finding exported schemas */ function visit ( node ) { if ( isNodeExported ( node ) && ts . isVariableDeclaration ( node ) ) { const schemaName = node . name . getText ( ) ; const schemaType = checker . getTypeAtLocation ( node ) ; result . set ( schemaName , extractDocEntries ( schemaType ! ) ) ; } if ( node . getChildCount ( ) > 0 ) { ts . forEachChild ( node , visit ) ; } } param schemaType function extractDocEntries ( schemaType ) { const collection = [ ] ; const members = getTypeMembers ( schemaType ) ; if ( ! members ) { return collection ; } members . forEach ( ( member ) => { collection . push ( serializeProperty ( member ) ) ; } ) ; return collection ; } param type function getTypeMembers ( type ) { const argsOfType = checker . getTypeArguments ( type as unknown as ts . TypeReference ) ; let members = type . getProperties ( ) ; if ( argsOfType && argsOfType . length > 0 ) { members = argsOfType [ 0 ] . getProperties ( ) ; } return members ; } param type function resolveTypeProperties ( type ) { let props = type . getProperties ( ) ; const typeArguments = checker . getTypeArguments ( type as unknown as ts . TypeReference ) ; if ( type . aliasTypeArguments ) { props = type . aliasTypeArguments [ 0 ] . getProperties ( ) ; } if ( typeArguments . length > 0 ) { props = resolveTypeProperties ( typeArguments [ 0 ] ) ; } return props ; } function serializeProperty ( symbol ) { const typeOfSymbol = symbol . type ; if ( typeOfSymbol === undefined ) { return { name : symbol . getName ( ) , documentation : getCommentString ( symbol ) , type : 'any' , } ; } let targetType = typeOfSymbol . getProperty ( 'type' ) ?. type ?? typeOfSymbol ; const isArrayOf = targetType . symbol ?. name === 'Array' ; if ( isArrayOf ) { targetType = checker . getTypeArguments ( targetType as ts . TypeReference ) [ 0 ] ; } let typeAsString = checker . typeToString ( targetType ) ; const nestedEntries = [ ] ; if ( targetType . aliasTypeArguments || checker . getTypeArguments ( targetType as ts . TypeReference ) . length > 0 ) { const typeProperties = resolveTypeProperties ( targetType ) ; if ( Array . isArray ( typeProperties ) && typeProperties . length > 0 ) { typeAsString = targetType . symbol ?. name === 'Array' || typeOfSymbol . symbol ?. name === 'Array' ? `${ symbol . getName ( ) }[]` : symbol . getName ( ) ; typeProperties . forEach ( ( member ) => { nestedEntries . push ( serializeProperty ( member ) ) ; } ) ; } } const res = { name : symbol . getName ( ) , documentation : getCommentString ( symbol ) , type : isArrayOf ? `${ typeAsString }[]` : typeAsString , ... ( nestedEntries . length > 0 ? { nested : nestedEntries } : { } ) , } ; return res ; } function getCommentString ( symbol ) { return ts . displayPartsToString ( symbol . getDocumentationComment ( checker ) ) . replace ( /\n/g , ' ' ) ; } /**
     * True if this is visible outside this file, false otherwise
     */ function isNodeExported ( node ) { return ( ( ts . getCombinedModifierFlags ( node as ts . Declaration ) & ts . ModifierFlags . Export ) !== 0 || ( ! ! node . parent && node . parent . kind === ts . SyntaxKind . SourceFile ) ) ; } }