Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ jobs:
with:
name: npm-release
path: ${{ runner.temp }}/npm-release/
# Each of the five npm packages must trust yourtion/node-erest/release.yml.
# Each of the six npm packages must trust yourtion/node-erest/release.yml.
# No GitHub environment is used; leave that npm field blank.
- name: Publish validated tarballs, then complete GitHub Release
env:
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,13 @@ jobs:
run: pnpm run build

- name: Build adapter packages
# 子包 (@erest/express|koa|leizmweb|gen) 的 dist/ 已 gitignore,
# 子包 (@erest/express|koa|leizmweb|h3|gen) 的 dist/ 已 gitignore,
# examples 测试通过 workspace 链接 import 它们,必须先构建才能解析入口。
run: pnpm run build:packages

- name: Check H3 adapter and handler types
run: pnpm run test:h3-types

- name: Run tests with coverage
run: pnpm run test:cov

Expand Down
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ packages/ # v3.0:框架适配器实现独立子包(npm scope @er
├── erest-express/ # @erest/express — ExpressAdapter(Express 4/5)
├── erest-koa/ # @erest/koa — KoaAdapter(Koa 3)
├── erest-leizmweb/ # @erest/leizmweb — LeizmWebAdapter(@leizm/web 2)
├── erest-h3/ # @erest/h3 — H3Adapter(H3 2)
└── erest-gen/ # @erest/gen — codegen CLI(命令名 erest-gen)
```

Expand All @@ -42,7 +43,7 @@ packages/ # v3.0:框架适配器实现独立子包(npm scope @er
|-----------|--------|
| 参数校验逻辑 | `params.ts` 的 `compileValidate`(bind 阶段预编译) |
| 加新框架适配 | 新建子包 `packages/erest-<fw>/src/index.ts`,实现 `FrameworkAdapter` 接口(见 `adapters/types.ts`) |
| 改某框架适配实现 | `packages/erest-{express,koa,leizmweb}/src/index.ts`(`makeParamsChecker`/`bindRoute`/`createGroupRouter`/`attachGroupRouter`) |
| 改某框架适配实现 | `packages/erest-{express,koa,leizmweb,h3}/src/index.ts`(`makeParamsChecker`/`bindRoute`/`createGroupRouter`/`attachGroupRouter`) |
| 改文档生成 | `plugin/<format>/` + `plugin/zod-meta.ts`(统一 Zod 提取) |
| 加生命周期 hook | `hooks.ts` 定义 + 各子包 `src/index.ts` 的 dispatch/checker 注入 |
| 改类型推导 | `api.ts` 的 `registerTyped` 泛型层 |
Expand All @@ -60,7 +61,7 @@ packages/ # v3.0:框架适配器实现独立子包(npm scope @er
5. **breaking change 登记 MIGRATION.md** —— 公开 API 变动同步更新迁移指南。
6. **`_def` 读取集中** —— Zod 内部结构(`_def.type`/`_def.typeName`/`_def.shape`)只在 `plugin/zod-meta.ts` 读取,且必须通过 `getZodTypeName`/`getZodShape`/`getZodInner`/`extractDocFields` 工具函数(双轨兼容 Zod 3 与 Zod 4)。其他文件禁止直接读 `_def`。
7. **改了就 bump 版本** —— 任何涉及公开 API(接口/类型/行为/依赖声明)的改动,**必须同步 bump 版本号**,不能只改代码不留版本痕迹:
- 根包 `erest` 与受影响的子包(`packages/erest-{express,koa,leizmweb,gen}/`)的 `version` 字段一起改,保持一致(`pnpm publish -r` 整体发布,版本不齐会乱)。
- 根包 `erest` 与受影响的子包(`packages/erest-{express,koa,leizmweb,h3,gen}/`)的 `version` 字段一起改,保持一致(`pnpm publish -r` 整体发布,版本不齐会乱)。
- 语义化版本:bug 修复/文档/非 breaking 新增 → patch(x.y.Z);新增向后兼容能力 → minor(x.Y.0);breaking → major(X.0.0)。
- 同步在 `MIGRATION.md` 加对应版本章节(即使非 breaking 也要登记新增/修复项)。
- 默认通过 `.github/workflows/release.yml` 自动发版:版本 PR 合入 `main` 且 CI 通过后,维护者推送匹配的 `vX.Y.Z` tag。每个 npm 包须先绑定 Trusted Publisher,详见 `RELEASING.md`。
Expand Down
7 changes: 4 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## 环境与构建

建议使用 Node.js 24、pnpm 9。仓库是 pnpm workspace,核心包和四个公开子包一起构建。
建议使用 Node.js 24、pnpm 9。仓库是 pnpm workspace,核心包和五个公开子包一起构建。

```bash
pnpm install --frozen-lockfile
Expand All @@ -13,7 +13,7 @@ pnpm run build:packages
```

源码修改位置、Zod 内部结构访问和版本约定见 [AGENTS.md](AGENTS.md)。
涉及公开 API/行为/依赖的修改,同步更新五个包的版本和 [MIGRATION.md](MIGRATION.md)。
涉及公开 API/行为/依赖的修改,同步更新六个包的版本和 [MIGRATION.md](MIGRATION.md)。

## 常用检查

Expand All @@ -25,6 +25,7 @@ pnpm run build:packages
| `pnpm run test:contract` | OpenAPI、SDK、运行时的合约专项 |
| `pnpm run test:cov` | 源码测试与覆盖率门槛 |
| `pnpm test` | 构建核心/子包,验证发布产物并运行示例测试 |
| `pnpm run test:h3-types` | H3 handler 的 Raw/State 与适配器兼容性类型检查 |
| `pnpm run test:release` | 发布工具的校验、模拟发布及失败重跑测试 |
| `pnpm run bench` | 性能基准 |

Expand All @@ -33,7 +34,7 @@ pnpm run build:packages

## 合约专项

`test:contract` 在 Express、Koa、LeizmWeb 下执行同一套 GET/POST/PUT/PATCH/DELETE 定义。
`test:contract` 在 Express、Koa、LeizmWeb、H3 下执行同一套 GET/POST/PUT/PATCH/DELETE 定义。
测试经公开入口生成 OpenAPI 3、Swagger 2 和完整 Axios ESM SDK,再发起真实 HTTP 请求:

- Swagger Parser 检查文档规范;独立 Ajv 校验实际参数与响应。
Expand Down
9 changes: 9 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@
按版本倒序列出升级动作与可见行为变化。使用说明见 `guides/`,开发检查和发布操作分别见
[贡献指南](CONTRIBUTING.md)与[发布流程](RELEASING.md)。

## v3.4.0 — H3 v2 适配器

- 新增独立包 `@erest/h3`,导出 `H3Adapter`、`h3Adapter`、`createERest` 和 `H3Raw`。
- 支持 H3 `^2.0.8`(Node.js >=20.19);不兼容 H3 v1 的 `createApp/createRouter` API。
- 复用标准中间件、Zod 分层校验、hooks、响应信封、文档和 SDK;`reply.raw` 推导为 `H3Event`。
- 支持 `router: new H3()` 及 forceGroup 下的 `router: H3` 分组挂载。
- 声明 body schema 时自动解析 JSON/URL-encoded,空 body 也会校验;未声明时保留原生请求流。
- 六个公开包统一为 `3.4.0`,新包纳入 workspace、构建与发布产物验证。

## v3.3.1 — SDK 位置参数兼容性与 3.3 系列首次发布

- 恢复已声明 params schema 的 SDK 位置参数顺序,避免字段声明顺序与路由顺序不同时,把既有调用的参数值对调。
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![Test](https://github.com/yourtion/node-erest/actions/workflows/test.yml/badge.svg)](https://github.com/yourtion/node-erest/actions/workflows/test.yml)
[![codecov](https://codecov.io/github/yourtion/node-erest/graph/badge.svg)](https://codecov.io/github/yourtion/node-erest)

用一份 Zod schema 定义和校验 API,为 Express、Koa、@leizm/web 生成文档与 Axios SDK。
用一份 Zod schema 定义和校验 API,为 Express、Koa、@leizm/web、H3 生成文档与 Axios SDK。
TypeScript handler 的参数类型由 schema 推导。

- **统一 API 定义**:支持分组、中间件、分层参数和响应校验。
Expand All @@ -24,6 +24,9 @@ npm install erest zod @erest/koa koa @koa/router@^13 koa-bodyparser

# @leizm/web 2
npm install erest zod @erest/leizmweb @leizm/web

# H3 2(需要 Node.js >=20.19)
npm install erest zod @erest/h3 h3@^2.0.8
```

