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-image | prebuild 鉤子,校驗預設 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)> 行程 envAPI_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. 擴充靠包裝,不改寫」