Skip to content

Instantly share code, notes, and snippets.

@mp035
Last active January 9, 2025 02:34

Revisions

  1. mp035 revised this gist Jan 9, 2025. 1 changed file with 7 additions and 0 deletions.
    7 changes: 7 additions & 0 deletions api-routes-module.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,7 @@
    import { Module } from '@nestjs/common';
    import { ApiRoutesCommand } from './api-routes.command';

    @Module({
    providers: [ApiRoutesCommand],
    })
    export class ApiRoutesModule {}
  2. mp035 revised this gist Jan 9, 2025. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion api-routes.command.ts
    Original file line number Diff line number Diff line change
    @@ -148,7 +148,7 @@ type ApiRoute = {
    }

    // find and overwrite the src/lib/api-routes.js file
    Throw new Error("You have to adjust the output path below to ensure the route list is generated where you expect.");
    throw new Error("You have to adjust the output path below to ensure the route list is generated where you expect.");
    const apiRoutesPath = path.join(
    __dirname,
    '../../../app/src/lib/api-routes.ts',
  3. mp035 revised this gist Jan 9, 2025. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions api-routes.command.ts
    Original file line number Diff line number Diff line change
    @@ -148,6 +148,7 @@ type ApiRoute = {
    }

    // find and overwrite the src/lib/api-routes.js file
    Throw new Error("You have to adjust the output path below to ensure the route list is generated where you expect.");
    const apiRoutesPath = path.join(
    __dirname,
    '../../../app/src/lib/api-routes.ts',
  4. mp035 created this gist Jan 9, 2025.
    160 changes: 160 additions & 0 deletions api-routes.command.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,160 @@
    import { Injectable } from '@nestjs/common';
    import { Command } from 'nestjs-command';
    import * as fs from 'fs';
    import * as glob from 'glob';
    import * as path from 'path';
    import * as ts from 'typescript';

    @Injectable()
    export class ApiRoutesCommand {
    @Command({
    command: 'api-routes:generate',
    describe: 'Generate a list of API routes for the frontend',
    })
    async generateApiRoutes() {
    // grab each controller file from ../server/src/**/*.controller.ts
    const fileList = glob.sync(path.join(__dirname, '../**/*.controller.ts'));

    function findNodesByType(node, type) {
    const nodes = [];
    function findNodes(node) {
    if (node.kind === type) {
    nodes.push(node);
    }
    ts.forEachChild(node, findNodes);
    }
    findNodes(node);
    return nodes;
    }

    const apiRoutes = {};

    // iterate over the file list
    for (const file of fileList) {
    // parse the file
    const program = ts.createProgram([file], { allowJs: true });
    const source = program.getSourceFile(file);
    const classes = findNodesByType(source, ts.SyntaxKind.ClassDeclaration);
    for (const classObj of classes) {
    // check if the class has a 'Controller' decorator, if not, continue, otherwise, get the first parameter as the route
    const classDecoratorNodes = findNodesByType(
    classObj,
    ts.SyntaxKind.Decorator,
    );
    const controllerDecorator = classDecoratorNodes.find(
    (decorator) =>
    decorator.expression.expression.escapedText === 'Controller',
    );

    if (!controllerDecorator) {
    continue;
    }

    const controllerRoute = controllerDecorator.expression.arguments.length
    ? controllerDecorator.expression.arguments[0].text
    : '';

    const className = classObj.name.escapedText;
    apiRoutes[className] = {};
    const apiController = apiRoutes[className];

    // find the public methods of the class with decorators
    const methods = findNodesByType(
    classObj,
    ts.SyntaxKind.MethodDeclaration,
    );
    for (const methodNode of methods) {
    const methodName = methodNode.name.escapedText;
    // find the decorators
    const methodDecoratorNodes = findNodesByType(
    methodNode,
    ts.SyntaxKind.Decorator,
    );
    const methodDecorators = [];
    for (const decorator of methodDecoratorNodes) {
    // get the decorator name and first parameter (if any)
    methodDecorators.push({
    name: decorator.expression.expression.escapedText,
    value: decorator.expression.arguments.length
    ? decorator.expression.arguments[0].text
    : '',
    });
    }

    // if the decorator names do not contain at least one of 'Get', 'Post', 'Put', 'Delete', 'Patch', 'Options', 'Head' or 'All' then continue to the next method
    const validDecorators = [
    'Get',
    'Post',
    'Put',
    'Delete',
    'Patch',
    'Options',
    'Head',
    'All',
    ];
    const routeDecorator = methodDecorators.find((decorator) =>
    validDecorators.includes(decorator.name),
    );
    if (!routeDecorator) {
    continue;
    }

    // if the method has a decorator, convert it to a http VERB
    const method = routeDecorator.name.toUpperCase();
    const path = routeDecorator.value;

    /*
    console.log(
    `${className}.${methodName} : ${method} ${controllerRoute}/${path}`,
    );
    */
    process.stdout.write('#');

    apiController[methodName] = {
    method,
    path: path ? `${controllerRoute}/${path}` : controllerRoute,
    };
    }
    }
    }

    //generate the source code for the api-routes file
    let sourceFileContent = `//THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
    //To update this file, run 'npx nestjs-command api-routes:generate'
    //from the server project directory.
    type ApiRoute = {
    method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
    path: string;
    };
    `;

    for (const controller in apiRoutes) {
    sourceFileContent += `class ${controller}Class {
    `;
    for (const method in apiRoutes[controller]) {
    sourceFileContent += `public ${method}: ApiRoute = ${JSON.stringify(
    apiRoutes[controller][method],
    )};
    `;
    }
    sourceFileContent += `}
    `;
    }

    for (const controller in apiRoutes) {
    sourceFileContent += `export const ${controller} = new ${controller}Class();
    `;
    }

    // find and overwrite the src/lib/api-routes.js file
    const apiRoutesPath = path.join(
    __dirname,
    '../../../app/src/lib/api-routes.ts',
    );

    fs.writeFileSync(apiRoutesPath, sourceFileContent);
    console.log('');
    console.log(`apiRoutes written to ${apiRoutesPath}`);
    }
    }