## 快速开始
Expand Down Expand Up @@ -80,7 +83,7 @@ curl -X POST http://localhost:3000/users \
| 需要做什么 | 文档 |
| --- | --- |
| 定义 schema、handler、响应、分组与中间件 | [API 定义](guides/api.md) |
| 接入三个框架、配置路由前缀和 TypeScript | [框架与 ESM](guides/adapters.md) |
| 接入四个框架、配置路由前缀和 TypeScript | [框架与 ESM](guides/adapters.md) |
| 生成 OpenAPI、其他文档和 Axios SDK | [文档与 SDK](guides/documentation.md) |
| 测试业务 API、保存真实示例 | [API 测试](guides/testing.md) |
| 运行完整博客示例 | [示例工程](examples/README.md) |
Expand Down
18 changes: 10 additions & 8 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,31 @@

## 一次性 npm 配置

维护者须登录 npm,分别进入以下五个包的 Settings → Trusted Publisher,选择 GitHub Actions。
它们已经在 npm 上存在,无需新包引导流程。仅合入 workflow 不会替代 npm 后台绑定;本仓库无法从代码确认绑定状态。
维护者须登录 npm,分别进入以下六个包的 Settings → Trusted Publisher,选择 GitHub Actions。
六个包均已在 npm 上存在,`@erest/h3` 已首次手动发布 `3.3.0`,后续随 erest 统一发布 `3.4.0`。
Trusted Publisher 绑定由包维护者在 npm 完成;仅合入 workflow 不会替代 npm 后台配置。

