wormaworma

自定义模板

使用 Handlebars 创建自定义代码生成模板

当预定义模板不满足需求时,创建完全自定义的 Handlebars 模板。

基本用法

自定义模板通过插件的 getTemplate hook 返回模板路径。也可以在 plugins 中使用直接返回模板路径的插件函数:

import { defineConfig } from "wormajs";
import { functional } from "wormajs/plugin";

export default defineConfig({
  generator: [
    {
      plugins: [
        functional({
          path: "./my-template",
          config: { customOption: "value" },
        }),
      ],
    },
  ],
});

目录结构与文件规则

基本结构

模板目录中的 .handlebars 文件(除 partials/ 外)都作为目标模板渲染,生成代码的文件结构与模板保持一致。

{tag} 动态文件名

在文件名中使用 {tag} 占位符,生成时按 OpenAPI 的 tag 遍历生成多个文件:

my-template/
├── index.ts.handlebars       # → index.ts
└── {tag}.ts.handlebars       # → user.ts, article.ts, order.ts ...

{tag} 模板中可访问 currentTag 对象(类型为 ApiDoc),通过 currentTag.tagName 访问当前 tag 名称,通过 currentTag.apis 访问当前 tag 下的接口列表。

{api} 动态文件名

文件名可指定为 {api}(例如 {api}.ts),生成时遍历 allApis 数组生成多个文件,将 {api} 替换为对应接口名称。在 {api} 模板中可访问 currentApi 对象(类型为 Api)。

my-template/
├── index.ts.handlebars       # → index.ts
└── {api}.ts.handlebars       # → getUser.ts, createUser.ts, deleteUser.ts ...

模板数据中 currentTagcurrentApi 仅在对应动态文件名的上下文中有效。

# 不覆盖前缀

文件名前加 #,表示首次生成后不再覆盖:

my-template/
├── #index.ts.handlebars      # 首次创建,后续不覆盖(生成后命名为 index.ts)
└── index.ts.handlebars       # 每次都覆盖

模块类型区分

按模块类型分子目录,适配不同项目配置:

my-template/
├── typescript/               # 生成 .ts 文件
│   ├── index.ts.handlebars
│   └── types.ts.handlebars
├── module/                   # 生成 .mjs 文件
│   ├── index.mjs.handlebars
│   └── types.d.ts.handlebars
└── common/                   # 生成 .cjs 文件
    ├── index.cjs.handlebars
    └── types.d.cts.handlebars

TemplateData.type 取值:

  • typescript — 使用 typescript/ 目录
  • module — 使用 module/ 目录
  • commonjs — 使用 common/ 目录
  • auto — 自动检测

模板可以只提供部分类型目录。缺少的类型会抛出明确错误:

Template "ky" does not support module type "commonjs". Supported types: typescript, module

不区分模块类型时,模板路径下的所有 handlebars 文件都作为目标模板渲染。

Handlebars 语法

{{! 注释 }}

{{! 变量输出 }}
{{keyName}}
{{{unescapedHtml}}}

