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.
@@ -0,0 +1,191 @@
1
+ import {
2
+ attachConsoleCapture,
3
+ takeScreenshot
4
+ } from "./chunk-O7NWWFIU.js";
5
+
6
+ // src/infrastructure/snapshot-store.ts
7
+ var SnapshotStore = class {
8
+ store = /* @__PURE__ */ new Map();
9
+ save(key, image) {
10
+ this.store.set(key, { image, timestamp: Date.now() });
11
+ }
12
+ get(key) {
13
+ return this.store.get(key);
14
+ }
15
+ list() {
16
+ return Array.from(this.store.entries()).map(([key, val]) => ({
17
+ key,
18
+ timestamp: val.timestamp
19
+ }));
20
+ }
21
+ };
22
+
23
+ // src/use-cases/audit.ts
24
+ var AuditUseCase = class {
25
+ constructor(pool, htmlBuilder) {
26
+ this.pool = pool;
27
+ this.htmlBuilder = htmlBuilder;
28
+ }
29
+ pool;
30
+ htmlBuilder;
31
+ async auditA11y(code, framework, options = {}) {
32
+ const html = this.htmlBuilder.build(code, framework, {
33
+ darkMode: options.darkMode ?? false,
34
+ css: options.css ?? "",
35
+ tailwindVersion: options.tailwindVersion ?? "3"
36
+ });
37
+ const page = await this.pool.getPage("chromium");
38
+ const viewport = options.viewport ?? { width: 1280, height: 800 };
39
+ await this.pool.setViewport("chromium", viewport);
40
+ await page.setContent(html, { waitUntil: "load", timeout: 1e4 });
41
+ const axeSource = await import("axe-core").then((m) => m.source);
42
+ await page.addScriptTag({ content: axeSource });
43
+ const results = await page.evaluate(() => {
44
+ return window.axe.run();
45
+ });
46
+ return {
47
+ violations: results.violations.map(
48
+ (v) => ({
49
+ id: v.id,
50
+ impact: v.impact,
51
+ description: v.description,
52
+ helpUrl: v.helpUrl,
53
+ nodes: v.nodes.map((n) => ({
54
+ html: n.html,
55
+ target: n.target
56
+ }))
57
+ })
58
+ ),
59
+ passes: results.passes.length,
60
+ incomplete: results.incomplete.length
61
+ };
62
+ }
63
+ async perfAudit(code, framework, options = {}) {
64
+ const html = this.htmlBuilder.build(code, framework, {
65
+ darkMode: options.darkMode ?? false,
66
+ css: options.css ?? "",
67
+ tailwindVersion: options.tailwindVersion ?? "3"
68
+ });
69
+ const page = await this.pool.getPage("chromium");
70
+ const viewport = options.viewport ?? { width: 1280, height: 800 };
71
+ await this.pool.setViewport("chromium", viewport);
72
+ const start = performance.now();
73
+ await page.setContent(html, { waitUntil: "load", timeout: 1e4 });
74
+ const renderTimeMs = Math.round(performance.now() - start);
75
+ const metrics = await page.evaluate(() => {
76
+ const all = document.querySelectorAll("*");
77
+ let maxDepth = 0;
78
+ for (const el of all) {
79
+ let depth = 0;
80
+ let node = el;
81
+ while (node) {
82
+ depth++;
83
+ node = node.parentElement;
84
+ }
85
+ if (depth > maxDepth) maxDepth = depth;
86
+ }
87
+ return {
88
+ domElements: all.length,
89
+ domDepth: maxDepth,
90
+ scriptCount: document.querySelectorAll("script").length,
91
+ styleSheetCount: document.styleSheets.length,
92
+ imageCount: document.querySelectorAll("img").length,
93
+ totalDomSize: document.documentElement.outerHTML.length
94
+ };
95
+ });
96
+ return { renderTimeMs, ...metrics };
97
+ }
98
+ };
99
+
100
+ // src/use-cases/screenshot.ts
101
+ var ScreenshotUseCase = class {
102
+ constructor(pool) {
103
+ this.pool = pool;
104
+ }
105
+ pool;
106
+ async screenshotUrl(url, options = {}) {
107
+ const engines = options.engines ?? ["chromium"];
108
+ const { waitForNetworkIdle = true } = options;
109
+ return Promise.all(
110
+ engines.map(async (engine) => {
111
+ const { width = 1280, height = 800 } = options.viewport ?? {};
112
+ const fullPage = options.fullPage ?? true;
113
+ const waitFor = options.waitFor ?? 0;
114
+ const page = await this.pool.getPage(engine);
115
+ await this.pool.setViewport(engine, { width, height });
116
+ const { errors, cleanup } = attachConsoleCapture(page);
117
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: 15e3 });
118
+ if (waitForNetworkIdle) {
119
+ await page.waitForLoadState("networkidle", { timeout: 5e3 }).catch(() => {
120
+ });
121
+ }
122
+ if (options.waitForSelector) {
123
+ await page.waitForSelector(options.waitForSelector, {
124
+ timeout: 1e4
125
+ });
126
+ }
127
+ if (waitFor > 0) {
128
+ await page.waitForTimeout(waitFor);
129
+ }
130
+ return takeScreenshot(page, engine, fullPage, errors, cleanup);
131
+ })
132
+ );
133
+ }
134
+ async screenshotUrlWithRetry(url, options = {}) {
135
+ const { retryCount = 0, ...screenshotOpts } = options;
136
+ const maxAttempts = retryCount + 1;
137
+ let lastError;
138
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
139
+ try {
140
+ if (attempt > 0) {
141
+ const delay = 300 * 2 ** (attempt - 1);
142
+ await new Promise((resolve) => setTimeout(resolve, delay));
143
+ }
144
+ return await this.screenshotUrl(url, screenshotOpts);
145
+ } catch (error) {
146
+ lastError = error;
147
+ }
148
+ }
149
+ throw lastError;
150
+ }
151
+ };
152
+
153
+ // src/use-cases/snapshot.ts
154
+ var SnapshotUseCase = class {
155
+ constructor(store, renderUseCase, diffUseCase) {
156
+ this.store = store;
157
+ this.renderUseCase = renderUseCase;
158
+ this.diffUseCase = diffUseCase;
159
+ }
160
+ store;
161
+ renderUseCase;
162
+ diffUseCase;
163
+ async save(key, code, framework, options = {}) {
164
+ const [result] = await this.renderUseCase.render(code, framework, {
165
+ ...options,
166
+ engines: ["chromium"]
167
+ });
168
+ this.store.save(key, result.image);
169
+ return { image: result.image, width: result.width, height: result.height };
170
+ }
171
+ async check(key, code, framework, options = {}) {
172
+ const snapshot = this.store.get(key);
173
+ if (!snapshot) return null;
174
+ return this.diffUseCase.diffFromReference(
175
+ code,
176
+ framework,
177
+ snapshot.image,
178
+ options
179
+ );
180
+ }
181
+ list() {
182
+ return this.store.list();
183
+ }
184
+ };
185
+
186
+ export {
187
+ SnapshotStore,
188
+ AuditUseCase,
189
+ ScreenshotUseCase,
190
+ SnapshotUseCase
191
+ };
@@ -0,0 +1,139 @@
1
+ import {
2
+ AuditUseCase,
3
+ BrowserPool,
4
+ CatalogUseCase,
5
+ DiffUseCase,
6
+ HtmlBuilder,
7
+ ImageComparator,
8
+ RenderUseCase,
9
+ ScreenshotUseCase,
10
+ SnapshotStore,
11
+ SnapshotUseCase,
12
+ ViteBundler
13
+ } from "./chunk-Q7NQA4ZM.js";
14
+
15
+ // src/use-cases/watch.ts
16
+ import { watch } from "chokidar";
17
+ var WatchUseCase = class {
18
+ constructor(renderUseCase) {
19
+ this.renderUseCase = renderUseCase;
20
+ }
21
+ renderUseCase;
22
+ sessions = /* @__PURE__ */ new Map();
23
+ nextId = 1;
24
+ start(patterns, props, callback) {
25
+ const id = `watch-${this.nextId++}`;
26
+ const watcher = watch(patterns, {
27
+ ignoreInitial: false,
28
+ awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 }
29
+ });
30
+ watcher.on("add", (path) => this.handleChange(id, path, props, callback));
31
+ watcher.on(
32
+ "change",
33
+ (path) => this.handleChange(id, path, props, callback)
34
+ );
35
+ const session = {
36
+ id,
37
+ patterns,
38
+ stop: () => {
39
+ watcher.close();
40
+ this.sessions.delete(id);
41
+ }
42
+ };
43
+ this.sessions.set(id, session);
44
+ return session;
45
+ }
46
+ stop(sessionId) {
47
+ const session = this.sessions.get(sessionId);
48
+ if (!session) return false;
49
+ session.stop();
50
+ return true;
51
+ }
52
+ stopAll() {
53
+ for (const session of this.sessions.values()) {
54
+ session.stop();
55
+ }
56
+ }
57
+ activeSessions() {
58
+ return Array.from(this.sessions.values());
59
+ }
60
+ async handleChange(sessionId, filePath, props, callback) {
61
+ const start = performance.now();
62
+ try {
63
+ const { results, mode } = await this.renderUseCase.renderFile(filePath, {
64
+ props,
65
+ autoFit: true,
66
+ fullPage: true,
67
+ engines: ["chromium"]
68
+ });
69
+ const result = results[0];
70
+ callback({
71
+ sessionId,
72
+ filePath,
73
+ image: result.image,
74
+ width: result.width,
75
+ height: result.height,
76
+ elapsedMs: Math.round(performance.now() - start),
77
+ mode,
78
+ consoleErrors: result.consoleErrors
79
+ });
80
+ } catch (err) {
81
+ callback({
82
+ sessionId,
83
+ filePath,
84
+ image: "",
85
+ width: 0,
86
+ height: 0,
87
+ elapsedMs: Math.round(performance.now() - start),
88
+ mode: "cdn",
89
+ consoleErrors: [],
90
+ error: err instanceof Error ? err.message : String(err)
91
+ });
92
+ }
93
+ }
94
+ };
95
+
96
+ // src/container.ts
97
+ function createContainer() {
98
+ const pool = new BrowserPool();
99
+ const htmlBuilder = new HtmlBuilder();
100
+ const imageComparator = new ImageComparator();
101
+ const snapshotStore = new SnapshotStore();
102
+ const viteBundler = new ViteBundler();
103
+ const renderUseCase = new RenderUseCase(
104
+ pool,
105
+ htmlBuilder,
106
+ imageComparator,
107
+ viteBundler
108
+ );
109
+ const screenshotUseCase = new ScreenshotUseCase(pool);
110
+ const diffUseCase = new DiffUseCase(renderUseCase, imageComparator);
111
+ const auditUseCase = new AuditUseCase(pool, htmlBuilder);
112
+ const snapshotUseCase = new SnapshotUseCase(
113
+ snapshotStore,
114
+ renderUseCase,
115
+ diffUseCase
116
+ );
117
+ const catalogUseCase = new CatalogUseCase(renderUseCase);
118
+ const watchUseCase = new WatchUseCase(renderUseCase);
119
+ return {
120
+ pool,
121
+ viteBundler,
122
+ renderUseCase,
123
+ screenshotUseCase,
124
+ diffUseCase,
125
+ auditUseCase,
126
+ snapshotUseCase,
127
+ catalogUseCase,
128
+ watchUseCase,
129
+ async shutdown() {
130
+ watchUseCase.stopAll();
131
+ await viteBundler.shutdown();
132
+ await pool.shutdown();
133
+ }
134
+ };
135
+ }
136
+
137
+ export {
138
+ createContainer
139
+ };
@@ -0,0 +1,191 @@
1
+ import {
2
+ attachConsoleCapture,
3
+ takeScreenshot
4
+ } from "./chunk-3CDSNOX5.js";
5
+
6
+ // src/infrastructure/snapshot-store.ts
7
+ var SnapshotStore = class {
8
+ store = /* @__PURE__ */ new Map();
9
+ save(key, image) {
10
+ this.store.set(key, { image, timestamp: Date.now() });
11
+ }
12
+ get(key) {
13
+ return this.store.get(key);
14
+ }
15
+ list() {
16
+ return Array.from(this.store.entries()).map(([key, val]) => ({
17
+ key,
18
+ timestamp: val.timestamp
19
+ }));
20
+ }
21
+ };
22
+
23
+ // src/use-cases/audit.ts
24
+ var AuditUseCase = class {
25
+ constructor(pool, htmlBuilder) {
26
+ this.pool = pool;
27
+ this.htmlBuilder = htmlBuilder;
28
+ }
29
+ pool;
30
+ htmlBuilder;
31
+ async auditA11y(code, framework, options = {}) {
32
+ const html = this.htmlBuilder.build(code, framework, {
33
+ darkMode: options.darkMode ?? false,
34
+ css: options.css ?? "",
35
+ tailwindVersion: options.tailwindVersion ?? "3"
36
+ });
37
+ const page = await this.pool.getPage("chromium");
38
+ const viewport = options.viewport ?? { width: 1280, height: 800 };
39
+ await this.pool.setViewport("chromium", viewport);
40
+ await page.setContent(html, { waitUntil: "load", timeout: 1e4 });
41
+ const axeSource = await import("axe-core").then((m) => m.source);
42
+ await page.addScriptTag({ content: axeSource });
43
+ const results = await page.evaluate(() => {
44
+ return window.axe.run();
45
+ });
46
+ return {
47
+ violations: results.violations.map(
48
+ (v) => ({
49
+ id: v.id,
50
+ impact: v.impact,
51
+ description: v.description,
52
+ helpUrl: v.helpUrl,
53
+ nodes: v.nodes.map((n) => ({
54
+ html: n.html,
55
+ target: n.target
56
+ }))
57
+ })
58
+ ),
59
+ passes: results.passes.length,
60
+ incomplete: results.incomplete.length
61
+ };
62
+ }
63
+ async perfAudit(code, framework, options = {}) {
64
+ const html = this.htmlBuilder.build(code, framework, {
65
+ darkMode: options.darkMode ?? false,
66
+ css: options.css ?? "",
67
+ tailwindVersion: options.tailwindVersion ?? "3"
68
+ });
69
+ const page = await this.pool.getPage("chromium");
70
+ const viewport = options.viewport ?? { width: 1280, height: 800 };
71
+ await this.pool.setViewport("chromium", viewport);
72
+ const start = performance.now();
73
+ await page.setContent(html, { waitUntil: "load", timeout: 1e4 });
74
+ const renderTimeMs = Math.round(performance.now() - start);
75
+ const metrics = await page.evaluate(() => {
76
+ const all = document.querySelectorAll("*");
77
+ let maxDepth = 0;
78
+ for (const el of all) {
79
+ let depth = 0;
80
+ let node = el;
81
+ while (node) {
82
+ depth++;
83
+ node = node.parentElement;
84
+ }
85
+ if (depth > maxDepth) maxDepth = depth;
86
+ }
87
+ return {
88
+ domElements: all.length,
89
+ domDepth: maxDepth,
90
+ scriptCount: document.querySelectorAll("script").length,
91
+ styleSheetCount: document.styleSheets.length,
92
+ imageCount: document.querySelectorAll("img").length,
93
+ totalDomSize: document.documentElement.outerHTML.length
94
+ };
95
+ });
96
+ return { renderTimeMs, ...metrics };
97
+ }
98
+ };
99
+
100
+ // src/use-cases/screenshot.ts
101
+ var ScreenshotUseCase = class {
102
+ constructor(pool) {
103
+ this.pool = pool;
104
+ }
105
+ pool;
106
+ async screenshotUrl(url, options = {}) {
107
+ const engines = options.engines ?? ["chromium"];
108
+ const { waitForNetworkIdle = true } = options;
109
+ return Promise.all(
110
+ engines.map(async (engine) => {
111
+ const { width = 1280, height = 800 } = options.viewport ?? {};
112
+ const fullPage = options.fullPage ?? true;
113
+ const waitFor = options.waitFor ?? 0;
114
+ const page = await this.pool.getPage(engine);
115
+ await this.pool.setViewport(engine, { width, height });
116
+ const { errors, cleanup } = attachConsoleCapture(page);
117
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: 15e3 });
118
+ if (waitForNetworkIdle) {
119
+ await page.waitForLoadState("networkidle", { timeout: 5e3 }).catch(() => {
120
+ });
121
+ }
122
+ if (options.waitForSelector) {
123
+ await page.waitForSelector(options.waitForSelector, {
124
+ timeout: 1e4
125
+ });
126
+ }
127
+ if (waitFor > 0) {
128
+ await page.waitForTimeout(waitFor);
129
+ }
130
+ return takeScreenshot(page, engine, fullPage, errors, cleanup);
131
+ })
132
+ );
133
+ }
134
+ async screenshotUrlWithRetry(url, options = {}) {
135
+ const { retryCount = 0, ...screenshotOpts } = options;
136
+ const maxAttempts = retryCount + 1;
137
+ let lastError;
138
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
139
+ try {
140
+ if (attempt > 0) {
141
+ const delay = 300 * 2 ** (attempt - 1);
142
+ await new Promise((resolve) => setTimeout(resolve, delay));
143
+ }
144
+ return await this.screenshotUrl(url, screenshotOpts);
145
+ } catch (error) {
146
+ lastError = error;
147
+ }
148
+ }
149
+ throw lastError;
150
+ }
151
+ };
152
+
153
+ // src/use-cases/snapshot.ts
154
+ var SnapshotUseCase = class {
155
+ constructor(store, renderUseCase, diffUseCase) {
156
+ this.store = store;
157
+ this.renderUseCase = renderUseCase;
158
+ this.diffUseCase = diffUseCase;
159
+ }
160
+ store;
161
+ renderUseCase;
162
+ diffUseCase;
163
+ async save(key, code, framework, options = {}) {
164
+ const [result] = await this.renderUseCase.render(code, framework, {
165
+ ...options,
166
+ engines: ["chromium"]
167
+ });
168
+ this.store.save(key, result.image);
169
+ return { image: result.image, width: result.width, height: result.height };
170
+ }
171
+ async check(key, code, framework, options = {}) {
172
+ const snapshot = this.store.get(key);
173
+ if (!snapshot) return null;
174
+ return this.diffUseCase.diffFromReference(
175
+ code,
176
+ framework,
177
+ snapshot.image,
178
+ options
179
+ );
180
+ }
181
+ list() {
182
+ return this.store.list();
183
+ }
184
+ };
185
+
186
+ export {
187
+ SnapshotStore,
188
+ AuditUseCase,
189
+ ScreenshotUseCase,
190
+ SnapshotUseCase
191
+ };
@@ -0,0 +1,157 @@
1
+ import {
2
+ AuditUseCase,
3
+ BrowserPool,
4
+ CatalogUseCase,
5
+ DiffUseCase,
6
+ HtmlBuilder,
7
+ ImageComparator,
8
+ RenderUseCase,
9
+ ScreenshotUseCase,
10
+ SnapshotStore,
11
+ SnapshotUseCase,
12
+ ViteBundler
13
+ } from "./chunk-PYWXJZTZ.js";
14
+
15
+ // src/use-cases/watch.ts
16
+ import { watch } from "chokidar";
17
+ var WatchUseCase = class {
18
+ constructor(renderUseCase) {
19
+ this.renderUseCase = renderUseCase;
20
+ }
21
+ renderUseCase;
22
+ sessions = /* @__PURE__ */ new Map();
23
+ latestRenders = /* @__PURE__ */ new Map();
24
+ inFlight = /* @__PURE__ */ new Set();
25
+ // key: `${sessionId}:${filePath}`
26
+ nextId = 1;
27
+ start(patterns, props, callback) {
28
+ const id = `watch-${this.nextId++}`;
29
+ const watcher = watch(patterns, {
30
+ ignoreInitial: true,
31
+ // only watch future changes, not existing files on startup
32
+ awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 }
33
+ });
34
+ watcher.on(
35
+ "change",
36
+ (path) => this.handleChange(id, path, props, callback)
37
+ );
38
+ const session = {
39
+ id,
40
+ patterns,
41
+ stop: () => {
42
+ watcher.close();
43
+ this.sessions.delete(id);
44
+ this.latestRenders.delete(id);
45
+ }
46
+ };
47
+ this.sessions.set(id, session);
48
+ return session;
49
+ }
50
+ stop(sessionId) {
51
+ const session = this.sessions.get(sessionId);
52
+ if (!session) return false;
53
+ session.stop();
54
+ return true;
55
+ }
56
+ stopAll() {
57
+ const sessions = Array.from(this.sessions.values());
58
+ for (const session of sessions) {
59
+ session.stop();
60
+ }
61
+ this.latestRenders.clear();
62
+ }
63
+ activeSessions() {
64
+ return Array.from(this.sessions.values());
65
+ }
66
+ getLatestRender(sessionId) {
67
+ return this.latestRenders.get(sessionId);
68
+ }
69
+ async handleChange(sessionId, filePath, props, callback) {
70
+ const key = `${sessionId}:${filePath}`;
71
+ if (this.inFlight.has(key)) return;
72
+ this.inFlight.add(key);
73
+ const start = performance.now();
74
+ try {
75
+ const { results, mode } = await this.renderUseCase.renderFile(filePath, {
76
+ props,
77
+ autoFit: true,
78
+ fullPage: true,
79
+ engines: ["chromium"]
80
+ });
81
+ const result = results[0];
82
+ const event = {
83
+ sessionId,
84
+ filePath,
85
+ image: result.image,
86
+ width: result.width,
87
+ height: result.height,
88
+ elapsedMs: Math.round(performance.now() - start),
89
+ mode,
90
+ consoleErrors: result.consoleErrors
91
+ };
92
+ this.latestRenders.set(sessionId, event);
93
+ await callback(event);
94
+ } catch (err) {
95
+ const event = {
96
+ sessionId,
97
+ filePath,
98
+ image: "",
99
+ width: 0,
100
+ height: 0,
101
+ elapsedMs: Math.round(performance.now() - start),
102
+ mode: "cdn",
103
+ consoleErrors: [],
104
+ error: err instanceof Error ? err.message : String(err)
105
+ };
106
+ this.latestRenders.set(sessionId, event);
107
+ await callback(event);
108
+ } finally {
109
+ this.inFlight.delete(key);
110
+ }
111
+ }
112
+ };
113
+
114
+ // src/container.ts
115
+ function createContainer() {
116
+ const pool = new BrowserPool();
117
+ const htmlBuilder = new HtmlBuilder();
118
+ const imageComparator = new ImageComparator();
119
+ const snapshotStore = new SnapshotStore();
120
+ const viteBundler = new ViteBundler();
121
+ const renderUseCase = new RenderUseCase(
122
+ pool,
123
+ htmlBuilder,
124
+ imageComparator,
125
+ viteBundler
126
+ );
127
+ const screenshotUseCase = new ScreenshotUseCase(pool);
128
+ const diffUseCase = new DiffUseCase(renderUseCase, imageComparator);
129
+ const auditUseCase = new AuditUseCase(pool, htmlBuilder);
130
+ const snapshotUseCase = new SnapshotUseCase(
131
+ snapshotStore,
132
+ renderUseCase,
133
+ diffUseCase
134
+ );
135
+ const catalogUseCase = new CatalogUseCase(renderUseCase);
136
+ const watchUseCase = new WatchUseCase(renderUseCase);
137
+ return {
138
+ pool,
139
+ viteBundler,
140
+ renderUseCase,
141
+ screenshotUseCase,
142
+ diffUseCase,
143
+ auditUseCase,
144
+ snapshotUseCase,
145
+ catalogUseCase,
146
+ watchUseCase,
147
+ async shutdown() {
148
+ watchUseCase.stopAll();
149
+ await viteBundler.shutdown();
150
+ await pool.shutdown();
151
+ }
152
+ };
153
+ }
154
+
155
+ export {
156
+ createContainer
157
+ };