ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Etherpad Admin 前端 API 类型生成:`@etherpad/openapi-codegen` 构建期 TypeScript 锁定方案解析

Etherpad Admin 前端 API 类型生成:`@etherpad/openapi-codegen` 构建期 TypeScript 锁定方案解析 后端协同办公WebSocket前端富文本【免费下载链接】etherpadEtherpad: A modern really-real-time collaborative document editor.项目地址https://gitcode.com/gh_mirrors/et/etherpad点击查看免费下载导读Etherpad 的管理后台admin/基于 React Vite通过openapi-typescript从服务端 OpenAPI 规范生成类型安全的 API 客户端。但openapi-typescript依赖 TypeScript编译器 API生成代码而 TypeScript 7原生移植版已不暴露该 API导致代码生成直接崩溃。本文基于仓库中的 admin/tools/openapi-codegen/README.md深入解析 Etherpad 如何通过一个私有的构建期包etherpad/openapi-codegen将openapi-typescript所用的 TypeScript 锁定在 6.x从而绕开该兼容性问题并完整梳理从 OpenAPI 规范导出、合并、生成schema.d.ts到前端类型客户端消费的整条调用链。一、背景为什么 Admin 前端需要“从 OpenAPI 生成类型”Etherpad 仓库的admin/目录是一个独立的 Vite React 前端工程见 admin/package.json它依赖openapi-fetch类型化 HTTP 客户端与openapi-react-query基于 TanStack Query 的 hooks二者都要求一个由 OpenAPI 规范生成的 TypeScript 类型文件作为泛型来源。从源码结构看对应 issue #7638 的类型安全 API 工作类型生成的完整链路是服务端在运行时构建 OpenAPI 文档公共 API 由 src/node/hooks/express/openapi.ts 的generateDefinitionForVersion(version, style)生成管理端 API 由 src/node/hooks/express/openapi-admin.ts 的generateAdminDefinition()生成构建脚本把两份文档合并成一份交给openapi-typescript输出admin/src/api/schema.d.ts前端代码import type { paths } from ./schema从而获得全程类型检查的 API 调用体验。其中第 2 步正是etherpad/openapi-codegen存在的核心场景。二、问题根源openapi-typescript依赖 TypeScript 编译器 APIopenapi-typescript生成输出的方式不是简单地拼接字符串而是调用 TypeScript 的编译器 APIts.factory、ts.SyntaxKind、printer来程序化地构建 AST 并打印为.d.ts文件。问题出在 TypeScript 7它是 TypeScript 的原生native移植版本其主入口只导出./lib/version.cjs编译器 API 完全不在其中。因此当openapi-typescript运行在 TS 7 环境下时拿不到ts.factory代码生成会直接崩溃报错信息为TypeError: Cannot read properties of undefined (reading createKeywordTypeNode)这本质上是一个上游生态迁移期的不兼容openapi-typescript声明的 peer 依赖范围是typescript: ^5.x而截至当前仓库版本openapi-typescript的最新版为 7.13.0尚没有可升级到“支持原生编译器”的版本因此短期内只能通过锁定 TypeScript 版本来规避。三、为什么根级 pin 无效pnpm peer 依赖解析机制一个自然的想法是在工作区根或admin里用overrides强制固定 TypeScript 版本。但仓库 README 明确指出这条路走不通原因在于 pnpm 的peer 依赖解析机制openapi-typescript把typescript声明为peer 依赖而非普通 devDependencypnpm 会从实际依赖openapi-typescript的那个包的环境里解析 peer而不是从工作区根解析在admin同时依赖openapi-typescript并声明typescript: ^7.0.2的情况下peer 始终被解析到 7.x无论是根pnpm.overrides还是packageExtensions都无法覆盖这个 peer 解析结果README 原文neitheroverridesnorpackageExtensionsoverrides that。也就是说想让openapi-typescript拿到一个带编译器 API 的 TypeScript就必须让“依赖它的包”自身只携带一个 6.x 的 TypeScript。四、解决方案独立的构建期私有包Etherpad 的解法是把生成器隔离进一个私有、仅构建期使用的独立包etherpad/openapi-codegen其完整package.json如下admin/tools/openapi-codegen/package.json{ name: etherpad/openapi-codegen, version: 0.0.0, private: true, description: Build-time wrapper that runs openapi-typescript against a TypeScript it can actually use. See README.md., devDependencies: { openapi-typescript: ^7.13.0, typescript: ^6.0.3 } }关键点在于该包是openapi-typescript的唯一“宿主”由于openapi-typescript在此包内声明为 devDependencypnpm 就会从这个包所在的依赖环境里解析它的 peertypescript该包只声明了typescript: ^6.0.3因此 peer 必然解析到带完整编译器 API 的 6.x而非admin里的^7.0.2见 admin/package.jsonprivate: true表明它绝不发布version: 0.0.0是典型的“内部工具占位”写法。要让 pnpm 工作区识别这个包还需要在 pnpm-workspace.yaml 中注册它该文件明确写有对应注释packages: - src - admin - bin - doc - ui # Build-time only: pins the TypeScript that openapi-typescript runs # against, which cannot be TypeScript 7. See its README. - admin/tools/openapi-codegen五、构建期调用链从规范导出到schema.d.tsetherpad/openapi-codegen不是被直接 import 的库而是通过admin的构建脚本以子进程方式调用。入口脚本为 admin/scripts/gen-api.mjs由admin/package.json的gen:api: node scripts/gen-api.mjs触发并被dev、build、test等脚本前置调用。5.1 第一步导出并合并 OpenAPI 文档gen-api.mjs 先通过pnpm exec tsx scripts/dump-spec.ts tmp-spec-path运行 admin/scripts/dump-spec.ts。dump-spec.ts会从源码动态 import 三个模块src/node/handler/APIHandler.ts提供latestApiVersionsrc/node/hooks/express/openapi.ts提供generateDefinitionForVersion与APIPathStylesrc/node/hooks/express/openapi-admin.ts提供generateAdminDefinition以FLAT 风格生成公共 API 规范openapi.generateDefinitionForVersion(apiHandler.latestApiVersion, openapi.APIPathStyle.FLAT)。从 src/node/hooks/express/openapi.ts 可见APIPathStyle定义了FLAT: api如/api/createGroup与REST: rest如/rest/group/create两种风格info 中的version取自apiHandler.latestApiVersion生成管理端规范openapiAdmin.generateAdminDefinition()其info.version取自getEpVersion()openapi字段固定为3.0.2见 src/node/hooks/express/openapi-admin.ts用 admin/scripts/merge-openapi.mjs 的mergeOpenAPI(publicSpec, adminSpec)深度合并paths与components.{schemas,parameters,responses,securitySchemes}按键联合碰撞即抛错根级info/servers/security以公共规范为准而 admin 路径上的 per-operationsecurity原样保留将合并后的 JSON 写入临时目录之所以用文件参数而非 stdout是因为 importopenapi*.ts会触发 Settings 初始化、log4js 向 stdout 写日志会污染 JSON 输出。5.2 第二步在锁定包内运行openapi-typescript合并后的 spec 由第二个子进程消费这正是etherpad/openapi-codegen发挥作用的地方const gen spawnSync( pnpm, [--filter, etherpad/openapi-codegen, exec, openapi-typescript, specPath, -o, outFile], spawnOpts, );即通过pnpm --filter etherpad/openapi-codegen exec openapi-typescript让生成器在这个包的依赖环境里运行从而使用被锁定的 TS 6.x。gen-api.mjs中还做了 Windows 兼容处理spawnSync查找pnpm.cmd需要 shell因此仅在process.platform win32时启用shell: true并注明所有参数均为固定值、无注入风险。5.3 第三步写回生成产物生成完成后脚本会给admin/src/api/schema.d.ts加上“GENERATED — do not edit. Runpnpm --filter admin gen:apito regenerate.”的文件头解析 spec 的info.version额外生成 admin/src/api/version.ts构建期产物export const LATEST_API_VERSION ...; export const API_BASE_URL /api/${LATEST_API_VERSION};注释说明生成的 paths 是不带前缀的如/createGroup而后端把 FLAT 风格规范挂载在/api/version/下因此需要该常量来拼出正确的baseUrl最后在finally中清理临时目录。需要强调的是schema.d.ts和version.ts都是构建期生成的纯文本文件不在仓库源码目录中提交其内容对tsc 7而言只是普通的.d.ts因此生成器虽然必须跑在 TS 6.x 上生成的产物却能毫无障碍地被admin的 TS 7 编译链消费——README 中that output is plain text whichtsc7 then consumes normally指的就是这一点。这也解释了为何锁包方案不会污染前端工程本身的 TS 版本。六、产物如何被前端消费生成的schema.d.ts被 admin/src/api/client.ts 以import type { paths } from ./schema引入。该文件利用路径前缀把合并后的paths拆成两个面公共版本化 API/api/version/下的路径如/createGroup管理端 API根路径下的/admin...路径如/admin-auth/。type AdminPath Extractkeyof paths, /admin${string}; type PublicPath Excludekeyof paths, AdminPath; type PublicPaths Pickpaths, PublicPath; type AdminPaths Pickpaths, AdminPath; export const fetchClient createClientPublicPaths({ baseUrl: API_BASE_URL }); export const adminFetchClient createClientAdminPaths({ baseUrl: / }); export const $api createQueryHooks(fetchClient); export const $adminApi createQueryHooks(adminFetchClient);这样 TypeScript 会在编译期拒绝“在公共客户端上调用 admin 路径”或反之避免出现共享客户端在运行时把baseUrl打到错误表面的问题。配套的冒烟测试 admin/src/api/tests/client.test.ts 会校验四个导出fetchClient、adminFetchClient、$api、$adminApi以及GET、useQuery等关键方法存在用于在工具链接线回归如 peer 依赖缺失、生成器输出没有paths导出时第一时间暴露问题。TanStack Query 的 provider 则在 admin/src/api/QueryProvider.tsx 中配置staleTime: 30_000devtools 仅在开发构建懒加载。另外合并规则的单元测试位于 admin/scripts/tests/merge-openapi.test.mjs。七、这个锁包方案的取舍与清理路径Etherpad 选择“独立私有包”而非“根级 pin”本质上是顺着 pnpm 的 peer 解析规则顺势而为与其对抗解析机制不如创建一个peer 依赖的唯一宿主让解析结果自然落入期望版本。代价是多了一个微包换来的是admin可以继续自由使用 TS 7前端构建、类型检查不受影响生成器始终运行在具备编译器 API 的 TS 6.x 上该包仅存在于构建期不会随任何产物发布READMENothing here ships。README 最后给出了明确的清理条件与步骤一旦openapi-typescript支持原生编译器TS 7就删除etherpad/openapi-codegen把openapi-typescript移回admin的 devDependencies并让 admin/scripts/gen-api.mjs 直接调用它。这是一个“临时补丁方案 显式退役条件”的教科书式写法——工具本身知道自己的生命周期终点。八、总结etherpad/openapi-codegen是 Etherpad 构建体系里一个精巧的“小手术”问题openapi-typescript依赖 TS 编译器 API而 TS 7原生移植不再暴露该 API报错createKeywordTypeNode原因typescript作为 peer 依赖由依赖它的包决定版本根级overrides/packageExtensions均无法干预方案用private构建期包独占openapi-typescript并把typescript锁在^6.0.3配合 pnpm-workspace.yaml 注册与pnpm --filter子进程调用边界锁定的只是“生成器运行环境”生成的schema.d.ts纯文本产物由tsc 7正常消费前端 TS 版本不受影响未来待openapi-typescript支持原生编译器后按 README 指定的清理路径删除该包。对于任何在 pnpm monorepo 中遭遇“peer 依赖版本与编译器 API 冲突”的团队这个案例提供了一个可复用的思路不要对抗 peer 解析而是为 peer 构造一个受控的宿主包。赞分享后端协同办公WebSocket前端富文本【免费下载链接】etherpadEtherpad: A modern really-real-time collaborative document editor.项目地址https://gitcode.com/gh_mirrors/et/etherpad点击查看免费下载相关推荐Bonsai-8B-GGUF最佳实践温度0.5-0.7Top-k 20-40的生成参数配置指南Bonsai 8B GGUF最佳实践温度0.5 0.7Top k 20 40的生成参数配置指南 Bonsai 8B GGUF是一款端到端1位语言模型专为l后端协同办公WebSocket前端富文本探索OpenAPI TypeScript Codegen: 构建类型安全的RESTful API客户端探索OpenAPI TypeScript Codegen: 构建类型安全的RESTful API客户端 在这个日益依赖API的时代开发人员需要快速、安全地与各让大模型真正懂化学ChemCrow 零基础实操指南用 LangChain 搭出你的专属化学 AI 助手让大模型真正懂化学ChemCrow 零基础实操指南用 LangChain 搭出你的专属化学 AI 助手 你问过 ChatGPT 化学问题吗比如泰诺的后端协同办公WebSocket前端富文本创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表