frameshot-mcp 0.7.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 CHANGED
@@ -46,6 +46,18 @@ Claude will automatically call `render_file`, `diff_component`, or `render_catal
46
46
 
47
47
  ---
48
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
55
+ ```
56
+
57
+ > First run installs Playwright's Chromium (~150MB). Subsequent renders are fast.
58
+
59
+ ---
60
+
49
61
  ## Render
50
62
 
51
63
  Point to a file. Get a screenshot. All imports resolved.
@@ -83,6 +95,46 @@ diff_component("src/components/MetricCard.tsx")
83
95
 
84
96
  ## Tools
85
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>
103
+
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
+ &nbsp;
113
+ <img src="docs/diff-after.png" alt="After" width="200" />
114
+ &nbsp;
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
+
86
138
  | Tool | What it does |
87
139
  |------|-------------|
88
140
  | `render_file` | **Render a file via Vite** — full dep resolution, props support |
@@ -122,24 +174,11 @@ diff_component("src/components/MetricCard.tsx")
122
174
 
123
175
  ---
124
176
 
125
- ## CLI
126
-
127
- No AI client required. Use frameshot standalone:
128
-
129
- ```bash
130
- npx frameshot render src/components/Button.tsx
131
- npx frameshot render src/Card.tsx --props '{"title":"Hello"}'
132
- npx frameshot catalog src/components/ --recursive
133
- npx frameshot diff src/components/Header.tsx
134
- ```
135
-
136
- Images display inline in iTerm2, Kitty, and Sixel terminals. Saved to `.frameshot/` by default.
137
-
138
- ---
139
-
140
177
  ## GitHub Action
141
178
 
142
- Free Chromatic alternative. Auto-posts before/after screenshots with diff on every PR:
179
+ Free Chromatic alternative. Auto-detects changed components and posts before/after screenshots on every PR.
180
+
181
+ Add this file to your repo:
143
182
 
144
183
  ```yaml
145
184
  # .github/workflows/visual-preview.yml
@@ -149,14 +188,43 @@ jobs:
149
188
  preview:
150
189
  runs-on: ubuntu-latest
151
190
  steps:
152
- - uses: actions/checkout@v4
191
+ - uses: actions/checkout@v7
153
192
  with:
154
193
  fetch-depth: 0
155
- - uses: kamegoro/frameshot@main
156
- with:
157
- paths: "./src/components/*.tsx"
194
+ - uses: kamegoro/frameshot@v0.8.0
158
195
  ```
159
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
224
+ ```
225
+
226
+ Images display inline in iTerm2, Kitty, and Sixel terminals. Saved to `.frameshot/` by default.
227
+
160
228
  ---
161
229
 
162
230
  ## Performance
package/action.yml CHANGED
@@ -6,8 +6,17 @@ branding:
6
6
 
7
7
  inputs:
8
8
  paths:
9
- description: "Glob patterns for component files to render (newline or comma separated)"
10
- required: true
9
+ description: "Glob patterns for component files to render. Leave empty to auto-detect changed component files."
10
+ required: false
11
+ default: ""
12
+ extensions:
13
+ description: "Comma-separated file extensions to treat as components. Default covers React, Vue, Svelte, Astro, MDX."
14
+ required: false
15
+ default: ".jsx,.tsx,.vue,.svelte,.astro,.mdx"
16
+ exclude:
17
+ description: "Patterns to exclude (comma-separated). Matches against full file path."
18
+ required: false
19
+ default: "*.test.*,*.spec.*,*.stories.*,*.story.*"
11
20
  width:
12
21
  description: "Viewport width in px. Leave empty for auto-fit (recommended)."
13
22
  required: false
@@ -35,22 +44,42 @@ runs:
35
44
 
36
45
  - name: Install frameshot
37
46
  shell: bash
38
- run: npm install -g frameshot-mcp@latest
47
+ run: |
48
+ cd "${{ github.action_path }}"
49
+ npm install --prefer-offline frameshot-mcp@latest
39
50
 
40
51
  - name: Install Playwright Chromium
41
52
  shell: bash
42
- run: npx playwright install chromium
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
43
69
 
44
70
  - name: Render changed components
45
71
  id: render
46
72
  shell: bash
47
73
  env:
48
74
  INPUT_PATHS: ${{ inputs.paths }}
75
+ INPUT_EXTENSIONS: ${{ inputs.extensions }}
76
+ INPUT_EXCLUDE: ${{ inputs.exclude }}
49
77
  INPUT_WIDTH: ${{ inputs.width }}
50
78
  INPUT_HEIGHT: ${{ inputs.height }}
51
79
  INPUT_BASE_REF: ${{ github.event.pull_request.base.sha }}
52
80
  run: |
53
- node "${{ github.action_path }}/scripts/render-changed.mjs"
81
+ cd "${{ github.action_path }}"
82
+ node scripts/render-changed.mjs
54
83
 
55
84
  - name: Comment on PR
56
85
  if: inputs.comment == 'true' && github.event_name == 'pull_request'
@@ -81,9 +110,60 @@ runs:
81
110
  return;
82
111
  }
83
112
 
84
- let body = '## 📸 frameshot Visual Preview\n\n';
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
+ }
85
164
 
86
165
  const hasBeforeAfter = manifest.some(m => m.hasBefore);
166
+ let body = '## 📸 frameshot — Visual Preview\n\n';
87
167
 
88
168
  if (hasBeforeAfter) {
89
169
  body += '| Component | Before | After | Diff |\n|---|---|---|---|\n';
@@ -95,23 +175,32 @@ runs:
95
175
  const afterFile = path.join(dir, `${entry.name}_after.png`);
96
176
  if (!fs.existsSync(afterFile)) continue;
97
177
 
98
- const afterData = fs.readFileSync(afterFile, 'base64');
99
-
100
178
  if (hasBeforeAfter) {
101
179
  const beforeFile = path.join(dir, `${entry.name}_before.png`);
102
180
  const diffFile = path.join(dir, `${entry.name}_diff.png`);
103
181
 
104
- const beforeImg = fs.existsSync(beforeFile)
105
- ? `<img src="data:image/png;base64,${fs.readFileSync(beforeFile, 'base64')}" width="250" />`
106
- : '*(new)*';
107
-
108
- const diffImg = fs.existsSync(diffFile) && entry.diffPercentage > 0
109
- ? `<img src="data:image/png;base64,${fs.readFileSync(diffFile, 'base64')}" width="250" /><br/>${entry.diffPercentage.toFixed(1)}% changed`
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`
110
195
  : entry.hasBefore ? '✅ No change' : '—';
111
196
 
112
- body += `| \`${entry.name}\` | ${beforeImg} | <img src="data:image/png;base64,${afterData}" width="250" /> | ${diffImg} |\n`;
197
+ body += `| \`${entry.name}\` | ${beforeCell} | ${afterCell} | ${diffCell} |\n`;
113
198
  } else {
114
- body += `| \`${entry.name}\` | <img src="data:image/png;base64,${afterData}" width="400" /> |\n`;
199
+ const afterUrl = await uploadImageToGitHub(afterFile);
200
+ const afterCell = afterUrl
201
+ ? `<img src="${afterUrl}" width="360" />`
202
+ : '*(unavailable)*';
203
+ body += `| \`${entry.name}\` | ${afterCell} |\n`;
115
204
  }
116
205
  }
117
206