cornhsu-parity 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +305 -0
- package/bin/parity.js +74 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# Parity
|
|
2
|
+
|
|
3
|
+
[](https://www.nuget.org/packages/Cornhsu.Parity)
|
|
4
|
+
[](https://www.nuget.org/packages/Cornhsu.Parity)
|
|
5
|
+
[](https://github.com/HSU-YU-MING/cornhsu-parity/actions/workflows/ci.yml)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
**[作品介紹與開發故事](https://cornhsu.com/parity) · [NuGet](https://www.nuget.org/packages/Cornhsu.Parity) · MIT**
|
|
9
|
+
|
|
10
|
+
> **能進 CI 的設計還原度檢查(Property-Level Design QA)。**
|
|
11
|
+
> 抓 Figma 設計稿與實際渲染畫面**兩邊的真實數值**做程式比對——「內距 8px,設計是 12px」,
|
|
12
|
+
> 精確到數字,所以能當 gate:**PR 讓畫面偏離設計就打紅**,並自動把還原度報告貼成 PR 留言。
|
|
13
|
+
|
|
14
|
+
**別人幫你「看見」設計落差,Parity 幫你「擋住」它。**
|
|
15
|
+
|
|
16
|
+
同類工具(Uiprobe、Loupe、OverlayQA、Floto Design Diff、PixelProof…)現在大多都做數值比對了,
|
|
17
|
+
真正的差別不在「數值 vs 疊圖」,而在**這件事能不能自動化**:
|
|
18
|
+
|
|
19
|
+
| | 手動抽查型<br>(Uiprobe / OverlayQA / Loupe …) | **Parity** |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| 誰來跑 | 人打開工具、點一點、看報告 | **CI 自動跑**,人不用在場 |
|
|
22
|
+
| 發現落差之後 | 自己開 ticket、或把結果複製貼到 PR | **exit code 擋 PR** + 機器人自動留言(原地更新不洗版) |
|
|
23
|
+
| 回歸把關 | 無 | **baseline 只擋「新增/惡化」**——有舊債的專案也能今天上車 |
|
|
24
|
+
| 沒有 Figma 檔時 | 不能用 | **快照模式**:把現在的畫面凍結成基準,重構不跑版 |
|
|
25
|
+
| 桌面 app | 不支援 | **Electron**(CDP attach 活視窗) |
|
|
26
|
+
| 價格 | $39–249 / 月(Loupe 免費但為桌面 GUI,無法自動化) | **MIT 免費** |
|
|
27
|
+
|
|
28
|
+
> 誠實說明:localhost 與登入後頁面**不再是差異點**——Uiprobe、OverlayQA 也支援。
|
|
29
|
+
> 「免費開源」也不是獨有——Loupe 同為 MIT。**Parity 獨有的是「免費 × 可自動化」的交集。**
|
|
30
|
+
|
|
31
|
+
## 畫面
|
|
32
|
+
|
|
33
|
+
**報告 UI(`parity serve --watch`)**——左欄逐項列出落差與一致的節點,右側把設計框與實作框直接疊在真實畫面上;點任一項即定位,存檔後自動重掃。
|
|
34
|
+
|
|
35
|
+

|
|
36
|
+
|
|
37
|
+
**還原度報告**——每一列是「期望 → 實際」的具體數值、嚴重度與建議修法(含對應的 design token)。GitHub Action 以此原文自動貼成 PR 留言,同一則原地更新、不洗版。
|
|
38
|
+
|
|
39
|
+

|
|
40
|
+
|
|
41
|
+
## 成果一覽
|
|
42
|
+
|
|
43
|
+
| | |
|
|
44
|
+
|---|---|
|
|
45
|
+
| 發佈 | NuGet 共 13 版(v0.1.0 → v0.9.3),推 tag 即以 OIDC Trusted Publishing 自動上架,repo 內零長效金鑰 |
|
|
46
|
+
| 比對維度 | 尺寸、內距、間距、字體、顏色(CIEDE2000 ΔE)、相對位置——**刻意不比絕對座標**(彈性版面下必然誤報) |
|
|
47
|
+
| 設計來源 | 4 種:Figma API、畫面快照、圖片 + 標註(像素取樣,任何工具匯出 PNG 即可)、JSON |
|
|
48
|
+
| 實作端 | 網頁(含 **Shadow DOM / 同源 iframe / RWD 多斷點**)+ **Electron**(以 CDP attach 活視窗) |
|
|
49
|
+
| 測試 | **154 條**,涵蓋 CIEDE2000 標準測資集(Sharma)、配對消歧、位置誤報防護、圖片取樣 |
|
|
50
|
+
| CI 實證 | GitHub Action 以**外部 repo 跑真實 PR** 驗證:擋 PR、自動留言(原地更新不洗版)、baseline 回歸把關 |
|
|
51
|
+
| 真實驗證 | **cornhsu.com 全站 20 頁由 Parity 自己守門**——首日 dogfooding 即揪出並修掉兩個 flaky 根因 |
|
|
52
|
+
|
|
53
|
+
## 安裝
|
|
54
|
+
|
|
55
|
+
兩條通路,選你手邊有的執行環境即可 —— **功能完全相同**。
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
# 有 Node(前端專案常見):不需要安裝 .NET
|
|
59
|
+
npx cornhsu-parity check
|
|
60
|
+
|
|
61
|
+
# 有 .NET
|
|
62
|
+
dotnet tool install -g Cornhsu.Parity
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
> npm 版是同一份自帶執行環境的原生執行檔,只下載你這個平台的那一份
|
|
66
|
+
> (win32-x64 / linux-x64 / darwin-x64 / darwin-arm64)。
|
|
67
|
+
> Playwright 預設會另外自帶一份 Node,npm 版把它拿掉、改用你現成的 Node,
|
|
68
|
+
> 每個平台包因此省下約 88 MB。
|
|
69
|
+
|
|
70
|
+
## 快速開始
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
parity init # 產生 parity.config.json 範本
|
|
74
|
+
parity install-browser # 第一次:下載 Playwright Chromium
|
|
75
|
+
export FIGMA_TOKEN=... # scope 只需 file_content:read
|
|
76
|
+
parity check # 比對,輸出報告 + exit code
|
|
77
|
+
parity report # 從既有 report.json 重生 Markdown 報告(免重掃;--md 寫檔,預設印 stdout)
|
|
78
|
+
parity snapshot # 把「現在跑著的畫面」凍結成設計基準——重構/改版守門,不需要 Figma
|
|
79
|
+
parity lint # design lint:設計稿的值是否落在 design token 允許集合(只看設計,不比實作)
|
|
80
|
+
parity check --reverse # 反向檢視:設計師照現有頁面重畫時,看自己的稿跟現況差在哪(不做把關)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
在這個 repo 裡開發時:
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
dotnet run --project src/Parity.Cli -- check --config samples/demo/parity.config.json
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`samples/demo` 是一組**離線示範**(設計來源用本機 JSON,不需要 Figma token):
|
|
90
|
+
`index.html` 刻意做壞了幾個地方,`parity check` 會精確報出每一條落差與未配對清單,
|
|
91
|
+
並因 serious 級落差回傳 exit code 1(GATE FAIL)——這正是 CI 把關的行為。
|
|
92
|
+
|
|
93
|
+
## 設定:`parity.config.json`
|
|
94
|
+
|
|
95
|
+
```jsonc
|
|
96
|
+
{
|
|
97
|
+
"figmaFileKey": "abcd1234",
|
|
98
|
+
"designToken": "env:FIGMA_TOKEN",
|
|
99
|
+
"mapFile": "parity.map.json", // 手動補漏的對應檔(圖層名 → selector)
|
|
100
|
+
"tokensFile": "tokens.json", // 選配:design token(名→值);建議修法會提示對應 token
|
|
101
|
+
"targets": [
|
|
102
|
+
{ "route": "/", "frame": "10:2", "url": "http://localhost:8080/" }
|
|
103
|
+
],
|
|
104
|
+
"compare": { "position": "relative" }, // relative = 比相對位置(預設);none = 不比位置
|
|
105
|
+
"tolerances": { "sizePx": 2, "spacingPx": 2, "colorDeltaE": 2.0, "positionPx": 4 },
|
|
106
|
+
"ignore": ["[data-parity-ignore]"],
|
|
107
|
+
"gate": {
|
|
108
|
+
"failOn": ["critical", "serious"],
|
|
109
|
+
"minMatchRate": 0 // 選配:配對率低於此值(0–1)直接 FAIL;0 = 不設門檻
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
> gate 除了看落差,也驗**配對可信度**:完全 0 配對(或設計端 0 節點)一律 GATE FAIL——
|
|
115
|
+
> 沒配到就沒落差可擋,沉默 PASS 會是假的通過(通常是 url/frame 指錯)。`--baseline` 模式也不豁免。
|
|
116
|
+
|
|
117
|
+
## 目標:網頁或 Electron 桌面 app
|
|
118
|
+
|
|
119
|
+
`target.url` 決定實作端是什麼,指令與報告完全一樣:
|
|
120
|
+
|
|
121
|
+
| url 形式 | 對應 |
|
|
122
|
+
|---|---|
|
|
123
|
+
| `http(s)://…` | 一般網頁 / 內網 staging |
|
|
124
|
+
| `file://…`(或相對路徑) | 本機 HTML 檔 |
|
|
125
|
+
| `cdp:http://host:port` | **已在跑的 Electron 桌面 app**(抓活視窗的 DOM) |
|
|
126
|
+
|
|
127
|
+
Electron:啟動時加遠端偵錯埠,再把 `url` 指過去即可——Parity 會 attach 進去讀當下畫面,不導頁、不干擾 app:
|
|
128
|
+
|
|
129
|
+
```sh
|
|
130
|
+
electron . --remote-debugging-port=9222 # 你的 app,加這個旗標
|
|
131
|
+
```
|
|
132
|
+
```jsonc
|
|
133
|
+
{ "route": "/", "frame": "20:5", "url": "cdp:http://localhost:9222" }
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
> 為什麼 Electron 幾乎免費:它的畫面就是一個 Chromium renderer,跟網頁同一套 DOM/CSS 量測。手機原生 / Flutter / 原生桌面不走 DOM,留待 v2.0。
|
|
137
|
+
|
|
138
|
+
## 設計來源:Figma、快照、或一張圖
|
|
139
|
+
|
|
140
|
+
| 設計來源 | config | 適合誰 |
|
|
141
|
+
|---|---|---|
|
|
142
|
+
| **Figma**(主力) | `figmaFileKey` + `FIGMA_TOKEN` | 有 Figma 檔的正規流程 |
|
|
143
|
+
| **快照**(`parity snapshot`) | `designFile` 指向產出的快照 JSON | **重構/改版守門**:現在的畫面是對的,存成基準,之後 check 保證不跑版(visual regression 的數值版)。配對走 selector 身分,100% 確定性 |
|
|
144
|
+
| **一張圖 + 標註** | `designImage`(PNG/JPG)+ `designFile`(標註) | 只有圖的場景:外包 PNG、老專案只剩截圖。**XD / Sketch / PS 等其他工具匯出圖片就能走這條**(萬用轉接頭)。標註 = DesignNode JSON,`fill` 可省略——顏色由引擎從圖片對應區域取樣(TEXT 字色刻意不取樣:反鋸齒混色取不準,可手填) |
|
|
145
|
+
| 手寫 JSON | `designFile` | 離線示範/測試 |
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
# 重構守門三步:
|
|
149
|
+
parity snapshot # 1. 凍結現在的畫面(產出 parity.snapshot.json + 參考截圖)
|
|
150
|
+
# config 改 designFile 指向它、target.frame 填 route
|
|
151
|
+
parity check # 2. 大膽重構;3. check 保證與快照一致
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## 比什麼、不比什麼
|
|
155
|
+
|
|
156
|
+
只比「不管版面怎麼流動都該一樣」的東西:
|
|
157
|
+
|
|
158
|
+
- **自身尺寸**(寬高;TEXT 節點例外——文字框量測天生不同,比了狂誤報)
|
|
159
|
+
- auto-layout 的 **HUG(隨內容)/ FILL(隨父層)** 那一軸也跳過——Figma 量的寬 ≠ 瀏覽器渲染寬是必然的,只比 **FIXED** 的軸
|
|
160
|
+
- **內距 / 間距**:padding 四邊、auto-layout `itemSpacing` ↔ 實際子元素 gap
|
|
161
|
+
- **相對位置**(`offsetX`/`offsetY`):自由擺放(非 auto-layout)容器的子元素,比「相對最近可靠兄弟/父層邊」的偏移——抓得到「尺寸顏色全對但擺錯位置」。參照只用可靠的邊(TEXT/HUG 的框不當參照、TEXT 不當目標、上方全是文字時 Y 誠實跳過),流動版面的行高漂移不會誤報。`compare.position: "none"` 可關閉
|
|
162
|
+
- **字體**:size / weight / line-height / letter-spacing 精確比;font-family 是**軟落差**(不擋 gate)
|
|
163
|
+
- **顏色**:CIEDE2000 (ΔE) 設門檻,不是 hex 全等;解析含現代語法(`rgb(37 99 235 / .5)`、`color(srgb …)`、`oklch()`、`color(display-p3 …)`)
|
|
164
|
+
|
|
165
|
+
> 設計來源是 Figma 時,報告(Markdown 與本機 UI)裡的圖層名會**連回 Figma 的那個節點**——設計師點一下直接跳到圖層,不用自己翻。
|
|
166
|
+
- **刻意不比絕對位置 x/y**:彈性版面下本來就會不同,比了 = 誤報 = 失去信任
|
|
167
|
+
|
|
168
|
+
## 給設計師的兩個方向
|
|
169
|
+
|
|
170
|
+
**新頁面守設計系統:`parity lint`**——只看設計稿這一邊,驗每個節點的值是否落在 `tokensFile` 的允許集合:顏色(ΔE 容差內命中即過)、fontSize / padding / itemSpacing / cornerRadius(等於任一尺寸 token 即過;間距/字級/圓角共用同一份 scale)。違規附「最近的 token」——訊息是「改成這個」,不是只有「你錯了」。有違規 exit 1,可進 CI。沒定義某維度的 token 就不 lint 該維度。
|
|
171
|
+
|
|
172
|
+
**照現況重畫/改版:`parity check --reverse`**——方向反過來:「期望」= 現況(實作)、「實際」= 你的設計稿。給設計師一張「我的稿跟現在線上差在哪」的清單;不做把關,永遠 exit 0。
|
|
173
|
+
|
|
174
|
+
## RWD 多斷點
|
|
175
|
+
|
|
176
|
+
同一個 URL、不同斷點,各對一個 Figma frame 即可——**渲染視窗 = frame 尺寸**,手機 frame 畫 375 寬,media query 自然生效:
|
|
177
|
+
|
|
178
|
+
```jsonc
|
|
179
|
+
"targets": [
|
|
180
|
+
{ "route": "/desktop", "frame": "10:2", "url": "http://localhost:8080/" },
|
|
181
|
+
{ "route": "/mobile", "frame": "10:99", "url": "http://localhost:8080/" } // frame 是 375 寬的手機版
|
|
182
|
+
]
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
route 只是報告上的標籤,取好認的名字就行。frame 寬 ≠ 想測的視窗寬時,才需要在 target 加 `width` / `height` 覆蓋。
|
|
186
|
+
|
|
187
|
+
## Shadow DOM / iframe
|
|
188
|
+
|
|
189
|
+
擷取走**組合樹**:open shadow root、`<slot>` 塞進來的內容、同源 iframe(含 `srcdoc`)都看得到、都會比——web components 網站不再整塊隱形。shadow / iframe 內的元素 selector 以 `host >>> 內部路徑` 表示。
|
|
190
|
+
|
|
191
|
+
限制(誠實列):closed shadow root 與跨域 iframe 拿不到,跳過;map 檔的 selector 搆不到 shadow 內(`data-parity` 屬性不受限,照常可用)。
|
|
192
|
+
|
|
193
|
+
## 配對策略(以設計端為錨)
|
|
194
|
+
|
|
195
|
+
1. **自動文字錨定**:設計 TEXT 文字 ↔ 頁面文字(唯一才配;多個同文字時用圖層名消歧,仍不硬湊)
|
|
196
|
+
2. **圖層名 ↔ id / class / aria-label**:`CTA Button` 自動對上 `class="cta-button"`
|
|
197
|
+
3. **容器推論**:配不到的容器,用「已配對子孫的最近共同祖先」反推(純結構,不猜)
|
|
198
|
+
4. **手動補漏**:HTML 加 `data-parity="圖層名"`,或 `parity.map.json` 寫 `{ "圖層名": "CSS selector" }`
|
|
199
|
+
5. 配不到 → **誠實列進未配對清單**(真正需要人補的才補)
|
|
200
|
+
|
|
201
|
+
## 架構
|
|
202
|
+
|
|
203
|
+
一顆引擎(`Parity.Engine`,純函式庫)+ 多個外殼。引擎只比對「兩棵正規化的樹」:
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
IDesignSource ──→ DesignNode 樹 ──┐
|
|
207
|
+
(Figma / JSON / 未來 XD、Sketch) ├─→ Normalizer → Matcher → DiffEngine → FidelityReport
|
|
208
|
+
IImplementationSource → RenderedNode ┘
|
|
209
|
+
(Playwright / 未來 WPF、桌面)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
src/Parity.Engine/ 引擎:唯一進入點 FidelityEngine
|
|
214
|
+
DesignSources/ IDesignSource + Figma(REST + 本機快取)/ Json
|
|
215
|
+
ImplementationSources/ IImplementationSource + Web(Playwright)
|
|
216
|
+
Comparison/ Normalizer / Matcher / DiffEngine / ColorDelta(CIEDE2000)
|
|
217
|
+
src/Parity.Cli/ dotnet tool 外殼:parity check / init / install-browser
|
|
218
|
+
tests/Parity.Tests/ 單元測試(含 CIEDE2000 標準測資)
|
|
219
|
+
samples/demo/ 離線示範:刻意做壞的頁面 + 設計 JSON
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## 本機報告 UI(M3)
|
|
223
|
+
|
|
224
|
+
```sh
|
|
225
|
+
parity serve --watch # http://127.0.0.1:4321,檔案變更自動重掃(SSE 即時更新)
|
|
226
|
+
parity map # 互動配對模式:點未配對圖層 → 點截圖上的元素 → 寫入 parity.map.json
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
- 落差清單(依嚴重度排序,精確數值 + 色票 + ΔE)
|
|
230
|
+
- 截圖疊框視圖:實線 = 實作框(顏色 = 嚴重度)、藍虛線 = 設計框、紅虛線 = 未配對
|
|
231
|
+
- **只綁 127.0.0.1**:報告含站點結構,不讓區網掃到
|
|
232
|
+
- UI 是零建置的靜態 SPA,dotnet tool 不需要 node 工具鏈
|
|
233
|
+
|
|
234
|
+
## 進 CI(M4)
|
|
235
|
+
|
|
236
|
+
Parity 的差異化就在「進 CI 把關」。這個 repo 本身就是一個 composite action:
|
|
237
|
+
|
|
238
|
+
```yaml
|
|
239
|
+
# .github/workflows/design-check.yml(你的專案)
|
|
240
|
+
name: Design fidelity
|
|
241
|
+
on: [pull_request]
|
|
242
|
+
jobs:
|
|
243
|
+
parity:
|
|
244
|
+
runs-on: ubuntu-latest
|
|
245
|
+
permissions:
|
|
246
|
+
contents: read
|
|
247
|
+
pull-requests: write # 讓 action 把還原度報告貼成 PR 留言
|
|
248
|
+
steps:
|
|
249
|
+
- uses: actions/checkout@v4
|
|
250
|
+
# 起你的站(或改成部署到 preview URL,讓 config 的 url 指過去)
|
|
251
|
+
- run: |
|
|
252
|
+
npm ci && npm run build
|
|
253
|
+
npm run preview & # 例:serve 在 localhost:8080
|
|
254
|
+
- uses: HSU-YU-MING/cornhsu-parity@v1
|
|
255
|
+
with:
|
|
256
|
+
config: parity.config.json
|
|
257
|
+
figma-token: ${{ secrets.FIGMA_TOKEN }} # 設計來源用本機 JSON 時可省略
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
行為:
|
|
261
|
+
- **PR 留言**:自動貼一則還原度報告(分數 + 落差表 + **建議修法**),同一則反覆更新不洗版——PM/reviewer 不用碰工具就看得到。
|
|
262
|
+
- **擋 PR**:落差超過 `gate.failOn` → exit 1 → PR 打紅(**留言會先貼、再擋**)。
|
|
263
|
+
- **artifact**:`report.json` + Markdown 報告上傳供下載。
|
|
264
|
+
|
|
265
|
+
action 輸入:`config` / `target` / `working-directory` / `version` / `figma-token` / `baseline`(回歸模式) / `comment`(關掉 PR 留言) / `upload-report`。
|
|
266
|
+
|
|
267
|
+
> action 透過 `dotnet tool install -g Cornhsu.Parity` 安裝,需先把套件發佈到 nuget.org(發佈是 release 步驟,尚未做)。本 repo 的 `.github/workflows/ci.yml` 則是**直接從原始碼建置**並跑離線示範自我把關,不依賴發佈。
|
|
268
|
+
|
|
269
|
+
## 回歸把關:baseline(M5)
|
|
270
|
+
|
|
271
|
+
已經有一堆落差的專案,不可能一開就「零落差才給過」。baseline 讓你**只擋新增/惡化**:
|
|
272
|
+
|
|
273
|
+
```sh
|
|
274
|
+
parity baseline save # 把當前落差 + 還原度分數存成基準快照(SQLite,存 parity.baseline.db,自動標 git commit)
|
|
275
|
+
parity check --baseline # 比對現況 vs 最新基準:只有「新增或惡化」才 GATE FAIL;並顯示分數走勢(基準 75 → 現在 83 ↑)
|
|
276
|
+
parity baseline list # 看歷史快照(含分數欄 = 還原度走勢,給 PM 看方向)
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
> **CI 要用 `--baseline`,記得 `git add parity.baseline.db` 一起 commit**——它刻意放 repo 根(不放 `.parity/`,那裡通常被 gitignore),否則 CI 找不到基準會靜默退回一般 gate。路徑可用 config `baselineFile` 改。
|
|
280
|
+
|
|
281
|
+
- **新增**(基準沒有、現在有)或**惡化**(嚴重度變高)→ exit 1 擋 PR
|
|
282
|
+
- **修好**(基準有、現在沒了)會列出來鼓勵;**不變**的既有落差不擋
|
|
283
|
+
- 適合漸進導入:先 `baseline save` 記錄現況,之後 CI 用 `check --baseline`,團隊只需「不要讓還原度更差」
|
|
284
|
+
|
|
285
|
+
儲存層是獨立的 `Parity.Storage`(EF Core + SQLite,`Pooling=False` 即時釋放檔案),引擎裡的 `BaselineComparer` 是純函式、可單元測試。
|
|
286
|
+
|
|
287
|
+
## 里程碑
|
|
288
|
+
|
|
289
|
+
- [x] **M1** 引擎 + CLI 雛形:設計端與實作端兩棵數值樹
|
|
290
|
+
- [x] **M2** 比對引擎:配對 + 數值 diff + 容差 + 未配對清單 + gate exit code
|
|
291
|
+
- [x] **M3** 本機報告 UI(`parity serve --watch`,Kestrel 綁 127.0.0.1)+ `parity map` 互動配對
|
|
292
|
+
- [x] **M4** GitHub Action:可重用 composite action(`action.yml`)+ 本 repo CI(build / test / 離線示範自我把關)
|
|
293
|
+
- [x] **M5** EF Core + SQLite baseline / 歷史(回歸把關 + 分數走勢)+ `ImageDesignSource`(圖片+標註+像素取樣)+ `parity snapshot`(凍結現況當基準)
|
|
294
|
+
|
|
295
|
+
> 未完成、已知盲點與下一步優先序見 [ROADMAP.md](ROADMAP.md);版本變更見 [CHANGELOG.md](CHANGELOG.md)。
|
|
296
|
+
- [ ] **M6**(選配)雲端外殼:公開網址掃描 + SSRF 防護
|
|
297
|
+
|
|
298
|
+
## 安全
|
|
299
|
+
|
|
300
|
+
- Figma token 走環境變數(`env:FIGMA_TOKEN`),不進 log、不進 URL(用 `X-Figma-Token` header)
|
|
301
|
+
- 抓過的 frame 存 `.parity/cache`(已 gitignore),重跑不再打 Figma、可離線比對
|
|
302
|
+
|
|
303
|
+
## License
|
|
304
|
+
|
|
305
|
+
MIT
|
package/bin/parity.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// npm 通路的啟動腳本。
|
|
3
|
+
//
|
|
4
|
+
// 為什麼要有這一層(而不是直接把執行檔當 bin):
|
|
5
|
+
// 1. 依平台挑出對應的子套件 —— npm 靠 optionalDependencies 的 os/cpu 只下載一份,
|
|
6
|
+
// 但仍需要在執行時解析出實際路徑。
|
|
7
|
+
// 2. 把 PLAYWRIGHT_NODEJS_PATH 指向「正在跑這支腳本的 Node」。
|
|
8
|
+
// Playwright for .NET 預設會自帶一份 Node(win-x64 是 88 MB),但 npm 的使用者
|
|
9
|
+
// 本來就有 Node —— 不指定的話等於是發一份 Node 給 Node 使用者。
|
|
10
|
+
// 指定之後每個平台包從 218 MB 降到 129 MB(壓縮後 85 MB → 53 MB)。
|
|
11
|
+
//
|
|
12
|
+
// 這支腳本刻意不做 postinstall 下載:那種做法會被 `npm ci --ignore-scripts` 擋掉,
|
|
13
|
+
// 而不少公司的 CI 預設就是關腳本的 —— 對一個主打「進 CI」的工具是致命的。
|
|
14
|
+
|
|
15
|
+
"use strict";
|
|
16
|
+
|
|
17
|
+
const { spawnSync } = require("node:child_process");
|
|
18
|
+
const path = require("node:path");
|
|
19
|
+
|
|
20
|
+
// 平台 → 子套件名 / 執行檔名
|
|
21
|
+
const TARGETS = {
|
|
22
|
+
"win32 x64": { pkg: "@cornhsu/parity-win32-x64", bin: "parity.exe" },
|
|
23
|
+
"linux x64": { pkg: "@cornhsu/parity-linux-x64", bin: "parity" },
|
|
24
|
+
"darwin x64": { pkg: "@cornhsu/parity-darwin-x64", bin: "parity" },
|
|
25
|
+
"darwin arm64": { pkg: "@cornhsu/parity-darwin-arm64", bin: "parity" },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const key = `${process.platform} ${process.arch}`;
|
|
29
|
+
const target = TARGETS[key];
|
|
30
|
+
|
|
31
|
+
if (!target) {
|
|
32
|
+
console.error(
|
|
33
|
+
`Cornhsu.Parity 沒有 ${key} 的預先建置版本。\n` +
|
|
34
|
+
`目前支援:${Object.keys(TARGETS).join("、")}\n` +
|
|
35
|
+
`其他平台請改用 .NET 版本:dotnet tool install -g Cornhsu.Parity`
|
|
36
|
+
);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let binary;
|
|
41
|
+
try {
|
|
42
|
+
// 從子套件的 package.json 反推安裝位置,不用猜 node_modules 的層級
|
|
43
|
+
// (pnpm / yarn 的實體結構與 npm 不同,require.resolve 才可靠)
|
|
44
|
+
const manifest = require.resolve(`${target.pkg}/package.json`);
|
|
45
|
+
binary = path.join(path.dirname(manifest), "bin", target.bin);
|
|
46
|
+
} catch {
|
|
47
|
+
console.error(
|
|
48
|
+
`找不到平台套件 ${target.pkg}。\n` +
|
|
49
|
+
`可能是安裝時加了 --no-optional,或安裝過程中斷。\n` +
|
|
50
|
+
`請重新安裝:npm install cornhsu-parity`
|
|
51
|
+
);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const result = spawnSync(binary, process.argv.slice(2), {
|
|
56
|
+
stdio: "inherit",
|
|
57
|
+
env: {
|
|
58
|
+
...process.env,
|
|
59
|
+
// 讓 Playwright 用使用者現成的 Node,平台包因此不必自帶一份
|
|
60
|
+
PLAYWRIGHT_NODEJS_PATH: process.env.PLAYWRIGHT_NODEJS_PATH || process.execPath,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (result.error) {
|
|
65
|
+
console.error(`無法執行 ${binary}:${result.error.message}`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 被訊號中斷時 status 會是 null;轉成慣例的 128+signal,別讓 CI 誤判為成功
|
|
70
|
+
if (result.status === null && result.signal) {
|
|
71
|
+
process.exit(128 + (require("node:os").constants.signals[result.signal] ?? 0));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
process.exit(result.status ?? 0);
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cornhsu-parity",
|
|
3
|
+
"version": "0.9.5",
|
|
4
|
+
"description": "Property-Level Design QA that can gate your CI. Compares real numeric values from your Figma file against the rendered UI and fails the PR when the implementation drifts from the design.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"figma",
|
|
7
|
+
"design-qa",
|
|
8
|
+
"property-level-design-qa",
|
|
9
|
+
"css-inspector",
|
|
10
|
+
"visual-regression",
|
|
11
|
+
"design-tokens",
|
|
12
|
+
"ui-testing",
|
|
13
|
+
"playwright",
|
|
14
|
+
"ci",
|
|
15
|
+
"pixel-perfect"
|
|
16
|
+
],
|
|
17
|
+
"homepage": "https://cornhsu.com/parity",
|
|
18
|
+
"bugs": "https://github.com/HSU-YU-MING/cornhsu-parity/issues",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/HSU-YU-MING/cornhsu-parity.git"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "許彧銘 Hsu Yu-Ming (https://cornhsu.com/)",
|
|
25
|
+
"type": "commonjs",
|
|
26
|
+
"bin": {
|
|
27
|
+
"parity": "bin/parity.js"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"bin/parity.js",
|
|
31
|
+
"README.md"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"optionalDependencies": {
|
|
37
|
+
"@cornhsu/parity-win32-x64": "0.9.5",
|
|
38
|
+
"@cornhsu/parity-linux-x64": "0.9.5",
|
|
39
|
+
"@cornhsu/parity-darwin-x64": "0.9.5",
|
|
40
|
+
"@cornhsu/parity-darwin-arm64": "0.9.5"
|
|
41
|
+
}
|
|
42
|
+
}
|