UNPKG

@sprucelabs/spruce-cli

Version:

Command line interface for building Spruce skills.

328 lines (277 loc) • 10.3 kB
import { dirname } from 'path' import { diskUtil } from '@sprucelabs/spruce-skill-utils' import _ from 'lodash' import * as tsutils from 'tsutils' import * as ts from 'typescript' const serializeSymbol = (options: { checker: ts.TypeChecker symbol: ts.Symbol }): DocEntry => { const { checker, symbol } = options const doc: DocEntry = { name: symbol.getName(), documentation: ts.displayPartsToString( symbol.getDocumentationComment(checker) ), } if (symbol.valueDeclaration) { doc.type = checker.typeToString( checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration) ) } return doc } const serializeSignature = (options: { checker: ts.TypeChecker signature: ts.Signature }) => { const { checker, signature } = options return { parameters: signature.parameters.map((p) => serializeSymbol({ symbol: p, checker }) ), returnType: checker.typeToString(signature.getReturnType()), documentation: ts.displayPartsToString( signature.getDocumentationComment(checker) ), } } const introspectionUtil = { introspect(tsFiles: string[]): Introspection[] { const filePaths = tsFiles const program = ts.createProgram(filePaths, {}) const checker = program.getTypeChecker() // for building results const introspects: Introspection[] = [] for (const tsFile of filePaths) { const sourceFile = program.getSourceFile(tsFile) const results: Introspection = { classes: [], interfaces: [] } if (sourceFile && _.includes(filePaths, sourceFile.fileName)) { if (!this.hasClassDefinition(sourceFile)) { const exports = this.getExports(sourceFile) const firstExport = exports[0] if (firstExport) { const declaration = this.getClassDeclarationFromImportedFile( firstExport, dirname(tsFile), program ) if (declaration) { const { classes, interfaces } = getDeclarationsFromNode( declaration, checker, sourceFile ) results.classes.push(...classes) results.interfaces.push(...interfaces) } else { // must have imported from somewhere else (another node module) const className = //@ts-ignore firstExport.exportClause?.elements?.[0] ?.propertyName?.text if (className) { results.classes.push({ className, classPath: tsFile, isAbstract: false, optionsInterfaceName: undefined, parentClassName: undefined, parentClassPath: undefined, staticProperties: {}, }) } } } } else { ts.forEachChild(sourceFile, (node) => { const { classes, interfaces } = getDeclarationsFromNode( node, checker, sourceFile ) results.classes.push(...classes) results.interfaces.push(...interfaces) }) } } introspects.push(results) } return introspects }, getExports(sourceFile: ts.SourceFile): ts.Node[] { const exports: ts.Node[] = [] const traverse = (node: ts.Node) => { if (ts.isExportDeclaration(node)) { exports.push(node) } ts.forEachChild(node, traverse) } traverse(sourceFile) return exports }, getClassDeclarationFromImportedFile( exportDeclaration: ts.Node, dirName: string, program: ts.Program ): ts.ClassDeclaration | undefined { if (!ts.isExportDeclaration(exportDeclaration)) { return undefined } const exportClause = exportDeclaration.exportClause if (!exportClause || !ts.isNamedExports(exportClause)) { return undefined } for (const element of exportClause.elements) { if (element.propertyName) { const propertyName = element.propertyName.text const moduleSpecifier = ( exportDeclaration.moduleSpecifier as ts.StringLiteral ).text const sourceFile = diskUtil.resolveFile( dirName, moduleSpecifier.replace(/^\.\//, '') ) if (!sourceFile) { return undefined } // Load the source file containing the class declaration const declarationSourceFile = program.getSourceFile(sourceFile) if (!declarationSourceFile) { return undefined } const traverse = ( node: ts.Node ): ts.ClassDeclaration | undefined => { if ( ts.isClassDeclaration(node) && node.name && node.name.text === propertyName ) { return node } for (const child of node.getChildren( declarationSourceFile )) { const result = traverse(child) if (result) { return result } } return undefined } return traverse(declarationSourceFile) } } return undefined }, hasClassDefinition(sourceFile: ts.SourceFile): boolean { let hasClass = false const traverse = (node: ts.Node) => { if (ts.isClassDeclaration(node)) { hasClass = true } if (!hasClass) { ts.forEachChild(node, traverse) } } traverse(sourceFile) return hasClass }, } export default introspectionUtil function getDeclarationsFromNode( node: ts.Node, checker: ts.TypeChecker, sourceFile: ts.SourceFile ) { const classes: IntrospectionClass[] = [] const interfaces: IntrospectionInterface[] = [] // if this is a class declaration if (ts.isClassDeclaration(node) && node.name) { const symbol = checker.getSymbolAtLocation(node.name) if (symbol?.valueDeclaration) { const details = serializeSymbol({ checker, symbol }) // Get the construct signatures const constructorType = checker.getTypeOfSymbolAtLocation( symbol, symbol.valueDeclaration ) let parentClassSymbol: ts.Symbol | undefined if (node.heritageClauses && node.heritageClauses[0]) { parentClassSymbol = checker .getTypeAtLocation(node.heritageClauses[0].types[0]) .getSymbol() } const parentClassName = // @ts-ignore parentClassSymbol?.valueDeclaration?.name?.text // @ts-ignore const parentClassPath = parentClassSymbol?.parent ?.getName() .replace('"', '') const isAbstractClass = tsutils.isModifierFlagSet( node, ts.ModifierFlags.Abstract ) details.constructors = constructorType .getConstructSignatures() .map((s) => serializeSignature({ signature: s, checker })) classes.push({ className: node.name.text, classPath: sourceFile.fileName, parentClassName, parentClassPath, staticProperties: pluckStaticProperties(node), optionsInterfaceName: details.constructors?.[0].parameters?.[0]?.type, isAbstract: isAbstractClass, }) } } else if (ts.isInterfaceDeclaration(node)) { interfaces.push({ interfaceName: node.name.text, }) } return { classes, interfaces } } function pluckStaticProperties(node: ts.ClassDeclaration): StaticProperties { const staticProps: StaticProperties = {} for (const member of node.members) { //@ts-ignore const name = member.name?.escapedText //@ts-ignore const value = member.initializer?.text if (name && value) { staticProps[name] = value } } return staticProps } export interface IntrospectionClass { className: string classPath: string parentClassName: string | undefined parentClassPath: string | undefined optionsInterfaceName: string | undefined isAbstract: boolean staticProperties: StaticProperties } type StaticProperties = Record<string, any> interface IntrospectionInterface { interfaceName: string } export interface Introspection { classes: IntrospectionClass[] interfaces: IntrospectionInterface[] } interface DocEntry { name?: string fileName?: string documentation?: string type?: string constructors?: DocEntry[] parameters?: DocEntry[] returnType?: string }