frameshot-mcp 0.3.0 → 0.7.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 +157 -47
- package/action.yml +144 -0
- package/dist/chunk-3LVWVDET.js +849 -0
- package/dist/chunk-47YJG5HR.js +690 -0
- package/dist/chunk-67JZQ6OI.js +819 -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-JGVKYXY2.js +857 -0
- package/dist/chunk-JYPEA4P2.js +846 -0
- package/dist/chunk-KHK35HDD.js +855 -0
- package/dist/chunk-Q7A3DLED.js +848 -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 +407 -0
- package/dist/index.js +1077 -653
- package/dist/renderer.d.ts +297 -0
- package/dist/renderer.js +34 -0
- package/package.json +32 -7
- package/scripts/render-changed.mjs +233 -0
- package/scripts/setup-labels.sh +48 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { Page } from 'playwright';
|
|
2
|
+
|
|
3
|
+
type Engine = "chromium" | "firefox" | "webkit";
|
|
4
|
+
type Framework = "html" | "react" | "vue" | "svelte";
|
|
5
|
+
type Theme = "light" | "dark";
|
|
6
|
+
interface Viewport {
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
}
|
|
10
|
+
interface RenderOptions {
|
|
11
|
+
viewport: Viewport;
|
|
12
|
+
deviceScaleFactor?: number;
|
|
13
|
+
engines: Engine[];
|
|
14
|
+
fullPage: boolean;
|
|
15
|
+
darkMode: boolean;
|
|
16
|
+
css: string;
|
|
17
|
+
tailwindVersion: "3" | "4";
|
|
18
|
+
waitFor: number;
|
|
19
|
+
autoFit: boolean;
|
|
20
|
+
}
|
|
21
|
+
interface ScreenshotResult {
|
|
22
|
+
engine: Engine;
|
|
23
|
+
image: string;
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
consoleErrors: string[];
|
|
27
|
+
}
|
|
28
|
+
interface DiffResult {
|
|
29
|
+
before: string;
|
|
30
|
+
after: string;
|
|
31
|
+
diff: string;
|
|
32
|
+
diffPixels: number;
|
|
33
|
+
totalPixels: number;
|
|
34
|
+
diffPercentage: number;
|
|
35
|
+
}
|
|
36
|
+
interface ReferenceDiffResult {
|
|
37
|
+
rendered: string;
|
|
38
|
+
diff: string;
|
|
39
|
+
diffPixels: number;
|
|
40
|
+
totalPixels: number;
|
|
41
|
+
diffPercentage: number;
|
|
42
|
+
passed: boolean;
|
|
43
|
+
}
|
|
44
|
+
interface A11yViolation {
|
|
45
|
+
id: string;
|
|
46
|
+
impact: string;
|
|
47
|
+
description: string;
|
|
48
|
+
helpUrl: string;
|
|
49
|
+
nodes: {
|
|
50
|
+
html: string;
|
|
51
|
+
target: string[];
|
|
52
|
+
}[];
|
|
53
|
+
}
|
|
54
|
+
interface A11yResult {
|
|
55
|
+
violations: A11yViolation[];
|
|
56
|
+
passes: number;
|
|
57
|
+
incomplete: number;
|
|
58
|
+
}
|
|
59
|
+
interface PerfMetrics {
|
|
60
|
+
renderTimeMs: number;
|
|
61
|
+
domElements: number;
|
|
62
|
+
domDepth: number;
|
|
63
|
+
scriptCount: number;
|
|
64
|
+
styleSheetCount: number;
|
|
65
|
+
imageCount: number;
|
|
66
|
+
totalDomSize: number;
|
|
67
|
+
}
|
|
68
|
+
interface AnimationFrame {
|
|
69
|
+
timestamp: number;
|
|
70
|
+
image: string;
|
|
71
|
+
}
|
|
72
|
+
interface Interaction {
|
|
73
|
+
action: "click" | "hover" | "focus" | "type" | "wait";
|
|
74
|
+
selector?: string;
|
|
75
|
+
value?: string;
|
|
76
|
+
ms?: number;
|
|
77
|
+
}
|
|
78
|
+
interface GridCell {
|
|
79
|
+
label: string;
|
|
80
|
+
code: string;
|
|
81
|
+
}
|
|
82
|
+
interface Snapshot {
|
|
83
|
+
image: string;
|
|
84
|
+
timestamp: number;
|
|
85
|
+
}
|
|
86
|
+
interface CatalogEntry {
|
|
87
|
+
path: string;
|
|
88
|
+
framework: Framework;
|
|
89
|
+
image: string;
|
|
90
|
+
width: number;
|
|
91
|
+
height: number;
|
|
92
|
+
consoleErrors: string[];
|
|
93
|
+
}
|
|
94
|
+
declare const DEVICE_PRESETS: Record<string, Viewport>;
|
|
95
|
+
declare const EXT_TO_FRAMEWORK: Record<string, Framework>;
|
|
96
|
+
interface FileRenderOptions extends RenderOptions {
|
|
97
|
+
props?: Record<string, unknown>;
|
|
98
|
+
projectRoot?: string;
|
|
99
|
+
}
|
|
100
|
+
interface ProjectConfig {
|
|
101
|
+
root: string;
|
|
102
|
+
viteConfigPath?: string;
|
|
103
|
+
framework: Framework;
|
|
104
|
+
hasVite: boolean;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
declare class BrowserPool {
|
|
108
|
+
private pool;
|
|
109
|
+
warmup(engines: Engine[]): Promise<void>;
|
|
110
|
+
getPage(engine: Engine): Promise<Page>;
|
|
111
|
+
setViewport(engine: Engine, viewport: Viewport): Promise<void>;
|
|
112
|
+
shutdown(): Promise<void>;
|
|
113
|
+
private getSlot;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
declare class HtmlBuilder {
|
|
117
|
+
build(code: string, framework: Framework, options: {
|
|
118
|
+
darkMode: boolean;
|
|
119
|
+
css: string;
|
|
120
|
+
tailwindVersion: "3" | "4";
|
|
121
|
+
}): string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface DiffComparison {
|
|
125
|
+
diff: string;
|
|
126
|
+
diffPixels: number;
|
|
127
|
+
totalPixels: number;
|
|
128
|
+
diffPercentage: number;
|
|
129
|
+
}
|
|
130
|
+
declare class ImageComparator {
|
|
131
|
+
diff(imageA: string, imageB: string, threshold?: number): DiffComparison;
|
|
132
|
+
composite(images: Buffer[], columns: number, labelHeight?: number): {
|
|
133
|
+
image: string;
|
|
134
|
+
width: number;
|
|
135
|
+
height: number;
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
declare class ProjectDetector {
|
|
140
|
+
detect(filePath: string): ProjectConfig;
|
|
141
|
+
private findProjectRoot;
|
|
142
|
+
private findViteConfig;
|
|
143
|
+
private detectFramework;
|
|
144
|
+
private checkViteAvailable;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
declare class SnapshotStore {
|
|
148
|
+
private store;
|
|
149
|
+
save(key: string, image: string): void;
|
|
150
|
+
get(key: string): Snapshot | undefined;
|
|
151
|
+
list(): {
|
|
152
|
+
key: string;
|
|
153
|
+
timestamp: number;
|
|
154
|
+
}[];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
declare class ViteBundler {
|
|
158
|
+
private servers;
|
|
159
|
+
private detector;
|
|
160
|
+
getUrl(filePath: string, options?: {
|
|
161
|
+
props?: Record<string, unknown>;
|
|
162
|
+
framework?: Framework;
|
|
163
|
+
projectRoot?: string;
|
|
164
|
+
}): Promise<{
|
|
165
|
+
url: string;
|
|
166
|
+
project: ProjectConfig;
|
|
167
|
+
}>;
|
|
168
|
+
shutdown(): Promise<void>;
|
|
169
|
+
private ensureServer;
|
|
170
|
+
private importVite;
|
|
171
|
+
private hasPackage;
|
|
172
|
+
private generateEntry;
|
|
173
|
+
private findGlobalCss;
|
|
174
|
+
private getOptimizeDepsInclude;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
declare class AuditUseCase {
|
|
178
|
+
private readonly pool;
|
|
179
|
+
private readonly htmlBuilder;
|
|
180
|
+
constructor(pool: BrowserPool, htmlBuilder: HtmlBuilder);
|
|
181
|
+
auditA11y(code: string, framework: Framework, options?: Partial<RenderOptions>): Promise<A11yResult>;
|
|
182
|
+
perfAudit(code: string, framework: Framework, options?: Partial<RenderOptions>): Promise<PerfMetrics>;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
declare class RenderUseCase {
|
|
186
|
+
private readonly pool;
|
|
187
|
+
private readonly htmlBuilder;
|
|
188
|
+
private readonly imageComparator;
|
|
189
|
+
private readonly viteBundler?;
|
|
190
|
+
constructor(pool: BrowserPool, htmlBuilder: HtmlBuilder, imageComparator: ImageComparator, viteBundler?: ViteBundler | undefined);
|
|
191
|
+
render(code: string, framework: Framework, options?: Partial<RenderOptions>): Promise<ScreenshotResult[]>;
|
|
192
|
+
renderFile(filePath: string, options?: Partial<FileRenderOptions>): Promise<{
|
|
193
|
+
results: ScreenshotResult[];
|
|
194
|
+
mode: "vite" | "cdn";
|
|
195
|
+
}>;
|
|
196
|
+
renderInteraction(code: string, framework: Framework, interactions: Interaction[], options?: Partial<RenderOptions>): Promise<{
|
|
197
|
+
image: string;
|
|
198
|
+
width: number;
|
|
199
|
+
height: number;
|
|
200
|
+
}>;
|
|
201
|
+
captureAnimation(code: string, framework: Framework, options?: Partial<RenderOptions> & {
|
|
202
|
+
frames?: number;
|
|
203
|
+
duration?: number;
|
|
204
|
+
}): Promise<AnimationFrame[]>;
|
|
205
|
+
renderGrid(cells: GridCell[], framework: Framework, options?: Partial<RenderOptions> & {
|
|
206
|
+
columns?: number;
|
|
207
|
+
}): Promise<{
|
|
208
|
+
image: string;
|
|
209
|
+
width: number;
|
|
210
|
+
height: number;
|
|
211
|
+
cells: number;
|
|
212
|
+
}>;
|
|
213
|
+
renderMatrix(code: string, framework: Framework, viewports: {
|
|
214
|
+
label: string;
|
|
215
|
+
width: number;
|
|
216
|
+
height: number;
|
|
217
|
+
}[], themes: Theme[], options?: Partial<Omit<RenderOptions, "viewport" | "darkMode">>): Promise<{
|
|
218
|
+
viewport: string;
|
|
219
|
+
theme: Theme;
|
|
220
|
+
image: string;
|
|
221
|
+
width: number;
|
|
222
|
+
height: number;
|
|
223
|
+
consoleErrors: string[];
|
|
224
|
+
}[]>;
|
|
225
|
+
private renderHtml;
|
|
226
|
+
private renderUrl;
|
|
227
|
+
private navigateAndWait;
|
|
228
|
+
private resolveAutoFitViewport;
|
|
229
|
+
private attachConsoleCapture;
|
|
230
|
+
private takeScreenshot;
|
|
231
|
+
private resolveOptions;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
declare class CatalogUseCase {
|
|
235
|
+
private readonly renderUseCase;
|
|
236
|
+
constructor(renderUseCase: RenderUseCase);
|
|
237
|
+
renderCatalog(directory: string, options?: Partial<RenderOptions> & {
|
|
238
|
+
recursive?: boolean;
|
|
239
|
+
}): Promise<CatalogEntry[]>;
|
|
240
|
+
private scanDirectory;
|
|
241
|
+
private detectFramework;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
interface Renderer {
|
|
245
|
+
render(code: string, framework: Framework, options?: Partial<RenderOptions>): Promise<ScreenshotResult[]>;
|
|
246
|
+
}
|
|
247
|
+
interface Comparator {
|
|
248
|
+
diff(a: string, b: string, threshold?: number): DiffComparison;
|
|
249
|
+
}
|
|
250
|
+
declare class DiffUseCase {
|
|
251
|
+
private readonly renderUseCase;
|
|
252
|
+
private readonly imageComparator;
|
|
253
|
+
constructor(renderUseCase: Renderer, imageComparator: Comparator);
|
|
254
|
+
diffComponent(before: string, after: string, framework: Framework, options?: Partial<RenderOptions>): Promise<DiffResult>;
|
|
255
|
+
diffFromReference(code: string, framework: Framework, referenceImage: string, options?: Partial<RenderOptions> & {
|
|
256
|
+
threshold?: number;
|
|
257
|
+
}): Promise<ReferenceDiffResult>;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
interface ScreenshotUrlOptions {
|
|
261
|
+
viewport?: {
|
|
262
|
+
width: number;
|
|
263
|
+
height: number;
|
|
264
|
+
};
|
|
265
|
+
engines?: Engine[];
|
|
266
|
+
fullPage?: boolean;
|
|
267
|
+
waitFor?: number;
|
|
268
|
+
waitForSelector?: string;
|
|
269
|
+
waitForNetworkIdle?: boolean;
|
|
270
|
+
}
|
|
271
|
+
declare class ScreenshotUseCase {
|
|
272
|
+
private readonly pool;
|
|
273
|
+
constructor(pool: BrowserPool);
|
|
274
|
+
screenshotUrl(url: string, options?: ScreenshotUrlOptions): Promise<ScreenshotResult[]>;
|
|
275
|
+
screenshotUrlWithRetry(url: string, options?: ScreenshotUrlOptions & {
|
|
276
|
+
retryCount?: number;
|
|
277
|
+
}): Promise<ScreenshotResult[]>;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
declare class SnapshotUseCase {
|
|
281
|
+
private readonly store;
|
|
282
|
+
private readonly renderUseCase;
|
|
283
|
+
private readonly diffUseCase;
|
|
284
|
+
constructor(store: SnapshotStore, renderUseCase: RenderUseCase, diffUseCase: DiffUseCase);
|
|
285
|
+
save(key: string, code: string, framework: Framework, options?: Partial<RenderOptions>): Promise<{
|
|
286
|
+
image: string;
|
|
287
|
+
width: number;
|
|
288
|
+
height: number;
|
|
289
|
+
}>;
|
|
290
|
+
check(key: string, code: string, framework: Framework, options?: Partial<RenderOptions>): Promise<ReferenceDiffResult | null>;
|
|
291
|
+
list(): {
|
|
292
|
+
key: string;
|
|
293
|
+
timestamp: number;
|
|
294
|
+
}[];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export { type A11yResult, type A11yViolation, type AnimationFrame, AuditUseCase, BrowserPool, type CatalogEntry, CatalogUseCase, DEVICE_PRESETS, type DiffResult, DiffUseCase, EXT_TO_FRAMEWORK, type Engine, type FileRenderOptions, type Framework, type GridCell, HtmlBuilder, ImageComparator, type Interaction, type PerfMetrics, type ProjectConfig, ProjectDetector, type ReferenceDiffResult, type RenderOptions, RenderUseCase, type ScreenshotResult, ScreenshotUseCase, type Snapshot, SnapshotStore, SnapshotUseCase, type Theme, type Viewport, ViteBundler };
|
package/dist/renderer.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AuditUseCase,
|
|
3
|
+
ScreenshotUseCase,
|
|
4
|
+
SnapshotStore,
|
|
5
|
+
SnapshotUseCase
|
|
6
|
+
} from "./chunk-FTYTZW6D.js";
|
|
7
|
+
import {
|
|
8
|
+
BrowserPool,
|
|
9
|
+
CatalogUseCase,
|
|
10
|
+
DEVICE_PRESETS,
|
|
11
|
+
DiffUseCase,
|
|
12
|
+
EXT_TO_FRAMEWORK,
|
|
13
|
+
HtmlBuilder,
|
|
14
|
+
ImageComparator,
|
|
15
|
+
ProjectDetector,
|
|
16
|
+
RenderUseCase,
|
|
17
|
+
ViteBundler
|
|
18
|
+
} from "./chunk-Q7A3DLED.js";
|
|
19
|
+
export {
|
|
20
|
+
AuditUseCase,
|
|
21
|
+
BrowserPool,
|
|
22
|
+
CatalogUseCase,
|
|
23
|
+
DEVICE_PRESETS,
|
|
24
|
+
DiffUseCase,
|
|
25
|
+
EXT_TO_FRAMEWORK,
|
|
26
|
+
HtmlBuilder,
|
|
27
|
+
ImageComparator,
|
|
28
|
+
ProjectDetector,
|
|
29
|
+
RenderUseCase,
|
|
30
|
+
ScreenshotUseCase,
|
|
31
|
+
SnapshotStore,
|
|
32
|
+
SnapshotUseCase,
|
|
33
|
+
ViteBundler
|
|
34
|
+
};
|
package/package.json
CHANGED
|
@@ -1,25 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frameshot-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Zero-config visual testing for AI agents. Render project components with full Vite dependency resolution — no stories, no config.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
},
|
|
12
|
+
"./renderer": {
|
|
13
|
+
"import": "./dist/renderer.js",
|
|
14
|
+
"types": "./dist/renderer.d.ts"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
7
17
|
"bin": {
|
|
8
|
-
"frameshot-mcp": "dist/index.js"
|
|
18
|
+
"frameshot-mcp": "dist/index.js",
|
|
19
|
+
"frameshot": "dist/cli.js"
|
|
9
20
|
},
|
|
10
21
|
"files": [
|
|
11
22
|
"dist",
|
|
23
|
+
"scripts",
|
|
24
|
+
"action.yml",
|
|
12
25
|
"README.md",
|
|
13
26
|
"LICENSE"
|
|
14
27
|
],
|
|
15
28
|
"scripts": {
|
|
16
|
-
"build": "tsup src/index.ts --format esm --dts",
|
|
17
|
-
"dev": "tsup src/index.ts --format esm --watch",
|
|
29
|
+
"build": "tsup src/index.ts src/renderer.ts src/cli.ts --format esm --dts --external vite",
|
|
30
|
+
"dev": "tsup src/index.ts src/renderer.ts --format esm --watch",
|
|
18
31
|
"start": "node dist/index.js",
|
|
19
32
|
"typecheck": "tsc --noEmit",
|
|
20
33
|
"lint": "biome check src/",
|
|
21
34
|
"lint:fix": "biome check --write src/",
|
|
22
35
|
"format": "biome format --write src/",
|
|
36
|
+
"test": "vitest run",
|
|
23
37
|
"prepublishOnly": "npm run build"
|
|
24
38
|
},
|
|
25
39
|
"keywords": [
|
|
@@ -57,13 +71,24 @@
|
|
|
57
71
|
"playwright": "^1.52.0",
|
|
58
72
|
"pngjs": "^7.0.0"
|
|
59
73
|
},
|
|
74
|
+
"peerDependencies": {
|
|
75
|
+
"vite": ">=5.0.0"
|
|
76
|
+
},
|
|
77
|
+
"peerDependenciesMeta": {
|
|
78
|
+
"vite": {
|
|
79
|
+
"optional": true
|
|
80
|
+
}
|
|
81
|
+
},
|
|
60
82
|
"devDependencies": {
|
|
61
83
|
"@biomejs/biome": "^2.5.0",
|
|
62
|
-
"@types/node": "^
|
|
84
|
+
"@types/node": "^26.0.0",
|
|
63
85
|
"@types/pngjs": "^6.0.5",
|
|
64
86
|
"tsup": "^8.0.0",
|
|
65
|
-
"typescript": "^5.7.0"
|
|
87
|
+
"typescript": "^5.7.0",
|
|
88
|
+
"vite": "^6.0.0",
|
|
89
|
+
"vitest": "^4.1.9"
|
|
66
90
|
},
|
|
91
|
+
"mcpName": "io.github.kamegoro/frameshot-mcp",
|
|
67
92
|
"engines": {
|
|
68
93
|
"node": ">=20.0.0"
|
|
69
94
|
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { basename, extname, join, relative } from "node:path";
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
INPUT_PATHS = "",
|
|
9
|
+
INPUT_WIDTH = "",
|
|
10
|
+
INPUT_HEIGHT = "",
|
|
11
|
+
INPUT_BASE_REF = "",
|
|
12
|
+
GITHUB_WORKSPACE = process.cwd(),
|
|
13
|
+
GITHUB_OUTPUT = "/dev/null",
|
|
14
|
+
} = process.env;
|
|
15
|
+
|
|
16
|
+
const outputDir = join(GITHUB_WORKSPACE, ".frameshot-screenshots");
|
|
17
|
+
mkdirSync(outputDir, { recursive: true });
|
|
18
|
+
|
|
19
|
+
const patterns = INPUT_PATHS.split(/[,\n]/)
|
|
20
|
+
.map((p) => p.trim())
|
|
21
|
+
.filter(Boolean);
|
|
22
|
+
|
|
23
|
+
const COMPONENT_EXTS = new Set([
|
|
24
|
+
".jsx",
|
|
25
|
+
".tsx",
|
|
26
|
+
".vue",
|
|
27
|
+
".svelte",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
function findFiles(workspace, patterns) {
|
|
31
|
+
const allFiles = [];
|
|
32
|
+
for (const pattern of patterns) {
|
|
33
|
+
try {
|
|
34
|
+
const result = execSync(
|
|
35
|
+
`find ${workspace} -path '${pattern}' -type f 2>/dev/null`,
|
|
36
|
+
{ encoding: "utf-8" },
|
|
37
|
+
);
|
|
38
|
+
allFiles.push(...result.split("\n").filter(Boolean));
|
|
39
|
+
} catch {
|
|
40
|
+
// Pattern didn't match
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return allFiles.filter((f) => COMPONENT_EXTS.has(extname(f).toLowerCase()));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getChangedFiles(baseRef) {
|
|
47
|
+
if (!baseRef) return null;
|
|
48
|
+
try {
|
|
49
|
+
const result = execSync(`git diff --name-only ${baseRef}...HEAD`, {
|
|
50
|
+
encoding: "utf-8",
|
|
51
|
+
cwd: GITHUB_WORKSPACE,
|
|
52
|
+
});
|
|
53
|
+
return new Set(result.split("\n").filter(Boolean));
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function checkoutFileAtRef(filePath, ref) {
|
|
60
|
+
const relativePath = relative(GITHUB_WORKSPACE, filePath);
|
|
61
|
+
try {
|
|
62
|
+
execSync(`git checkout ${ref} -- ${relativePath}`, {
|
|
63
|
+
cwd: GITHUB_WORKSPACE,
|
|
64
|
+
stdio: "pipe",
|
|
65
|
+
});
|
|
66
|
+
return true;
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function restoreFile(filePath) {
|
|
73
|
+
const relativePath = relative(GITHUB_WORKSPACE, filePath);
|
|
74
|
+
execSync(`git checkout HEAD -- ${relativePath}`, {
|
|
75
|
+
cwd: GITHUB_WORKSPACE,
|
|
76
|
+
stdio: "pipe",
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function main() {
|
|
81
|
+
const { BrowserPool, HtmlBuilder, ImageComparator, RenderUseCase, ViteBundler } =
|
|
82
|
+
await import("frameshot-mcp/renderer");
|
|
83
|
+
|
|
84
|
+
const browserPool = new BrowserPool();
|
|
85
|
+
const htmlBuilder = new HtmlBuilder();
|
|
86
|
+
const imageComparator = new ImageComparator();
|
|
87
|
+
const viteBundler = new ViteBundler();
|
|
88
|
+
const renderUseCase = new RenderUseCase(
|
|
89
|
+
browserPool,
|
|
90
|
+
htmlBuilder,
|
|
91
|
+
imageComparator,
|
|
92
|
+
viteBundler,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
await browserPool.warmup(["chromium"]);
|
|
96
|
+
|
|
97
|
+
const allFiles = findFiles(GITHUB_WORKSPACE, patterns);
|
|
98
|
+
|
|
99
|
+
if (allFiles.length === 0) {
|
|
100
|
+
console.log("No component files found matching the specified patterns.");
|
|
101
|
+
await viteBundler.shutdown();
|
|
102
|
+
await browserPool.shutdown();
|
|
103
|
+
process.exit(0);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const changedFiles = getChangedFiles(INPUT_BASE_REF);
|
|
107
|
+
const filesToRender = changedFiles
|
|
108
|
+
? allFiles.filter((f) => {
|
|
109
|
+
const rel = relative(GITHUB_WORKSPACE, f);
|
|
110
|
+
return changedFiles.has(rel);
|
|
111
|
+
})
|
|
112
|
+
: allFiles;
|
|
113
|
+
|
|
114
|
+
if (filesToRender.length === 0) {
|
|
115
|
+
console.log("No changed component files to render.");
|
|
116
|
+
await viteBundler.shutdown();
|
|
117
|
+
await browserPool.shutdown();
|
|
118
|
+
process.exit(0);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log(`Found ${filesToRender.length} component file(s) to render...`);
|
|
122
|
+
|
|
123
|
+
const autoFit = !INPUT_WIDTH || !INPUT_HEIGHT;
|
|
124
|
+
const renderOpts = {
|
|
125
|
+
viewport: {
|
|
126
|
+
width: INPUT_WIDTH ? parseInt(INPUT_WIDTH) : 1280,
|
|
127
|
+
height: INPUT_HEIGHT ? parseInt(INPUT_HEIGHT) : 800,
|
|
128
|
+
},
|
|
129
|
+
autoFit,
|
|
130
|
+
fullPage: true,
|
|
131
|
+
engines: ["chromium"],
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const results = [];
|
|
135
|
+
|
|
136
|
+
for (const filePath of filesToRender) {
|
|
137
|
+
const name = basename(filePath, extname(filePath));
|
|
138
|
+
const safeName = name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
// Render current (after) version
|
|
142
|
+
console.log(` Rendering (after): ${filePath}`);
|
|
143
|
+
const { results: afterResults, mode } = await renderUseCase.renderFile(
|
|
144
|
+
filePath,
|
|
145
|
+
renderOpts,
|
|
146
|
+
);
|
|
147
|
+
const afterResult = afterResults[0];
|
|
148
|
+
|
|
149
|
+
const afterPath = join(outputDir, `${safeName}_after.png`);
|
|
150
|
+
writeFileSync(afterPath, Buffer.from(afterResult.image, "base64"));
|
|
151
|
+
|
|
152
|
+
const entry = {
|
|
153
|
+
name: safeName,
|
|
154
|
+
after: afterResult.image,
|
|
155
|
+
before: null,
|
|
156
|
+
diff: null,
|
|
157
|
+
diffPercentage: 0,
|
|
158
|
+
mode,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Render base (before) version if base_ref is provided
|
|
162
|
+
if (INPUT_BASE_REF) {
|
|
163
|
+
const checkedOut = checkoutFileAtRef(filePath, INPUT_BASE_REF);
|
|
164
|
+
if (checkedOut) {
|
|
165
|
+
try {
|
|
166
|
+
console.log(` Rendering (before): ${filePath}`);
|
|
167
|
+
const { results: beforeResults } = await renderUseCase.renderFile(
|
|
168
|
+
filePath,
|
|
169
|
+
renderOpts,
|
|
170
|
+
);
|
|
171
|
+
const beforeResult = beforeResults[0];
|
|
172
|
+
entry.before = beforeResult.image;
|
|
173
|
+
|
|
174
|
+
const beforePath = join(outputDir, `${safeName}_before.png`);
|
|
175
|
+
writeFileSync(
|
|
176
|
+
beforePath,
|
|
177
|
+
Buffer.from(beforeResult.image, "base64"),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const diffResult = imageComparator.diff(
|
|
181
|
+
beforeResult.image,
|
|
182
|
+
afterResult.image,
|
|
183
|
+
);
|
|
184
|
+
entry.diff = diffResult.diff;
|
|
185
|
+
entry.diffPercentage = diffResult.diffPercentage;
|
|
186
|
+
|
|
187
|
+
if (diffResult.diffPercentage > 0) {
|
|
188
|
+
const diffPath = join(outputDir, `${safeName}_diff.png`);
|
|
189
|
+
writeFileSync(diffPath, Buffer.from(diffResult.diff, "base64"));
|
|
190
|
+
}
|
|
191
|
+
} finally {
|
|
192
|
+
restoreFile(filePath);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
results.push(entry);
|
|
198
|
+
console.log(
|
|
199
|
+
` ✓ ${safeName} (${afterResult.width}x${afterResult.height}, ${mode})${entry.diffPercentage > 0 ? ` — ${entry.diffPercentage.toFixed(1)}% changed` : ""}`,
|
|
200
|
+
);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.error(` ✗ Failed: ${filePath} — ${error.message}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
await viteBundler.shutdown();
|
|
207
|
+
await browserPool.shutdown();
|
|
208
|
+
|
|
209
|
+
const manifest = JSON.stringify(
|
|
210
|
+
results.map((r) => ({
|
|
211
|
+
name: r.name,
|
|
212
|
+
hasBefore: r.before !== null,
|
|
213
|
+
diffPercentage: r.diffPercentage,
|
|
214
|
+
mode: r.mode,
|
|
215
|
+
})),
|
|
216
|
+
);
|
|
217
|
+
writeFileSync(join(outputDir, "manifest.json"), manifest);
|
|
218
|
+
|
|
219
|
+
const hasChanges = results.some((r) => r.diffPercentage > 0 || !r.before);
|
|
220
|
+
try {
|
|
221
|
+
const outputContent = `has_changes=${hasChanges}\ncount=${results.length}\n`;
|
|
222
|
+
writeFileSync(GITHUB_OUTPUT, outputContent, { flag: "a" });
|
|
223
|
+
} catch {
|
|
224
|
+
// Not in GitHub Actions
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
console.log(`\nDone. ${results.length} screenshot(s) saved to ${outputDir}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
main().catch((e) => {
|
|
231
|
+
console.error(e);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Setup GitHub labels for the frameshot repository.
|
|
3
|
+
# Usage: ./scripts/setup-labels.sh [owner/repo]
|
|
4
|
+
# Requires: gh CLI authenticated
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
REPO="${1:-kamegoro/frameshot}"
|
|
9
|
+
|
|
10
|
+
# Delete default labels that add noise
|
|
11
|
+
for label in "invalid" "wontfix" "help wanted" "question"; do
|
|
12
|
+
gh label delete "$label" --repo "$REPO" --yes 2>/dev/null || true
|
|
13
|
+
done
|
|
14
|
+
|
|
15
|
+
# --- Status labels (S-) ---
|
|
16
|
+
gh label create "S-needs triage" --color "D93F0B" --description "Awaiting maintainer review" --repo "$REPO" --force
|
|
17
|
+
gh label create "S-accepted" --color "0E8A16" --description "Confirmed and ready for work" --repo "$REPO" --force
|
|
18
|
+
gh label create "S-in progress" --color "1D76DB" --description "Currently being worked on" --repo "$REPO" --force
|
|
19
|
+
gh label create "S-blocked" --color "B60205" --description "Waiting on external dependency" --repo "$REPO" --force
|
|
20
|
+
|
|
21
|
+
# --- Priority labels (P-) ---
|
|
22
|
+
gh label create "P0-critical" --color "B60205" --description "Production broken, fix immediately" --repo "$REPO" --force
|
|
23
|
+
gh label create "P1-high" --color "D93F0B" --description "Important, fix this week" --repo "$REPO" --force
|
|
24
|
+
gh label create "P2-medium" --color "FBCA04" --description "Normal priority" --repo "$REPO" --force
|
|
25
|
+
gh label create "P3-low" --color "0075CA" --description "Nice to have, no urgency" --repo "$REPO" --force
|
|
26
|
+
|
|
27
|
+
# --- Type labels ---
|
|
28
|
+
gh label create "bug" --color "D73A4A" --description "Something isn't working" --repo "$REPO" --force
|
|
29
|
+
gh label create "enhancement" --color "A2EEEF" --description "New feature or improvement" --repo "$REPO" --force
|
|
30
|
+
gh label create "documentation" --color "0075CA" --description "Documentation improvements" --repo "$REPO" --force
|
|
31
|
+
gh label create "performance" --color "F9D0C4" --description "Performance improvement" --repo "$REPO" --force
|
|
32
|
+
gh label create "breaking change" --color "B60205" --description "Introduces a breaking change" --repo "$REPO" --force
|
|
33
|
+
|
|
34
|
+
# --- Area labels (A-) ---
|
|
35
|
+
gh label create "A-rendering" --color "C5DEF5" --description "Component rendering pipeline" --repo "$REPO" --force
|
|
36
|
+
gh label create "A-screenshot" --color "C5DEF5" --description "URL screenshot capture" --repo "$REPO" --force
|
|
37
|
+
gh label create "A-diff" --color "C5DEF5" --description "Visual regression diffing" --repo "$REPO" --force
|
|
38
|
+
gh label create "A-audit" --color "C5DEF5" --description "Accessibility & performance audits" --repo "$REPO" --force
|
|
39
|
+
gh label create "A-mcp-protocol" --color "C5DEF5" --description "MCP transport & tool definitions" --repo "$REPO" --force
|
|
40
|
+
gh label create "A-github-action" --color "C5DEF5" --description "GitHub Action integration" --repo "$REPO" --force
|
|
41
|
+
gh label create "A-config" --color "C5DEF5" --description "Configuration & setup" --repo "$REPO" --force
|
|
42
|
+
|
|
43
|
+
# --- Community labels ---
|
|
44
|
+
gh label create "good first issue" --color "7057FF" --description "Good for newcomers" --repo "$REPO" --force
|
|
45
|
+
gh label create "help wanted" --color "008672" --description "Extra attention is needed" --repo "$REPO" --force
|
|
46
|
+
gh label create "duplicate" --color "CFD3D7" --description "This issue already exists" --repo "$REPO" --force
|
|
47
|
+
|
|
48
|
+
echo "Labels setup complete for $REPO"
|