建立外掛
打造自訂的 PikaCSS 引擎外掛,為引擎擴充新功能。
結構
PikaCSS 外掛是一個回傳 EnginePlugin 物件的函式。建議的寫法:
import { defineEnginePlugin } from '@pikacss/core'
export function myPlugin() {
return defineEnginePlugin({
name: 'my-plugin',
configureRawConfig: (config) => {
config.layers ??= {}
config.layers['my-layer'] = 5
},
configureEngine: async (engine) => {
engine.runtime.addPreflight('/* my-plugin preflight */')
},
})
}defineEnginePlugin
defineEnginePlugin 輔助函式會為外掛物件提供型別推導。它接受一個物件,包含:
name:識別這個外掛的唯一字串。order:選擇性的執行順序,'pre'、'post',或省略以使用預設值。- Hook 方法:會在引擎生命週期的特定時機執行的函式。
上面的範例直接使用 defineEnginePlugin(),讓 config 與 engine hook 參數不需額外的輔助型別就能保持推導。
order
外掛的執行順序決定一個外掛的 hook 相對於其他外掛何時執行:
| 值 | 行為 |
|---|---|
'pre' | 在預設順序的外掛之前執行 |
| (省略) | 預設順序,依註冊順序執行 |
'post' | 在預設順序的外掛之後執行 |
在同一個順序群組內,外掛會依照它們在 plugins 陣列中出現的順序執行。核心外掛(variables、keyframes、selectors、shortcuts、important)會自動加到最前面並使用預設順序,因此預設順序的使用者外掛一定會在它們之後執行。
每引擎狀態
defineEnginePlugin() 回傳的外掛物件是可重用的定義:同一個物件可以傳給任意數量的 createEngine() 呼叫,無論是循序或並發。因此可變的每引擎資料絕不能放在外掛工廠的 closure 裡 — 第二個重用該定義的引擎會覆寫它,而第一個引擎仍在讀取。
用 createState 宣告 Engine-local state。一般 hook 透過 context.state 存取;configureEngine 則收到 EngineConfigurator,並透過 configurator.state 取得同一份 Engine-local value:
defineEnginePlugin({
name: 'my-plugin',
createState: () => ({ resolved: {} as MyPluginOptions }),
configureRawConfig: (config, context) => {
context.state.resolved = config.myPlugin ?? {}
},
configureEngine: (configurator) => {
// The configurator is stable for this plugin/engine initialization.
// Long-lived callbacks should capture its engine-local `state`.
configurator.runtime.addPreflight(() => renderCss(configurator.state.resolved))
},
})Engine 對每個 plugin definition 每個 Engine至多呼叫一次 createState(),時機在該 plugin 於該 Engine 的第一個 hook 執行之前。這個 plugin / Engine 配對中的所有 hook 都會觀察同一份 Engine-local state、host context 與 diagnostic sink;configureEngine facade 會把這些值與 owner-bound runtime / Pika / Typegen capabilities 組合在一起。Stateless plugin 直接省略 createState 即可;永不變動的 factory arguments 可以留在 closure 中作為 immutable definition configuration。
兩個要遵守的邊界:
- 刻意共享的 process 全域快取,只有在它的鍵涵蓋所有可能影響結果的輸入時才允許 — 優先使用每引擎狀態。
- 每引擎狀態是引擎生命週期的狀態。暫定階段的 transform hook 在模組提交之前執行(見交易式生命週期),所以不要在暫定 transform 中急切地變動永久的
context.state,並期待模組失敗或被取代時會回滾。
生命週期與注意事項
第一次撰寫外掛時容易忽略的運作行為。
Hook 錯誤會先回報、再重新拋出
如果某個 hook 拋出錯誤,引擎會回報一筆 plugin-hook-error 診斷,然後重新拋出(packages/core/src/plugin.ts):設定類 hook 失敗時 createEngine() 會 reject,暫定階段的 transform hook 失敗時 engine.use() 會 reject — 失敗的生命週期絕不會被轉換成默默的部分結果。有兩個影響:
- 失敗的外掛會中止觸發它的那次呼叫。開發時請留意
Plugin "<name>" failed to execute hook "<hook>"診斷;bundler 整合會把設定失敗以 config-load 診斷呈現。 - 唯一的例外是已提交的通知
atomicStyleAdded:它在樣式已註冊之後才觸發,因此拋錯的觀察者會以診斷回報,但絕不會回滾該次提交 — 且後續外掛的觀察者會跳過那一次通知。見可用的 Hook。
在 Engine 建構前 lower semantic definitions
Selector、shortcut、variable、keyframe都是 config-backed semantic domain,刻意沒有 public runtime .add() ingress。Plugin應在 configureRawConfig append object definitions,由Core一致負責 normalization、runtime resolution、Typegen與finalization。
configureEngine 用於 initialized Engine API與 owner-bound engine.pika / engine.typegen capability。order仍控制 lifecycle順序,但不是用來取得 mutable Core producer service的機制。
在初始化期間註冊 configuration inputs
若 plugin 會讀取定義 Engine generation 的外部檔案,請在初始化期間註冊絕對路徑:
defineEnginePlugin({
name: 'my-plugin',
configureEngine(engine) {
engine.runtime.addConfigDependency('/absolute/path/to/tokens.json')
},
})若 direct directory member 的 create/delete/rename 會影響設定,使用獨立的 initialization-only engine.runtime.addConfigDirectoryMembershipDependency()。
Engine 初始化完成後 dependency set 就會 freeze。之後從 engine.use()、resolver 或其他 runtime phase 再註冊 dependency 會直接報錯;不會動態擴張 active watcher。Integration 會把 finalized Engine dependencies 與 canonical config-module dependencies 合併成整個 ProjectGeneration 的 watch inputs。
測試外掛
外掛的 hook 都是單純函式,因此大多數外掛行為測試不需要真正的 Engine,可以比照官方 @pikacss/plugin-reset 測試(packages/plugin-reset/src/index.test.ts):用最小 mock 直接呼叫 hook 並斷言效果。每個模擬 Engine 建立一個 base context({ onDiagnostic, state: plugin.createState?.(), host: {} }),並讓該 Engine 的所有 hook 共用相同的 state / host / diagnostic 值。configureEngine 是一般 hook 形狀的例外:它只收到一個 EngineConfigurator,所以 direct unit test 要把同一個 base context 與 runtime 組合;若 plugin 會使用 owner-bound pika / typegen capability,也要一併提供。
import { describe, expect, it, vi } from 'vitest'
import { myPlugin } from './index'
function createContext(plugin: any) {
return { onDiagnostic: vi.fn(), state: plugin.createState?.(), host: {} }
}
describe('myPlugin', () => {
it('registers its layer and preflight', async () => {
const plugin = myPlugin()
const context = createContext(plugin)
const runtime = { addPreflight: vi.fn() }
const config: Record<string, any> = {}
plugin.configureRawConfig?.(config as any, context)
await plugin.configureEngine?.({
...context,
runtime,
pika: { extendStatic: vi.fn() },
typegen: { add: vi.fn() },
} as any)
expect(config.layers).toEqual({ 'my-layer': 5 })
expect(runtime.addPreflight).toHaveBeenCalled()
})
})如果要對產生的 CSS 做端對端斷言,請改為建立一個真正的引擎:const engine = await createEngine({ plugins: [myPlugin()] }),接著 await engine.use({ ... }),再對 await engine.renderAtomicStyles(true) 拍快照。
下一步
- 可用的 Hook:所有你可以實作的生命週期 hook。
- 型別擴增:為你的外掛擴充 PikaCSS 型別。
- Define 輔助函式:
defineEngineConfig與defineEnginePlugin。