Extracting data from typescript code before it is compiled

Viewed 43

So i have an application on NestJS and Typescript. My goal is to basically extract to a file each controller name, route, and some metadata about it.

@Controller('baseurl')
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get('hello')
  @MetaData('abc')
  getHello(): string {
    return this.appService.getHello();
  }

  @Post('hello')
  @MetaData('abc')
  getHello(): string {
    return this.appService.getHello();
  }
}

The output file would be something like this (formatting don't matter, just the data):

[
  {
    'module': 'AppController',
    'controller': 'baseurl',
    'route': 'hello',
    'method': 'GET',
    'medatada': 'abc'
  },
  {
    'module': 'AppController',
    'controller': 'baseurl',
    'route': 'hello',
    'method': 'POST',
    'medatada': 'abc'
  },
  { ... }
]

Ideally this is done on as a standalone script that i can run on either a githook or as part of the pre-compile process for the actual application. This file is a file that I would use to feed other processes, so it's not part of the runtime of the application itself.

I imagine there is a way to capture this data in a typescript/node sort of way, but I'm really clueless as to what the right process would be (without parsing each file by hand), or what to otherwise google to get on the right track.

3 Answers

As I know, based on how it works in babel, it's possible if create a custom plugin for the builder. In which, implement a visitor that will collect needed data and store them to file.

You can try this page: Nestjs Compodoc

If you want to generate schema before running build. I recommend you to custom your build script:

...
 # you can custom your output file before build
 "build": "npx @compodoc/compodoc -p tsconfig.json -s && nest build"
...

Usage for Compodoc: Compodoc

So based on the @caTS hint, I used the example for delinting to create my own script:

import * as glob from 'glob';
import * as ts from 'typescript';
import { readFileSync } from 'fs';

export function report(sourceFile: ts.SourceFile) {
  const ast: Record<string, any>[] = [];
  reportNode(sourceFile, ast);
  console.log(JSON.stringify(ast, null, 2));

  function reportNode(
    node: ts.Node,
    ast: Record<string, any>[] = [],
    tree: Record<string, any> = {},
  ) {
    if (ts.isClassDeclaration(node)) {
      tree = {
        class: node.name?.getText(),
      };
      ts.getDecorators(node)?.forEach((dec) => {
        const rep = reportDecorator(dec);
        tree.baseurl = rep.args.join('/');
      });
    }

    if (ts.isMethodDeclaration(node)) {
      tree.methods = [];
      tree.method_name = node.name.getText();
      ts.getDecorators(node)?.forEach((dec) => {
        const rep = reportDecorator(dec);
        tree.methods.push(rep);
      });

      ast.push({ ...tree });
    }

    ts.forEachChild(node, (s) => reportNode(s, ast, tree));
  }

  function reportDecorator(decorator: ts.Decorator) {
    const expression = decorator.expression as ts.CallExpression;
    const name = expression.getFirstToken()?.getText();
    const args = expression.arguments.map((e: any) => {
      if (e.kind == 10) return e.text;
    });
    return { name, args };
  }
}

fileNames = glob.sync('src/**/*.controller.ts');
fileNames.forEach((fileName) => {
  // Parse a file
  const sourceFile = ts.createSourceFile(
    fileName,
    readFileSync(fileName).toString(),
    ts.ScriptTarget.ES2015,
    /*setParentNodes */ true,
  );

  // report it
  report(sourceFile);
});

/**
 Output:

[
  {
    "class": "AppController",
    "baseurl": "this_is_the_url",
    "methods": [
      {
        "name": "Get",
        "args": []
      }
    ],
    "method_name": "this_is_the_method"
  }
]
 */
Related