從發布地獄到 API 測試場:Yoin v0.1.0 正式上線的那一天

6750 字
17 min
2/18/2026
從發布地獄到 API 測試場:Yoin v0.1.0 正式上線的那一天

簡單來說

今天是個充實到幾乎爆炸的一天。 本來以為今天只是「按下發布鍵」的收尾工作,結果從早上一路折騰到下午:先是陷入打包流程的泥淖,接著意外發現一個 Plugin Bug,最後為了真正驗證 SDK 的「開箱即用」體驗,直接在 Monorepo 裡移植了一個全功能的 API 測試應用。 讓我從頭說起。

TL;DR

一、準備發布 v0.1.0:比想像中更多的事

昨天(2/17)已經通過了技術總監級別的最終程式碼審查,四個 CRITICAL 問題全部修復,89/89 單元測試通過,npm pack --dry-run 也確認了 20 個檔案、202.8 kB 的乾淨套件。理論上,今天只需要:

git tag -a v0.1.0 -m "Release v0.1.0"
git push origin main --tags
pnpm publish --filter @yoin/client --access public

然後就可以開慶功宴了。

然而現實並不是這樣的。


二、打包地獄:workspace:* 的背刺

在整個發布流程中,最詭異的問題出現在「嘗試用 npm publish 驗證」的那一刻:

npm error code EUNSUPPORTEDPROTOCOL
npm error Unsupported URL Type "workspace:": workspace:*

這個錯誤一度讓人以為打包設定炸裂了。但追查後才發現,罪魁禍首在於 Monorepo 的 workspace 協議

packages/client/package.jsondevDependencies 裡有:

"@yoin/core": "workspace:*"

這個 workspace:*pnpm 的私有語法,用來引用 Monorepo 內的本地套件。當 pnpm 打包時,它會自動將其轉換為實際版本號(0.1.0)。但如果你用 npm 直接發布,或者用 npm pack 預覽,npm 不認識這個協議,直接報錯。

教訓很清晰:在 pnpm Monorepo 裡,所有涉及發布的操作都必須透過 pnpm 執行。

# ✅ 正確:讓 pnpm 介入,自動解析 workspace 協議
pnpm publish --filter @yoin/client --access public

# ❌ 錯誤:npm 不懂 workspace:*
npm publish --access public

這個陷阱相當隱蔽,因為在 dev 開發環境下完全正常,唯有在打包/發布這個特定情境才會爆炸。為此,寫了一份 PUBLISH_CHECKLIST.md 專門記錄這個「血淚教訓」,避免日後自己又踩一次坑。

經過多次 pnpm pack --dry-run 的反覆確認,最終的套件內容令人滿意:

關鍵指標結果
總檔案數20 個
壓縮後大小202.8 kB
WASM 引擎dist/core_bg.wasm (308 KB) ✅
workspace:* 殘留無 ✅
原始碼/測試洩漏無 ✅

三、v0.1.0 正式發布:一個里程碑

在確認一切就緒後,v0.1.0 正式推送到 npm:

pnpm publish --filter @yoin/client --access public

這是 Yoin 的第一個公開版本。從 Day 1 的單一 index.html + Rust WASM 原型,到現在:

  • 🦀 Rust/WASM CRDT 引擎(基於 yrs,約 300 行精煉的 Rust)
  • 📦 TypeScript Client SDK(微核心 + Plugin 架構)
  • ☁️ Cloudflare Durable Objects WebSocket Relay(含速率限制與 Hibernation API)
  • 💾 IndexedDB 持久化 Plugin
  • ↩️ Undo/Redo Plugin
  • 👥 Awareness 系統(即時游標與在場感知)
  • Proxy 透明寫入createMapProxy / createArrayProxy
  • ⚛️ React HooksuseYoinMap, useYoinArray, useYoinAwareness
  • 🔒 Schema 驗證(Zod 整合)

