frameshot-mcp 0.7.0 → 0.9.7

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.
Files changed (67) hide show
  1. package/README.md +83 -69
  2. package/action.yml +114 -16
  3. package/dist/chunk-MEBQ7ZWA.js +1774 -0
  4. package/dist/chunk-VUYZHZBH.js +157 -0
  5. package/dist/cli.js +131 -133
  6. package/dist/index.js +519 -572
  7. package/dist/renderer.d.ts +17 -7
  8. package/dist/renderer.js +4 -6
  9. package/dist/stubs/gatsby.js +20 -0
  10. package/dist/stubs/next-font.js +9 -0
  11. package/dist/stubs/next-headers.js +20 -0
  12. package/dist/stubs/next-image.js +34 -0
  13. package/dist/stubs/next-link.js +25 -0
  14. package/dist/stubs/next-navigation.js +17 -0
  15. package/dist/stubs/next-router.js +19 -0
  16. package/dist/stubs/nuxt-app.js +37 -0
  17. package/dist/stubs/nuxt-imports.js +13 -0
  18. package/dist/stubs/qwik-city.js +33 -0
  19. package/dist/stubs/react-router.js +67 -0
  20. package/dist/stubs/server-only.js +2 -0
  21. package/dist/stubs/solid-router.js +27 -0
  22. package/dist/stubs/solid-start.js +18 -0
  23. package/dist/stubs/stubs/gatsby.js +20 -0
  24. package/dist/stubs/stubs/next-font.js +9 -0
  25. package/dist/stubs/stubs/next-headers.js +20 -0
  26. package/dist/stubs/stubs/next-image.js +34 -0
  27. package/dist/stubs/stubs/next-link.js +25 -0
  28. package/dist/stubs/stubs/next-navigation.js +17 -0
  29. package/dist/stubs/stubs/next-router.js +19 -0
  30. package/dist/stubs/stubs/nuxt-app.js +37 -0
  31. package/dist/stubs/stubs/nuxt-imports.js +13 -0
  32. package/dist/stubs/stubs/qwik-city.js +33 -0
  33. package/dist/stubs/stubs/react-router.js +67 -0
  34. package/dist/stubs/stubs/server-only.js +2 -0
  35. package/dist/stubs/stubs/solid-router.js +27 -0
  36. package/dist/stubs/stubs/solid-start.js +18 -0
  37. package/dist/stubs/stubs/sveltekit-environment.js +5 -0
  38. package/dist/stubs/stubs/sveltekit-navigation.js +11 -0
  39. package/dist/stubs/stubs/sveltekit-stores.js +15 -0
  40. package/dist/stubs/stubs/vike.js +11 -0
  41. package/dist/stubs/sveltekit-environment.js +5 -0
  42. package/dist/stubs/sveltekit-navigation.js +11 -0
  43. package/dist/stubs/sveltekit-stores.js +15 -0
  44. package/dist/stubs/vike.js +11 -0
  45. package/package.json +10 -4
  46. package/scripts/render-changed.mjs +140 -18
  47. package/dist/chunk-3LVWVDET.js +0 -849
  48. package/dist/chunk-47YJG5HR.js +0 -690
  49. package/dist/chunk-67JZQ6OI.js +0 -819
  50. package/dist/chunk-AZCGKIMU.js +0 -850
  51. package/dist/chunk-B3CLIGWU.js +0 -786
  52. package/dist/chunk-C6QSY4WR.js +0 -811
  53. package/dist/chunk-DX54PJKO.js +0 -603
  54. package/dist/chunk-EMCJGIMY.js +0 -984
  55. package/dist/chunk-FQNWGR62.js +0 -849
  56. package/dist/chunk-FTYTZW6D.js +0 -203
  57. package/dist/chunk-JGVKYXY2.js +0 -857
  58. package/dist/chunk-JYPEA4P2.js +0 -846
  59. package/dist/chunk-KHK35HDD.js +0 -855
  60. package/dist/chunk-Q7A3DLED.js +0 -848
  61. package/dist/chunk-SIA6XEHM.js +0 -811
  62. package/dist/chunk-ST35YDI6.js +0 -834
  63. package/dist/chunk-T5OBJK35.js +0 -855
  64. package/dist/chunk-U3GHS7KO.js +0 -837
  65. package/dist/chunk-WS2ASCD6.js +0 -683
  66. package/dist/chunk-WZMHVSUA.js +0 -847
  67. package/dist/chunk-ZZST6K7Y.js +0 -987
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/frameshot-mcp)](https://www.npmjs.com/package/frameshot-mcp) [![npm downloads](https://img.shields.io/npm/dm/frameshot-mcp)](https://www.npmjs.com/package/frameshot-mcp) [![GitHub stars](https://img.shields.io/github/stars/kamegoro/frameshot)](https://github.com/kamegoro/frameshot) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
4
 
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.
5
+ **Give your AI agent eyes.** Render components with real Vite imports, Tailwind, and CSS Modules — right in your terminal or AI chat. No story files required.
6
6
 
7
7
  <p align="center">
8
8
  <img src="docs/demo.gif" alt="frameshot demo" width="720" />
@@ -32,70 +32,118 @@ claude mcp add frameshot -- npx frameshot-mcp@latest
32
32
 
33
33
  ---
34
34
 
35
- ## Using with Claude
35
+ ## Watch mode — live AI feedback loop
36
36
 
37
- Once installed, just tell Claude what you want:
37
+ The AI edits a file, saves it, and immediately sees how it looks — without you calling anything.
38
38
 
39
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"
40
+ watch_start({ patterns: ["src/components/Button.tsx"] })
41
+ renders on every save call watch_get_latest("watch-1") to see it
43
42
  ```
44
43
 
45
- Claude will automatically call `render_file`, `diff_component`, or `render_catalog` as needed. No manual tool calls required.
44
+ The agent fixes saves sees fixes again. You just watch.
46
45
 
47
46
  ---
48
47
 
49
- ## Render
48
+ ## GitHub Action — free Chromatic alternative
50
49
 
51
- Point to a file. Get a screenshot. All imports resolved.
50
+ Two lines. Auto-detects changed components. Posts before/after/diff screenshots directly in the PR comment.
52
51
 
53
- ```
54
- render_file("src/components/Dashboard.tsx")
52
+ ```yaml
53
+ - uses: actions/checkout@v7
54
+ with:
55
+ fetch-depth: 0
56
+ - uses: kamegoro/frameshot@v0.8.0
55
57
  ```
56
58
 
57
59
  <p align="center">
58
- <img src="docs/example-output.png" alt="Rendered PricingCard with full Tailwind styling" width="280" />
60
+ <img src="docs/diff-before.png" alt="Before" width="210" />
61
+ &nbsp;
62
+ <img src="docs/diff-after.png" alt="After" width="210" />
63
+ &nbsp;
64
+ <img src="docs/diff-result.png" alt="Diff" width="210" />
59
65
  </p>
60
66
 
61
- Real Tailwind. Real CSS Modules. Real dependencies not a CDN polyfill.
67
+ Changed `.tsx`, `.jsx`, `.vue`, `.svelte`, `.astro`, `.mdx` files detected automatically. No Storybook. No signup. Free forever.
68
+
69
+ <details>
70
+ <summary>Options</summary>
71
+
72
+ ```yaml
73
+ - uses: kamegoro/frameshot@v0.8.0
74
+ with:
75
+ paths: "./src/components/*.tsx" # default: auto-detect
76
+ extensions: ".jsx,.tsx,.vue" # default: .jsx,.tsx,.vue,.svelte,.astro,.mdx
77
+ exclude: "*.test.*,*.spec.*" # default: *.test.*,*.spec.*,*.stories.*
78
+ ```
79
+
80
+ </details>
62
81
 
63
82
  ---
64
83
 
65
- ## Visual diff
84
+ ## Render any component
66
85
 
67
- Catch regressions before you ship. Before / after / changed pixels — all at once.
86
+ Point to a file. Get a screenshot. Full Vite pipeline.
87
+
88
+ ```
89
+ render_file("src/components/Dashboard.tsx")
90
+ ```
68
91
 
69
92
  <p align="center">
70
- <img src="docs/diff-before.png" alt="Before" width="210" />
71
- &nbsp;
72
- <img src="docs/diff-after.png" alt="After" width="210" />
73
- &nbsp;
74
- <img src="docs/diff-result.png" alt="Diff" width="210" />
93
+ <img src="docs/example-output.png" alt="Rendered PricingCard with full Tailwind styling" width="280" />
75
94
  </p>
76
95
 
96
+ Resolves your project's real imports, Tailwind config, CSS Modules, and path aliases — not a CDN polyfill.
97
+
98
+ ---
99
+
100
+ ## Using with Claude
101
+
102
+ After installing, just describe what you want:
103
+
77
104
  ```
78
- diff_component("src/components/MetricCard.tsx")
79
- 0.7% changed
105
+ "Render src/components/PricingCard.tsx — does it look right?"
106
+ "Refactor the layout to CSS grid and show me before/after"
107
+ "Watch Button.tsx while I edit — tell me when something looks off"
80
108
  ```
81
109
 
110
+ Claude calls `render_file`, `diff_component`, `watch_start`, or any other tool automatically.
111
+
112
+ ---
113
+
114
+ ## Try it without an AI client
115
+
116
+ Works standalone on any React / Vue / Svelte project:
117
+
118
+ ```bash
119
+ npx frameshot render src/components/Button.tsx
120
+ npx frameshot diff src/components/Header.tsx
121
+ npx frameshot catalog src/components/ --recursive
122
+ ```
123
+
124
+ Images display inline in iTerm2, Kitty, and Sixel terminals. Saved to `.frameshot/` by default.
125
+
126
+ > First run installs Playwright's Chromium (~150MB). Subsequent renders are fast.
127
+
82
128
  ---
83
129
 
84
130
  ## Tools
85
131
 
86
132
  | Tool | What it does |
87
133
  |------|-------------|
88
- | `render_file` | **Render a file via Vite**full dep resolution, props support |
89
- | `render_component` | Render a self-contained snippet (React / Vue / Svelte / HTML) |
134
+ | `render_file` | Render a file via Vite — real imports, Tailwind, CSS Modules |
135
+ | `diff_component` | Pixel diff before/after % changed, highlighted pixels |
136
+ | `watch_start` | Watch files — auto-render on every save |
137
+ | `watch_get_latest` | Get the latest render from a watch session |
138
+ | `render_catalog` | Render every component in a directory at once |
90
139
  | `screenshot_url` | Screenshot any URL — localhost, staging, prod |
91
- | `diff_component` | Pixel diff before/after with % changed |
92
140
  | `render_responsive` | Mobile + tablet + desktop in one call |
93
141
  | `render_theme` | Light + dark mode side by side |
94
- | `audit_a11y` | axe-core accessibility audit (WCAG) |
95
- | `render_catalog` | Render every component in a directory |
142
+ | `audit_a11y` | axe-core accessibility audit (WCAG violations) |
143
+ | `render_component` | Render a self-contained code snippet |
96
144
 
97
145
  <details>
98
- <summary>All 18 tools</summary>
146
+ <summary>All 22 tools</summary>
99
147
 
100
148
  | Tool | What it does |
101
149
  |------|-------------|
@@ -117,52 +165,17 @@ diff_component("src/components/MetricCard.tsx")
117
165
  | `snapshot_save` | Save a render as named baseline |
118
166
  | `snapshot_check` | Compare current render against saved baseline |
119
167
  | `snapshot_list` | List all saved snapshots |
168
+ | `watch_start` | Start watching files — renders on every save |
169
+ | `watch_stop` | Stop a watch session |
170
+ | `watch_get_latest` | Get the latest rendered screenshot from a session |
171
+ | `watch_list` | List active watch sessions |
120
172
 
121
173
  </details>
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
- ## GitHub Action
141
-
142
- Free Chromatic alternative. Auto-posts before/after screenshots with diff on every PR:
143
-
144
- ```yaml
145
- # .github/workflows/visual-preview.yml
146
- name: Visual Preview
147
- on: [pull_request]
148
- jobs:
149
- preview:
150
- runs-on: ubuntu-latest
151
- steps:
152
- - uses: actions/checkout@v4
153
- with:
154
- fetch-depth: 0
155
- - uses: kamegoro/frameshot@main
156
- with:
157
- paths: "./src/components/*.tsx"
158
- ```
159
-
160
- ---
161
-
162
177
  ## Performance
163
178
 
164
- Typical measured times (varies by machine and project size):
165
-
166
179
  | Scenario | Time |
167
180
  |----------|------|
168
181
  | Warm render (Vite server cached) | **~200–500ms** |
@@ -181,9 +194,10 @@ Vite server is cached per project root. Browser pool stays warm between MCP call
181
194
  | Setup | Stories + config | SaaS signup | Browser install | **`npx` — done** |
182
195
  | Speed | Dev server startup | Cloud round-trip | 2–5s | **~200ms warm** |
183
196
  | Cost | Free (labor cost) | $149–800+/mo | Free | **Free forever** |
184
- | Stories needed | Yes | Yes | No | **No** |
185
- | Real imports | Via Storybook | Via Storybook | No | **Via Vite** |
197
+ | Story files needed | Yes | Yes | No | **No** |
198
+ | Real imports resolved | Via Storybook | Via Storybook | No | **Via Vite** |
186
199
  | Works offline | Yes | No | Yes | **Yes** |
200
+ | Watch mode | No | No | No | **Yes** |
187
201
  | AI-native (MCP) | No | No | Full-page only | **Component-level** |
188
202
 
189
203
  ---
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,51 @@ 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
+ # Install frameshot from the action's own checked-out source (this repo)
50
+ # so users get exactly the version pinned by their `uses:` tag,
51
+ # regardless of npm publish state.
52
+ if [ -f package.json ] && [ -d src ]; then
53
+ npm install --prefer-offline --no-audit --no-fund
54
+ npm run build
55
+ else
56
+ npm install --prefer-offline --no-audit --no-fund frameshot-mcp@latest
57
+ fi
39
58
 
40
59
  - name: Install Playwright Chromium
41
60
  shell: bash
42
- run: npx playwright install chromium
61
+ run: |
62
+ cd "${{ github.action_path }}"
63
+ npx playwright install chromium
64
+
65
+ - name: Install project dependencies (for Vite pipeline)
66
+ shell: bash
67
+ run: |
68
+ # Install deps in any directory that has a package.json with a build framework
69
+ # Supports Vite, Next.js, Nuxt, SvelteKit, Astro, etc.
70
+ for pkg in $(find "$GITHUB_WORKSPACE" -name "package.json" -not -path "*/node_modules/*" -maxdepth 3); do
71
+ dir=$(dirname "$pkg")
72
+ if grep -qE '"vite"|"next"|"nuxt"|"@sveltejs/kit"|"astro"' "$pkg" 2>/dev/null; then
73
+ echo "Installing deps in $dir"
74
+ cd "$dir" && npm install --legacy-peer-deps 2>/dev/null || true
75
+ cd "$GITHUB_WORKSPACE"
76
+ fi
77
+ done
43
78
 
44
79
  - name: Render changed components
45
80
  id: render
46
81
  shell: bash
47
82
  env:
48
83
  INPUT_PATHS: ${{ inputs.paths }}
84
+ INPUT_EXTENSIONS: ${{ inputs.extensions }}
85
+ INPUT_EXCLUDE: ${{ inputs.exclude }}
49
86
  INPUT_WIDTH: ${{ inputs.width }}
50
87
  INPUT_HEIGHT: ${{ inputs.height }}
51
88
  INPUT_BASE_REF: ${{ github.event.pull_request.base.sha }}
52
89
  run: |
53
- node "${{ github.action_path }}/scripts/render-changed.mjs"
90
+ cd "${{ github.action_path }}"
91
+ node scripts/render-changed.mjs
54
92
 
55
93
  - name: Comment on PR
56
94
  if: inputs.comment == 'true' && github.event_name == 'pull_request'
@@ -81,9 +119,60 @@ runs:
81
119
  return;
82
120
  }
