---
url: /zh/reference/typescript/wow-generator/generated-output.md
description: 生成产物与重新生成 — @ahoo-wang/wow-generator
---

# 生成产物与重新生成

生成的是 TypeScript 源码，不是独立 HTTP 实现。装饰器类需启用 `experimentalDecorators: true` 编译；安装实际输出 import 的包。

## 产物类型

| 产物                      | 生成规则                                                                                               |
| ------------------------- | ------------------------------------------------------------------------------------------------------ |
| `types.ts`                | 按 schema 名称空间分组的组件模型类型/枚举及导入的 Wow 类型                                             |
| `*ApiClient.ts`           | 带 operationId 的普通 tagged 操作；操作全部 tag 都必须是可用 API tag；排除 wow/Actuator/Wow 聚合 tag   |
| `commandClient.ts`        | 已解析聚合的命令路径、请求体别名、普通及流式命令客户端                                                 |
| `queryClient.ts`          | 聚合 QueryClientFactory、状态/字段类型、领域事件联合（空集为 never）及事件标题枚举                     |
| `boundedContext.ts`       | 已解析上下文的别名常量                                                                                 |
| `index.ts`                | .ts 文件及非空子目录的递归导出                                                                         |
| `.wow-generator.json`     | 版本 1 所有权清单，记录生成 .ts 文件的 SHA-256；旧的 `.fetcher-generator.json` 会被读取一次并替换      |

## 源码约定

每个生成文件都以 `// Code generated by wow-generator. DO NOT EDIT.` 开头，使用单引号、两空格缩进和分号。相对导入以 `.js` 结尾，每个 barrel 以 `./dir/index.js` 重新导出，因此输出在 `"moduleResolution": "NodeNext"` 和 `"bundler"` 下都能编译。

导入在生成时显式写出，所以无论输出目录能否解析 `@ahoo-wang/*`，输出都逐字节相同。保存前生成器会检查每个文件都声明或导入了它用到的名字、且没有重复声明；不满足时不写入任何文件。这项检查不能替代消费方的编译器。

两个同级模块都导出的名字（例如两个包里同名的模型）不会进入它们共同的 `index.ts`，并给出警告；请从它自己的模块导入。

模型的文档注释默认只含摘要：标题、描述、schema 键、format、默认值、示例和约束。`--schema-docs full`（选项 `schemaDocs: 'full'`）还会嵌入完整的 JSON schema。

## 方法

方法名依次取：[配置](./configuration)中的 `apiClients[tag].methodNames[operationId]`、操作的 `x-fetcher-method` 扩展，否则取 operationId 按点号分隔的最后一段并转为 camelCase：

| operationId                  | 方法名           |
| ---------------------------- | ---------------- |
| `getUserById`                | `getUserById`    |
| `delete_user_by_id`          | `deleteUserById` |
| `getUser_1`                  | `getUser1`       |
| `users.list`                 | `list`           |
| `example.cart.add_cart_item` | `addCartItem`    |

方法名只取决于操作本身，所以新增操作不会让已有方法改名。同一客户端的两个操作得到相同方法名时，本次运行以退出码 4 失败，并点名这两个操作；用 `methodNames` 或 `x-fetcher-method` 给其中一个命名即可。

普通方法把每个 path、query、header 参数都生成为带类型的位置参数，以参数名命名（`item-id` → `itemId`），请求体则是单独的 `@body()` 参数。必填的在前，按文档顺序排列——path、query、header，然后是请求体——可选的在后，调用方不必为了传必填参数而先传 `undefined`。最后是 `httpRequest?: ParameterRequest`（用于请求可设置的其他内容：额外请求头、超时、signal）和 `attributes?: Record<string, unknown>`：

```ts
search(@path('item-id') itemId: string, @query('q') q: string,
       @header('X-Tenant') xTenant: string, @query('page') page?: number,
       @query('status') status?: 'open' | 'closed',
       @request() httpRequest?: ParameterRequest,
       @attribute() attributes?: Record<string, unknown>): Promise<Item[]>
```

| 参数或请求体                               | 生成的类型                                                                          |
| ------------------------------------------ | ----------------------------------------------------------------------------------- |
| path 参数                                  | 始终必填；`ignorePathParameters` 会省略其中一部分                                   |
| query 或 header 参数                       | 文档写 `required: true` 时必填；原始类型枚举生成为其字面量的联合                    |
| cookie 参数                                | 省略并给出警告：cookie 由浏览器发送，fetch 无法设置                                 |
| JSON 请求体（`application/json`、`+json`） | schema 的类型；schema 未要求的属性通过 `PartialBy<Item, 'id'>` 保持可选             |
| `multipart/form-data`                      | `FormData`                                                                          |
| `application/x-www-form-urlencoded`        | `URLSearchParams`                                                                   |
| `text/*`                                   | `string`                                                                            |
| 其他媒体类型                               | `BodyInit`                                                                          |

`requestBody.required` 为 true 时请求体必填。参数的描述会成为方法文档注释中的 `@param` 标签。路径级参数会继承，操作级参数按 in/name 覆盖。缺少 operationId 或 tag 的操作会被跳过并给出警告。