整個架構從單一檔案演化成一個嚴格分層的工業級框架,這個過程本身就是一段值得記錄的歷程。


四、意外的額外測試:YoinUndoPlugin 的隱藏 Bug

v0.1.0 發布後,我決定用「最終使用者的視角」來親自驗證一遍 SDK 的完整體驗。就在測試 createUndoPlugin() 的時候,發現了一個令人頭痛的行為:

安裝了 Plugin,呼叫了 undo(),但什麼事都沒發生。

追查下去,問題根源在於 onInstall() 的實作遺漏了兩個關鍵步驟:

  1. 安裝 Plugin 後,沒有自動呼叫 doc.enable_undo()(CRDT 層面的 undo 追蹤根本沒開啟)。
  2. 呼叫 setMap() 時,沒有自動呼叫 doc.expand_undo_scope(mapName),導致 undo 的作用域為空,無法匡住任何變更。

這就像是裝了一個錄影機,但從來沒按下「錄影」按鈕,事後當然回放不了任何東西。

修復方案如下:

// YoinUndoPlugin.onInstall()
onInstall(client) {
  // ✅ 修復:自動啟用 CRDT 層的 undo 追蹤
  client.getDoc().enable_undo();

  // ✅ 修復:包裝 setMap,首次寫入時自動 expand_undo_scope
  const seenMaps = new Set<string>();
  const originalSetMap = client.setMap.bind(client);
  client.setMap = (mapName, key, value) => {
    if (!seenMaps.has(mapName)) {
      client.getDoc().expand_undo_scope(mapName);
      seenMaps.add(mapName);
    }
    return originalSetMap(mapName, key, value);
  };
}

onDestroy(client) {
  // ✅ 完整清理,不留副作用
  client.setMap = originalSetMap;
}

修復的核心原則:Plugin 應該對使用者透明。 使用者只要呼叫 client.use(createUndoPlugin()),接下來的一切就應該自動運作,不該要求使用者手動記得去呼叫 enable_undo()expand_undo_scope()。這些是 Plugin 的責任,不是使用者的責任。

這個修復隨即推出為 v0.1.1,就在 v0.1.0 發布後的同一天。


五、api-test 的誕生:第一個「外部消費者」測試場

修完 Bug 之後,我意識到一件事:到目前為止,所有的測試(89 個單元測試、60 個整合測試)都是在 Monorepo 內部進行的,從未真正模擬過「一個陌生的開發者安裝 @yoin/client 後從零開始使用」的情境。

這正是 apps/api-test 的誕生動機:打造一個完整的 API 全功能測試應用,作為 SDK 的第一個真實消費者

設計哲學

這個應用不追求漂亮的 UI,它的唯一目標是:對每一個公開 API 進行可被人眼確認的測試,並輸出 ✅ / ❌ 的測試結果。

Yoin API 全功能測試
結果:32 通過 / 0 失敗

✅ initYoin()  —  首次初始化成功
✅ initYoin() 冪等  —  重複呼叫不報錯
✅ isYoinInitialized()  —  回傳 true
✅ new YoinClient()  —  docId = api-test-1739900712345
✅ getDoc()  —  typeof = object
✅ setText()  —  'Hello Yoin!'
✅ getText()  —  取得文字正確
✅ setMap()  —  age = 30
✅ getMap()  —  取得 Map 正確
✅ UndoPlugin undo()  —  成功還原
...

技術選型:用 workspace:* 模擬真實消費者

apps/api-test/package.json 最關鍵的一行:

"dependencies": {
  "@yoin/client": "workspace:*"
}

在 Monorepo 內使用 workspace:*,意味著 api-test 直接消費本地的 packages/client 原始碼。任何對 SDK 的改動都會立即反映在這裡,沒有任何快取層。這是一種極度緊密的整合測試

Vite 設定的優雅性

apps/api-test/vite.config.ts 只有四行:

import { defineConfig } from 'vite';
import { yoinViteConfig } from '@yoin/client/vite';