| npm 包 | Organization or user | Repository | Workflow filename | Environment name |
| --- | --- | --- | --- | --- |
| `erest` | `yourtion` | `node-erest` | `release.yml` | 留空 |
| `@erest/express` | `yourtion` | `node-erest` | `release.yml` | 留空 |
| `@erest/koa` | `yourtion` | `node-erest` | `release.yml` | 留空 |
| `@erest/leizmweb` | `yourtion` | `node-erest` | `release.yml` | 留空 |
| `@erest/h3` | `yourtion` | `node-erest` | `release.yml` | 留空 |
| `@erest/gen` | `yourtion` | `node-erest` | `release.yml` | 留空 |

workflow 名称填写文件名 `release.yml`,不要填写显示名称 `Release` 或完整路径。如果 npm 界面
提供 Allowed actions,须允许直接 `npm publish`;本流程不使用 staged publishing。
本 workflow 没有 GitHub environment,所以 npm 的 Environment name 留空。将来添加 environment
或重命名 workflow 时,要同步修改每个包的绑定。

四个子包的 `repository.url` 与根包一致,且包含各自的 `repository.directory`。provenance
五个子包的 `repository.url` 与根包一致,且包含各自的 `repository.directory`。provenance
要求仓库 URL 与实际发布仓库匹配。不要用 token 掩盖 OIDC 配置错误,也不要将凭据写入仓库或日志。
官方说明:[Trusted publishers](https://docs.npmjs.com/trusted-publishers/)。

## 正式发版

1. 将根包和四个公开子包的版本统一更新,更新 `MIGRATION.md`,通过 PR 合入 `main`。
1. 将根包和五个公开子包的版本统一更新,更新 `MIGRATION.md`,通过 PR 合入 `main`。
2. 确认该提交的 CI 通过,且上一次发版已经完成。拉取 `main` 后创建并推送精确 tag:

```bash
Expand All @@ -41,7 +43,7 @@ workflow 名称填写文件名 `release.yml`,不要填写显示名称 `Release
git push origin "v${RELEASE_VERSION}"
```

`RELEASE_VERSION` 读取当前 manifest 版本;推送前核对五个包的版本一致。`pnpm run tag`
`RELEASE_VERSION` 读取当前 manifest 版本;推送前核对六个包的版本一致。`pnpm run tag`
只创建本地 tag,不执行 npm 发布。也可用 `git tag v<version>` 创建 tag。
3. 在 Actions → Release 查看结果。只有推送 tag 触发发布,普通 PR/main push 不发布。

Expand All @@ -60,15 +62,15 @@ tag 必须是严格的 `vX.Y.Z`,支持例如 `v3.3.0-rc.1` 的预发布,拒
- frozen-lockfile 安装、`check`、`test:release`;
- 显式执行 `prepublishOnly`(format、根包与子包构建、发布产物测试及示例测试)和覆盖率门槛;
- pnpm workspace-aware pack,检查每个真实 tarball 的 manifest、exports/main/types、CLI shebang/执行权限及内部依赖;
- 在临时消费项目安装五个 tarball,验证核心与三框架 adapter 的 ESM 导入以及 codegen CLI 入口;
- 在临时消费项目安装六个 tarball,验证核心与四框架 adapter 的 ESM 导入以及 codegen CLI 入口;
- 确认 lifecycle 未改动 tag 的受跟踪源码。

产物以 `npm-release` Actions artifact 保留 30 天,包含五个 `.tgz` 和记录 SHA-256 的
产物以 `npm-release` Actions artifact 保留 30 天,包含六个 `.tgz` 和记录 SHA-256 的
`release.json`。发布 job 重新校验全部产物的身份、内容哈希和内部依赖,使用 npm CLI 发布同一批
tarball;不重新 pack 或构建。pnpm 负责 workspace 协议转换,不能直接用 npm 在 workspace
目录打包。声明与实际文件必须一致;通配符 exports 需要先扩展验证器。

先发布依赖,再发布消费者;当前顺序为 `erest`、三个 adapter、`@erest/gen`。每个包先查询
先发布依赖,再发布消费者;当前顺序为 `erest`、四个 adapter、`@erest/gen`。每个包先查询
精确版本,已存在则跳过,只有结构化 npm `E404` 才视为缺失。认证、网络、限流、publish 错误均
终止流程。全部必需包完成后才创建或完成 GitHub Release,notes 由 GitHub 生成;tarball 保留在
Actions artifact,不额外附加 GitHub Release assets。
Expand Down
51 changes: 49 additions & 2 deletions guides/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
| `@erest/express` | `createERest` / `ExpressAdapter` | Express 4、5 |
| `@erest/koa` | `createERest` / `KoaAdapter` | Koa 3,`@koa/router` 13 或 `koa-router` 13 |
| `@erest/leizmweb` | `createERest` / `LeizmWebAdapter` | @leizm/web 2 |
| `@erest/h3` | `createERest` / `H3Adapter`(或 `h3Adapter`) | H3 ^2.0.8 |

每个工厂创建的实例都使用相同的 [API 定义方式](api.md),并推导该框架的 `ctx.reply.raw` 类型。
以下绑定片段假定 `api` 已创建并注册路由,通用错误格式器从 `erest` 导入。
Expand Down Expand Up @@ -82,6 +83,50 @@ app.server.listen(3000);

需要表单时,添加 `component.bodyParser.urlencoded({ extended: true })`。

## H3

以下是可运行的 H3 v2 示例(Node.js >=20.19),不需要额外 body parser:

```javascript
import { createServer } from 'node:http';
import { H3 } from 'h3';
import { toNodeHandler } from 'h3/node';
import { z, defaultErrorFormatter } from 'erest';
import { createERest, h3Adapter } from '@erest/h3';

const app = new H3({
onError(error, event) {
// H3 将原始 erest 错误保存在 cause 中。
const { status, body } = defaultErrorFormatter(error.cause instanceof Error ? error.cause : error);
event.res.status = status;
return body;
},
});
const api = createERest({ groups: { users: '用户' } });
api.api.post('/users').group('users').registerTyped(
{ body: z.object({ name: z.string().min(1) }) },
(req, ctx) => ctx.reply.status(201).json({ name: req.body.name }),
);
api.bind({ adapter: h3Adapter, router: app });
createServer(toNodeHandler(app)).listen(3000);
```

`H3Adapter` 和单例 `h3Adapter` 等价。只支持 H3 v2;v1 的 `createApp/createRouter` 不适用。
声明 body schema 时,适配器通过 H3 `readBody()` 解析 JSON 或 URL-encoded;缺少请求体按 `{}` 校验,
非法 JSON 进入错误处理/错误信封。未声明 body schema 时不读取请求流,可用 `ctx.reply.raw.req`
处理文本、multipart 或流。`onRequest` 在解析 body 前触发;校验后可从 `ctx.body` 读取原始解析值。

错误可通过 H3 应用的 `onError` 处理,或使用 erest 的 `setResponseEnvelopers({ error })`。
`reply.raw` 是 `H3Event`,例如 `ctx.reply.raw.res.headers.set('x-trace', '1')` 或 `setCookie(ctx.reply.raw, ...)`。
要从 `registerTyped` 返回原生 `Response`(下载、重定向、流)并跳过成功信封,先调用 `ctx.reply.markSent()`:

```typescript
api.api.get('/download').group('users').registerTyped({}, (_req, ctx) => {
ctx.reply.markSent();
return new Response('id\n1', { headers: { 'content-type': 'text/csv' } });
});
```

## 路由前缀与分组

`info.basePath` 用于文档和 SDK URL,不会替应用挂载路由。
Expand All @@ -106,7 +151,9 @@ const api = createERest({
api.bind({ adapter: new ExpressAdapter(), app, router: express.Router });
```

Koa 对应 `router: Router`,LeizmWeb 对应其 `Router` 构造函数;它们使用各自的适配器。
Koa 对应 `router: Router`,LeizmWeb 对应其 `Router` 构造函数。H3 使用
`api.bind({ adapter: h3Adapter, app, router: H3 })`,通过 `app.mount()` 挂载各分组。
非 forceGroup 下也可绑定到独立 H3 实例,再用 `app.mount('/api', router)` 挂载前缀。

## 原生上下文

Expand All @@ -117,7 +164,7 @@ Koa 对应 `router: Router`,LeizmWeb 对应其 `Router` 构造函数;它们
| 重定向 | `.res.redirect(...)` | `.redirect(...)` | `.response.redirect(...)` |
| 响应头 | `.res.setHeader(...)` | `.set(...)` | `.response.setHeader(...)` |

使用 raw 的代码遵循宿主框架的语义,不再通用于三个框架。响应头应在发送响应体之前设置。
使用 raw 的代码遵循宿主框架的语义,不再通用于所有框架。响应头应在发送响应体之前设置。
return 模式下手动发送响应,记得调用 [markSent()](api.md#原生响应)。

自定义适配器实现 `erest` 导出的 `FrameworkAdapter` 接口;实现约定见 [架构导航](../AGENTS.md)。
Expand Down
21 changes: 12 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "erest",
"version": "3.3.1",
"description": "Framework-agnostic REST API framework with Zod validation, auto docs & test scaffolding. Express/Koa/@leizm/web adapters as separate packages.",
"version": "3.4.0",
"description": "Framework-agnostic REST API framework with Zod validation, auto docs & test scaffolding. Express/Koa/@leizm/web/H3 adapters as separate packages.",
"type": "module",
"main": "dist/lib/index.js",
"types": "dist/lib/index.d.ts",
Expand All @@ -18,7 +18,7 @@
"scripts": {
"doc": "typedoc --excludeExternals --out docs/ src/lib/",
"dev": "export ISLIB=1 && vitest --watch",
"test": "npm run build && npm run build:packages && vitest run && pnpm --filter erest-example test",
"test": "npm run build && npm run build:packages && npm run test:h3-types && vitest run && pnpm --filter erest-example test",
"test:contract": "ISLIB=1 vitest run src/test/test-contract-consistency.ts src/test/test-openapi.ts",
"test:lib": "export ISLIB=1 && vitest run",
"test:cov": "export ISLIB=1 && vitest run --coverage",
Expand All @@ -29,14 +29,15 @@
"test:examples": "pnpm --filter erest-example test",
"bench": "export ISLIB=1 && vitest bench --config vitest.bench.config.ts",
"tag": "git tag v`node -p 'require(\"./package\").version'`",
"format": "oxfmt --write src examples/src examples/test",
"check": "oxlint src examples/src examples/test && oxfmt --check src examples/src examples/test",
"format": "oxfmt --write src packages/erest-h3/src examples/src examples/test",
"check": "oxlint src packages/erest-h3/src examples/src examples/test && oxfmt --check src packages/erest-h3/src examples/src examples/test",
"clean": "rm -rf dist",
"build": "npm run clean && tsc",
"build:packages": "pnpm --filter @erest/express --filter @erest/koa --filter @erest/leizmweb --filter @erest/gen build",
"build:packages": "pnpm --filter @erest/express --filter @erest/koa --filter @erest/leizmweb --filter @erest/h3 --filter @erest/gen build",
"publish:all": "pnpm publish -r --no-git-checks --access public",
"prepublishOnly": "npm run format && npm run test",
"prepare": "husky"
"prepare": "husky",
"test:h3-types": "tsc -p packages/erest-h3/tsconfig.test.json"
},
"repository": {
"type": "git",
Expand All @@ -52,7 +53,8 @@
"zod",
"typescript",
"swagger",
"postman"
"postman",
"h3"
],
"author": "Yourtion Guo <yourtion@gmail.com>",
"license": "MIT",
Expand Down Expand Up @@ -101,7 +103,8 @@
"typedoc": "^0.28.19",
"typescript": "^5.8.3",
"vitest": "^3.2.4",
"zod": "^4.4.3"
"zod": "^4.4.3",
"h3": "^2.0.8"
},
"lint-staged": {
"*.{ts,js}": [
Expand Down
2 changes: 1 addition & 1 deletion packages/erest-express/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@erest/express",
"version": "3.3.1",
"version": "3.4.0",
"repository": {
"type": "git",
"url": "git+https://github.com/yourtion/node-erest.git",
Expand Down
2 changes: 1 addition & 1 deletion packages/erest-gen/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@erest/gen",
"version": "3.3.1",
"version": "3.4.0",
"repository": {
"type": "git",
"url": "git+https://github.com/yourtion/node-erest.git",
Expand Down
Loading
Loading