frameshot-mcp 0.6.0 → 0.8.0
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 +214 -89
- package/action.yml +139 -36
- package/dist/chunk-3CDSNOX5.js +869 -0
- package/dist/chunk-3LVWVDET.js +849 -0
- package/dist/chunk-67JZQ6OI.js +819 -0
- package/dist/chunk-AUACBLHM.js +191 -0
- package/dist/chunk-AZCGKIMU.js +850 -0
- package/dist/chunk-B3CLIGWU.js +786 -0
- package/dist/chunk-C6QSY4WR.js +811 -0
- package/dist/chunk-DX54PJKO.js +603 -0
- package/dist/chunk-EMCJGIMY.js +984 -0
- package/dist/chunk-FQNWGR62.js +849 -0
- package/dist/chunk-FTYTZW6D.js +203 -0
- package/dist/chunk-GVOEFYEX.js +139 -0
- package/dist/chunk-JGVKYXY2.js +857 -0
- package/dist/chunk-JYPEA4P2.js +846 -0
- package/dist/chunk-KHK35HDD.js +855 -0
- package/dist/chunk-L2CADTS7.js +191 -0
- package/dist/chunk-MA3FOIQY.js +157 -0
- package/dist/chunk-O7NWWFIU.js +871 -0
- package/dist/chunk-PYWXJZTZ.js +1123 -0
- package/dist/chunk-Q7A3DLED.js +848 -0
- package/dist/chunk-Q7NQA4ZM.js +1095 -0
- package/dist/chunk-SIA6XEHM.js +811 -0
- package/dist/chunk-ST35YDI6.js +834 -0
- package/dist/chunk-T5OBJK35.js +855 -0
- package/dist/chunk-U3GHS7KO.js +837 -0
- package/dist/chunk-WS2ASCD6.js +683 -0
- package/dist/chunk-WZMHVSUA.js +847 -0
- package/dist/chunk-ZZST6K7Y.js +987 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +392 -0
- package/dist/index.js +167 -50
- package/dist/renderer.d.ts +73 -11
- package/dist/renderer.js +7 -3
- package/package.json +16 -5
- package/scripts/render-changed.mjs +249 -39
package/README.md
CHANGED
|
@@ -1,35 +1,21 @@
|
|
|
1
1
|
# frameshot
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/frameshot-mcp) [](https://www.npmjs.com/package/frameshot-mcp) [](https://github.com/kamegoro/frameshot) [](https://www.npmjs.com/package/frameshot-mcp) [](https://www.npmjs.com/package/frameshot-mcp) [](https://github.com/kamegoro/frameshot) [](https://opensource.org/licenses/MIT)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**Give your AI agent eyes.** Render any component — with real imports, Tailwind, and CSS Modules resolved via Vite — directly in your terminal or AI chat. No stories. No config. Just point at a file.
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<img src="docs/demo.
|
|
8
|
+
<img src="docs/demo.gif" alt="frameshot demo" width="720" />
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
AI coding agents (Claude Code, Cursor, Copilot, Cline) write UI code **blind**. They generate HTML/CSS/React but never see the rendered result. You end up:
|
|
14
|
-
|
|
15
|
-
- Switching to the browser to check every change
|
|
16
|
-
- Screenshotting and pasting back into chat
|
|
17
|
-
- Burning tokens on fix loops that never converge
|
|
18
|
-
|
|
19
|
-
**frameshot closes this loop.** The agent calls one tool, gets a screenshot back in 120ms, and self-corrects.
|
|
20
|
-
|
|
21
|
-
```
|
|
22
|
-
AI writes code → frameshot renders → AI sees result → AI self-corrects → ships
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
## Install
|
|
11
|
+
**Claude Code**
|
|
26
12
|
|
|
27
13
|
```bash
|
|
28
14
|
claude mcp add frameshot -- npx frameshot-mcp@latest
|
|
29
15
|
```
|
|
30
16
|
|
|
31
17
|
<details>
|
|
32
|
-
<summary>Cursor
|
|
18
|
+
<summary>Cursor · VS Code · Windsurf · Cline</summary>
|
|
33
19
|
|
|
34
20
|
```json
|
|
35
21
|
{
|
|
@@ -44,72 +30,155 @@ claude mcp add frameshot -- npx frameshot-mcp@latest
|
|
|
44
30
|
|
|
45
31
|
</details>
|
|
46
32
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
### Example
|
|
71
|
-
|
|
72
|
-
```typescript
|
|
73
|
-
// AI renders its own output to verify
|
|
74
|
-
render_component({
|
|
75
|
-
code: `function App() {
|
|
76
|
-
return (
|
|
77
|
-
<div className="p-8 bg-gradient-to-r from-blue-500 to-purple-600 text-white rounded-2xl shadow-xl">
|
|
78
|
-
<h1 className="text-3xl font-bold">Pricing</h1>
|
|
79
|
-
<p className="text-5xl mt-4">$29<span className="text-lg">/mo</span></p>
|
|
80
|
-
</div>
|
|
81
|
-
)
|
|
82
|
-
}`,
|
|
83
|
-
framework: "react"
|
|
84
|
-
})
|
|
85
|
-
// → Screenshot returned in 118ms. AI can see it looks correct.
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Using with Claude
|
|
36
|
+
|
|
37
|
+
Once installed, just tell Claude what you want:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
"Render src/components/PricingCard.tsx and check the layout looks right"
|
|
41
|
+
"Refactor the dashboard to use CSS grid — render it before and after and make sure nothing broke"
|
|
42
|
+
"Show me all components in src/components/ as screenshots"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Claude will automatically call `render_file`, `diff_component`, or `render_catalog` as needed. No manual tool calls required.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Try it now
|
|
50
|
+
|
|
51
|
+
No AI client required. Works with any React / Vue / Svelte project:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npx frameshot render src/components/Button.tsx
|
|
86
55
|
```
|
|
87
56
|
|
|
88
|
-
|
|
57
|
+
> First run installs Playwright's Chromium (~150MB). Subsequent renders are fast.
|
|
89
58
|
|
|
90
|
-
|
|
91
|
-
|---|-----------|-----------------|-------------|---------------|
|
|
92
|
-
| Setup | stories + addons + server | SaaS signup + billing | Browser install | **`npx` — done** |
|
|
93
|
-
| Speed | Dev server startup | Cloud round-trip | 2-5s | **~120ms** |
|
|
94
|
-
| Cost | Free (stories = labor) | $149-800+/mo | Free | **Free forever** |
|
|
95
|
-
| AI-native | Needs pre-written stories | No MCP support | Full-page only | **Any code snippet** |
|
|
96
|
-
| Frameworks | React (+ addons) | Whatever Storybook supports | HTML only | **React/Vue/Svelte/HTML** |
|
|
97
|
-
| Cross-browser | Chromatic ($$$) | Per-snapshot pricing | Single browser | **3 engines, free** |
|
|
59
|
+
---
|
|
98
60
|
|
|
99
|
-
##
|
|
61
|
+
## Render
|
|
100
62
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
63
|
+
Point to a file. Get a screenshot. All imports resolved.
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
render_file("src/components/Dashboard.tsx")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
<p align="center">
|
|
70
|
+
<img src="docs/example-output.png" alt="Rendered PricingCard with full Tailwind styling" width="280" />
|
|
71
|
+
</p>
|
|
72
|
+
|
|
73
|
+
Real Tailwind. Real CSS Modules. Real dependencies — not a CDN polyfill.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Visual diff
|
|
78
|
+
|
|
79
|
+
Catch regressions before you ship. Before / after / changed pixels — all at once.
|
|
80
|
+
|
|
81
|
+
<p align="center">
|
|
82
|
+
<img src="docs/diff-before.png" alt="Before" width="210" />
|
|
83
|
+
|
|
84
|
+
<img src="docs/diff-after.png" alt="After" width="210" />
|
|
85
|
+
|
|
86
|
+
<img src="docs/diff-result.png" alt="Diff" width="210" />
|
|
87
|
+
</p>
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
diff_component("src/components/MetricCard.tsx")
|
|
91
|
+
→ 0.7% changed
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Tools
|
|
97
|
+
|
|
98
|
+
### `render_file` — Render a component with full dependency resolution
|
|
99
|
+
|
|
100
|
+
<p align="center">
|
|
101
|
+
<img src="docs/example-output.png" alt="Rendered PricingCard" width="280" />
|
|
102
|
+
</p>
|
|
107
103
|
|
|
108
|
-
|
|
104
|
+
Real Tailwind. Real CSS Modules. Real imports — resolved by Vite.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
### `diff_component` — Visual diff against git HEAD
|
|
109
|
+
|
|
110
|
+
<p align="center">
|
|
111
|
+
<img src="docs/diff-before.png" alt="Before" width="200" />
|
|
112
|
+
|
|
113
|
+
<img src="docs/diff-after.png" alt="After" width="200" />
|
|
114
|
+
|
|
115
|
+
<img src="docs/diff-result.png" alt="Diff" width="200" />
|
|
116
|
+
</p>
|
|
117
|
+
|
|
118
|
+
Pixel-accurate before/after comparison. Returns diff percentage.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
### `render_catalog` — Screenshot every component in a directory
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npx frameshot catalog src/components/
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Renders all components at once and saves PNGs to the output directory.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
### `audit_a11y` — Accessibility audit with axe-core
|
|
133
|
+
|
|
134
|
+
Returns WCAG violations with severity and selector. No browser extension needed.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
| Tool | What it does |
|
|
139
|
+
|------|-------------|
|
|
140
|
+
| `render_file` | **Render a file via Vite** — full dep resolution, props support |
|
|
141
|
+
| `render_component` | Render a self-contained snippet (React / Vue / Svelte / HTML) |
|
|
142
|
+
| `screenshot_url` | Screenshot any URL — localhost, staging, prod |
|
|
143
|
+
| `diff_component` | Pixel diff before/after with % changed |
|
|
144
|
+
| `render_responsive` | Mobile + tablet + desktop in one call |
|
|
145
|
+
| `render_theme` | Light + dark mode side by side |
|
|
146
|
+
| `audit_a11y` | axe-core accessibility audit (WCAG) |
|
|
147
|
+
| `render_catalog` | Render every component in a directory |
|
|
148
|
+
|
|
149
|
+
<details>
|
|
150
|
+
<summary>All 18 tools</summary>
|
|
151
|
+
|
|
152
|
+
| Tool | What it does |
|
|
153
|
+
|------|-------------|
|
|
154
|
+
| `render_file` | Render a project file with full Vite dependency resolution |
|
|
155
|
+
| `render_component` | Render a self-contained snippet → screenshot |
|
|
156
|
+
| `screenshot_url` | Screenshot any URL with retry and network idle wait |
|
|
157
|
+
| `render_responsive` | Mobile + tablet + desktop in one call |
|
|
158
|
+
| `render_variants` | Multiple prop/state variants at once |
|
|
159
|
+
| `render_theme` | Light + dark mode side by side |
|
|
160
|
+
| `render_interaction` | Simulate click/hover/type, then screenshot |
|
|
161
|
+
| `render_grid` | Multiple snippets in a labeled grid |
|
|
162
|
+
| `render_matrix` | Viewport × theme matrix in one call |
|
|
163
|
+
| `capture_animation` | Multi-frame CSS animation capture |
|
|
164
|
+
| `diff_component` | Before/after pixel diff with % changed |
|
|
165
|
+
| `diff_reference` | Compare render against a reference image (Figma QA) |
|
|
166
|
+
| `audit_a11y` | axe-core accessibility audit |
|
|
167
|
+
| `perf_audit` | DOM count, depth, render timing |
|
|
168
|
+
| `render_catalog` | Render all components in a directory |
|
|
169
|
+
| `snapshot_save` | Save a render as named baseline |
|
|
170
|
+
| `snapshot_check` | Compare current render against saved baseline |
|
|
171
|
+
| `snapshot_list` | List all saved snapshots |
|
|
172
|
+
|
|
173
|
+
</details>
|
|
174
|
+
|
|
175
|
+
---
|
|
109
176
|
|
|
110
177
|
## GitHub Action
|
|
111
178
|
|
|
112
|
-
|
|
179
|
+
Free Chromatic alternative. Auto-detects changed components and posts before/after screenshots on every PR.
|
|
180
|
+
|
|
181
|
+
Add this file to your repo:
|
|
113
182
|
|
|
114
183
|
```yaml
|
|
115
184
|
# .github/workflows/visual-preview.yml
|
|
@@ -119,29 +188,85 @@ jobs:
|
|
|
119
188
|
preview:
|
|
120
189
|
runs-on: ubuntu-latest
|
|
121
190
|
steps:
|
|
122
|
-
- uses: actions/checkout@
|
|
123
|
-
- uses: kamegoro/frameshot@main
|
|
191
|
+
- uses: actions/checkout@v7
|
|
124
192
|
with:
|
|
125
|
-
|
|
126
|
-
|
|
193
|
+
fetch-depth: 0
|
|
194
|
+
- uses: kamegoro/frameshot@v0.8.0
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
That's it. No config needed — changed `.tsx`, `.jsx`, `.vue`, `.svelte` files are detected automatically.
|
|
198
|
+
|
|
199
|
+
Optionally scope to specific paths or customize which files are included:
|
|
200
|
+
|
|
201
|
+
```yaml
|
|
202
|
+
- uses: kamegoro/frameshot@v0.8.0
|
|
203
|
+
with:
|
|
204
|
+
# Render only these files (default: auto-detect changed components)
|
|
205
|
+
paths: "./src/components/*.tsx"
|
|
206
|
+
|
|
207
|
+
# File extensions to treat as components
|
|
208
|
+
# Default: .jsx,.tsx,.vue,.svelte,.astro,.mdx
|
|
209
|
+
extensions: ".jsx,.tsx,.vue"
|
|
210
|
+
|
|
211
|
+
# Patterns to exclude (matched against filename)
|
|
212
|
+
# Default: *.test.*,*.spec.*,*.stories.*,*.story.*
|
|
213
|
+
exclude: "*.test.*,*.spec.*,*.stories.*"
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## CLI — advanced usage
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
npx frameshot render src/Card.tsx --props '{"title":"Hello"}'
|
|
222
|
+
npx frameshot catalog src/components/ --recursive
|
|
223
|
+
npx frameshot diff src/components/Header.tsx
|
|
127
224
|
```
|
|
128
225
|
|
|
129
|
-
|
|
226
|
+
Images display inline in iTerm2, Kitty, and Sixel terminals. Saved to `.frameshot/` by default.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Performance
|
|
231
|
+
|
|
232
|
+
Typical measured times (varies by machine and project size):
|
|
233
|
+
|
|
234
|
+
| Scenario | Time |
|
|
235
|
+
|----------|------|
|
|
236
|
+
| Warm render (Vite server cached) | **~200–500ms** |
|
|
237
|
+
| CDN fallback (no Vite) | **~120ms** |
|
|
238
|
+
| Cold start (first render) | ~1–3s |
|
|
239
|
+
| Subsequent renders (same session) | ~200ms |
|
|
240
|
+
|
|
241
|
+
Vite server is cached per project root. Browser pool stays warm between MCP calls.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## vs. alternatives
|
|
130
246
|
|
|
131
|
-
|
|
247
|
+
| | Storybook | Chromatic | Browser MCP | **frameshot** |
|
|
248
|
+
|---|-----------|-----------|-------------|---------------|
|
|
249
|
+
| Setup | Stories + config | SaaS signup | Browser install | **`npx` — done** |
|
|
250
|
+
| Speed | Dev server startup | Cloud round-trip | 2–5s | **~200ms warm** |
|
|
251
|
+
| Cost | Free (labor cost) | $149–800+/mo | Free | **Free forever** |
|
|
252
|
+
| Stories needed | Yes | Yes | No | **No** |
|
|
253
|
+
| Real imports | Via Storybook | Via Storybook | No | **Via Vite** |
|
|
254
|
+
| Works offline | Yes | No | Yes | **Yes** |
|
|
255
|
+
| AI-native (MCP) | No | No | Full-page only | **Component-level** |
|
|
132
256
|
|
|
133
|
-
|
|
134
|
-
- [Cursor rules](examples/cursor-rules.md) — Verify UI on every edit
|
|
257
|
+
---
|
|
135
258
|
|
|
136
259
|
## Development
|
|
137
260
|
|
|
138
261
|
```bash
|
|
139
|
-
git clone https://github.com/kamegoro/frameshot.git
|
|
140
|
-
|
|
262
|
+
git clone https://github.com/kamegoro/frameshot.git
|
|
263
|
+
cd frameshot && npm install
|
|
264
|
+
npx playwright install chromium
|
|
265
|
+
npm run build && npm test
|
|
141
266
|
```
|
|
142
267
|
|
|
143
|
-
See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
|
268
|
+
See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for architecture details.
|
|
144
269
|
|
|
145
|
-
|
|
270
|
+
---
|
|
146
271
|
|
|
147
|
-
MIT
|
|
272
|
+
MIT © [kamegoro](https://github.com/kamegoro)
|
package/action.yml
CHANGED
|
@@ -1,33 +1,30 @@
|
|
|
1
1
|
name: "frameshot — Visual Preview"
|
|
2
|
-
description: "Render components and attach before/after screenshots to PRs.
|
|
2
|
+
description: "Render changed components with full Vite dependency resolution and attach before/after screenshots to PRs."
|
|
3
3
|
branding:
|
|
4
4
|
icon: "camera"
|
|
5
5
|
color: "blue"
|
|
6
6
|
|
|
7
7
|
inputs:
|
|
8
8
|
paths:
|
|
9
|
-
description: "Glob patterns for component files to render
|
|
10
|
-
required: true
|
|
11
|
-
framework:
|
|
12
|
-
description: "Framework: react, vue, svelte, or html"
|
|
9
|
+
description: "Glob patterns for component files to render. Leave empty to auto-detect changed component files."
|
|
13
10
|
required: false
|
|
14
|
-
default: "
|
|
15
|
-
|
|
16
|
-
description: "
|
|
11
|
+
default: ""
|
|
12
|
+
extensions:
|
|
13
|
+
description: "Comma-separated file extensions to treat as components. Default covers React, Vue, Svelte, Astro, MDX."
|
|
17
14
|
required: false
|
|
18
|
-
default: "
|
|
19
|
-
|
|
20
|
-
description: "
|
|
15
|
+
default: ".jsx,.tsx,.vue,.svelte,.astro,.mdx"
|
|
16
|
+
exclude:
|
|
17
|
+
description: "Patterns to exclude (comma-separated). Matches against full file path."
|
|
21
18
|
required: false
|
|
22
|
-
default: "
|
|
23
|
-
|
|
24
|
-
description: "
|
|
19
|
+
default: "*.test.*,*.spec.*,*.stories.*,*.story.*"
|
|
20
|
+
width:
|
|
21
|
+
description: "Viewport width in px. Leave empty for auto-fit (recommended)."
|
|
25
22
|
required: false
|
|
26
|
-
default: "
|
|
27
|
-
|
|
28
|
-
description: "
|
|
23
|
+
default: ""
|
|
24
|
+
height:
|
|
25
|
+
description: "Viewport height in px. Leave empty for auto-fit (recommended)."
|
|
29
26
|
required: false
|
|
30
|
-
default: "
|
|
27
|
+
default: ""
|
|
31
28
|
comment:
|
|
32
29
|
description: "Post a PR comment with screenshots"
|
|
33
30
|
required: false
|
|
@@ -41,30 +38,48 @@ runs:
|
|
|
41
38
|
using: "composite"
|
|
42
39
|
steps:
|
|
43
40
|
- name: Setup Node.js
|
|
44
|
-
uses: actions/setup-node@
|
|
41
|
+
uses: actions/setup-node@v6
|
|
45
42
|
with:
|
|
46
43
|
node-version: "20"
|
|
47
44
|
|
|
48
45
|
- name: Install frameshot
|
|
49
46
|
shell: bash
|
|
50
|
-
run:
|
|
47
|
+
run: |
|
|
48
|
+
cd "${{ github.action_path }}"
|
|
49
|
+
npm install --prefer-offline frameshot-mcp@latest
|
|
51
50
|
|
|
52
51
|
- name: Install Playwright Chromium
|
|
53
52
|
shell: bash
|
|
54
|
-
run:
|
|
53
|
+
run: |
|
|
54
|
+
cd "${{ github.action_path }}"
|
|
55
|
+
npx playwright install chromium
|
|
56
|
+
|
|
57
|
+
- name: Install project dependencies (for Vite pipeline)
|
|
58
|
+
shell: bash
|
|
59
|
+
run: |
|
|
60
|
+
# Install deps in any subdirectory that has vite as a dependency
|
|
61
|
+
for pkg in $(find "$GITHUB_WORKSPACE" -name "package.json" -not -path "*/node_modules/*" -maxdepth 3); do
|
|
62
|
+
dir=$(dirname "$pkg")
|
|
63
|
+
if grep -q '"vite"' "$pkg" 2>/dev/null; then
|
|
64
|
+
echo "Installing deps in $dir"
|
|
65
|
+
cd "$dir" && npm install --legacy-peer-deps 2>/dev/null || true
|
|
66
|
+
cd "$GITHUB_WORKSPACE"
|
|
67
|
+
fi
|
|
68
|
+
done
|
|
55
69
|
|
|
56
70
|
- name: Render changed components
|
|
57
71
|
id: render
|
|
58
72
|
shell: bash
|
|
59
73
|
env:
|
|
60
74
|
INPUT_PATHS: ${{ inputs.paths }}
|
|
61
|
-
|
|
75
|
+
INPUT_EXTENSIONS: ${{ inputs.extensions }}
|
|
76
|
+
INPUT_EXCLUDE: ${{ inputs.exclude }}
|
|
62
77
|
INPUT_WIDTH: ${{ inputs.width }}
|
|
63
78
|
INPUT_HEIGHT: ${{ inputs.height }}
|
|
64
|
-
|
|
65
|
-
INPUT_TAILWIND_VERSION: ${{ inputs.tailwind-version }}
|
|
79
|
+
INPUT_BASE_REF: ${{ github.event.pull_request.base.sha }}
|
|
66
80
|
run: |
|
|
67
|
-
|
|
81
|
+
cd "${{ github.action_path }}"
|
|
82
|
+
node scripts/render-changed.mjs
|
|
68
83
|
|
|
69
84
|
- name: Comment on PR
|
|
70
85
|
if: inputs.comment == 'true' && github.event_name == 'pull_request'
|
|
@@ -83,26 +98,114 @@ runs:
|
|
|
83
98
|
return;
|
|
84
99
|
}
|
|
85
100
|
|
|
86
|
-
const
|
|
87
|
-
if (
|
|
88
|
-
console.log('No
|
|
101
|
+
const manifestPath = path.join(dir, 'manifest.json');
|
|
102
|
+
if (!fs.existsSync(manifestPath)) {
|
|
103
|
+
console.log('No manifest found');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
108
|
+
if (manifest.length === 0) {
|
|
109
|
+
console.log('No components rendered');
|
|
89
110
|
return;
|
|
90
111
|
}
|
|
91
112
|
|
|
92
|
-
//
|
|
113
|
+
// Store image in repo on a dedicated branch and return raw URL.
|
|
114
|
+
// Uses a special orphan branch "frameshot-previews" as image storage.
|
|
115
|
+
async function uploadImageToGitHub(filePath) {
|
|
116
|
+
const bytes = fs.readFileSync(filePath);
|
|
117
|
+
const base64 = bytes.toString('base64');
|
|
118
|
+
const filename = `pr-${context.issue.number}-${path.basename(filePath)}-${Date.now()}.png`;
|
|
119
|
+
const BRANCH = 'frameshot-previews';
|
|
120
|
+
|
|
121
|
+
// Ensure branch exists
|
|
122
|
+
try {
|
|
123
|
+
await github.rest.repos.getBranch({
|
|
124
|
+
owner: context.repo.owner,
|
|
125
|
+
repo: context.repo.repo,
|
|
126
|
+
branch: BRANCH,
|
|
127
|
+
});
|
|
128
|
+
} catch {
|
|
129
|
+
// Branch doesn't exist — create it from main
|
|
130
|
+
try {
|
|
131
|
+
const { data: ref } = await github.rest.git.getRef({
|
|
132
|
+
owner: context.repo.owner,
|
|
133
|
+
repo: context.repo.repo,
|
|
134
|
+
ref: 'heads/main',
|
|
135
|
+
});
|
|
136
|
+
await github.rest.git.createRef({
|
|
137
|
+
owner: context.repo.owner,
|
|
138
|
+
repo: context.repo.repo,
|
|
139
|
+
ref: `refs/heads/${BRANCH}`,
|
|
140
|
+
sha: ref.object.sha,
|
|
141
|
+
});
|
|
142
|
+
} catch (e) {
|
|
143
|
+
console.log('Failed to create branch:', e.message);
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Commit the image file
|
|
149
|
+
try {
|
|
150
|
+
await github.rest.repos.createOrUpdateFileContents({
|
|
151
|
+
owner: context.repo.owner,
|
|
152
|
+
repo: context.repo.repo,
|
|
153
|
+
path: filename,
|
|
154
|
+
message: `Add PR #${context.issue.number} preview image`,
|
|
155
|
+
content: base64,
|
|
156
|
+
branch: BRANCH,
|
|
157
|
+
});
|
|
158
|
+
return `https://raw.githubusercontent.com/${context.repo.owner}/${context.repo.repo}/${BRANCH}/${filename}`;
|
|
159
|
+
} catch (e) {
|
|
160
|
+
console.log('Failed to commit image:', e.message);
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const hasBeforeAfter = manifest.some(m => m.hasBefore);
|
|
93
166
|
let body = '## 📸 frameshot — Visual Preview\n\n';
|
|
94
|
-
body += '| Component | Screenshot |\n|---|---|\n';
|
|
95
167
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
168
|
+
if (hasBeforeAfter) {
|
|
169
|
+
body += '| Component | Before | After | Diff |\n|---|---|---|---|\n';
|
|
170
|
+
} else {
|
|
171
|
+
body += '| Component | Screenshot |\n|---|---|\n';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (const entry of manifest) {
|
|
175
|
+
const afterFile = path.join(dir, `${entry.name}_after.png`);
|
|
176
|
+
if (!fs.existsSync(afterFile)) continue;
|
|
177
|
+
|
|
178
|
+
if (hasBeforeAfter) {
|
|
179
|
+
const beforeFile = path.join(dir, `${entry.name}_before.png`);
|
|
180
|
+
const diffFile = path.join(dir, `${entry.name}_diff.png`);
|
|
181
|
+
|
|
182
|
+
// Upload sequentially to avoid commit conflicts
|
|
183
|
+
const afterUrl = await uploadImageToGitHub(afterFile);
|
|
184
|
+
const beforeUrl = fs.existsSync(beforeFile) ? await uploadImageToGitHub(beforeFile) : null;
|
|
185
|
+
const diffUrl = fs.existsSync(diffFile) ? await uploadImageToGitHub(diffFile) : null;
|
|
186
|
+
|
|
187
|
+
const beforeCell = beforeUrl
|
|
188
|
+
? `<img src="${beforeUrl}" width="240" />`
|
|
189
|
+
: entry.hasBefore ? '*(before unavailable)*' : '*(new)*';
|
|
190
|
+
const afterCell = afterUrl
|
|
191
|
+
? `<img src="${afterUrl}" width="240" />`
|
|
192
|
+
: '*(after unavailable)*';
|
|
193
|
+
const diffCell = diffUrl && entry.diffPercentage > 0
|
|
194
|
+
? `<img src="${diffUrl}" width="240" /><br/>${entry.diffPercentage.toFixed(1)}% changed`
|
|
195
|
+
: entry.hasBefore ? '✅ No change' : '—';
|
|
196
|
+
|
|
197
|
+
body += `| \`${entry.name}\` | ${beforeCell} | ${afterCell} | ${diffCell} |\n`;
|
|
198
|
+
} else {
|
|
199
|
+
const afterUrl = await uploadImageToGitHub(afterFile);
|
|
200
|
+
const afterCell = afterUrl
|
|
201
|
+
? `<img src="${afterUrl}" width="360" />`
|
|
202
|
+
: '*(unavailable)*';
|
|
203
|
+
body += `| \`${entry.name}\` | ${afterCell} |\n`;
|
|
204
|
+
}
|
|
101
205
|
}
|
|
102
206
|
|
|
103
207
|
body += '\n---\n*Generated by [frameshot](https://github.com/kamegoro/frameshot)*';
|
|
104
208
|
|
|
105
|
-
// Find existing comment
|
|
106
209
|
const { data: comments } = await github.rest.issues.listComments({
|
|
107
210
|
owner: context.repo.owner,
|
|
108
211
|
repo: context.repo.repo,
|