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 +88 -20
- package/action.yml +105 -16
- package/dist/chunk-3CDSNOX5.js +869 -0
- package/dist/chunk-AUACBLHM.js +191 -0
- package/dist/chunk-GVOEFYEX.js +139 -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-Q7NQA4ZM.js +1095 -0
- package/dist/cli.js +8 -23
- package/dist/index.js +144 -46
- package/dist/renderer.d.ts +11 -5
- package/dist/renderer.js +4 -6
- package/package.json +3 -2
- package/scripts/render-changed.mjs +83 -16
package/dist/cli.js
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
EXT_TO_FRAMEWORK
|
|
7
|
-
|
|
8
|
-
ImageComparator,
|
|
9
|
-
RenderUseCase,
|
|
10
|
-
ViteBundler
|
|
11
|
-
} from "./chunk-Q7A3DLED.js";
|
|
3
|
+
createContainer
|
|
4
|
+
} from "./chunk-MA3FOIQY.js";
|
|
5
|
+
import {
|
|
6
|
+
EXT_TO_FRAMEWORK
|
|
7
|
+
} from "./chunk-PYWXJZTZ.js";
|
|
12
8
|
|
|
13
9
|
// src/cli.ts
|
|
14
10
|
import { execFileSync, execSync } from "child_process";
|
|
@@ -196,18 +192,8 @@ async function main() {
|
|
|
196
192
|
log(`
|
|
197
193
|
${brand()} ${c.dim}v0.7.0${c.reset}
|
|
198
194
|
`);
|
|
199
|
-
const
|
|
200
|
-
const
|
|
201
|
-
const imageComparator = new ImageComparator();
|
|
202
|
-
const viteBundler = new ViteBundler();
|
|
203
|
-
const renderUseCase = new RenderUseCase(
|
|
204
|
-
pool,
|
|
205
|
-
htmlBuilder,
|
|
206
|
-
imageComparator,
|
|
207
|
-
viteBundler
|
|
208
|
-
);
|
|
209
|
-
const catalogUseCase = new CatalogUseCase(renderUseCase);
|
|
210
|
-
const diffUseCase = new DiffUseCase(renderUseCase, imageComparator);
|
|
195
|
+
const container = createContainer();
|
|
196
|
+
const { pool, renderUseCase, catalogUseCase, diffUseCase } = container;
|
|
211
197
|
const outDir = resolve(flags.out);
|
|
212
198
|
mkdirSync(outDir, { recursive: true });
|
|
213
199
|
try {
|
|
@@ -228,8 +214,7 @@ async function main() {
|
|
|
228
214
|
);
|
|
229
215
|
}
|
|
230
216
|
} finally {
|
|
231
|
-
await
|
|
232
|
-
await pool.shutdown();
|
|
217
|
+
await container.shutdown();
|
|
233
218
|
}
|
|
234
219
|
log("");
|
|
235
220
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,22 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
SnapshotStore,
|
|
6
|
-
SnapshotUseCase
|
|
7
|
-
} from "./chunk-FTYTZW6D.js";
|
|
3
|
+
createContainer
|
|
4
|
+
} from "./chunk-MA3FOIQY.js";
|
|
8
5
|
import {
|
|
9
|
-
BrowserPool,
|
|
10
|
-
CatalogUseCase,
|
|
11
6
|
DEVICE_PRESETS,
|
|
12
|
-
DiffUseCase,
|
|
13
7
|
EXT_TO_FRAMEWORK,
|
|
14
|
-
HtmlBuilder,
|
|
15
|
-
ImageComparator,
|
|
16
|
-
RenderUseCase,
|
|
17
|
-
ViteBundler,
|
|
18
8
|
__export
|
|
19
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-PYWXJZTZ.js";
|
|
20
10
|
|
|
21
11
|
// src/index.ts
|
|
22
12
|
import { execSync } from "child_process";
|
|
@@ -15581,6 +15571,133 @@ ${lines.join("\n")}`
|
|
|
15581
15571
|
);
|
|
15582
15572
|
}
|
|
15583
15573
|
|
|
15574
|
+
// src/tools/watch-tools.ts
|
|
15575
|
+
function registerWatchTools(server2, useCase) {
|
|
15576
|
+
server2.tool(
|
|
15577
|
+
"watch_start",
|
|
15578
|
+
"Start watching component files for changes. On every save, the component is automatically rendered and you will receive a notification \u2014 call watch_get_latest(id) to retrieve the rendered screenshot. Use this to create a live feedback loop while editing UI \u2014 the AI sees each change without you needing to call render_file manually.",
|
|
15579
|
+
{
|
|
15580
|
+
patterns: external_exports.array(external_exports.string()).describe(
|
|
15581
|
+
"Glob patterns or absolute file paths to watch (e.g. ['src/components/Button.tsx'])"
|
|
15582
|
+
),
|
|
15583
|
+
props: external_exports.record(external_exports.string(), external_exports.unknown()).optional().describe("Props to pass to the component on each render")
|
|
15584
|
+
},
|
|
15585
|
+
async ({ patterns, props }) => {
|
|
15586
|
+
const session = useCase.start(patterns, props, async (event) => {
|
|
15587
|
+
if (event.error) {
|
|
15588
|
+
await server2.server.notification({
|
|
15589
|
+
method: "notifications/message",
|
|
15590
|
+
params: {
|
|
15591
|
+
level: "error",
|
|
15592
|
+
logger: "frameshot/watch",
|
|
15593
|
+
data: `[${event.sessionId}] \u274C ${event.filePath}: ${event.error}`
|
|
15594
|
+
}
|
|
15595
|
+
});
|
|
15596
|
+
return;
|
|
15597
|
+
}
|
|
15598
|
+
const summary = `[${event.sessionId}] \u2713 ${event.filePath} \u2014 ${event.width}\xD7${event.height} \xB7 ${event.elapsedMs}ms \xB7 ${event.mode}` + (event.consoleErrors.length ? `
|
|
15599
|
+
\u26A0\uFE0F ${event.consoleErrors.join("\n")}` : "") + `
|
|
15600
|
+
Call watch_get_latest("${event.sessionId}") to see the render.`;
|
|
15601
|
+
await server2.server.notification({
|
|
15602
|
+
method: "notifications/message",
|
|
15603
|
+
params: {
|
|
15604
|
+
level: "info",
|
|
15605
|
+
logger: "frameshot/watch",
|
|
15606
|
+
data: summary
|
|
15607
|
+
}
|
|
15608
|
+
});
|
|
15609
|
+
});
|
|
15610
|
+
return {
|
|
15611
|
+
content: [
|
|
15612
|
+
{
|
|
15613
|
+
type: "text",
|
|
15614
|
+
text: `\u2713 Watch started (${session.id})
|
|
15615
|
+
Watching: ${patterns.join(", ")}
|
|
15616
|
+
|
|
15617
|
+
Every time a matched file is saved, it will be rendered automatically and a notification will appear in your MCP log stream. Call \`watch_get_latest\` with id "${session.id}" to retrieve the screenshot.
|
|
15618
|
+
|
|
15619
|
+
Call \`watch_stop\` with id "${session.id}" to stop.`
|
|
15620
|
+
}
|
|
15621
|
+
]
|
|
15622
|
+
};
|
|
15623
|
+
}
|
|
15624
|
+
);
|
|
15625
|
+
server2.tool(
|
|
15626
|
+
"watch_stop",
|
|
15627
|
+
"Stop a running file watcher started with watch_start.",
|
|
15628
|
+
{
|
|
15629
|
+
id: external_exports.string().describe("Watch session ID returned by watch_start (e.g. 'watch-1')")
|
|
15630
|
+
},
|
|
15631
|
+
async ({ id }) => {
|
|
15632
|
+
const stopped = useCase.stop(id);
|
|
15633
|
+
return {
|
|
15634
|
+
content: [
|
|
15635
|
+
{
|
|
15636
|
+
type: "text",
|
|
15637
|
+
text: stopped ? `\u2713 Watch session "${id}" stopped.` : `No active watch session with id "${id}".`
|
|
15638
|
+
}
|
|
15639
|
+
]
|
|
15640
|
+
};
|
|
15641
|
+
}
|
|
15642
|
+
);
|
|
15643
|
+
server2.tool(
|
|
15644
|
+
"watch_list",
|
|
15645
|
+
"List all currently active file watch sessions.",
|
|
15646
|
+
{},
|
|
15647
|
+
async () => {
|
|
15648
|
+
const sessions = useCase.activeSessions();
|
|
15649
|
+
if (sessions.length === 0) {
|
|
15650
|
+
return {
|
|
15651
|
+
content: [{ type: "text", text: "No active watch sessions." }]
|
|
15652
|
+
};
|
|
15653
|
+
}
|
|
15654
|
+
const lines = sessions.map(
|
|
15655
|
+
(s) => `${s.id}: watching ${s.patterns.join(", ")}`
|
|
15656
|
+
);
|
|
15657
|
+
return {
|
|
15658
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
15659
|
+
};
|
|
15660
|
+
}
|
|
15661
|
+
);
|
|
15662
|
+
server2.tool(
|
|
15663
|
+
"watch_get_latest",
|
|
15664
|
+
"Get the latest rendered screenshot from a watch session. Call this after receiving a watch notification to see the current state of the component.",
|
|
15665
|
+
{
|
|
15666
|
+
id: external_exports.string().describe("Watch session ID from watch_start")
|
|
15667
|
+
},
|
|
15668
|
+
async ({ id }) => {
|
|
15669
|
+
const render = useCase.getLatestRender(id);
|
|
15670
|
+
if (!render) {
|
|
15671
|
+
return {
|
|
15672
|
+
content: [
|
|
15673
|
+
{ type: "text", text: `No render yet for session "${id}".` }
|
|
15674
|
+
]
|
|
15675
|
+
};
|
|
15676
|
+
}
|
|
15677
|
+
if (render.error) {
|
|
15678
|
+
return {
|
|
15679
|
+
content: [
|
|
15680
|
+
{ type: "text", text: `Last render failed: ${render.error}` }
|
|
15681
|
+
]
|
|
15682
|
+
};
|
|
15683
|
+
}
|
|
15684
|
+
return {
|
|
15685
|
+
content: [
|
|
15686
|
+
{
|
|
15687
|
+
type: "image",
|
|
15688
|
+
data: render.image,
|
|
15689
|
+
mimeType: "image/png"
|
|
15690
|
+
},
|
|
15691
|
+
{
|
|
15692
|
+
type: "text",
|
|
15693
|
+
text: `${render.filePath} \u2014 ${render.width}\xD7${render.height} \xB7 ${render.elapsedMs}ms \xB7 ${render.mode}`
|
|
15694
|
+
}
|
|
15695
|
+
]
|
|
15696
|
+
};
|
|
15697
|
+
}
|
|
15698
|
+
);
|
|
15699
|
+
}
|
|
15700
|
+
|
|
15584
15701
|
// src/tools/register.ts
|
|
15585
15702
|
function registerAllTools(server2, useCases) {
|
|
15586
15703
|
registerRenderTools(server2, useCases.render);
|
|
@@ -15589,36 +15706,18 @@ function registerAllTools(server2, useCases) {
|
|
|
15589
15706
|
registerAuditTools(server2, useCases.audit);
|
|
15590
15707
|
registerSnapshotTools(server2, useCases.snapshot);
|
|
15591
15708
|
registerCatalogTools(server2, useCases.catalog);
|
|
15709
|
+
registerWatchTools(server2, useCases.watch);
|
|
15592
15710
|
}
|
|
15593
15711
|
|
|
15594
15712
|
// src/index.ts
|
|
15595
|
-
var
|
|
15596
|
-
var htmlBuilder = new HtmlBuilder();
|
|
15597
|
-
var imageComparator = new ImageComparator();
|
|
15598
|
-
var snapshotStore = new SnapshotStore();
|
|
15599
|
-
var viteBundler = new ViteBundler();
|
|
15600
|
-
var renderUseCase = new RenderUseCase(
|
|
15601
|
-
browserPool,
|
|
15602
|
-
htmlBuilder,
|
|
15603
|
-
imageComparator,
|
|
15604
|
-
viteBundler
|
|
15605
|
-
);
|
|
15606
|
-
var screenshotUseCase = new ScreenshotUseCase(browserPool);
|
|
15607
|
-
var diffUseCase = new DiffUseCase(renderUseCase, imageComparator);
|
|
15608
|
-
var auditUseCase = new AuditUseCase(browserPool, htmlBuilder);
|
|
15609
|
-
var snapshotUseCase = new SnapshotUseCase(
|
|
15610
|
-
snapshotStore,
|
|
15611
|
-
renderUseCase,
|
|
15612
|
-
diffUseCase
|
|
15613
|
-
);
|
|
15614
|
-
var catalogUseCase = new CatalogUseCase(renderUseCase);
|
|
15713
|
+
var container = createContainer();
|
|
15615
15714
|
async function ensureBrowser() {
|
|
15616
15715
|
try {
|
|
15617
|
-
await
|
|
15716
|
+
await container.pool.warmup(["chromium"]);
|
|
15618
15717
|
} catch {
|
|
15619
15718
|
try {
|
|
15620
15719
|
execSync("npx playwright install chromium", { stdio: "pipe" });
|
|
15621
|
-
await
|
|
15720
|
+
await container.pool.warmup(["chromium"]);
|
|
15622
15721
|
} catch {
|
|
15623
15722
|
}
|
|
15624
15723
|
}
|
|
@@ -15626,25 +15725,24 @@ async function ensureBrowser() {
|
|
|
15626
15725
|
await ensureBrowser();
|
|
15627
15726
|
var server = new McpServer({
|
|
15628
15727
|
name: "frameshot",
|
|
15629
|
-
version: "0.
|
|
15728
|
+
version: "0.8.0"
|
|
15630
15729
|
});
|
|
15631
15730
|
registerAllTools(server, {
|
|
15632
|
-
render: renderUseCase,
|
|
15633
|
-
screenshot: screenshotUseCase,
|
|
15634
|
-
diff: diffUseCase,
|
|
15635
|
-
audit: auditUseCase,
|
|
15636
|
-
snapshot: snapshotUseCase,
|
|
15637
|
-
catalog: catalogUseCase
|
|
15731
|
+
render: container.renderUseCase,
|
|
15732
|
+
screenshot: container.screenshotUseCase,
|
|
15733
|
+
diff: container.diffUseCase,
|
|
15734
|
+
audit: container.auditUseCase,
|
|
15735
|
+
snapshot: container.snapshotUseCase,
|
|
15736
|
+
catalog: container.catalogUseCase,
|
|
15737
|
+
watch: container.watchUseCase
|
|
15638
15738
|
});
|
|
15639
15739
|
var transport = new StdioServerTransport();
|
|
15640
15740
|
await server.connect(transport);
|
|
15641
15741
|
process.on("SIGINT", async () => {
|
|
15642
|
-
await
|
|
15643
|
-
await browserPool.shutdown();
|
|
15742
|
+
await container.shutdown();
|
|
15644
15743
|
process.exit(0);
|
|
15645
15744
|
});
|
|
15646
15745
|
process.on("SIGTERM", async () => {
|
|
15647
|
-
await
|
|
15648
|
-
await browserPool.shutdown();
|
|
15746
|
+
await container.shutdown();
|
|
15649
15747
|
process.exit(0);
|
|
15650
15748
|
});
|
package/dist/renderer.d.ts
CHANGED
|
@@ -90,6 +90,7 @@ interface CatalogEntry {
|
|
|
90
90
|
width: number;
|
|
91
91
|
height: number;
|
|
92
92
|
consoleErrors: string[];
|
|
93
|
+
error?: string;
|
|
93
94
|
}
|
|
94
95
|
declare const DEVICE_PRESETS: Record<string, Viewport>;
|
|
95
96
|
declare const EXT_TO_FRAMEWORK: Record<string, Framework>;
|
|
@@ -106,11 +107,18 @@ interface ProjectConfig {
|
|
|
106
107
|
|
|
107
108
|
declare class BrowserPool {
|
|
108
109
|
private pool;
|
|
110
|
+
private waiters;
|
|
111
|
+
private totalPages;
|
|
112
|
+
private readonly maxPages;
|
|
113
|
+
constructor(maxConcurrentPages?: number);
|
|
109
114
|
warmup(engines: Engine[]): Promise<void>;
|
|
110
115
|
getPage(engine: Engine): Promise<Page>;
|
|
111
|
-
|
|
116
|
+
releasePage(engine: Engine, page: Page): void;
|
|
117
|
+
private acquireSlot;
|
|
118
|
+
private releaseSlot;
|
|
112
119
|
shutdown(): Promise<void>;
|
|
113
120
|
private getSlot;
|
|
121
|
+
private createPage;
|
|
114
122
|
}
|
|
115
123
|
|
|
116
124
|
declare class HtmlBuilder {
|
|
@@ -129,7 +137,7 @@ interface DiffComparison {
|
|
|
129
137
|
}
|
|
130
138
|
declare class ImageComparator {
|
|
131
139
|
diff(imageA: string, imageB: string, threshold?: number): DiffComparison;
|
|
132
|
-
composite(images: Buffer[], columns: number
|
|
140
|
+
composite(images: Buffer[], columns: number): {
|
|
133
141
|
image: string;
|
|
134
142
|
width: number;
|
|
135
143
|
height: number;
|
|
@@ -169,7 +177,7 @@ declare class ViteBundler {
|
|
|
169
177
|
private ensureServer;
|
|
170
178
|
private importVite;
|
|
171
179
|
private hasPackage;
|
|
172
|
-
|
|
180
|
+
generateEntry(componentPath: string, framework: Framework, props?: Record<string, unknown>, projectRoot?: string): string;
|
|
173
181
|
private findGlobalCss;
|
|
174
182
|
private getOptimizeDepsInclude;
|
|
175
183
|
}
|
|
@@ -226,8 +234,6 @@ declare class RenderUseCase {
|
|
|
226
234
|
private renderUrl;
|
|
227
235
|
private navigateAndWait;
|
|
228
236
|
private resolveAutoFitViewport;
|
|
229
|
-
private attachConsoleCapture;
|
|
230
|
-
private takeScreenshot;
|
|
231
237
|
private resolveOptions;
|
|
232
238
|
}
|
|
233
239
|
|
package/dist/renderer.js
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AuditUseCase,
|
|
3
|
-
ScreenshotUseCase,
|
|
4
|
-
SnapshotStore,
|
|
5
|
-
SnapshotUseCase
|
|
6
|
-
} from "./chunk-FTYTZW6D.js";
|
|
7
|
-
import {
|
|
8
3
|
BrowserPool,
|
|
9
4
|
CatalogUseCase,
|
|
10
5
|
DEVICE_PRESETS,
|
|
@@ -14,8 +9,11 @@ import {
|
|
|
14
9
|
ImageComparator,
|
|
15
10
|
ProjectDetector,
|
|
16
11
|
RenderUseCase,
|
|
12
|
+
ScreenshotUseCase,
|
|
13
|
+
SnapshotStore,
|
|
14
|
+
SnapshotUseCase,
|
|
17
15
|
ViteBundler
|
|
18
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-PYWXJZTZ.js";
|
|
19
17
|
export {
|
|
20
18
|
AuditUseCase,
|
|
21
19
|
BrowserPool,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frameshot-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
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",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"dependencies": {
|
|
68
68
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
69
69
|
"axe-core": "^4.12.1",
|
|
70
|
+
"chokidar": "^5.0.0",
|
|
70
71
|
"pixelmatch": "^7.2.0",
|
|
71
72
|
"playwright": "^1.52.0",
|
|
72
73
|
"pngjs": "^7.0.0"
|
|
@@ -85,7 +86,7 @@
|
|
|
85
86
|
"@types/pngjs": "^6.0.5",
|
|
86
87
|
"tsup": "^8.0.0",
|
|
87
88
|
"typescript": "^5.7.0",
|
|
88
|
-
"vite": "^
|
|
89
|
+
"vite": "^8.0.16",
|
|
89
90
|
"vitest": "^4.1.9"
|
|
90
91
|
},
|
|
91
92
|
"mcpName": "io.github.kamegoro/frameshot-mcp",
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { execSync } from "node:child_process";
|
|
4
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { basename, extname, join, relative } from "node:path";
|
|
6
6
|
|
|
7
7
|
const {
|
|
8
8
|
INPUT_PATHS = "",
|
|
9
|
+
INPUT_EXTENSIONS = ".jsx,.tsx,.vue,.svelte,.astro,.mdx",
|
|
10
|
+
INPUT_EXCLUDE = "*.test.*,*.spec.*,*.stories.*,*.story.*",
|
|
9
11
|
INPUT_WIDTH = "",
|
|
10
12
|
INPUT_HEIGHT = "",
|
|
11
13
|
INPUT_BASE_REF = "",
|
|
@@ -20,19 +22,33 @@ const patterns = INPUT_PATHS.split(/[,\n]/)
|
|
|
20
22
|
.map((p) => p.trim())
|
|
21
23
|
.filter(Boolean);
|
|
22
24
|
|
|
23
|
-
const COMPONENT_EXTS = new Set(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
const COMPONENT_EXTS = new Set(
|
|
26
|
+
INPUT_EXTENSIONS.split(",").map((e) => e.trim().toLowerCase()).filter(Boolean),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const EXCLUDE_PATTERNS = INPUT_EXCLUDE.split(",")
|
|
30
|
+
.map((p) => p.trim())
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
.map((p) => {
|
|
33
|
+
// Convert glob pattern to regex: *.test.* → /[^/]*\.test\.[^/]*/
|
|
34
|
+
const escaped = p.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*");
|
|
35
|
+
return new RegExp(escaped);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
function isExcluded(filePath) {
|
|
39
|
+
const base = filePath.split("/").pop() ?? "";
|
|
40
|
+
return EXCLUDE_PATTERNS.some((re) => re.test(base));
|
|
41
|
+
}
|
|
29
42
|
|
|
30
43
|
function findFiles(workspace, patterns) {
|
|
31
44
|
const allFiles = [];
|
|
32
45
|
for (const pattern of patterns) {
|
|
33
46
|
try {
|
|
47
|
+
const absPattern = pattern.startsWith("/")
|
|
48
|
+
? pattern
|
|
49
|
+
: join(workspace, pattern.replace(/^\.\//, ""));
|
|
34
50
|
const result = execSync(
|
|
35
|
-
`find ${workspace} -path
|
|
51
|
+
`find "${workspace}" -path "${absPattern}" -type f 2>/dev/null`,
|
|
36
52
|
{ encoding: "utf-8" },
|
|
37
53
|
);
|
|
38
54
|
allFiles.push(...result.split("\n").filter(Boolean));
|
|
@@ -40,7 +56,23 @@ function findFiles(workspace, patterns) {
|
|
|
40
56
|
// Pattern didn't match
|
|
41
57
|
}
|
|
42
58
|
}
|
|
43
|
-
return allFiles
|
|
59
|
+
return allFiles
|
|
60
|
+
.filter((f) => COMPONENT_EXTS.has(extname(f).toLowerCase()))
|
|
61
|
+
.filter((f) => !isExcluded(f));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// When no paths specified, scan all component files in the workspace
|
|
65
|
+
function findAllComponentFiles(workspace) {
|
|
66
|
+
try {
|
|
67
|
+
const exts = [...COMPONENT_EXTS].map((e) => `-name "*${e}"`).join(" -o ");
|
|
68
|
+
const result = execSync(
|
|
69
|
+
`find "${workspace}" \\( ${exts} \\) -not -path "*/node_modules/*" -not -path "*/.git/*" -type f 2>/dev/null`,
|
|
70
|
+
{ encoding: "utf-8" },
|
|
71
|
+
);
|
|
72
|
+
return result.split("\n").filter(Boolean).filter((f) => !isExcluded(f));
|
|
73
|
+
} catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
44
76
|
}
|
|
45
77
|
|
|
46
78
|
function getChangedFiles(baseRef) {
|
|
@@ -78,8 +110,26 @@ function restoreFile(filePath) {
|
|
|
78
110
|
}
|
|
79
111
|
|
|
80
112
|
async function main() {
|
|
81
|
-
|
|
82
|
-
|
|
113
|
+
// Try to resolve frameshot-mcp/renderer from multiple locations:
|
|
114
|
+
// 1. The action's own node_modules (when run as a GitHub Action)
|
|
115
|
+
// 2. Standard module resolution (when installed globally or in PATH)
|
|
116
|
+
let renderer;
|
|
117
|
+
const actionDir = new URL("..", import.meta.url).pathname;
|
|
118
|
+
const localPath = new URL(
|
|
119
|
+
"node_modules/frameshot-mcp/dist/renderer.js",
|
|
120
|
+
`file://${actionDir}/`,
|
|
121
|
+
).href;
|
|
122
|
+
try {
|
|
123
|
+
renderer = await import(localPath);
|
|
124
|
+
} catch {
|
|
125
|
+
try {
|
|
126
|
+
renderer = await import("frameshot-mcp/renderer");
|
|
127
|
+
} catch {
|
|
128
|
+
console.error("Could not load frameshot-mcp. Run: npm install frameshot-mcp");
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const { BrowserPool, HtmlBuilder, ImageComparator, RenderUseCase, ViteBundler } = renderer;
|
|
83
133
|
|
|
84
134
|
const browserPool = new BrowserPool();
|
|
85
135
|
const htmlBuilder = new HtmlBuilder();
|
|
@@ -94,16 +144,33 @@ async function main() {
|
|
|
94
144
|
|
|
95
145
|
await browserPool.warmup(["chromium"]);
|
|
96
146
|
|
|
97
|
-
|
|
147
|
+
// If no paths specified, use changed files detected from git diff
|
|
148
|
+
let allFiles;
|
|
149
|
+
if (patterns.length === 0) {
|
|
150
|
+
const changedFiles = getChangedFiles(INPUT_BASE_REF);
|
|
151
|
+
if (!changedFiles) {
|
|
152
|
+
// No base ref — render all component files in workspace
|
|
153
|
+
allFiles = findAllComponentFiles(GITHUB_WORKSPACE);
|
|
154
|
+
} else {
|
|
155
|
+
// Render only the changed component files
|
|
156
|
+
allFiles = [...changedFiles]
|
|
157
|
+
.filter((f) => COMPONENT_EXTS.has(extname(f).toLowerCase()))
|
|
158
|
+
.filter((f) => !isExcluded(f))
|
|
159
|
+
.map((f) => join(GITHUB_WORKSPACE, f))
|
|
160
|
+
.filter((f) => existsSync(f));
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
allFiles = findFiles(GITHUB_WORKSPACE, patterns);
|
|
164
|
+
}
|
|
98
165
|
|
|
99
166
|
if (allFiles.length === 0) {
|
|
100
|
-
console.log("No component files
|
|
167
|
+
console.log("No changed component files to render.");
|
|
101
168
|
await viteBundler.shutdown();
|
|
102
169
|
await browserPool.shutdown();
|
|
103
170
|
process.exit(0);
|
|
104
171
|
}
|
|
105
172
|
|
|
106
|
-
const changedFiles = getChangedFiles(INPUT_BASE_REF);
|
|
173
|
+
const changedFiles = patterns.length > 0 ? getChangedFiles(INPUT_BASE_REF) : null;
|
|
107
174
|
const filesToRender = changedFiles
|
|
108
175
|
? allFiles.filter((f) => {
|
|
109
176
|
const rel = relative(GITHUB_WORKSPACE, f);
|
|
@@ -123,8 +190,8 @@ async function main() {
|
|
|
123
190
|
const autoFit = !INPUT_WIDTH || !INPUT_HEIGHT;
|
|
124
191
|
const renderOpts = {
|
|
125
192
|
viewport: {
|
|
126
|
-
width: INPUT_WIDTH ? parseInt(INPUT_WIDTH) : 1280,
|
|
127
|
-
height: INPUT_HEIGHT ? parseInt(INPUT_HEIGHT) : 800,
|
|
193
|
+
width: INPUT_WIDTH ? parseInt(INPUT_WIDTH, 10) : 1280,
|
|
194
|
+
height: INPUT_HEIGHT ? parseInt(INPUT_HEIGHT, 10) : 800,
|
|
128
195
|
},
|
|
129
196
|
autoFit,
|
|
130
197
|
fullPage: true,
|