export default defineConfig({
  ...yoinViteConfig(),
});

這裡用了 SDK 自身匯出的 yoinViteConfig(),它封裝了 vite-plugin-wasmvite-plugin-top-level-await 的配置,讓消費者不需要手動處理 WASM 的跨源隔離標頭(COOP/COEP)等複雜設定。這本身就是一種 API 設計的驗證:SDK 把複雜度吞入自身,外部使用者保持簡單。

涵蓋的測試範圍

1. 初始化        initYoin() 冪等性、isYoinInitialized()
2. YoinClient    建構、底層 getDoc()/getConfig() 存取
3. Text API      setText / getText / deleteText
4. Map API       setMap / getMap / setMapDeep / getMapJson
5. Array API     arrayPush / arrayGet / arrayGetAll
6. Awareness     setAwareness / getLocalAwareness / subscribeAwareness
7. 網路狀態      subscribeNetwork / 狀態值驗證
8. Plugin 系統   createLoggerPlugin / createUndoPlugin (+ undo/redo)
9. Proxy API     createMapProxy 透明讀寫驗證
10. 生命週期     client.destroy() 後操作的回應

這份測試清單幾乎就是公開 API 的一份活文件——它跑過就代表 API 在這個環境下一定能動。


六、今日工作全覽

時間軸工作項目結果
上午準備發布 v0.1.0,排查 workspace:* 打包問題✅ 完整的發布檢查清單
中午多次 pnpm pack --dry-run,確認套件內容✅ 套件乾淨,20 檔 / 202.8 kB
下午早pnpm publish,v0.1.0 正式上線✅ npm 套件首次公開發布
下午中手動驗測,發現 YoinUndoPlugin 的 undo 失效 Bug🐛 發現 & 修復
下午晚v0.1.1 補丁發布✅ Plugin 現自動管理 undo scope
傍晚建立 apps/api-test,移植全功能 API 測試應用✅ 覆蓋全部公開 API

七、反思:發布是一面鏡子

今天最深的體會是:發布這個動作本身,就是最殘酷的 Code Review。

在開發環境裡,一切都太舒適了。workspace:* 幫你解決依賴,Vite HMR 幫你即時更新,測試只在 Monorepo 的沙盒裡跑。但當你真的要把東西包成 .tgz 推到 npm,讓一個完全陌生的開發者在他的電腦上下載時,很多「假設成立」會突然崩塌。

YoinUndoPlugin 的 Bug 就是一個例子。它在單元測試裡沒有被抓到,因為測試裡有人工地呼叫了 enable_undo()。但真實用戶不會這樣做,他們只會呼叫 client.use(createUndoPlugin()),然後期待 undo 能動。

api-test 的建立,正是為了填補這個「使用者視角的測試空白」。在未來,每次修改公開 API 之前,都應該先看看 api-test 會不會紅。


八、下一步

  • createDbPlugin / storage.ts 的測試覆蓋率:目前是 0%,這是 v0.1.2 的首要目標。
  • Safari 相容性:WASM + COOP/COEP 在 Safari < 14 的行為仍未完整驗證。
  • api-test 自動化:目前是人工跑瀏覽器確認,未來應整合進 CI 流程(考慮用 Playwright)。
  • 文件補強:升級指南與 Troubleshooting 章節仍是空缺。

今天就到這裡。從零打包到正式上線,中間踩了無數坑——但每一個坑都讓 Yoin 更健壯一點。

如果你對 Local-First 協作框架、CRDT、或 Rust WASM 開發有興趣,歡迎在 GitHub 追蹤

GitHub - Saisai568/Yoin: A high-performance, developer-friendly Local-First state synchronization framework

GitHub - Saisai568/Yoin: A high-performance, developer-friendly Local-First state synchronization framework

A high-performance, developer-friendly Local-First state synchronization framework - Saisai568/Yoin

GitHub

— Saisai568,2026 年 2 月 18 日晚上