くらげになりたい。

くらげのようにふわふわ生きたい日曜プログラマなブログ。趣味の備忘録です。

tsoaのテンプレートをカスタマイズしてみる

tsoaNitro/H3に対応したいと思って、
routeファイルのテンプレートをカスタマイズする方法について調べたときの備忘録(*´ω`*)

ドキュメントにちょっと書かれているけど、詳細はほぼ無い。。

{
  // ...
  "routes": {
    "routesDir": "...",
    "middleware": "express",
    "middlewareTemplate": "custom-template.ts"
    ...
  }
}

tsoaのテンプレートの仕組み

expressのテンプレート自体はこれっぽい。

Handlebarsで書かれているよう。

デフォルトのRouteGeneratorはこれっぽい。

コンストラクタを見ると、tsoa.jsonroutes.middlewareroutes.middlewareTemplateで、
テンプレートファイル(.hbs)を設定しているよう。

tsoa routesで呼ばれる本体は、これっぽい。

ここでRouteGeneratorを取得して、実行している感じ。

// packages/cli/src/module/generate-routes.ts
const routeGenerator = await getRouteGenerator(metadata, routesConfig);

await fsMkDir(routesConfig.routesDir, { recursive: true });
await routeGenerator.GenerateCustomRoutes();

defaultRouteGenerator.tshandlebarsを呼び出しているところはこんな感じ。

// packages/cli/src/routeGeneration/defaultRouteGenerator.ts
public buildContent(middlewareTemplate: string) {
  handlebars.registerHelper('json', (context: any) => {
    return JSON.stringify(context);
  });
  const additionalPropsHelper = (additionalProperties: TsoaRoute.RefObjectModelSchema['additionalProperties']) => {
    if (additionalProperties) {
      // Then the model for this type explicitly allows additional properties and thus we should assign that
      return JSON.stringify(additionalProperties);
    } else if (this.options.noImplicitAdditionalProperties === 'silently-remove-extras') {
      return JSON.stringify(false);
    } else if (this.options.noImplicitAdditionalProperties === 'throw-on-extras') {
      return JSON.stringify(false);
    } else if (this.options.noImplicitAdditionalProperties === 'ignore') {
      return JSON.stringify(true);
    } else {
      return assertNever(this.options.noImplicitAdditionalProperties);
    }
  };
  handlebars.registerHelper('additionalPropsHelper', additionalPropsHelper);

  const routesTemplate = handlebars.compile(middlewareTemplate, { noEscape: true });

  return routesTemplate(this.buildContext());
}

handlebarsで使われるContextはAbstractRouteGenerator.buildContext()で生成されているよう。

protected buildContext() {
    // ...
    return {
      authenticationModule,
      basePath: normalisedBasePath,
      canImportByAlias,
      controllers: this.metadata.controllers.map(controller => {
        return {
          actions: controller.methods.map(method => {
            // ... 
            return {
              fullPath: normalisedFullPath,
              method: method.method.toLowerCase(),
              name: method.name,
              parameters: parameterObjs,
              path: normalisedMethodPath,
              uploadFile: !!uploadFileParameter,
              uploadFileName: uploadFileParameter?.name,
              uploadFiles: !!uploadFilesParameter,
              uploadFilesName: uploadFilesParameter?.name,
              security: method.security,
              successStatus: method.successStatus ? method.successStatus : 'undefined',
            };
          }),
          modulePath: this.getRelativeImportPath(controller.location),
          name: controller.name,
          path: normalisedControllerPath,
        };
      }),
      environment: process.env,
      iocModule,
      minimalSwaggerConfig: { noImplicitAdditionalProperties: this.options.noImplicitAdditionalProperties },
      models: this.buildModels(),
      useFileUploads: //...
      multerOpts: this.options.multerOpts,
      useSecurity: this.metadata.controllers.some(controller => controller.methods.some(method => !!method.security.length)),
      esm: this.options.esm,
    };
  }

かなり大きいけど、以下あたりがメインどこぽい?

  • controllers / models
  • authenticationModule / useSecurity
  • useFileUploads / multerOpts

カスタムテンプレートのHello world

まずは、コンテキストを全部表示するサンプル。

こんな感じのテンプレートを

{{!-- ./context-all.hbs --}}
{{json @root}}

利用するように設定を変更して、

// tsoa.json
{
  // ...
  "routes": {
    "middlewareTemplate": "./context-all.hbs"
    ...
  }
}

pnpm tsoa routesを実行すると、
こんな感じのJSONが出力される。

{
  "basePath": "",
  "canImportByAlias": true,
  "controllers": [
    {
      "actions": [
        {
          "fullPath": "/auth/me",
          "method": "post",
          "name": "me",
          "parameters": {
            "event": {
              "in": "request",
              "name": "event",
              "required": true,
              "dataType": "object"
            }
          },
          "path": "/me",
          "uploadFile": false,
          "uploadFiles": false,
          "security": [
            {
              "jwt": []
            }
          ],
          "successStatus": "undefined"
        }
      ],
      "modulePath": "./../../controllers/1.authController",
      "name": "AuthController",
      "path": "/auth"
    },
    // ...
  "models": {
    "PagingParams": {
      "dataType": "refObject",
      "properties": {
        "page": {
          "dataType": "integer"
        },
        "pageSize": {
          "dataType": "integer"
        }
      },
      "additionalProperties": false
    },
    // ...
  },
  "useFileUploads": false,
  "useSecurity": true
}

あとは、これを参照しながらテンプレートを組み立てていけばよさそう!


とりあえず、カスタマイズ方法がわかった気がする(*´ω`*)
これでnitroでもtsoa使えそうかも(*´ω`*)