{{! 条件 }}
{{#if pathParameters}} ... {{/if}}
{{#or pathParameters queryParameters}} ... {{/or}}

{{! 循环 }}
{{#each tagedApis}}
  {{#apis}}
    export const
    {{{name}}}
    = () => {};
  {{/apis}}
{{/each}}

{{! 比较 }}
{{#if (eq type "typescript")}} ... {{/if}}

预定义 Helper

worma 在模板渲染前自动注册了一批常用 Handlebars helper,可在模板中直接使用:

Helper签名说明
isType(value, type, options)判断 value 是否为指定类型(如 'array''object'),配合 {{#isType}} 块级使用
and(...args)所有参数均为 truthy 时返回块内容,配合 {{#and}} 使用
or(...args)任一参数为 truthy 时返回块内容,配合 {{#or}} 使用
eq(a, b)判断 a === b,配合 {{#if (eq a b)}} 使用
not(a, b)判断 a !== b,配合 {{#if (not a b)}} 使用
join(...args)拼接所有参数为字符串
raw(text)输出原始 HTML,不转义特殊字符
stripStarPrefix(text)去除每行开头的 * 前缀,常用于清理 JSDoc 注释

示例:

{{! 类型判断 }}
{{#isType pathParameters "object"}}
  // 路径参数是对象类型
{{else}}
  // 路径参数是其他类型
{{/isType}}

{{! 多条件组合 }}
{{#and hasQueryParams (eq method "GET")}}
  // 有查询参数且为 GET 请求
{{/and}}

{{! 去除 * 前缀 }}
{{{stripStarPrefix queryParametersComment}}}

也可以通过插件的 onHandlebarsCreated hook 注册额外的自定义 helper:

{
  name: 'my-helpers',
  onHandlebarsCreated({ hbs }) {
    hbs.registerHelper('uppercase', (str) => str.toUpperCase());
  },
}

模板数据结构

模板渲染时可以访问以下数据。TemplateData 继承自 OpenAPI 文档标准结构,并附加了 worma 自动生成的辅助字段。

顶层字段

字段类型说明
openapistringOpenAPI 规范版本号
baseUrlstringAPI 基础 URL,从 OpenAPI 的 servers[0].url 提取
typestring模块类型:'typescript' | 'module' | 'commonjs'
titlestringOpenAPI 文档的 info.title
versionstringOpenAPI 文档的 info.version
descriptionstringOpenAPI 文档的 info.description
contactobjectOpenAPI 文档的 info.contact
frameworkstring框架标记:vue | react | svelte | solid-js | nuxt
defaultKeyboolean是否为默认配置
componentsstring[]Schema/Component 定义
componentNamesstring[]所有生成的 component schema 名称
allApisApi[]所有 API 的扁平数组,按 tag 分组前的原始顺序
tagedApisApiDoc[]按 OpenAPI tag 分组的 API 列表
configRecord<string, any>由模板配置传入的自定义参数

继承自 OpenAPI 的字段

TemplateData 展开了整个 OpenAPI 文档对象,因此模板中也可直接访问:

字段说明
openapiOpenAPI 规范版本号
info.title文档标题
info.version文档版本
info.description文档描述
info.contact联系人信息
servers服务器列表
pathsAPI 路径定义
components组件定义(schemas、responses 等)
tags标签列表

生成的 API 分组数据

tagedApis: ApiDoc[]

按 OpenAPI tag 分组的 API 列表。每个 ApiDoc 结构如下:

interface ApiDoc {
  /** 该分组的 tag 名称(原 tag 字段更名为 tagName) */
  tagName: string;
  /** 该 tag 下的所有 API */
  apis: Api[];
}

interface Api {
  /** 所属 tag */
  tag: string;
  /** 函数名(operationId) */
  name: string;
  /** HTTP 方法(大写),如 GET、POST */
  method: string;
  /** API 摘要 / 描述 */
  summary: string;
  /** URL 路径 */
  path: string;
  /** 路径参数类型字符串 */
  pathParameters: string;
  /** 查询参数类型字符串 */
  queryParameters: string;
  /** 路径参数 JSDoc 注释(可选) */
  pathParametersComment?: string;
  /** 查询参数 JSDoc 注释(可选) */
  queryParametersComment?: string;
  /** 响应体 JSDoc 注释(可选) */
  responseComment?: string;
  /** 请求体 JSDoc 注释(可选) */
  requestBodyComment?: string;
  /** 响应体 TypeScript 类型字符串 */
  response: string;
  /** 请求体 TypeScript 类型字符串(可选) */
  requestBody?: string;
  /** API 调用代码示例 */
  callingCode?: string;
}

模板中遍历写法示例:

{{#each tagedApis}}
  // tag:
  {{tagName}}
  {{#each apis}}
    export function
    {{{name}}}(params:
    {{{pathParameters}}}) { return request('{{method}}', '{{path}}'); }
  {{/each}}
{{/each}}

allApis: Api[]

所有 API 的扁平数组,按 tag 分组前的原始顺序排列。

Partials

模板根目录下 partials/ 文件夹自动注册为 partial,通过文件名引入:

my-template/
├── partials/
│   └── comment.handlebars     # {{> comment}}
├── index.ts.handlebars
└── {tag}.ts.handlebars
{{> comment}}  <!-- 引入 partials/comment.handlebars -->

完整示例

my-template/
├── partials/
│   └── license.handlebars
├── typescript/
│   ├── index.ts.handlebars
│   ├── {tag}.ts.handlebars
│   └── #config.ts.handlebars
└── module/
    ├── index.mjs.handlebars
    └── {tag}.mjs.handlebars

On this page