I wish to harvest class, properties and method information using the Typescript compiler.
I am using nodejs and wish to use the information to construct client side forms etc based on my server side class definitions.
I have made good progress using stack overflow as a start eg: Correct way of getting type for a variable declaration in a typescript AST? but would like to extend further to get Method parameter information which is currently missing from the classes.json file as shown below. Any suggestions would be appreciated. My code:
import ts from 'typescript';
import * as fs from "fs";
interface DocEntry {
name?: string;
fileName?: string;
documentation?: string;
type?: string;
constructors?: DocEntry[];
parameters?: DocEntry[];
returnType?: string;
}
/** Generate documentation for all classes in a set of .ts files */
function generateDocumentation(
fileNames: string[],
options: ts.CompilerOptions
): void {
// Build a program using the set of root file names in fileNames
let program = ts.createProgram(fileNames, options);
// Get the checker, we will use it to find more about classes
let checker = program.getTypeChecker();
let output = {
component: [],
fields: [],
methods: []
};
// Visit every sourceFile in the program
for (const sourceFile of program.getSourceFiles()) {
if (!sourceFile.isDeclarationFile) {
// Walk the tree to search for classes
ts.forEachChild(sourceFile, visit);
}
}
// print out the definitions
fs.writeFileSync("classes.json", JSON.stringify(output, undefined, 4));
return;
/** visit nodes */
function visit(node: ts.Node) {
if (ts.isClassDeclaration(node) && node.name) {
// This is a top level class, get its symbol
let symbol = checker.getSymbolAtLocation(node.name);
if (symbol) {
const details = serializeClass(symbol);
output.component.push(details);
}
ts.forEachChild(node, visit);
}
else if (ts.isPropertyDeclaration(node)) {
const x = 0;
let symbol = checker.getSymbolAtLocation(node.name);
if (symbol) {
output.fields.push(serializeClass(symbol));
}
} else if (ts.isMethodDeclaration(node)) {
const x = 0;
let symbol = checker.getSymbolAtLocation(node.name);
if (symbol) {
output.methods.push(serializeClass(symbol));
}
}
}
/** Serialize a symbol into a json object */
function serializeSymbol(symbol: ts.Symbol): DocEntry {
return {
name: symbol.getName(),
documentation: ts.displayPartsToString(symbol.getDocumentationComment(checker)),
type: checker.typeToString(
checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration!)
)
};
}
/** Serialize a class symbol information */
function serializeClass(symbol: ts.Symbol) {
let details = serializeSymbol(symbol);
// Get the construct signatures
let constructorType = checker.getTypeOfSymbolAtLocation(
symbol,
symbol.valueDeclaration!
);
details.constructors = constructorType
.getConstructSignatures()
.map(serializeSignature);
return details;
}
/** Serialize a signature (call or construct) */
function serializeSignature(signature: ts.Signature) {
return {
parameters: signature.parameters.map(serializeSymbol),
returnType: checker.typeToString(signature.getReturnType()),
documentation: ts.displayPartsToString(signature.getDocumentationComment(checker))
};
}
}
generateDocumentation(["source1.ts"], {
target: ts.ScriptTarget.ES5,
module: ts.ModuleKind.CommonJS
});
target source file source1.ts:
* Documentation for C
*/
class C {
/**bg2 is very cool*/
bg2: number = 2;
bg4: number = 4;
bgA: string = "A";
/**
* constructor documentation
* @param a my parameter documentation
* @param b another parameter documentation
*/
constructor(a: string, b: C) {
}
/** MethodA is an A type Method*/
methodA(myarga1: string): number {
return 22;
}
/** definitely a B grade Method
* @param myargb1 is very argumentative*/
methodB(myargb1: string): string {
return "abc";
}
}
resulting JSON file classes.json:
{
"component": [
{
"name": "C",
"documentation": "Documentation for C",
"type": "typeof C",
"constructors": [
{
"parameters": [
{
"name": "a",
"documentation": "my parameter documentation",
"type": "string"
},
{
"name": "b",
"documentation": "another parameter documentation",
"type": "C"
}
],
"returnType": "C",
"documentation": "constructor documentation"
}
]
}
],
"fields": [
{
"name": "bg2",
"documentation": "bg2 is very cool",
"type": "number",
"constructors": []
},
{
"name": "bg4",
"documentation": "",
"type": "number",
"constructors": []
},
{
"name": "bgA",
"documentation": "",
"type": "string",
"constructors": []
}
],
"methods": [
{
"name": "methodA",
"documentation": "MethodA is an A type Method",
"type": "(myarga1: string) => number",
"constructors": []
},
{
"name": "methodB",
"documentation": "definitely a B grade Method",
"type": "(myargb1: string) => string",
"constructors": []
}
]
}
Added a check for ts.isMethodDeclaration(node) in the visit function to caputure Method details. Also added support for multiple files and for documentation tags (eg like @DummyTag written in the documentation comments like:
/** @DummyTag Mary had a little lamb */
So new file works well:
// @ts-ignore
import ts from 'typescript';
import * as fs from "fs";
interface DocEntry {
name?: string;
fileName?: string;
documentation?: string;
type?: string;
constructors?: DocEntry[];
parameters?: DocEntry[];
returnType?: string;
tags?: Record<string, string>;
}
/** Generate documentation for all classes in a set of .ts files */
function generateDocumentation(
fileNames: string[],
options: ts.CompilerOptions
): void {
// Build a program using the set of root file names in fileNames
let program = ts.createProgram(fileNames, options);
console.log("ROOT FILES:",program.getRootFileNames());
// Get the checker, we will use it to find more about classes
let checker = program.getTypeChecker();
let allOutput = [];
let output = null;
let exportStatementFound = false;
let currentMethod = null;
let fileIndex = 0;
// Visit the sourceFile for each "source file" in the program
//ie don't use program.getSourceFiles() as it gets all the imports as well
for (let i=0; i<fileNames.length; i++) {
const fileName = fileNames[i];
const sourceFile = program.getSourceFile(fileName);
// console.log("sourceFile.kind:", sourceFile.kind);
if (sourceFile.kind === ts.SyntaxKind.ImportDeclaration){
console.log("IMPORT");
}
exportStatementFound = false;
if (!sourceFile.isDeclarationFile) {
// Walk the tree to search for classes
output = {
fileName: fileName,
component: [],
fields: [],
methods: []
};
ts.forEachChild(sourceFile, visit);
if (output) {
allOutput.push(output);
}
if (!exportStatementFound){
console.log("WARNING: no export statement found in:", fileName);
}
}
}
// print out the definitions
fs.writeFileSync("classes.json", JSON.stringify(allOutput, undefined, 4));
return;
/** visit nodes */
function visit(node: ts.Node) {
if (!output){
return;
}
if (node.kind === ts.SyntaxKind.ImportDeclaration){
console.log("IMPORT");
//output = null;
return;
}
if (node.kind === ts.SyntaxKind.DefaultKeyword){
console.log("DEFAULT");
return;
}
if (node.kind === ts.SyntaxKind.ExportKeyword){
exportStatementFound = true;
console.log("EXPORT");
return;
}
if (ts.isClassDeclaration(node) && node.name) {
// This is a top level class, get its symbol
let symbol = checker.getSymbolAtLocation(node.name);
if (symbol) {
//need localSymbol for the name, if there is one because otherwise exported as "default"
symbol = (symbol.valueDeclaration?.localSymbol)?symbol.valueDeclaration?.localSymbol: symbol;
const details = serializeClass(symbol);
output.component.push(details);
}
ts.forEachChild(node, visit);
}
else if (ts.isPropertyDeclaration(node)) {
let symbol = checker.getSymbolAtLocation(node.name);
if (symbol) {
output.fields.push(serializeField(symbol));
}
} else if (ts.isMethodDeclaration(node)) {
let symbol = checker.getSymbolAtLocation(node.name);
if (symbol) {
currentMethod = serializeMethod(symbol);
output.methods.push(currentMethod);
}
ts.forEachChild(node, visit);
}
}
/** Serialize a symbol into a json object */
function serializeSymbol(symbol: ts.Symbol): DocEntry {
const tags = symbol.getJsDocTags();
let tagMap = null;
if (tags?.length){
console.log("TAGS:", tags);
for (let i=0; i<tags.length; i++){
const tag = tags[i];
if (tag.name !== "param"){
tagMap = tagMap?tagMap:{};
tagMap[tag.name] = tag.text;
}
}
}
return {
name: symbol.getName(),
documentation: ts.displayPartsToString(symbol.getDocumentationComment(checker)),
type: checker.typeToString(
checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration!)
),
tags: tagMap
};
}
/** Serialize a class symbol information */
function serializeClass(symbol: ts.Symbol) {
let details = serializeSymbol(symbol);
// Get the construct signatures
let constructorType = checker.getTypeOfSymbolAtLocation(
symbol,
symbol.valueDeclaration!
);
details.constructors = constructorType
.getConstructSignatures()
.map(serializeSignature);
return details;
}
function serializeField(symbol: ts.Symbol) {
return serializeSymbol(symbol);
}
function serializeMethod(symbol: ts.Symbol) {
let details = serializeSymbol(symbol);
// Get the construct signatures
let methodType = checker.getTypeOfSymbolAtLocation(
symbol,
symbol.valueDeclaration!
);
let callingDetails = methodType.getCallSignatures()
.map(serializeSignature)["0"];
details = {...details, ...callingDetails};
return details;
}
/** Serialize a signature (call or construct) */
function serializeSignature(signature: ts.Signature) {
return {
parameters: signature.parameters.map(serializeSymbol),
returnType: checker.typeToString(signature.getReturnType()),
documentation: ts.displayPartsToString(signature.getDocumentationComment(checker))
};
}
}
generateDocumentation(["source1.ts", "source2.ts"], {
target: ts.ScriptTarget.ES5,
module: ts.ModuleKind.CommonJS
});