# 自定义模板 (/docs/template-system/custom-templates)



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

如果不想从零手写模板，可以安装 worma Skill，让 AI 编码助手基于本页文档帮你生成自定义 worma 模板：

```bash
npx skills add alovajs/skills --skill worma-guidelines
```

然后在 AI 对话中输入提示词，例如：

```text
创建自定义 worma 模板，我的需求是：xxx
```

## 基本用法 [#基本用法]

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

```javascript
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-动态文件名]

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

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

在 `{tag}` 模板中可访问 `currentTag` 对象（类型为 [`ApiDoc`](#tagedApis)），通过 `currentTag.tagName` 访问当前 tag 名称，通过 `currentTag.apis` 访问当前 tag 下的接口列表。

### `{api}` 动态文件名 [#api-动态文件名]

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

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

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

### `#` 不覆盖前缀 [#-不覆盖前缀]

文件名前加 `#`，表示首次生成后不再覆盖：

```text
my-template/
├── #index.ts.handlebars      # 首次创建，后续不覆盖（生成后命名为 index.ts）
└── index.ts.handlebars       # 每次都覆盖
```

### 模块类型区分 [#模块类型区分]

按模块类型分子目录，适配不同项目配置：

```text
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` — 自动检测

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

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

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

## Handlebars 语法 [#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 [#预定义-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 注释                                   |

示例：

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

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

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

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

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

## 模板数据结构 [#模板数据结构]

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

### 顶层字段 [#顶层字段]

| 字段               | 类型                       | 说明                                                        |
| ---------------- | ------------------------ | --------------------------------------------------------- |
| `openapi`        | `string`                 | OpenAPI 规范版本号                                             |
| `baseUrl`        | `string`                 | API 基础 URL，从 OpenAPI 的 `servers[0].url` 提取                |
| `type`           | `string`                 | 模块类型：`'typescript'` \| `'module'` \| `'commonjs'`         |
| `title`          | `string`                 | OpenAPI 文档的 `info.title`                                  |
| `version`        | `string`                 | OpenAPI 文档的 `info.version`                                |
| `description`    | `string`                 | OpenAPI 文档的 `info.description`                            |
| `contact`        | `object`                 | OpenAPI 文档的 `info.contact`                                |
| `framework`      | `string`                 | 框架标记：`vue` \| `react` \| `svelte` \| `solid-js` \| `nuxt` |
| `defaultKey`     | `boolean`                | 是否为默认配置                                                   |
| `components`     | `string[]`               | Schema/Component 定义                                       |
| `componentNames` | `string[]`               | 所有生成的 component schema 名称                                 |
| `allApis`        | [`Api[]`](#allApis)      | 所有 API 的扁平数组，按 tag 分组前的原始顺序                               |
| `tagedApis`      | [`ApiDoc[]`](#tagedApis) | 按 OpenAPI tag 分组的 API 列表                                  |
| `config`         | `Record<string, any>`    | 由模板配置传入的自定义参数                                             |

### 继承自 OpenAPI 的字段 [#继承自-openapi-的字段]

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

| 字段                 | 说明                        |
| ------------------ | ------------------------- |
| `openapi`          | OpenAPI 规范版本号             |
| `info.title`       | 文档标题                      |
| `info.version`     | 文档版本                      |
| `info.description` | 文档描述                      |
| `info.contact`     | 联系人信息                     |
| `servers`          | 服务器列表                     |
| `paths`            | API 路径定义                  |
| `components`       | 组件定义（schemas、responses 等） |
| `tags`             | 标签列表                      |

### 生成的 API 分组数据 [#生成的-api-分组数据]

#### `tagedApis: ApiDoc[]` [#tagedApis]

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

```typescript
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;
}
```

模板中遍历写法示例：

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

#### `allApis: Api[]` [#allApis]

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

## Partials [#partials]

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

```text
my-template/
├── partials/
│   └── comment.handlebars     # {{> comment}}
├── index.ts.handlebars
└── {tag}.ts.handlebars
```

```handlebars
{{> comment}}  <!-- 引入 partials/comment.handlebars -->
```

## 完整示例 [#完整示例]

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