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. 擴充靠包裝,不改寫」