83
121
 
84
- let body = '## 📸 frameshot Visual Preview\n\n';
122
+ // Store image in repo on a dedicated branch and return raw URL.
123
+ // Uses a special orphan branch "frameshot-previews" as image storage.
124
+ async function uploadImageToGitHub(filePath) {
125
+ const bytes = fs.readFileSync(filePath);
126
+ const base64 = bytes.toString('base64');
127
+ const filename = `pr-${context.issue.number}-${path.basename(filePath)}-${Date.now()}.png`;
128
+ const BRANCH = 'frameshot-previews';
129
+
130
+ // Ensure branch exists
131
+ try {
132
+ await github.rest.repos.getBranch({
133
+ owner: context.repo.owner,
134
+ repo: context.repo.repo,
135
+ branch: BRANCH,
136
+ });
137
+ } catch {
138
+ // Branch doesn't exist — create it from main
139
+ try {
140
+ const { data: ref } = await github.rest.git.getRef({
141
+ owner: context.repo.owner,
142
+ repo: context.repo.repo,
143
+ ref: 'heads/main',
144
+ });
145
+ await github.rest.git.createRef({
146
+ owner: context.repo.owner,
147
+ repo: context.repo.repo,
148
+ ref: `refs/heads/${BRANCH}`,
149
+ sha: ref.object.sha,
150
+ });
151
+ } catch (e) {
152
+ console.log('Failed to create branch:', e.message);
153
+ return null;
154
+ }
155
+ }
156
+
157
+ // Commit the image file
158
+ try {
159
+ await github.rest.repos.createOrUpdateFileContents({
160
+ owner: context.repo.owner,
161
+ repo: context.repo.repo,
162
+ path: filename,
163
+ message: `Add PR #${context.issue.number} preview image`,
164
+ content: base64,
165
+ branch: BRANCH,
166
+ });
167
+ return `https://raw.githubusercontent.com/${context.repo.owner}/${context.repo.repo}/${BRANCH}/${filename}`;
168
+ } catch (e) {
169
+ console.log('Failed to commit image:', e.message);
170
+ return null;
171
+ }
172
+ }
85
173
 
