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
|
@@ -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
|
+
};
|