Framework Codegen 模块

@swift-web/framework 的 src/codegen/ 子路径下提供 6 个构建期工具,是 Swift Web 项目的统一脚手架能力。applier 项目通过薄封装方式调用(一两行 import + 一行调用),核心逻辑由 framework 统一维护,升级 framework 包即同步获得最新实现。

设计原则

  • 下游零核心逻辑:applier 的 scripts/*.ts 只做参数传递,永远不要把生成算法复制到下游
  • 常量集中管控:图片宽度、质量参数等共享常量集中在 @swift-web/framework/codegen/image-config,多模块复用同一份事实源
  • 幂等可重跑:所有 codegen 输出对应输入是确定的,重复执行不产生 diff(除非源变了)

6 个模块一览

模块子路径用途
generate-api@swift-web/framework/codegen/generate-api从接口文档源(YApi / OpenAPI spec)生成 TS 请求函数和类型
generate-icons@swift-web/framework/codegen/generate-icons从 app/assets/icon.{svg,png} 派生 favicon / iOS 主屏图标 / PWA icons(192/512)
inject-icons@swift-web/framework/codegen/inject-icons批量 SVG 注入到 AppIcon.vue 的 v-if 模板块
optimize-images@swift-web/framework/codegen/optimize-images把 public/images/ 下源图预生成 webp/avif 多尺寸版本
check-og-image@swift-web/framework/codegen/check-og-imageprebuild 钩子,校验默认 OG 分享图存在且 ≤ 8MB
image-config@swift-web/framework/codegen/image-config图像处理共享常量(WIDTHS / FORMATS / QUALITY),SwImage + optimize-images 共用

典型调用

starter 模板已经包含所有薄封装脚本,applier 项目按需挑选。一个典型 applier 的 scripts/ 长这样:

// scripts/generate-api.ts
import { generateApi } from '@swift-web/framework/codegen/generate-api'
await generateApi({ apiDir: 'app/api' })

// scripts/generate-icons.ts
import { generateIcons } from '@swift-web/framework/codegen/generate-icons'
await generateIcons()

// scripts/inject-icons.ts
import { injectIcons } from '@swift-web/framework/codegen/inject-icons'
injectIcons()

// scripts/optimize-images.ts
import { optimizeImages } from '@swift-web/framework/codegen/optimize-images'
await optimizeImages()

// scripts/check-og-image.ts (prebuild)
import { checkOgImage } from '@swift-web/framework/codegen/check-og-image'
try { checkOgImage() } catch (e) { console.error(e.message); process.exit(1) }

对应在 package.json 注册 npm scripts:

{
  "scripts": {
    "generate-api": "tsx scripts/generate-api.ts",
    "generate-icons": "tsx scripts/generate-icons.ts",
    "inject-icons": "tsx scripts/inject-icons.ts",
    "optimize-images": "tsx scripts/optimize-images.ts",
    "prebuild": "tsx scripts/check-og-image.ts"
  }
}

sw run 会自动列出这些脚本,AI / 终端友好。

generate-api 双源(YApi / OpenAPI)与配置归属

generate-api 支持两种文档源,产物形态完全一致(modules/ + types/ + index.ts)。配置按归属分两处——非秘密进 nuxt.config.ts › swiftConfig.apiDocs(入库、有类型、接后端时本来就在改的文件;swift.config.json 只放 sw create 那一刻能定的项),秘密只留 .env 的 API_DOCS_TOKEN:

源nuxt.config.ts → swiftConfig.apiDocs.env适用
yapi{ source: 'yapi', url: '<站点根>', project: <项目 id> }API_DOCS_TOKEN(必填)YApi 平台
openapi{ source: 'openapi', url: '<spec URL 或本地 JSON>', pathVars: { type: 'movie' } }API_DOCS_TOKEN(spec 在鉴权后面时作 Bearer,否则不需要)OpenAPI 3 spec——自研 swift-web server 的 /c-api/docs-json 即此源
  • 优先级:generateApi(options) > 进程 env API_DOCS_SOURCE / API_DOCS_URL / API_DOCS_PROJECT / API_DOCS_PATH_VARS / API_DOCS_TOKEN(临时覆盖,平时不写)> nuxt.config.ts。两源字段同名同义,不再按源分前缀。起步第一行打印 文档源:openapi · <url>(来自 nuxt.config.ts › swiftConfig.apiDocs),配没配对当场可见。
  • 产物首行是 @generated 机器标记(// @generated by @swift-web/framework generate-api — DO NOT EDIT):mr-review 据此跳过生成物 review、cli MCP 据此识别产物(standards「生成物统一标识」)。modules/ types/ 整目录归 codegen,每次重生成整删重建。
  • 手写模块放 app/api/manual/:generate-api 不读不删不改写这个目录,只在生成 index.ts 时按文件名 camelCase 自动注册进 api 对象(manual/search-extra.ts → api.searchExtra),与生成模块并列消费;类型写在手写文件里,别放 types/。手写模块名与文档源模块撞名会在清产物之前报错。硬放进 modules/ 的手写文件照样能跑,但下次重生成会被删(删前 console.warn 点名)。
  • source 必填、不推断;apiDocs 里放 token 直接报错(秘密不入库);swift.config.json 里残留 apiDocs 块也报错(已迁走)。
  • OpenAPI 分模块只看路径首段(与 YApi 源同规则):tags 是 Swagger UI 的分组标签,不参与模块名——路径分段且唯一,后端换个 tag 不该让前端 api.<module> 跟着变。
  • 旧键 API_SOURCE / YAPI_* / OPENAPI_* 读到即报错并列出「旧键 → 新位置」对照表,不双读、不 warn 后继续。

OpenAPI 源补充说明:

  • 字典类型:没有固定属性的 type: object,当 additionalProperties 为 schema 时生成 Record<string, 值类型>,支持基础类型、$ref、嵌套字典及 allOf + nullable(如 Record<string, RetentionCell | null>)。字典自身的 nullable 单独生成 Record<...> | null。未提供值类型时仍回退为 Record<string, unknown>;固定属性与 additionalProperties 混合时目前只生成固定属性。
  • 路径参数代入:spec 中 /{type}/navBlock 这类参数化路径,用 apiDocs.pathVars(env 形式 API_DOCS_PATH_VARS=type=movie,逗号分隔多组)在生成期代入;未代入的参数会成为生成函数的必填入参。
  • visibility 标注:server 端点标注的 x-sw-visibility(public/private)会生成到函数 JSDoc 的 @swVisibility 标签——ISR 页面禁调 private API 的护栏依据。
  • 信封零感知:spec 描述的是信封内净 payload,生成代码照常走 request(),加解密由 framework request 层承担。
  • CI 漂移检查 --check(fw#245):tsx scripts/generate-api.ts --check(或 generateApi({ check: true }))生成到临时目录与现有产物 diff——一致通过、漂移打印文件清单并非零退出。挂进 verify CI 后「server 改协议但 applier 忘重生成」当场红;文档源在 apiDocs 块里,CI 不用再内联任何变量。
  • visibility 分文件与护栏 manifest(fw#243):混合可见性的模块拆 modules/<m>.public.ts / <m>.private.ts(index 合并 namespace,页面消费 api.<m>.<fn> 零感知);同时生成 app/api/visibility.json(<module>.<fn> → public|private)——eslint 规则 swift-web/isr-no-private-api 据此拦「ISR/prerender 页调 private API」(recommended 默认 error;manifest 缺失时静默不生效,YApi 源项目零影响)。private 函数请求自带 visibility 元数据,dev 期 SSR 上下文调用会收到 console 警戒。
// nuxt.config.ts(openapi 源示例)
swiftConfig: {
  ...swiftConfig.framework,
  apiDocs: { source: 'openapi', url: 'https://api.your-host.com/docs/c.json', pathVars: { type: 'movie' } },
},

🔑 自研后端的话,x-sw-visibility 值得标 —— 它换来两道开发期机检

上面把 x-sw-visibility 的产物讲清楚了,但没讲为什么该花这个力气。对「不用公司旧后端、 打算自研 API」的项目,这是个明确的收益差:

标了 x-sw-visibility(OpenAPI 源)没标(YAPI 源 / 不标注的 OpenAPI)
swift-web/isr-no-private-api(静态 lint)✅ 写代码时当场报错❌ 无 manifest ⇒ 静默 no-op
dev 期 SSR 调 private 的运行时警戒✅ 跑 dev 就看到❌ 无元数据可判
运行时护栏 L5(可缓存页 SSR 期剥离身份)✅✅ 两者都有

差别在「什么时候发现」:标了,问题在写代码 / 跑 lint 时就暴露;没标,只剩 L5 这道 运行时防线——它确实兜得住(那正是存量 YAPI 项目的主力防线),但要等到真的跑起来、 而且只在可缓存页的 SSR 路径上生效。

成本几乎为零:给端点加一个 OpenAPI 扩展字段而已。自研 server 的路由装饰器上标一次, generate-api 自动产出 visibility.json + JSDoc + .public.ts / .private.ts 分文件。

目前只支持 yapi / openapi 两个源(ApiSource = 'yapi' | 'openapi',源码里写死的两个 adapter)。x-sw-visibility 是写在 OpenAPI 文档里的扩展字段、只有 openapi 源认,不是一个 跨格式的通用协议。想接第三种文档格式(Swagger 2 / Apifox 导出 / 自定义 JSON)需要加 adapter —— 架构上留了位(source adapter,fw#242),但那是加代码不是改配置。

判 private 的完整语义、三层防御各自对两类后端源的生效情况,见 渲染模式选用指南 · Public / Private API 分离。

YApi 路径参数与失败保护

YApi 支持 {name}、:name,以及 /m3u8/p{token} 这种带静态前缀的参数段。参数名使用英文字母或下划线开头,后接字母、数字或下划线。路径中的参数生成必填 string 字段,与 query/body 类型合并;调用时传原始值,生成的请求函数用 encodeURIComponent 编码后代入 URL。

例如 /m3u8/p{token} 生成 api.m3u8.pToken({ token: 'abc' }),请求路径是 /m3u8/pabc;静态路径的函数名保持不变。

YApi 生成器会先完成全部接口详情拉取、解析及文件内容生成,再替换旧产物。上述阶段失败时,原有 index.ts、modules/、types/、visibility.json 保持不变;manual/ 始终不被覆盖。这不提供磁盘写入失败或进程强制终止时的原子恢复保证。

请求体边界(YApi):带运行时路径参数的端点只支持 YApi 解析器能处理的普通对象请求体(对象字段可以是数组)。数组、字符串等非对象请求体、可空或无法确认是对象的 schema,生成时会明确报错,并保留旧产物;不自动包装或转换请求体。只有路径参数时不发送 query/body;同时存在 query/body 时只转发非路径字段。

首段含路径参数时也会规范化模块名:/api/{tenant}/users → api.tenant.users(),/p{tenant}/users → api.pTenant.users()。路径参数若与声明的 query/body 顶层字段同名,生成会提前报错并保留旧产物;嵌套对象内的同名字段不受影响。

YApi 若有两个端点生成同一模块内相同的最终导出名或 Params/Response 类型名(例如 GET /book/foo 与 GET /book/Foo),生成会报错列出两个端点、冲突名称和处理办法,并保留旧产物。可调整接口路径;或先从自动生成源移除冲突端点,再改用 app/api/manual/ 下不撞名的手写模块。不会交互选择、自动改名或静默跳过。

上述限制仅适用于 YApi;OpenAPI 保持原有行为。YApi 不推断 $ref、allOf、oneOf、anyOf 请求体 schema。非法模块名及多方法对象导出名(包括 delete 等保留字)也会在替换产物前报错,列出相关端点并提供相同处理办法;单方法已有的 $ 后缀转义保持不变。

升级体验

framework 发版后,applier 跑 sw upgrade 即可(或手动改版本范围后 pnpm install --lockfile-only && pnpm install --frozen-lockfile;别用 pnpm update,它会重解析整棵依赖树、带出大片无关 lockfile 变动)。无需改 scripts/、无需手工同步算法。

扩展原则

业务对 codegen 有个性化需求时:

  • 能进 framework 就进 framework:泛化逻辑请提 issue 到 framework,由 framework 维护,所有 applier 享受
  • 真业务特异:在 applier scripts/ 自建脚本,但不要 fork framework codegen 然后魔改
  • 详见根 CLAUDE.md「9. 扩展靠包装,不改写」