86
174
  const hasBeforeAfter = manifest.some(m => m.hasBefore);
175
+ let body = '## 📸 frameshot — Visual Preview\n\n';
87
176
 
88
177
  if (hasBeforeAfter) {
89
178
  body += '| Component | Before | After | Diff |\n|---|---|---|---|\n';
@@ -95,23 +184,32 @@ runs:
95
184
  const afterFile = path.join(dir, `${entry.name}_after.png`);
96
185
  if (!fs.existsSync(afterFile)) continue;
97
186
 
98
- const afterData = fs.readFileSync(afterFile, 'base64');
99
-
100
187
  if (hasBeforeAfter) {
101
188
  const beforeFile = path.join(dir, `${entry.name}_before.png`);
102
189
  const diffFile = path.join(dir, `${entry.name}_diff.png`);
103
190
 
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`
191
+ // Upload sequentially to avoid commit conflicts
192
+ const afterUrl = await uploadImageToGitHub(afterFile);
193
+ const beforeUrl = fs.existsSync(beforeFile) ? await uploadImageToGitHub(beforeFile) : null;
194
+ const diffUrl = fs.existsSync(diffFile) ? await uploadImageToGitHub(diffFile) : null;
195
+
196
+ const beforeCell = beforeUrl
197
+ ? `<img src="${beforeUrl}" width="240" />`
198
+ : entry.hasBefore ? '*(before unavailable)*' : '*(new)*';
199
+ const afterCell = afterUrl
200
+ ? `<img src="${afterUrl}" width="240" />`
201
+ : '*(after unavailable)*';
202
+ const diffCell = diffUrl && entry.diffPercentage > 0
203
+ ? `<img src="${diffUrl}" width="240" /><br/>${entry.diffPercentage.toFixed(1)}% changed`
110
204
  : entry.hasBefore ? '✅ No change' : '—';
111
205
 
112
- body += `| \`${entry.name}\` | ${beforeImg} | <img src="data:image/png;base64,${afterData}" width="250" /> | ${diffImg} |\n`;
206
+ body += `| \`${entry.label ?? entry.name}\` | ${beforeCell} | ${afterCell} | ${diffCell} |\n`;
113
207
  } else {
114
- body += `| \`${entry.name}\` | <img src="data:image/png;base64,${afterData}" width="400" /> |\n`;
208
+ const afterUrl = await uploadImageToGitHub(afterFile);
209
+ const afterCell = afterUrl
210
+ ? `<img src="${afterUrl}" width="360" />`
211
+ : '*(unavailable)*';
212
+ body += `| \`${entry.label ?? entry.name}\` | ${afterCell} |\n`;
115
213
  }
116
214
  }
117
215