返回类型取自成功响应：`200`，否则取最小的其他 2xx，再否则取 `2XX`。任何 JSON 媒体类型（`application/json`、`+json`，带不带 charset 均可）按其 schema 推导，然后是 wildcard schema，再是 SSE。`text/*` 以及 wildcard 下的字符串 schema 返回 `Promise<string>`；识别 SSE 后返回 JsonServerSentEventStream，无法推导事件模型时回退到 any。无法推导正文时回退为 `Promise<Response>`，提取原生 Response。生成方法依赖装饰器在运行时替换 `throw autoGeneratedError(...)` 占位实现。

## 名字与 schema

* 名字会转为标识符：参数 `item-id` → `itemId`，schema `Page«User»` → `PageUser`，`1stThing` → `_1stThing`，命令 `pay-order` → `PAY_ORDER` 与 `payOrder`。描述中的 `*/` 会被转义。
* 两个 schema 归一化后生成同一个模型时以退出码 4 失败。归一化后相同的枚举值会得到不同的成员。归一化后指向同一客户端的 tag 会生成带编号的类（`User2ApiClient`），并给出警告。
* 指向不存在目标的 `$ref` 以退出码 4 失败，并列出这些引用。
* `{ nullable: true, allOf: [{ $ref }] }` 允许 `null`。带 discriminator 的 `oneOf` 按判别属性收窄各分支。以自身为值的 map 生成带索引签名的接口。名为 `Record` 或 `Response` 的模型以别名导入，不会遮蔽全局类型。
* 空的命令或事件体为 `Record<string, never>`。

Wow 查询 schema 映射为 `@ahoo-wang/wow-client` 的类型。带 `filter` 属性的 `wow.api.query.ListQuery`、`PagedQuery` schema（Wow 8.11 及以后）映射为根入口的 `FilterListQuery`、`FilterPagedQuery`；`wow.api.query.Condition`、`ConditionOptions`、`Operator`，以及不带 `filter` 的 `ListQuery`、`PagedQuery` schema（Wow 8.10）映射为 `@ahoo-wang/wow-client/legacy` 中已弃用的类型，该子路径在 v10 删除。

## 运行时配置

```bash
pnpm add @ahoo-wang/fetcher @ahoo-wang/fetcher-decorator @ahoo-wang/fetcher-eventstream @ahoo-wang/wow-client
pnpm exec wow-generator generate -i ./openapi.json -o ./src/generated -t ./tsconfig.json
pnpm exec tsc --noEmit -p ./tsconfig.json
```

通过 ApiMetadata 构造器创建生成的客户端，通常传 `{ fetcher }`。为目标服务配置 Fetcher.baseURL；生成过程不调用生成 API。

命令客户端，以及文档带 `x-wow-context-alias` 时的 API 客户端，会把构造器收到的 `apiMetadata` 合并到默认值之上，所以 `new CartCommandClient({ fetcher })` 保留限界上下文的基础路径，请求发往 `/example/...`。流式命令客户端继承同一个构造器。不经按上下文别名路由的网关、直接访问服务时，传 `basePath: ''`；查询客户端工厂对应传 `contextAlias: ''`：

```ts
import { Fetcher } from '@ahoo-wang/fetcher';
import { CartCommandClient, cartQueryClientFactory } from './generated/index.js';

const fetcher = new Fetcher({ baseURL: 'http://localhost:8080' });
const commands = new CartCommandClient({ fetcher, basePath: '' });
const snapshots = cartQueryClientFactory.createSnapshotQueryClient({
  fetcher,
  contextAlias: '',
});
```

查询客户端工厂的 `aggregateName` 是从聚合快照路由读出的路由段，例如以 `@AggregateRoute(resourceName = "sales-order")` 声明的聚合 `order` 为 `sales-order`。字段类型参数是 `` `${OrderAggregatedFields}` ``，即字段枚举的字符串值：枚举成员和对应字符串都能编译，拼错的字段不能。把查询标注为 `ListQuery` 或 `FilterListQuery`（字段默认为 `string`）的代码需要写明字段类型：``ListQuery<`${CartAggregatedFields}`>``。参见[声明式端点](https://fetcher.ahoo.me/zh/reference/decorator/services-and-endpoints)。

## 所有权与失败

再次生成到相同路径的文件会被替换：手写定制应放在生成文件之外。陈旧文件仅在旧清单记录且内容 hash 未改变时删除。已修改的陈旧文件及无关文件会保留；保留不代表它属于当前生成 API。重建 index 仍可能包含项目中已有的源文件。

清单无效或生成路径逃出输出根目录会抛错。保存完成后才删除陈旧文件并写新清单，但整个目录不是原子事务：失败可能留下部分写入。不要通过删清单强制清理；使用专用输出目录并审查再生成差异。生成器自身的名字检查不能证明输出在你的依赖下能通过类型检查，必须执行消费者编译器。

## 实现源码

[typescript/wow-generator/src/utils/sourceFiles.ts](https://github.com/Ahoo-Wang/Wow/blob/main/typescript/wow-generator/src/utils/sourceFiles.ts)

[typescript/wow-generator/src/client/apiClientGenerator.ts](https://github.com/Ahoo-Wang/Wow/blob/main/typescript/wow-generator/src/client/apiClientGenerator.ts)

[typescript/wow-generator/src/client/queryClientGenerator.ts](https://github.com/Ahoo-Wang/Wow/blob/main/typescript/wow-generator/src/client/queryClientGenerator.ts)

[typescript/wow-generator/src/model/modelGenerator.ts](https://github.com/Ahoo-Wang/Wow/blob/main/typescript/wow-generator/src/model/modelGenerator.ts)
