frameshot-mcp 0.7.0 → 0.9.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -69
- package/action.yml +114 -16
- package/dist/chunk-MEBQ7ZWA.js +1774 -0
- package/dist/chunk-VUYZHZBH.js +157 -0
- package/dist/cli.js +131 -133
- package/dist/index.js +519 -572
- package/dist/renderer.d.ts +17 -7
- package/dist/renderer.js +4 -6
- package/dist/stubs/gatsby.js +20 -0
- package/dist/stubs/next-font.js +9 -0
- package/dist/stubs/next-headers.js +20 -0
- package/dist/stubs/next-image.js +34 -0
- package/dist/stubs/next-link.js +25 -0
- package/dist/stubs/next-navigation.js +17 -0
- package/dist/stubs/next-router.js +19 -0
- package/dist/stubs/nuxt-app.js +37 -0
- package/dist/stubs/nuxt-imports.js +13 -0
- package/dist/stubs/qwik-city.js +33 -0
- package/dist/stubs/react-router.js +67 -0
- package/dist/stubs/server-only.js +2 -0
- package/dist/stubs/solid-router.js +27 -0
- package/dist/stubs/solid-start.js +18 -0
- package/dist/stubs/stubs/gatsby.js +20 -0
- package/dist/stubs/stubs/next-font.js +9 -0
- package/dist/stubs/stubs/next-headers.js +20 -0
- package/dist/stubs/stubs/next-image.js +34 -0
- package/dist/stubs/stubs/next-link.js +25 -0
- package/dist/stubs/stubs/next-navigation.js +17 -0
- package/dist/stubs/stubs/next-router.js +19 -0
- package/dist/stubs/stubs/nuxt-app.js +37 -0
- package/dist/stubs/stubs/nuxt-imports.js +13 -0
- package/dist/stubs/stubs/qwik-city.js +33 -0
- package/dist/stubs/stubs/react-router.js +67 -0
- package/dist/stubs/stubs/server-only.js +2 -0
- package/dist/stubs/stubs/solid-router.js +27 -0
- package/dist/stubs/stubs/solid-start.js +18 -0
- package/dist/stubs/stubs/sveltekit-environment.js +5 -0
- package/dist/stubs/stubs/sveltekit-navigation.js +11 -0
- package/dist/stubs/stubs/sveltekit-stores.js +15 -0
- package/dist/stubs/stubs/vike.js +11 -0
- package/dist/stubs/sveltekit-environment.js +5 -0
- package/dist/stubs/sveltekit-navigation.js +11 -0
- package/dist/stubs/sveltekit-stores.js +15 -0
- package/dist/stubs/vike.js +11 -0
- package/package.json +10 -4
- package/scripts/render-changed.mjs +140 -18
- package/dist/chunk-3LVWVDET.js +0 -849
- package/dist/chunk-47YJG5HR.js +0 -690
- package/dist/chunk-67JZQ6OI.js +0 -819
- package/dist/chunk-AZCGKIMU.js +0 -850
- package/dist/chunk-B3CLIGWU.js +0 -786
- package/dist/chunk-C6QSY4WR.js +0 -811
- package/dist/chunk-DX54PJKO.js +0 -603
- package/dist/chunk-EMCJGIMY.js +0 -984
- package/dist/chunk-FQNWGR62.js +0 -849
- package/dist/chunk-FTYTZW6D.js +0 -203
- package/dist/chunk-JGVKYXY2.js +0 -857
- package/dist/chunk-JYPEA4P2.js +0 -846
- package/dist/chunk-KHK35HDD.js +0 -855
- package/dist/chunk-Q7A3DLED.js +0 -848
- package/dist/chunk-SIA6XEHM.js +0 -811
- package/dist/chunk-ST35YDI6.js +0 -834
- package/dist/chunk-T5OBJK35.js +0 -855
- package/dist/chunk-U3GHS7KO.js +0 -837
- package/dist/chunk-WS2ASCD6.js +0 -683
- package/dist/chunk-WZMHVSUA.js +0 -847
- package/dist/chunk-ZZST6K7Y.js +0 -987
|
@@ -0,0 +1,1774 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// src/domain/types.ts
|
|
8
|
+
var DEFAULT_RENDER_OPTIONS = {
|
|
9
|
+
viewport: { width: 1280, height: 800 },
|
|
10
|
+
engines: ["chromium"],
|
|
11
|
+
fullPage: true,
|
|
12
|
+
darkMode: false,
|
|
13
|
+
css: "",
|
|
14
|
+
tailwindVersion: "3",
|
|
15
|
+
waitFor: 0,
|
|
16
|
+
autoFit: false
|
|
17
|
+
};
|
|
18
|
+
var DEVICE_PRESETS = {
|
|
19
|
+
mobile: { width: 375, height: 667 },
|
|
20
|
+
tablet: { width: 768, height: 1024 },
|
|
21
|
+
desktop: { width: 1280, height: 800 }
|
|
22
|
+
};
|
|
23
|
+
var EXT_TO_FRAMEWORK = {
|
|
24
|
+
".jsx": "react",
|
|
25
|
+
".tsx": "react",
|
|
26
|
+
".vue": "vue",
|
|
27
|
+
".svelte": "svelte",
|
|
28
|
+
".html": "html",
|
|
29
|
+
".htm": "html"
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// src/infrastructure/browser-pool.ts
|
|
33
|
+
import { chromium, firefox, webkit } from "playwright";
|
|
34
|
+
var BrowserPool = class {
|
|
35
|
+
pool = /* @__PURE__ */ new Map();
|
|
36
|
+
waiters = /* @__PURE__ */ new Map();
|
|
37
|
+
totalPages = /* @__PURE__ */ new Map();
|
|
38
|
+
maxPages;
|
|
39
|
+
constructor(maxConcurrentPages = 4) {
|
|
40
|
+
this.maxPages = maxConcurrentPages;
|
|
41
|
+
}
|
|
42
|
+
async warmup(engines) {
|
|
43
|
+
await Promise.all(
|
|
44
|
+
engines.map(async (e) => {
|
|
45
|
+
const page = await this.getPage(e);
|
|
46
|
+
this.releasePage(e, page);
|
|
47
|
+
})
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
async getPage(engine) {
|
|
51
|
+
await this.acquireSlot(engine);
|
|
52
|
+
const slot = await this.getSlot(engine);
|
|
53
|
+
const idle = slot.idlePages.pop();
|
|
54
|
+
if (idle) return idle;
|
|
55
|
+
return this.createPage(slot.browser);
|
|
56
|
+
}
|
|
57
|
+
releasePage(engine, page) {
|
|
58
|
+
const slot = this.pool.get(engine);
|
|
59
|
+
if (slot) {
|
|
60
|
+
slot.idlePages.push(page);
|
|
61
|
+
} else {
|
|
62
|
+
page.close().catch(() => {
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
this.releaseSlot(engine);
|
|
66
|
+
}
|
|
67
|
+
async acquireSlot(engine) {
|
|
68
|
+
const total = this.totalPages.get(engine) ?? 0;
|
|
69
|
+
if (total < this.maxPages) {
|
|
70
|
+
this.totalPages.set(engine, total + 1);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
return new Promise((resolve3) => {
|
|
74
|
+
if (!this.waiters.has(engine)) this.waiters.set(engine, []);
|
|
75
|
+
this.waiters.get(engine)?.push(resolve3);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
releaseSlot(engine) {
|
|
79
|
+
const waiters = this.waiters.get(engine) ?? [];
|
|
80
|
+
if (waiters.length > 0) {
|
|
81
|
+
waiters.shift()?.();
|
|
82
|
+
} else {
|
|
83
|
+
const total = this.totalPages.get(engine) ?? 0;
|
|
84
|
+
this.totalPages.set(engine, Math.max(0, total - 1));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async shutdown() {
|
|
88
|
+
for (const slot of this.pool.values()) {
|
|
89
|
+
for (const page of slot.idlePages) {
|
|
90
|
+
await page.close().catch(() => {
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
await slot.browser.close().catch(() => {
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
this.pool.clear();
|
|
97
|
+
}
|
|
98
|
+
async getSlot(engine) {
|
|
99
|
+
const existing = this.pool.get(engine);
|
|
100
|
+
if (existing?.browser.isConnected()) {
|
|
101
|
+
return existing;
|
|
102
|
+
}
|
|
103
|
+
const launcher = { chromium, firefox, webkit }[engine];
|
|
104
|
+
let browser;
|
|
105
|
+
try {
|
|
106
|
+
browser = await launcher.launch({ headless: true });
|
|
107
|
+
} catch {
|
|
108
|
+
try {
|
|
109
|
+
browser = await launcher.launch({
|
|
110
|
+
headless: true,
|
|
111
|
+
...engine === "chromium" ? { channel: "chrome" } : {}
|
|
112
|
+
});
|
|
113
|
+
} catch (_e) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`${engine} is not installed. Run: npx playwright install ${engine}`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const slot = { browser, idlePages: [] };
|
|
120
|
+
this.pool.set(engine, slot);
|
|
121
|
+
return slot;
|
|
122
|
+
}
|
|
123
|
+
async createPage(browser) {
|
|
124
|
+
const context = await browser.newContext({
|
|
125
|
+
viewport: { width: 1280, height: 800 },
|
|
126
|
+
deviceScaleFactor: 2
|
|
127
|
+
});
|
|
128
|
+
const page = await context.newPage();
|
|
129
|
+
await page.setContent(
|
|
130
|
+
'<html><head><script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio"></script></head><body></body></html>',
|
|
131
|
+
{ waitUntil: "networkidle" }
|
|
132
|
+
);
|
|
133
|
+
return page;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// src/infrastructure/html-builder.ts
|
|
138
|
+
var HtmlBuilder = class {
|
|
139
|
+
build(code, framework, options) {
|
|
140
|
+
const { darkMode, css, tailwindVersion } = options;
|
|
141
|
+
const tailwind = tailwindVersion === "4" ? '<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>' : '<script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio"></script>';
|
|
142
|
+
const tailwindConfig = tailwindVersion === "4" ? "" : "<script>tailwind.config={darkMode:'class'}</script>";
|
|
143
|
+
const customCss = css ? `<style>${css}</style>` : "";
|
|
144
|
+
const baseStyle = `<style>*{margin:0;box-sizing:border-box}body{padding:16px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}</style>`;
|
|
145
|
+
const htmlClass = darkMode ? ' class="dark"' : "";
|
|
146
|
+
if (framework === "html") {
|
|
147
|
+
if (code.includes("<html")) return code;
|
|
148
|
+
return `<!DOCTYPE html><html${htmlClass}><head><meta charset="utf-8">${tailwind}${tailwindConfig}${baseStyle}${customCss}</head><body>${code}</body></html>`;
|
|
149
|
+
}
|
|
150
|
+
if (framework === "react") {
|
|
151
|
+
const cleanedCode = code.replace(/['"]use client['"];?\n?/g, "").replace(/['"]use server['"];?\n?/g, "").replace(/import\s+.*?\s+from\s+['"]next\/image['"];?\n?/g, "").replace(/import\s+.*?\s+from\s+['"]next\/link['"];?\n?/g, "");
|
|
152
|
+
return `<!DOCTYPE html><html${htmlClass}><head><meta charset="utf-8">${tailwind}${tailwindConfig}
|
|
153
|
+
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
|
154
|
+
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
|
155
|
+
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
|
156
|
+
${baseStyle}${customCss}</head><body><div id="root"></div>
|
|
157
|
+
<script type="text/babel">
|
|
158
|
+
const Image = (props) => React.createElement('img', {...props, src: props.src?.src || props.src});
|
|
159
|
+
const Link = ({href, children, ...props}) => React.createElement('a', {href, ...props}, children);
|
|
160
|
+
${cleanedCode}
|
|
161
|
+
const _C = typeof App!=='undefined'?App:typeof Component!=='undefined'?Component:typeof Default!=='undefined'?Default:null;
|
|
162
|
+
if(_C)ReactDOM.createRoot(document.getElementById('root')).render(React.createElement(_C));
|
|
163
|
+
</script></body></html>`;
|
|
164
|
+
}
|
|
165
|
+
if (framework === "vue") {
|
|
166
|
+
return `<!DOCTYPE html><html${htmlClass}><head><meta charset="utf-8">${tailwind}${tailwindConfig}
|
|
167
|
+
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
|
168
|
+
${baseStyle}${customCss}</head><body><div id="app"></div>
|
|
169
|
+
<script>
|
|
170
|
+
const{createApp,ref,reactive,computed,onMounted,watch,watchEffect}=Vue;
|
|
171
|
+
${code}
|
|
172
|
+
const _C=typeof App!=='undefined'?App:typeof Component!=='undefined'?Component:null;
|
|
173
|
+
if(_C)createApp(_C).mount('#app');
|
|
174
|
+
</script></body></html>`;
|
|
175
|
+
}
|
|
176
|
+
if (framework === "svelte") {
|
|
177
|
+
return `<!DOCTYPE html><html${htmlClass}><head><meta charset="utf-8">${tailwind}${tailwindConfig}
|
|
178
|
+
<script src="https://unpkg.com/svelte@4/compiler.cjs"></script>
|
|
179
|
+
${baseStyle}${customCss}</head><body><div id="app"></div>
|
|
180
|
+
<script type="module">
|
|
181
|
+
import "https://unpkg.com/svelte@4/internal/index.mjs";
|
|
182
|
+
${code}
|
|
183
|
+
const _C=typeof App!=='undefined'?App:typeof Component!=='undefined'?Component:null;
|
|
184
|
+
if(_C)new _C({target:document.getElementById('app')});
|
|
185
|
+
</script></body></html>`;
|
|
186
|
+
}
|
|
187
|
+
return code;
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// src/infrastructure/image-comparator.ts
|
|
192
|
+
import pixelmatch from "pixelmatch";
|
|
193
|
+
import { PNG } from "pngjs";
|
|
194
|
+
var ImageComparator = class {
|
|
195
|
+
diff(imageA, imageB, threshold = 0.1) {
|
|
196
|
+
const bufA = Buffer.from(imageA, "base64");
|
|
197
|
+
const bufB = Buffer.from(imageB, "base64");
|
|
198
|
+
const pngA = PNG.sync.read(bufA);
|
|
199
|
+
const pngB = PNG.sync.read(bufB);
|
|
200
|
+
const width = Math.max(pngA.width, pngB.width);
|
|
201
|
+
const height = Math.max(pngA.height, pngB.height);
|
|
202
|
+
const normalizedA = new PNG({ width, height });
|
|
203
|
+
const normalizedB = new PNG({ width, height });
|
|
204
|
+
PNG.bitblt(pngA, normalizedA, 0, 0, pngA.width, pngA.height, 0, 0);
|
|
205
|
+
PNG.bitblt(pngB, normalizedB, 0, 0, pngB.width, pngB.height, 0, 0);
|
|
206
|
+
const diffPng = new PNG({ width, height });
|
|
207
|
+
const diffPixels = pixelmatch(
|
|
208
|
+
normalizedA.data,
|
|
209
|
+
normalizedB.data,
|
|
210
|
+
diffPng.data,
|
|
211
|
+
width,
|
|
212
|
+
height,
|
|
213
|
+
{ threshold }
|
|
214
|
+
);
|
|
215
|
+
const totalPixels = width * height;
|
|
216
|
+
return {
|
|
217
|
+
diff: PNG.sync.write(diffPng).toString("base64"),
|
|
218
|
+
diffPixels,
|
|
219
|
+
totalPixels,
|
|
220
|
+
diffPercentage: Math.round(diffPixels / totalPixels * 1e4) / 100
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
composite(images, columns) {
|
|
224
|
+
const pngs = images.map((buf) => PNG.sync.read(buf));
|
|
225
|
+
const cellWidth = Math.max(...pngs.map((p) => p.width));
|
|
226
|
+
const cellHeight = Math.max(...pngs.map((p) => p.height));
|
|
227
|
+
const rows = Math.ceil(pngs.length / columns);
|
|
228
|
+
const gridWidth = cellWidth * columns;
|
|
229
|
+
const gridHeight = cellHeight * rows;
|
|
230
|
+
const grid = new PNG({ width: gridWidth, height: gridHeight });
|
|
231
|
+
grid.data.fill(255);
|
|
232
|
+
for (let i = 0; i < pngs.length; i++) {
|
|
233
|
+
const col = i % columns;
|
|
234
|
+
const row = Math.floor(i / columns);
|
|
235
|
+
const x = col * cellWidth;
|
|
236
|
+
const y = row * cellHeight;
|
|
237
|
+
PNG.bitblt(pngs[i], grid, 0, 0, pngs[i].width, pngs[i].height, x, y);
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
image: PNG.sync.write(grid).toString("base64"),
|
|
241
|
+
width: gridWidth,
|
|
242
|
+
height: gridHeight
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// src/infrastructure/snapshot-store.ts
|
|
248
|
+
var SnapshotStore = class {
|
|
249
|
+
store = /* @__PURE__ */ new Map();
|
|
250
|
+
save(key, image) {
|
|
251
|
+
this.store.set(key, { image, timestamp: Date.now() });
|
|
252
|
+
}
|
|
253
|
+
get(key) {
|
|
254
|
+
return this.store.get(key);
|
|
255
|
+
}
|
|
256
|
+
list() {
|
|
257
|
+
return Array.from(this.store.entries()).map(([key, val]) => ({
|
|
258
|
+
key,
|
|
259
|
+
timestamp: val.timestamp
|
|
260
|
+
}));
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// src/infrastructure/project-detector.ts
|
|
265
|
+
import { existsSync, readFileSync } from "fs";
|
|
266
|
+
import { dirname, join, resolve } from "path";
|
|
267
|
+
var VITE_CONFIG_NAMES = [
|
|
268
|
+
"vite.config.ts",
|
|
269
|
+
"vite.config.js",
|
|
270
|
+
"vite.config.mjs",
|
|
271
|
+
"vite.config.mts"
|
|
272
|
+
];
|
|
273
|
+
var ProjectDetector = class {
|
|
274
|
+
detect(filePath) {
|
|
275
|
+
const root = this.findProjectRoot(filePath);
|
|
276
|
+
const viteConfigPath = this.findViteConfig(root);
|
|
277
|
+
const framework = this.detectFramework(root);
|
|
278
|
+
const hasVite = this.checkViteAvailable(root);
|
|
279
|
+
const isNextJs = this.checkIsNextJs(root);
|
|
280
|
+
const pathAliases = this.readTsconfigAliases(root);
|
|
281
|
+
return { root, viteConfigPath, framework, hasVite, isNextJs, pathAliases };
|
|
282
|
+
}
|
|
283
|
+
findProjectRoot(startPath) {
|
|
284
|
+
let dir = dirname(resolve(startPath));
|
|
285
|
+
while (dir !== dirname(dir)) {
|
|
286
|
+
if (existsSync(join(dir, "package.json"))) {
|
|
287
|
+
return dir;
|
|
288
|
+
}
|
|
289
|
+
dir = dirname(dir);
|
|
290
|
+
}
|
|
291
|
+
return dirname(resolve(startPath));
|
|
292
|
+
}
|
|
293
|
+
findViteConfig(root) {
|
|
294
|
+
for (const name of VITE_CONFIG_NAMES) {
|
|
295
|
+
const configPath = join(root, name);
|
|
296
|
+
if (existsSync(configPath)) {
|
|
297
|
+
return configPath;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return void 0;
|
|
301
|
+
}
|
|
302
|
+
detectFramework(root) {
|
|
303
|
+
const pkgPath = join(root, "package.json");
|
|
304
|
+
if (!existsSync(pkgPath)) return "html";
|
|
305
|
+
try {
|
|
306
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
307
|
+
const allDeps = {
|
|
308
|
+
...pkg.dependencies,
|
|
309
|
+
...pkg.devDependencies
|
|
310
|
+
};
|
|
311
|
+
if (allDeps["solid-js"]) return "solid";
|
|
312
|
+
if (allDeps.preact) return "preact";
|
|
313
|
+
if (allDeps.react || allDeps["react-dom"]) return "react";
|
|
314
|
+
if (allDeps.vue) return "vue";
|
|
315
|
+
if (allDeps.svelte) return "svelte";
|
|
316
|
+
} catch {
|
|
317
|
+
}
|
|
318
|
+
return "html";
|
|
319
|
+
}
|
|
320
|
+
checkViteAvailable(root) {
|
|
321
|
+
const vitePkgPath = join(root, "node_modules", "vite", "package.json");
|
|
322
|
+
return existsSync(vitePkgPath);
|
|
323
|
+
}
|
|
324
|
+
checkIsNextJs(root) {
|
|
325
|
+
const pkgPath = join(root, "package.json");
|
|
326
|
+
if (!existsSync(pkgPath)) return false;
|
|
327
|
+
try {
|
|
328
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
329
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
330
|
+
return !!allDeps.next;
|
|
331
|
+
} catch {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
readTsconfigAliases(root) {
|
|
336
|
+
const tscPath = join(root, "tsconfig.json");
|
|
337
|
+
if (!existsSync(tscPath)) return {};
|
|
338
|
+
try {
|
|
339
|
+
const content = readFileSync(tscPath, "utf-8");
|
|
340
|
+
const stripped = content.replace(/^\s*\/\/[^\n]*/gm, "");
|
|
341
|
+
const tsc = JSON.parse(stripped);
|
|
342
|
+
const paths = tsc.compilerOptions?.paths ?? {};
|
|
343
|
+
const baseUrl = tsc.compilerOptions?.baseUrl ?? ".";
|
|
344
|
+
const result = {};
|
|
345
|
+
for (const [alias, targets] of Object.entries(paths)) {
|
|
346
|
+
const key = alias.replace(/\/\*$/, "");
|
|
347
|
+
const target = (targets[0] ?? "").replace(/\/\*$/, "");
|
|
348
|
+
result[key] = join(root, baseUrl, target);
|
|
349
|
+
}
|
|
350
|
+
return result;
|
|
351
|
+
} catch {
|
|
352
|
+
return {};
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
// src/infrastructure/vite-bundler.ts
|
|
358
|
+
import { existsSync as existsSync4 } from "fs";
|
|
359
|
+
import { createRequire } from "module";
|
|
360
|
+
import { dirname as dirname2, join as join18, resolve as resolve2 } from "path";
|
|
361
|
+
import { fileURLToPath } from "url";
|
|
362
|
+
|
|
363
|
+
// src/infrastructure/adapters/astro.ts
|
|
364
|
+
import { join as join3 } from "path";
|
|
365
|
+
|
|
366
|
+
// src/infrastructure/adapters/helpers.ts
|
|
367
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
368
|
+
import { join as join2 } from "path";
|
|
369
|
+
function hasDep(pkgPath, names) {
|
|
370
|
+
if (!existsSync2(pkgPath)) return false;
|
|
371
|
+
try {
|
|
372
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
373
|
+
const all = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
374
|
+
return names.some((n) => !!all[n]);
|
|
375
|
+
} catch {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function hasInstalled(projectRoot, name) {
|
|
380
|
+
return existsSync2(join2(projectRoot, "node_modules", ...name.split("/")));
|
|
381
|
+
}
|
|
382
|
+
async function loadPlugin(pkg, fnName) {
|
|
383
|
+
try {
|
|
384
|
+
const mod = await import(pkg);
|
|
385
|
+
const candidate = fnName ? mod[fnName] : mod.default ?? mod;
|
|
386
|
+
if (typeof candidate === "function") {
|
|
387
|
+
const result = candidate();
|
|
388
|
+
return Array.isArray(result) ? result : [result];
|
|
389
|
+
}
|
|
390
|
+
} catch {
|
|
391
|
+
process.stderr.write(`[frameshot] ${pkg} not available
|
|
392
|
+
`);
|
|
393
|
+
}
|
|
394
|
+
return [];
|
|
395
|
+
}
|
|
396
|
+
function loadReactPlugin() {
|
|
397
|
+
return loadPlugin("@vitejs/plugin-react");
|
|
398
|
+
}
|
|
399
|
+
function rscDirectivePlugin() {
|
|
400
|
+
return {
|
|
401
|
+
name: "frameshot-rsc-directives",
|
|
402
|
+
transform(code) {
|
|
403
|
+
return code.replace(/^['"]use (client|server)['"];?\s*/m, "");
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// src/infrastructure/adapters/astro.ts
|
|
409
|
+
var AstroAdapter = class {
|
|
410
|
+
id = "astro";
|
|
411
|
+
detect(projectRoot) {
|
|
412
|
+
return hasDep(join3(projectRoot, "package.json"), ["astro"]);
|
|
413
|
+
}
|
|
414
|
+
getAliases() {
|
|
415
|
+
return {};
|
|
416
|
+
}
|
|
417
|
+
getOptimizeDeps() {
|
|
418
|
+
return [];
|
|
419
|
+
}
|
|
420
|
+
async getPlugins(projectRoot) {
|
|
421
|
+
const plugins = [];
|
|
422
|
+
if (hasInstalled(projectRoot, "react"))
|
|
423
|
+
plugins.push(...await loadReactPlugin());
|
|
424
|
+
if (hasInstalled(projectRoot, "vue"))
|
|
425
|
+
plugins.push(...await loadPlugin("@vitejs/plugin-vue"));
|
|
426
|
+
if (hasInstalled(projectRoot, "svelte"))
|
|
427
|
+
plugins.push(
|
|
428
|
+
...await loadPlugin("@sveltejs/vite-plugin-svelte", "svelte")
|
|
429
|
+
);
|
|
430
|
+
if (hasInstalled(projectRoot, "solid-js"))
|
|
431
|
+
plugins.push(...await loadPlugin("vite-plugin-solid"));
|
|
432
|
+
return plugins;
|
|
433
|
+
}
|
|
434
|
+
useFrameshotVite() {
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
skipProjectConfig() {
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
// src/infrastructure/adapters/gatsby.ts
|
|
443
|
+
import { join as join4 } from "path";
|
|
444
|
+
var GatsbyAdapter = class {
|
|
445
|
+
id = "gatsby";
|
|
446
|
+
detect(projectRoot) {
|
|
447
|
+
return hasDep(join4(projectRoot, "package.json"), ["gatsby"]);
|
|
448
|
+
}
|
|
449
|
+
getAliases(_root, stubsDir) {
|
|
450
|
+
return { gatsby: join4(stubsDir, "gatsby.js") };
|
|
451
|
+
}
|
|
452
|
+
getOptimizeDeps(_root) {
|
|
453
|
+
return ["react", "react-dom/client"];
|
|
454
|
+
}
|
|
455
|
+
async getPlugins(_root) {
|
|
456
|
+
return loadReactPlugin();
|
|
457
|
+
}
|
|
458
|
+
useFrameshotVite() {
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
461
|
+
skipProjectConfig() {
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
// src/infrastructure/adapters/generic.ts
|
|
467
|
+
var GenericAdapter = class {
|
|
468
|
+
id = "generic";
|
|
469
|
+
detect() {
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
getAliases() {
|
|
473
|
+
return {};
|
|
474
|
+
}
|
|
475
|
+
getOptimizeDeps(projectRoot) {
|
|
476
|
+
const deps = [];
|
|
477
|
+
if (hasInstalled(projectRoot, "react"))
|
|
478
|
+
deps.push("react", "react-dom/client");
|
|
479
|
+
if (hasInstalled(projectRoot, "vue")) deps.push("vue");
|
|
480
|
+
if (hasInstalled(projectRoot, "solid-js"))
|
|
481
|
+
deps.push("solid-js", "solid-js/web");
|
|
482
|
+
if (hasInstalled(projectRoot, "preact")) deps.push("preact");
|
|
483
|
+
return deps;
|
|
484
|
+
}
|
|
485
|
+
async getPlugins(projectRoot) {
|
|
486
|
+
const plugins = [];
|
|
487
|
+
if (hasInstalled(projectRoot, "react"))
|
|
488
|
+
plugins.push(...await loadReactPlugin());
|
|
489
|
+
if (hasInstalled(projectRoot, "vue"))
|
|
490
|
+
plugins.push(...await loadPlugin("@vitejs/plugin-vue"));
|
|
491
|
+
if (hasInstalled(projectRoot, "svelte"))
|
|
492
|
+
plugins.push(
|
|
493
|
+
...await loadPlugin("@sveltejs/vite-plugin-svelte", "svelte")
|
|
494
|
+
);
|
|
495
|
+
if (hasInstalled(projectRoot, "solid-js"))
|
|
496
|
+
plugins.push(...await loadPlugin("vite-plugin-solid"));
|
|
497
|
+
if (hasInstalled(projectRoot, "preact"))
|
|
498
|
+
plugins.push(...await loadPlugin("@preact/preset-vite"));
|
|
499
|
+
return plugins;
|
|
500
|
+
}
|
|
501
|
+
useFrameshotVite() {
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
skipProjectConfig() {
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
// src/infrastructure/adapters/lit.ts
|
|
510
|
+
import { join as join5 } from "path";
|
|
511
|
+
var LitAdapter = class {
|
|
512
|
+
id = "lit";
|
|
513
|
+
detect(projectRoot) {
|
|
514
|
+
return hasDep(join5(projectRoot, "package.json"), ["lit", "lit-element"]);
|
|
515
|
+
}
|
|
516
|
+
getAliases() {
|
|
517
|
+
return {};
|
|
518
|
+
}
|
|
519
|
+
getOptimizeDeps() {
|
|
520
|
+
return ["lit"];
|
|
521
|
+
}
|
|
522
|
+
async getPlugins(_root) {
|
|
523
|
+
return [];
|
|
524
|
+
}
|
|
525
|
+
useFrameshotVite() {
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
skipProjectConfig() {
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
// src/infrastructure/adapters/next.ts
|
|
534
|
+
import { join as join6 } from "path";
|
|
535
|
+
var NextJsAdapter = class {
|
|
536
|
+
id = "next";
|
|
537
|
+
detect(projectRoot) {
|
|
538
|
+
return hasDep(join6(projectRoot, "package.json"), ["next"]);
|
|
539
|
+
}
|
|
540
|
+
getAliases(_root, stubsDir) {
|
|
541
|
+
return {
|
|
542
|
+
"next/navigation": join6(stubsDir, "next-navigation.js"),
|
|
543
|
+
"next/router": join6(stubsDir, "next-router.js"),
|
|
544
|
+
"next/image": join6(stubsDir, "next-image.js"),
|
|
545
|
+
"next/link": join6(stubsDir, "next-link.js"),
|
|
546
|
+
"next/font/google": join6(stubsDir, "next-font.js"),
|
|
547
|
+
"next/font/local": join6(stubsDir, "next-font.js"),
|
|
548
|
+
"next/headers": join6(stubsDir, "next-headers.js"),
|
|
549
|
+
"server-only": join6(stubsDir, "server-only.js"),
|
|
550
|
+
"client-only": join6(stubsDir, "server-only.js")
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
getOptimizeDeps(projectRoot) {
|
|
554
|
+
const base = ["react", "react-dom/client"];
|
|
555
|
+
const optional = [
|
|
556
|
+
"@mui/material",
|
|
557
|
+
"@mui/icons-material",
|
|
558
|
+
"@emotion/react",
|
|
559
|
+
"@emotion/styled",
|
|
560
|
+
"@chakra-ui/react",
|
|
561
|
+
"styled-components"
|
|
562
|
+
];
|
|
563
|
+
return [
|
|
564
|
+
...base,
|
|
565
|
+
...optional.filter((p) => hasInstalled(projectRoot, p.split("/")[0]))
|
|
566
|
+
];
|
|
567
|
+
}
|
|
568
|
+
async getPlugins(_root) {
|
|
569
|
+
return [...await loadReactPlugin(), rscDirectivePlugin()];
|
|
570
|
+
}
|
|
571
|
+
useFrameshotVite() {
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
skipProjectConfig() {
|
|
575
|
+
return true;
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
// src/infrastructure/adapters/nuxt.ts
|
|
580
|
+
import { join as join7 } from "path";
|
|
581
|
+
var NuxtAdapter = class {
|
|
582
|
+
id = "nuxt";
|
|
583
|
+
detect(projectRoot) {
|
|
584
|
+
return hasDep(join7(projectRoot, "package.json"), ["nuxt"]);
|
|
585
|
+
}
|
|
586
|
+
getAliases(_root, stubsDir) {
|
|
587
|
+
return {
|
|
588
|
+
"#app": join7(stubsDir, "nuxt-app.js"),
|
|
589
|
+
"#imports": join7(stubsDir, "nuxt-imports.js"),
|
|
590
|
+
"#head": join7(stubsDir, "nuxt-app.js")
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
getOptimizeDeps(_root) {
|
|
594
|
+
return ["vue"];
|
|
595
|
+
}
|
|
596
|
+
async getPlugins(_root) {
|
|
597
|
+
return loadPlugin("@vitejs/plugin-vue");
|
|
598
|
+
}
|
|
599
|
+
useFrameshotVite() {
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
602
|
+
skipProjectConfig() {
|
|
603
|
+
return true;
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
// src/infrastructure/adapters/preact.ts
|
|
608
|
+
import { join as join8 } from "path";
|
|
609
|
+
var PreactAdapter = class {
|
|
610
|
+
id = "preact";
|
|
611
|
+
detect(projectRoot) {
|
|
612
|
+
return hasDep(join8(projectRoot, "package.json"), ["preact"]);
|
|
613
|
+
}
|
|
614
|
+
getAliases() {
|
|
615
|
+
return {};
|
|
616
|
+
}
|
|
617
|
+
getOptimizeDeps() {
|
|
618
|
+
return ["preact", "preact/hooks"];
|
|
619
|
+
}
|
|
620
|
+
async getPlugins(_root) {
|
|
621
|
+
return loadPlugin("@preact/preset-vite");
|
|
622
|
+
}
|
|
623
|
+
useFrameshotVite() {
|
|
624
|
+
return true;
|
|
625
|
+
}
|
|
626
|
+
skipProjectConfig() {
|
|
627
|
+
return true;
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
// src/infrastructure/adapters/qwik.ts
|
|
632
|
+
import { join as join9 } from "path";
|
|
633
|
+
var QwikAdapter = class {
|
|
634
|
+
id = "qwik";
|
|
635
|
+
detect(projectRoot) {
|
|
636
|
+
return hasDep(join9(projectRoot, "package.json"), ["@builder.io/qwik"]);
|
|
637
|
+
}
|
|
638
|
+
getAliases(_root, stubsDir) {
|
|
639
|
+
return { "@builder.io/qwik-city": join9(stubsDir, "qwik-city.js") };
|
|
640
|
+
}
|
|
641
|
+
getOptimizeDeps(_root) {
|
|
642
|
+
return ["@builder.io/qwik"];
|
|
643
|
+
}
|
|
644
|
+
async getPlugins(_root) {
|
|
645
|
+
return loadPlugin("@builder.io/qwik/optimizer", "qwikVite");
|
|
646
|
+
}
|
|
647
|
+
useFrameshotVite() {
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
skipProjectConfig() {
|
|
651
|
+
return true;
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
// src/infrastructure/adapters/react-router.ts
|
|
656
|
+
import { join as join10 } from "path";
|
|
657
|
+
var ReactRouterAdapter = class {
|
|
658
|
+
id = "react-router";
|
|
659
|
+
detect(projectRoot) {
|
|
660
|
+
return hasDep(join10(projectRoot, "package.json"), [
|
|
661
|
+
"@react-router/dev",
|
|
662
|
+
"@remix-run/react"
|
|
663
|
+
]);
|
|
664
|
+
}
|
|
665
|
+
getAliases(_root, stubsDir) {
|
|
666
|
+
return {
|
|
667
|
+
"react-router": join10(stubsDir, "react-router.js"),
|
|
668
|
+
"react-router-dom": join10(stubsDir, "react-router.js"),
|
|
669
|
+
"@remix-run/react": join10(stubsDir, "react-router.js")
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
getOptimizeDeps(_root) {
|
|
673
|
+
return ["react", "react-dom/client"];
|
|
674
|
+
}
|
|
675
|
+
async getPlugins(_root) {
|
|
676
|
+
return [...await loadReactPlugin(), rscDirectivePlugin()];
|
|
677
|
+
}
|
|
678
|
+
useFrameshotVite() {
|
|
679
|
+
return true;
|
|
680
|
+
}
|
|
681
|
+
skipProjectConfig() {
|
|
682
|
+
return true;
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
// src/infrastructure/adapters/solid.ts
|
|
687
|
+
import { join as join11 } from "path";
|
|
688
|
+
var SolidAdapter = class {
|
|
689
|
+
id = "solid";
|
|
690
|
+
detect(projectRoot) {
|
|
691
|
+
return hasDep(join11(projectRoot, "package.json"), ["solid-js"]);
|
|
692
|
+
}
|
|
693
|
+
getAliases(_root, stubsDir) {
|
|
694
|
+
return { "@solidjs/router": join11(stubsDir, "solid-router.js") };
|
|
695
|
+
}
|
|
696
|
+
getOptimizeDeps() {
|
|
697
|
+
return ["solid-js", "solid-js/web"];
|
|
698
|
+
}
|
|
699
|
+
async getPlugins(_root) {
|
|
700
|
+
return loadPlugin("vite-plugin-solid");
|
|
701
|
+
}
|
|
702
|
+
useFrameshotVite() {
|
|
703
|
+
return true;
|
|
704
|
+
}
|
|
705
|
+
skipProjectConfig() {
|
|
706
|
+
return true;
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
// src/infrastructure/adapters/solid-start.ts
|
|
711
|
+
import { join as join12 } from "path";
|
|
712
|
+
var SolidStartAdapter = class {
|
|
713
|
+
id = "solid-start";
|
|
714
|
+
detect(projectRoot) {
|
|
715
|
+
return hasDep(join12(projectRoot, "package.json"), ["@solidjs/start"]);
|
|
716
|
+
}
|
|
717
|
+
getAliases(_root, stubsDir) {
|
|
718
|
+
return {
|
|
719
|
+
"@solidjs/start": join12(stubsDir, "solid-start.js"),
|
|
720
|
+
"@solidjs/start/router": join12(stubsDir, "solid-router.js"),
|
|
721
|
+
"@solidjs/router": join12(stubsDir, "solid-router.js")
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
getOptimizeDeps(_root) {
|
|
725
|
+
return ["solid-js", "solid-js/web"];
|
|
726
|
+
}
|
|
727
|
+
async getPlugins(_root) {
|
|
728
|
+
return loadPlugin("vite-plugin-solid");
|
|
729
|
+
}
|
|
730
|
+
useFrameshotVite() {
|
|
731
|
+
return true;
|
|
732
|
+
}
|
|
733
|
+
skipProjectConfig() {
|
|
734
|
+
return true;
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
// src/infrastructure/adapters/svelte.ts
|
|
739
|
+
import { join as join13 } from "path";
|
|
740
|
+
var SvelteAdapter = class {
|
|
741
|
+
id = "svelte";
|
|
742
|
+
detect(projectRoot) {
|
|
743
|
+
return hasDep(join13(projectRoot, "package.json"), ["svelte"]);
|
|
744
|
+
}
|
|
745
|
+
getAliases() {
|
|
746
|
+
return {};
|
|
747
|
+
}
|
|
748
|
+
getOptimizeDeps() {
|
|
749
|
+
return [];
|
|
750
|
+
}
|
|
751
|
+
async getPlugins(_root) {
|
|
752
|
+
return loadPlugin("@sveltejs/vite-plugin-svelte", "svelte");
|
|
753
|
+
}
|
|
754
|
+
useFrameshotVite() {
|
|
755
|
+
return true;
|
|
756
|
+
}
|
|
757
|
+
skipProjectConfig() {
|
|
758
|
+
return true;
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
// src/infrastructure/adapters/sveltekit.ts
|
|
763
|
+
import { join as join14 } from "path";
|
|
764
|
+
var SvelteKitAdapter = class {
|
|
765
|
+
id = "sveltekit";
|
|
766
|
+
detect(projectRoot) {
|
|
767
|
+
return hasDep(join14(projectRoot, "package.json"), ["@sveltejs/kit"]);
|
|
768
|
+
}
|
|
769
|
+
getAliases(_root, stubsDir) {
|
|
770
|
+
return {
|
|
771
|
+
"$app/navigation": join14(stubsDir, "sveltekit-navigation.js"),
|
|
772
|
+
"$app/stores": join14(stubsDir, "sveltekit-stores.js"),
|
|
773
|
+
"$app/environment": join14(stubsDir, "sveltekit-environment.js")
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
getOptimizeDeps(_root) {
|
|
777
|
+
return [];
|
|
778
|
+
}
|
|
779
|
+
async getPlugins(_root) {
|
|
780
|
+
return loadPlugin("@sveltejs/vite-plugin-svelte", "svelte");
|
|
781
|
+
}
|
|
782
|
+
useFrameshotVite() {
|
|
783
|
+
return true;
|
|
784
|
+
}
|
|
785
|
+
skipProjectConfig() {
|
|
786
|
+
return true;
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
// src/infrastructure/adapters/vike.ts
|
|
791
|
+
import { join as join15 } from "path";
|
|
792
|
+
var VikeAdapter = class {
|
|
793
|
+
id = "vike";
|
|
794
|
+
detect(projectRoot) {
|
|
795
|
+
return hasDep(join15(projectRoot, "package.json"), ["vike"]);
|
|
796
|
+
}
|
|
797
|
+
getAliases(_root, stubsDir) {
|
|
798
|
+
return {
|
|
799
|
+
vike: join15(stubsDir, "vike.js"),
|
|
800
|
+
"vike/client/router": join15(stubsDir, "vike.js")
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
getOptimizeDeps(projectRoot) {
|
|
804
|
+
const deps = [];
|
|
805
|
+
if (hasInstalled(projectRoot, "react"))
|
|
806
|
+
deps.push("react", "react-dom/client");
|
|
807
|
+
if (hasInstalled(projectRoot, "vue")) deps.push("vue");
|
|
808
|
+
return deps;
|
|
809
|
+
}
|
|
810
|
+
async getPlugins(projectRoot) {
|
|
811
|
+
const plugins = [];
|
|
812
|
+
if (hasInstalled(projectRoot, "react"))
|
|
813
|
+
plugins.push(...await loadReactPlugin());
|
|
814
|
+
if (hasInstalled(projectRoot, "vue"))
|
|
815
|
+
plugins.push(...await loadPlugin("@vitejs/plugin-vue"));
|
|
816
|
+
return plugins;
|
|
817
|
+
}
|
|
818
|
+
useFrameshotVite() {
|
|
819
|
+
return true;
|
|
820
|
+
}
|
|
821
|
+
skipProjectConfig() {
|
|
822
|
+
return true;
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
// src/infrastructure/adapters/vite-react.ts
|
|
827
|
+
import { existsSync as existsSync3 } from "fs";
|
|
828
|
+
import { join as join16 } from "path";
|
|
829
|
+
var ViteReactAdapter = class {
|
|
830
|
+
id = "vite-react";
|
|
831
|
+
detect(projectRoot) {
|
|
832
|
+
return hasDep(join16(projectRoot, "package.json"), ["react"]) && this.hasViteConfig(projectRoot);
|
|
833
|
+
}
|
|
834
|
+
hasViteConfig(root) {
|
|
835
|
+
return ["vite.config.ts", "vite.config.js", "vite.config.mjs"].some(
|
|
836
|
+
(n) => existsSync3(join16(root, n))
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
getAliases() {
|
|
840
|
+
return {};
|
|
841
|
+
}
|
|
842
|
+
getOptimizeDeps() {
|
|
843
|
+
return ["react", "react-dom/client"];
|
|
844
|
+
}
|
|
845
|
+
async getPlugins() {
|
|
846
|
+
return [];
|
|
847
|
+
}
|
|
848
|
+
useFrameshotVite() {
|
|
849
|
+
return false;
|
|
850
|
+
}
|
|
851
|
+
skipProjectConfig() {
|
|
852
|
+
return false;
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
// src/infrastructure/adapters/vue.ts
|
|
857
|
+
import { join as join17 } from "path";
|
|
858
|
+
var VueAdapter = class {
|
|
859
|
+
id = "vue";
|
|
860
|
+
detect(projectRoot) {
|
|
861
|
+
return hasDep(join17(projectRoot, "package.json"), ["vue"]);
|
|
862
|
+
}
|
|
863
|
+
getAliases() {
|
|
864
|
+
return {};
|
|
865
|
+
}
|
|
866
|
+
getOptimizeDeps() {
|
|
867
|
+
return ["vue"];
|
|
868
|
+
}
|
|
869
|
+
async getPlugins(_root) {
|
|
870
|
+
return loadPlugin("@vitejs/plugin-vue");
|
|
871
|
+
}
|
|
872
|
+
useFrameshotVite() {
|
|
873
|
+
return true;
|
|
874
|
+
}
|
|
875
|
+
skipProjectConfig() {
|
|
876
|
+
return true;
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
// src/infrastructure/adapters/registry.ts
|
|
881
|
+
var ADAPTERS = [
|
|
882
|
+
// Meta-frameworks first — most specific match wins.
|
|
883
|
+
new NextJsAdapter(),
|
|
884
|
+
new NuxtAdapter(),
|
|
885
|
+
new SvelteKitAdapter(),
|
|
886
|
+
new SolidStartAdapter(),
|
|
887
|
+
new ReactRouterAdapter(),
|
|
888
|
+
new GatsbyAdapter(),
|
|
889
|
+
new QwikAdapter(),
|
|
890
|
+
new AstroAdapter(),
|
|
891
|
+
new VikeAdapter(),
|
|
892
|
+
// Core UI libraries.
|
|
893
|
+
new VueAdapter(),
|
|
894
|
+
new SvelteAdapter(),
|
|
895
|
+
new SolidAdapter(),
|
|
896
|
+
new PreactAdapter(),
|
|
897
|
+
new LitAdapter(),
|
|
898
|
+
new ViteReactAdapter(),
|
|
899
|
+
// Catch-all.
|
|
900
|
+
new GenericAdapter()
|
|
901
|
+
];
|
|
902
|
+
function selectAdapter(projectRoot) {
|
|
903
|
+
for (const adapter of ADAPTERS) {
|
|
904
|
+
if (adapter.detect(projectRoot)) return adapter;
|
|
905
|
+
}
|
|
906
|
+
return new GenericAdapter();
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// src/infrastructure/vite-bundler.ts
|
|
910
|
+
var STUBS_DIR = join18(dirname2(fileURLToPath(import.meta.url)), "stubs");
|
|
911
|
+
var ViteBundler = class {
|
|
912
|
+
servers = /* @__PURE__ */ new Map();
|
|
913
|
+
detector = new ProjectDetector();
|
|
914
|
+
async getUrl(filePath, options = {}) {
|
|
915
|
+
const absPath = resolve2(filePath);
|
|
916
|
+
const project = options.projectRoot ? this.detector.detect(join18(options.projectRoot, "package.json")) : this.detector.detect(absPath);
|
|
917
|
+
const adapter = selectAdapter(project.root);
|
|
918
|
+
if (!project.hasVite && !adapter.useFrameshotVite()) {
|
|
919
|
+
throw new Error(
|
|
920
|
+
`Vite not found in ${project.root}. Install vite: npm install -D vite`
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
const framework = options.framework ?? project.framework;
|
|
924
|
+
const entryCode = this.generateEntry(
|
|
925
|
+
absPath,
|
|
926
|
+
framework,
|
|
927
|
+
options.props,
|
|
928
|
+
project.root
|
|
929
|
+
);
|
|
930
|
+
const cached = await this.ensureServer(project, adapter);
|
|
931
|
+
cached.currentEntry = entryCode;
|
|
932
|
+
const mod = cached.server.moduleGraph.getModuleById(
|
|
933
|
+
"\0virtual:frameshot-entry"
|
|
934
|
+
);
|
|
935
|
+
if (mod) {
|
|
936
|
+
cached.server.moduleGraph.invalidateModule(mod);
|
|
937
|
+
}
|
|
938
|
+
return {
|
|
939
|
+
url: `http://127.0.0.1:${cached.port}/@frameshot/`,
|
|
940
|
+
project
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
async shutdown() {
|
|
944
|
+
for (const cached of this.servers.values()) {
|
|
945
|
+
await cached.server.close().catch(() => {
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
this.servers.clear();
|
|
949
|
+
}
|
|
950
|
+
// Public for testing
|
|
951
|
+
generateEntry(componentPath, framework, props, projectRoot) {
|
|
952
|
+
const propsJson = props ? JSON.stringify(props) : "{}";
|
|
953
|
+
const cssImport = projectRoot ? this.findGlobalCss(projectRoot) : "";
|
|
954
|
+
switch (framework) {
|
|
955
|
+
case "react": {
|
|
956
|
+
const hasRouter = projectRoot && this.hasPackage(projectRoot, "react-router-dom");
|
|
957
|
+
const routerImport = hasRouter ? `import { MemoryRouter } from "react-router-dom";` : "";
|
|
958
|
+
const wrap = hasRouter ? (inner) => `createElement(MemoryRouter, null, ${inner})` : (inner) => inner;
|
|
959
|
+
return `${cssImport}
|
|
960
|
+
import { createElement, Suspense } from "react";
|
|
961
|
+
import { createRoot } from "react-dom/client";
|
|
962
|
+
${routerImport}
|
|
963
|
+
import Component from "${componentPath}";
|
|
964
|
+
const props = ${propsJson};
|
|
965
|
+
const root = createRoot(document.getElementById("app"));
|
|
966
|
+
root.render(${wrap(`createElement(Suspense, { fallback: createElement("div", null, "Loading...") }, createElement(Component, props))`)});
|
|
967
|
+
`;
|
|
968
|
+
}
|
|
969
|
+
case "vue":
|
|
970
|
+
return `${cssImport}
|
|
971
|
+
import { createApp } from "vue";
|
|
972
|
+
import Component from "${componentPath}";
|
|
973
|
+
const props = ${propsJson};
|
|
974
|
+
createApp(Component, props).mount("#app");
|
|
975
|
+
`;
|
|
976
|
+
case "svelte":
|
|
977
|
+
return `${cssImport}
|
|
978
|
+
import Component from "${componentPath}";
|
|
979
|
+
const props = ${propsJson};
|
|
980
|
+
const target = document.getElementById("app");
|
|
981
|
+
// Svelte 5 uses mount(); Svelte 4 uses new Component(). Try v5 first.
|
|
982
|
+
try {
|
|
983
|
+
const svelte = await import("svelte");
|
|
984
|
+
if (typeof svelte.mount === "function") {
|
|
985
|
+
svelte.mount(Component, { target, props });
|
|
986
|
+
} else {
|
|
987
|
+
new Component({ target, props });
|
|
988
|
+
}
|
|
989
|
+
} catch {
|
|
990
|
+
new Component({ target, props });
|
|
991
|
+
}
|
|
992
|
+
`;
|
|
993
|
+
case "solid":
|
|
994
|
+
return `${cssImport}
|
|
995
|
+
import { render } from "solid-js/web";
|
|
996
|
+
import Component from "${componentPath}";
|
|
997
|
+
const props = ${propsJson};
|
|
998
|
+
render(() => Component(props), document.getElementById("app"));
|
|
999
|
+
`;
|
|
1000
|
+
case "preact":
|
|
1001
|
+
return `${cssImport}
|
|
1002
|
+
import { h, render } from "preact";
|
|
1003
|
+
import Component from "${componentPath}";
|
|
1004
|
+
const props = ${propsJson};
|
|
1005
|
+
render(h(Component, props), document.getElementById("app"));
|
|
1006
|
+
`;
|
|
1007
|
+
default:
|
|
1008
|
+
return `${cssImport}
|
|
1009
|
+
const res = await fetch("${componentPath}");
|
|
1010
|
+
const html = await res.text();
|
|
1011
|
+
document.getElementById("app").innerHTML = html;
|
|
1012
|
+
`;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
async ensureServer(project, adapter) {
|
|
1016
|
+
const existing = this.servers.get(project.root);
|
|
1017
|
+
if (existing) return existing;
|
|
1018
|
+
const vite = adapter.useFrameshotVite() ? await this.importFrameshotVite() : await this.importVite(project.root);
|
|
1019
|
+
if (!vite) {
|
|
1020
|
+
throw new Error(`Failed to import vite for ${project.root}`);
|
|
1021
|
+
}
|
|
1022
|
+
const adapterPlugins = await adapter.getPlugins(project.root);
|
|
1023
|
+
const adapterAliases = adapter.getAliases(project.root, STUBS_DIR);
|
|
1024
|
+
const aliasEntries = [];
|
|
1025
|
+
for (const [key, value] of Object.entries(project.pathAliases)) {
|
|
1026
|
+
aliasEntries.push({
|
|
1027
|
+
find: new RegExp(`^${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/`),
|
|
1028
|
+
replacement: `${value}/`
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
for (const [key, value] of Object.entries(adapterAliases)) {
|
|
1032
|
+
aliasEntries.push({ find: key, replacement: value });
|
|
1033
|
+
}
|
|
1034
|
+
const self = this;
|
|
1035
|
+
const server = await vite.createServer({
|
|
1036
|
+
root: project.root,
|
|
1037
|
+
configFile: adapter.skipProjectConfig() ? false : project.viteConfigPath ?? false,
|
|
1038
|
+
server: {
|
|
1039
|
+
port: 0,
|
|
1040
|
+
strictPort: false,
|
|
1041
|
+
host: "127.0.0.1",
|
|
1042
|
+
hmr: false
|
|
1043
|
+
},
|
|
1044
|
+
logLevel: "error",
|
|
1045
|
+
resolve: aliasEntries.length > 0 ? {
|
|
1046
|
+
alias: aliasEntries,
|
|
1047
|
+
dedupe: ["react", "react-dom", "vue", "svelte"]
|
|
1048
|
+
} : void 0,
|
|
1049
|
+
plugins: [
|
|
1050
|
+
{
|
|
1051
|
+
name: "frameshot-virtual",
|
|
1052
|
+
resolveId(id) {
|
|
1053
|
+
if (id === "virtual:frameshot-entry")
|
|
1054
|
+
return "\0virtual:frameshot-entry";
|
|
1055
|
+
return null;
|
|
1056
|
+
},
|
|
1057
|
+
load(id) {
|
|
1058
|
+
if (id === "\0virtual:frameshot-entry") {
|
|
1059
|
+
return self.servers.get(project.root)?.currentEntry ?? "";
|
|
1060
|
+
}
|
|
1061
|
+
return null;
|
|
1062
|
+
}
|
|
1063
|
+
},
|
|
1064
|
+
{
|
|
1065
|
+
name: "frameshot-html",
|
|
1066
|
+
configureServer(srv) {
|
|
1067
|
+
srv.middlewares.use((req, res, next) => {
|
|
1068
|
+
if (req.url === "/@frameshot/" || req.url === "/@frameshot") {
|
|
1069
|
+
const html = `<!DOCTYPE html>
|
|
1070
|
+
<html>
|
|
1071
|
+
<head>
|
|
1072
|
+
<meta charset="utf-8">
|
|
1073
|
+
<style>
|
|
1074
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
1075
|
+
html, body { margin: 0; padding: 0; }
|
|
1076
|
+
body { display: flex; align-items: center; justify-content: center; min-height: 100vh; }
|
|
1077
|
+
#app { display: flex; align-items: center; justify-content: center; }
|
|
1078
|
+
</style>
|
|
1079
|
+
</head>
|
|
1080
|
+
<body>
|
|
1081
|
+
<div id="app"></div>
|
|
1082
|
+
<script type="module">import "virtual:frameshot-entry";</script>
|
|
1083
|
+
</body>
|
|
1084
|
+
</html>`;
|
|
1085
|
+
srv.transformIndexHtml(req.url ?? "/", html).then((transformed) => {
|
|
1086
|
+
res.statusCode = 200;
|
|
1087
|
+
res.setHeader("Content-Type", "text/html");
|
|
1088
|
+
res.end(transformed);
|
|
1089
|
+
}).catch(next);
|
|
1090
|
+
} else {
|
|
1091
|
+
next();
|
|
1092
|
+
}
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
},
|
|
1096
|
+
...adapterPlugins
|
|
1097
|
+
],
|
|
1098
|
+
optimizeDeps: {
|
|
1099
|
+
include: adapter.getOptimizeDeps(project.root)
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1102
|
+
await server.listen();
|
|
1103
|
+
const address = server.httpServer?.address();
|
|
1104
|
+
const port = typeof address === "object" && address ? address.port : 5173;
|
|
1105
|
+
const cached = {
|
|
1106
|
+
server,
|
|
1107
|
+
port,
|
|
1108
|
+
currentEntry: "",
|
|
1109
|
+
adapter
|
|
1110
|
+
};
|
|
1111
|
+
this.servers.set(project.root, cached);
|
|
1112
|
+
return cached;
|
|
1113
|
+
}
|
|
1114
|
+
async importFrameshotVite() {
|
|
1115
|
+
try {
|
|
1116
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
1117
|
+
const vitePath = requireFromHere.resolve("vite");
|
|
1118
|
+
const mod = await import(vitePath);
|
|
1119
|
+
if (mod && typeof mod === "object" && "createServer" in mod) {
|
|
1120
|
+
return mod;
|
|
1121
|
+
}
|
|
1122
|
+
} catch {
|
|
1123
|
+
}
|
|
1124
|
+
try {
|
|
1125
|
+
const mod = await import("vite");
|
|
1126
|
+
if (mod && typeof mod === "object" && "createServer" in mod) {
|
|
1127
|
+
return mod;
|
|
1128
|
+
}
|
|
1129
|
+
} catch {
|
|
1130
|
+
}
|
|
1131
|
+
return null;
|
|
1132
|
+
}
|
|
1133
|
+
async importVite(projectRoot) {
|
|
1134
|
+
try {
|
|
1135
|
+
const require2 = createRequire(join18(projectRoot, "package.json"));
|
|
1136
|
+
const vitePath = require2.resolve("vite");
|
|
1137
|
+
const mod = await import(vitePath);
|
|
1138
|
+
if (mod && typeof mod === "object" && "createServer" in mod) {
|
|
1139
|
+
return mod;
|
|
1140
|
+
}
|
|
1141
|
+
} catch {
|
|
1142
|
+
}
|
|
1143
|
+
return this.importFrameshotVite();
|
|
1144
|
+
}
|
|
1145
|
+
hasPackage(projectRoot, name) {
|
|
1146
|
+
try {
|
|
1147
|
+
const require2 = createRequire(join18(projectRoot, "package.json"));
|
|
1148
|
+
require2.resolve(name);
|
|
1149
|
+
return true;
|
|
1150
|
+
} catch {
|
|
1151
|
+
return false;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
findGlobalCss(projectRoot) {
|
|
1155
|
+
const candidates = [
|
|
1156
|
+
"src/index.css",
|
|
1157
|
+
"src/main.css",
|
|
1158
|
+
"src/globals.css",
|
|
1159
|
+
"src/app.css",
|
|
1160
|
+
"src/style.css",
|
|
1161
|
+
"src/styles.css",
|
|
1162
|
+
"app/globals.css"
|
|
1163
|
+
];
|
|
1164
|
+
for (const candidate of candidates) {
|
|
1165
|
+
const fullPath = join18(projectRoot, candidate);
|
|
1166
|
+
if (existsSync4(fullPath)) {
|
|
1167
|
+
return `import "${fullPath}";`;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
return "";
|
|
1171
|
+
}
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
// src/use-cases/audit.ts
|
|
1175
|
+
var AuditUseCase = class {
|
|
1176
|
+
constructor(pool, htmlBuilder) {
|
|
1177
|
+
this.pool = pool;
|
|
1178
|
+
this.htmlBuilder = htmlBuilder;
|
|
1179
|
+
}
|
|
1180
|
+
pool;
|
|
1181
|
+
htmlBuilder;
|
|
1182
|
+
async auditA11y(code, framework, options = {}) {
|
|
1183
|
+
const html = this.htmlBuilder.build(code, framework, {
|
|
1184
|
+
darkMode: options.darkMode ?? false,
|
|
1185
|
+
css: options.css ?? "",
|
|
1186
|
+
tailwindVersion: options.tailwindVersion ?? "3"
|
|
1187
|
+
});
|
|
1188
|
+
const page = await this.pool.getPage("chromium");
|
|
1189
|
+
const viewport = options.viewport ?? { width: 1280, height: 800 };
|
|
1190
|
+
await page.setViewportSize(viewport);
|
|
1191
|
+
await page.setContent(html, { waitUntil: "load", timeout: 1e4 });
|
|
1192
|
+
const axeSource = await import("axe-core").then((m) => m.source);
|
|
1193
|
+
await page.addScriptTag({ content: axeSource });
|
|
1194
|
+
const results = await page.evaluate(() => {
|
|
1195
|
+
return window.axe.run();
|
|
1196
|
+
});
|
|
1197
|
+
this.pool.releasePage("chromium", page);
|
|
1198
|
+
return {
|
|
1199
|
+
violations: results.violations.map(
|
|
1200
|
+
(v) => ({
|
|
1201
|
+
id: v.id,
|
|
1202
|
+
impact: v.impact,
|
|
1203
|
+
description: v.description,
|
|
1204
|
+
helpUrl: v.helpUrl,
|
|
1205
|
+
nodes: v.nodes.map((n) => ({
|
|
1206
|
+
html: n.html,
|
|
1207
|
+
target: n.target
|
|
1208
|
+
}))
|
|
1209
|
+
})
|
|
1210
|
+
),
|
|
1211
|
+
passes: results.passes.length,
|
|
1212
|
+
incomplete: results.incomplete.length
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
async perfAudit(code, framework, options = {}) {
|
|
1216
|
+
const html = this.htmlBuilder.build(code, framework, {
|
|
1217
|
+
darkMode: options.darkMode ?? false,
|
|
1218
|
+
css: options.css ?? "",
|
|
1219
|
+
tailwindVersion: options.tailwindVersion ?? "3"
|
|
1220
|
+
});
|
|
1221
|
+
const page = await this.pool.getPage("chromium");
|
|
1222
|
+
const viewport = options.viewport ?? { width: 1280, height: 800 };
|
|
1223
|
+
await page.setViewportSize(viewport);
|
|
1224
|
+
const start = performance.now();
|
|
1225
|
+
await page.setContent(html, { waitUntil: "load", timeout: 1e4 });
|
|
1226
|
+
const renderTimeMs = Math.round(performance.now() - start);
|
|
1227
|
+
const metrics = await page.evaluate(() => {
|
|
1228
|
+
const all = document.querySelectorAll("*");
|
|
1229
|
+
let maxDepth = 0;
|
|
1230
|
+
for (const el of all) {
|
|
1231
|
+
let depth = 0;
|
|
1232
|
+
let node = el;
|
|
1233
|
+
while (node) {
|
|
1234
|
+
depth++;
|
|
1235
|
+
node = node.parentElement;
|
|
1236
|
+
}
|
|
1237
|
+
if (depth > maxDepth) maxDepth = depth;
|
|
1238
|
+
}
|
|
1239
|
+
return {
|
|
1240
|
+
domElements: all.length,
|
|
1241
|
+
domDepth: maxDepth,
|
|
1242
|
+
scriptCount: document.querySelectorAll("script").length,
|
|
1243
|
+
styleSheetCount: document.styleSheets.length,
|
|
1244
|
+
imageCount: document.querySelectorAll("img").length,
|
|
1245
|
+
totalDomSize: document.documentElement.outerHTML.length
|
|
1246
|
+
};
|
|
1247
|
+
});
|
|
1248
|
+
this.pool.releasePage("chromium", page);
|
|
1249
|
+
return { renderTimeMs, ...metrics };
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
1252
|
+
|
|
1253
|
+
// src/use-cases/catalog.ts
|
|
1254
|
+
import { readdirSync, statSync } from "fs";
|
|
1255
|
+
import { extname, join as join19, relative } from "path";
|
|
1256
|
+
var CatalogUseCase = class {
|
|
1257
|
+
constructor(renderUseCase) {
|
|
1258
|
+
this.renderUseCase = renderUseCase;
|
|
1259
|
+
}
|
|
1260
|
+
renderUseCase;
|
|
1261
|
+
async renderCatalog(directory, options = {}) {
|
|
1262
|
+
const { recursive = false, ...renderOptions } = options;
|
|
1263
|
+
const files = this.scanDirectory(directory, recursive);
|
|
1264
|
+
const results = [];
|
|
1265
|
+
for (const filePath of files) {
|
|
1266
|
+
const relativePath = relative(directory, filePath);
|
|
1267
|
+
const framework = this.detectFramework(filePath);
|
|
1268
|
+
try {
|
|
1269
|
+
const { results: renderResults } = await this.renderUseCase.renderFile(
|
|
1270
|
+
filePath,
|
|
1271
|
+
{
|
|
1272
|
+
...renderOptions,
|
|
1273
|
+
engines: ["chromium"]
|
|
1274
|
+
}
|
|
1275
|
+
);
|
|
1276
|
+
const result = renderResults[0];
|
|
1277
|
+
results.push({
|
|
1278
|
+
path: relativePath,
|
|
1279
|
+
framework,
|
|
1280
|
+
image: result.image,
|
|
1281
|
+
width: result.width,
|
|
1282
|
+
height: result.height,
|
|
1283
|
+
consoleErrors: result.consoleErrors
|
|
1284
|
+
});
|
|
1285
|
+
} catch (err) {
|
|
1286
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1287
|
+
results.push({
|
|
1288
|
+
path: relativePath,
|
|
1289
|
+
framework,
|
|
1290
|
+
image: "",
|
|
1291
|
+
width: 0,
|
|
1292
|
+
height: 0,
|
|
1293
|
+
consoleErrors: [],
|
|
1294
|
+
error: msg
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
return results;
|
|
1299
|
+
}
|
|
1300
|
+
scanDirectory(dir, recursive) {
|
|
1301
|
+
const componentExtensions = /* @__PURE__ */ new Set([
|
|
1302
|
+
".jsx",
|
|
1303
|
+
".tsx",
|
|
1304
|
+
".vue",
|
|
1305
|
+
".svelte",
|
|
1306
|
+
".html",
|
|
1307
|
+
".htm"
|
|
1308
|
+
]);
|
|
1309
|
+
const files = [];
|
|
1310
|
+
const entries = readdirSync(dir);
|
|
1311
|
+
for (const entry of entries) {
|
|
1312
|
+
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
1313
|
+
const fullPath = join19(dir, entry);
|
|
1314
|
+
const stat = statSync(fullPath);
|
|
1315
|
+
if (stat.isDirectory() && recursive) {
|
|
1316
|
+
files.push(...this.scanDirectory(fullPath, recursive));
|
|
1317
|
+
} else if (stat.isFile() && componentExtensions.has(extname(entry).toLowerCase())) {
|
|
1318
|
+
files.push(fullPath);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
return files;
|
|
1322
|
+
}
|
|
1323
|
+
detectFramework(filePath) {
|
|
1324
|
+
return EXT_TO_FRAMEWORK[extname(filePath).toLowerCase()] ?? "react";
|
|
1325
|
+
}
|
|
1326
|
+
};
|
|
1327
|
+
|
|
1328
|
+
// src/use-cases/diff.ts
|
|
1329
|
+
var DiffUseCase = class {
|
|
1330
|
+
constructor(renderUseCase, imageComparator) {
|
|
1331
|
+
this.renderUseCase = renderUseCase;
|
|
1332
|
+
this.imageComparator = imageComparator;
|
|
1333
|
+
}
|
|
1334
|
+
renderUseCase;
|
|
1335
|
+
imageComparator;
|
|
1336
|
+
async diffComponent(before, after, framework, options = {}) {
|
|
1337
|
+
const [beforeResults, afterResults] = await Promise.all([
|
|
1338
|
+
this.renderUseCase.render(before, framework, {
|
|
1339
|
+
...options,
|
|
1340
|
+
engines: ["chromium"]
|
|
1341
|
+
}),
|
|
1342
|
+
this.renderUseCase.render(after, framework, {
|
|
1343
|
+
...options,
|
|
1344
|
+
engines: ["chromium"]
|
|
1345
|
+
})
|
|
1346
|
+
]);
|
|
1347
|
+
const comparison = this.imageComparator.diff(
|
|
1348
|
+
beforeResults[0].image,
|
|
1349
|
+
afterResults[0].image
|
|
1350
|
+
);
|
|
1351
|
+
return {
|
|
1352
|
+
before: beforeResults[0].image,
|
|
1353
|
+
after: afterResults[0].image,
|
|
1354
|
+
...comparison
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
async diffFromReference(code, framework, referenceImage, options = {}) {
|
|
1358
|
+
const { threshold = 0.1, ...renderOptions } = options;
|
|
1359
|
+
const [renderResult] = await this.renderUseCase.render(code, framework, {
|
|
1360
|
+
...renderOptions,
|
|
1361
|
+
engines: ["chromium"]
|
|
1362
|
+
});
|
|
1363
|
+
const comparison = this.imageComparator.diff(
|
|
1364
|
+
referenceImage,
|
|
1365
|
+
renderResult.image,
|
|
1366
|
+
threshold
|
|
1367
|
+
);
|
|
1368
|
+
return {
|
|
1369
|
+
rendered: renderResult.image,
|
|
1370
|
+
...comparison,
|
|
1371
|
+
passed: comparison.diffPercentage === 0
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
};
|
|
1375
|
+
|
|
1376
|
+
// src/use-cases/render.ts
|
|
1377
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
1378
|
+
import { extname as extname2 } from "path";
|
|
1379
|
+
|
|
1380
|
+
// src/infrastructure/page-utils.ts
|
|
1381
|
+
function attachConsoleCapture(page) {
|
|
1382
|
+
const errors = [];
|
|
1383
|
+
const onMessage = (msg) => {
|
|
1384
|
+
if (msg.type() === "error") errors.push(msg.text());
|
|
1385
|
+
};
|
|
1386
|
+
const onPageError = (err) => errors.push(err.message);
|
|
1387
|
+
page.on("console", onMessage);
|
|
1388
|
+
page.on("pageerror", onPageError);
|
|
1389
|
+
return {
|
|
1390
|
+
errors,
|
|
1391
|
+
cleanup: () => {
|
|
1392
|
+
page.removeListener("console", onMessage);
|
|
1393
|
+
page.removeListener("pageerror", onPageError);
|
|
1394
|
+
}
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
async function takeScreenshot(page, engine, fullPage, errors, cleanup) {
|
|
1398
|
+
const screenshot = await page.screenshot({ type: "png", fullPage });
|
|
1399
|
+
const metrics = await page.evaluate(() => ({
|
|
1400
|
+
w: document.documentElement.scrollWidth,
|
|
1401
|
+
h: document.documentElement.scrollHeight
|
|
1402
|
+
}));
|
|
1403
|
+
cleanup();
|
|
1404
|
+
return {
|
|
1405
|
+
engine,
|
|
1406
|
+
image: screenshot.toString("base64"),
|
|
1407
|
+
width: metrics.w,
|
|
1408
|
+
height: metrics.h,
|
|
1409
|
+
consoleErrors: errors
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// src/use-cases/render.ts
|
|
1414
|
+
var AUTO_FIT_VIEWPORT = { width: 1600, height: 1200 };
|
|
1415
|
+
var AUTO_FIT_PAD = 48;
|
|
1416
|
+
var AUTO_FIT_MIN = { width: 160, height: 120 };
|
|
1417
|
+
var RenderUseCase = class {
|
|
1418
|
+
constructor(pool, htmlBuilder, imageComparator, viteBundler) {
|
|
1419
|
+
this.pool = pool;
|
|
1420
|
+
this.htmlBuilder = htmlBuilder;
|
|
1421
|
+
this.imageComparator = imageComparator;
|
|
1422
|
+
this.viteBundler = viteBundler;
|
|
1423
|
+
}
|
|
1424
|
+
pool;
|
|
1425
|
+
htmlBuilder;
|
|
1426
|
+
imageComparator;
|
|
1427
|
+
viteBundler;
|
|
1428
|
+
async render(code, framework, options = {}) {
|
|
1429
|
+
const opts = this.resolveOptions(options);
|
|
1430
|
+
const html = this.htmlBuilder.build(code, framework, {
|
|
1431
|
+
darkMode: opts.darkMode,
|
|
1432
|
+
css: opts.css,
|
|
1433
|
+
tailwindVersion: opts.tailwindVersion
|
|
1434
|
+
});
|
|
1435
|
+
return Promise.all(
|
|
1436
|
+
opts.engines.map((engine) => this.renderHtml(engine, html, opts))
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1439
|
+
async renderFile(filePath, options = {}) {
|
|
1440
|
+
const opts = this.resolveOptions(options);
|
|
1441
|
+
const ext = extname2(filePath).toLowerCase();
|
|
1442
|
+
const framework = EXT_TO_FRAMEWORK[ext] ?? "react";
|
|
1443
|
+
if (this.viteBundler) {
|
|
1444
|
+
try {
|
|
1445
|
+
const { url } = await this.viteBundler.getUrl(filePath, {
|
|
1446
|
+
props: options.props,
|
|
1447
|
+
framework,
|
|
1448
|
+
projectRoot: options.projectRoot
|
|
1449
|
+
});
|
|
1450
|
+
const results2 = await Promise.all(
|
|
1451
|
+
opts.engines.map((engine) => this.renderUrl(engine, url, opts))
|
|
1452
|
+
);
|
|
1453
|
+
return { results: results2, mode: "vite" };
|
|
1454
|
+
} catch (err) {
|
|
1455
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1456
|
+
process.stderr.write(
|
|
1457
|
+
`[frameshot] Vite pipeline failed (${msg}), falling back to CDN
|
|
1458
|
+
`
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
const code = readFileSync3(filePath, "utf-8");
|
|
1463
|
+
const results = await this.render(code, framework, options);
|
|
1464
|
+
return { results, mode: "cdn" };
|
|
1465
|
+
}
|
|
1466
|
+
async renderInteraction(code, framework, interactions, options = {}) {
|
|
1467
|
+
const opts = this.resolveOptions(options);
|
|
1468
|
+
const html = this.htmlBuilder.build(code, framework, {
|
|
1469
|
+
darkMode: opts.darkMode,
|
|
1470
|
+
css: opts.css,
|
|
1471
|
+
tailwindVersion: opts.tailwindVersion
|
|
1472
|
+
});
|
|
1473
|
+
const page = await this.pool.getPage("chromium");
|
|
1474
|
+
await page.setViewportSize(opts.viewport);
|
|
1475
|
+
await page.setContent(html, { waitUntil: "load", timeout: 1e4 });
|
|
1476
|
+
for (const interaction of interactions) {
|
|
1477
|
+
switch (interaction.action) {
|
|
1478
|
+
case "click":
|
|
1479
|
+
if (interaction.selector) await page.click(interaction.selector);
|
|
1480
|
+
break;
|
|
1481
|
+
case "hover":
|
|
1482
|
+
if (interaction.selector) await page.hover(interaction.selector);
|
|
1483
|
+
break;
|
|
1484
|
+
case "focus":
|
|
1485
|
+
if (interaction.selector) await page.focus(interaction.selector);
|
|
1486
|
+
break;
|
|
1487
|
+
case "type":
|
|
1488
|
+
if (interaction.selector && interaction.value)
|
|
1489
|
+
await page.fill(interaction.selector, interaction.value);
|
|
1490
|
+
break;
|
|
1491
|
+
case "wait":
|
|
1492
|
+
await page.waitForTimeout(interaction.ms ?? 300);
|
|
1493
|
+
break;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
const screenshot = await page.screenshot({ type: "png", fullPage: true });
|
|
1497
|
+
const metrics = await page.evaluate(() => ({
|
|
1498
|
+
w: document.documentElement.scrollWidth,
|
|
1499
|
+
h: document.documentElement.scrollHeight
|
|
1500
|
+
}));
|
|
1501
|
+
this.pool.releasePage("chromium", page);
|
|
1502
|
+
return {
|
|
1503
|
+
image: screenshot.toString("base64"),
|
|
1504
|
+
width: metrics.w,
|
|
1505
|
+
height: metrics.h
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
async captureAnimation(code, framework, options = {}) {
|
|
1509
|
+
const { frames = 5, duration = 1e3 } = options;
|
|
1510
|
+
const opts = this.resolveOptions(options);
|
|
1511
|
+
const html = this.htmlBuilder.build(code, framework, {
|
|
1512
|
+
darkMode: opts.darkMode,
|
|
1513
|
+
css: opts.css,
|
|
1514
|
+
tailwindVersion: opts.tailwindVersion
|
|
1515
|
+
});
|
|
1516
|
+
const page = await this.pool.getPage("chromium");
|
|
1517
|
+
await page.setViewportSize(opts.viewport);
|
|
1518
|
+
await page.setContent(html, { waitUntil: "load", timeout: 1e4 });
|
|
1519
|
+
const interval = duration / (frames - 1);
|
|
1520
|
+
const results = [];
|
|
1521
|
+
for (let i = 0; i < frames; i++) {
|
|
1522
|
+
if (i > 0) await page.waitForTimeout(interval);
|
|
1523
|
+
const screenshot = await page.screenshot({
|
|
1524
|
+
type: "png",
|
|
1525
|
+
fullPage: false
|
|
1526
|
+
});
|
|
1527
|
+
results.push({
|
|
1528
|
+
timestamp: Math.round(i * interval),
|
|
1529
|
+
image: screenshot.toString("base64")
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
this.pool.releasePage("chromium", page);
|
|
1533
|
+
return results;
|
|
1534
|
+
}
|
|
1535
|
+
async renderGrid(cells, framework, options = {}) {
|
|
1536
|
+
const { columns = Math.min(cells.length, 3) } = options;
|
|
1537
|
+
const screenshots = await Promise.all(
|
|
1538
|
+
cells.map(async (cell) => {
|
|
1539
|
+
const [result] = await this.render(cell.code, framework, {
|
|
1540
|
+
...options,
|
|
1541
|
+
engines: ["chromium"]
|
|
1542
|
+
});
|
|
1543
|
+
return Buffer.from(result.image, "base64");
|
|
1544
|
+
})
|
|
1545
|
+
);
|
|
1546
|
+
const composite = this.imageComparator.composite(screenshots, columns);
|
|
1547
|
+
return { ...composite, cells: cells.length };
|
|
1548
|
+
}
|
|
1549
|
+
async renderMatrix(code, framework, viewports, themes, options = {}) {
|
|
1550
|
+
const combinations = [];
|
|
1551
|
+
for (const viewport of viewports) {
|
|
1552
|
+
for (const theme of themes) {
|
|
1553
|
+
combinations.push({ viewport, theme });
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
return Promise.all(
|
|
1557
|
+
combinations.map(async ({ viewport, theme }) => {
|
|
1558
|
+
const [result] = await this.render(code, framework, {
|
|
1559
|
+
...options,
|
|
1560
|
+
viewport: { width: viewport.width, height: viewport.height },
|
|
1561
|
+
darkMode: theme === "dark",
|
|
1562
|
+
engines: ["chromium"],
|
|
1563
|
+
fullPage: true
|
|
1564
|
+
});
|
|
1565
|
+
return {
|
|
1566
|
+
viewport: viewport.label,
|
|
1567
|
+
theme,
|
|
1568
|
+
image: result.image,
|
|
1569
|
+
width: result.width,
|
|
1570
|
+
height: result.height,
|
|
1571
|
+
consoleErrors: result.consoleErrors
|
|
1572
|
+
};
|
|
1573
|
+
})
|
|
1574
|
+
);
|
|
1575
|
+
}
|
|
1576
|
+
// ─── Private helpers ────────────────────────────────────────────────────────
|
|
1577
|
+
async renderHtml(engine, html, options) {
|
|
1578
|
+
const page = await this.pool.getPage(engine);
|
|
1579
|
+
await page.setViewportSize(options.viewport);
|
|
1580
|
+
const { errors, cleanup } = attachConsoleCapture(page);
|
|
1581
|
+
await page.setContent(html, { waitUntil: "load", timeout: 1e4 });
|
|
1582
|
+
if (options.waitFor > 0) await page.waitForTimeout(options.waitFor);
|
|
1583
|
+
const result = await takeScreenshot(
|
|
1584
|
+
page,
|
|
1585
|
+
engine,
|
|
1586
|
+
options.fullPage,
|
|
1587
|
+
errors,
|
|
1588
|
+
cleanup
|
|
1589
|
+
);
|
|
1590
|
+
this.pool.releasePage(engine, page);
|
|
1591
|
+
return result;
|
|
1592
|
+
}
|
|
1593
|
+
async renderUrl(engine, url, options) {
|
|
1594
|
+
const page = await this.pool.getPage(engine);
|
|
1595
|
+
const { errors, cleanup } = attachConsoleCapture(page);
|
|
1596
|
+
const viewport = options.autoFit ? await this.resolveAutoFitViewport(page, url, options) : options.viewport;
|
|
1597
|
+
await page.setViewportSize(viewport);
|
|
1598
|
+
await this.navigateAndWait(page, url, options.waitFor);
|
|
1599
|
+
const overlayState = await page.evaluate(() => {
|
|
1600
|
+
const overlay = document.querySelector("vite-error-overlay");
|
|
1601
|
+
const app = document.getElementById("app");
|
|
1602
|
+
const hasContent = !!app?.firstElementChild;
|
|
1603
|
+
if (!overlay) return { overlay: null, hasContent };
|
|
1604
|
+
const shadow = overlay.shadowRoot;
|
|
1605
|
+
const messageEl = shadow?.querySelector(".message-body");
|
|
1606
|
+
return {
|
|
1607
|
+
overlay: messageEl?.textContent?.trim() ?? "Vite error overlay",
|
|
1608
|
+
hasContent
|
|
1609
|
+
};
|
|
1610
|
+
}).catch(() => ({ overlay: null, hasContent: false }));
|
|
1611
|
+
if (overlayState.overlay && !overlayState.hasContent) {
|
|
1612
|
+
cleanup();
|
|
1613
|
+
this.pool.releasePage(engine, page);
|
|
1614
|
+
throw new Error(`Vite compilation error: ${overlayState.overlay}`);
|
|
1615
|
+
}
|
|
1616
|
+
const result = await takeScreenshot(
|
|
1617
|
+
page,
|
|
1618
|
+
engine,
|
|
1619
|
+
options.fullPage,
|
|
1620
|
+
errors,
|
|
1621
|
+
cleanup
|
|
1622
|
+
);
|
|
1623
|
+
this.pool.releasePage(engine, page);
|
|
1624
|
+
return result;
|
|
1625
|
+
}
|
|
1626
|
+
async navigateAndWait(page, url, waitFor) {
|
|
1627
|
+
await page.goto(url, { waitUntil: "load", timeout: 15e3 });
|
|
1628
|
+
await page.waitForLoadState("networkidle", { timeout: 5e3 }).catch(() => {
|
|
1629
|
+
});
|
|
1630
|
+
if (waitFor > 0) await page.waitForTimeout(waitFor);
|
|
1631
|
+
}
|
|
1632
|
+
async resolveAutoFitViewport(page, url, options) {
|
|
1633
|
+
await page.setViewportSize(AUTO_FIT_VIEWPORT);
|
|
1634
|
+
await this.navigateAndWait(page, url, options.waitFor);
|
|
1635
|
+
await page.waitForSelector("#app > *", { timeout: 3e3 }).catch(() => {
|
|
1636
|
+
});
|
|
1637
|
+
const measured = await page.evaluate(() => {
|
|
1638
|
+
const child = document.getElementById("app")?.firstElementChild;
|
|
1639
|
+
if (!child) return null;
|
|
1640
|
+
const rect = child.getBoundingClientRect();
|
|
1641
|
+
if (rect.width === 0 || rect.height === 0) return null;
|
|
1642
|
+
return { width: Math.ceil(rect.width), height: Math.ceil(rect.height) };
|
|
1643
|
+
});
|
|
1644
|
+
if (!measured) return options.viewport;
|
|
1645
|
+
return {
|
|
1646
|
+
width: Math.max(measured.width + AUTO_FIT_PAD * 2, AUTO_FIT_MIN.width),
|
|
1647
|
+
height: Math.max(measured.height + AUTO_FIT_PAD * 2, AUTO_FIT_MIN.height)
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
resolveOptions(partial) {
|
|
1651
|
+
return {
|
|
1652
|
+
viewport: partial.viewport ?? DEFAULT_RENDER_OPTIONS.viewport,
|
|
1653
|
+
engines: partial.engines ?? DEFAULT_RENDER_OPTIONS.engines,
|
|
1654
|
+
fullPage: partial.fullPage ?? DEFAULT_RENDER_OPTIONS.fullPage,
|
|
1655
|
+
darkMode: partial.darkMode ?? DEFAULT_RENDER_OPTIONS.darkMode,
|
|
1656
|
+
css: partial.css ?? DEFAULT_RENDER_OPTIONS.css,
|
|
1657
|
+
tailwindVersion: partial.tailwindVersion ?? DEFAULT_RENDER_OPTIONS.tailwindVersion,
|
|
1658
|
+
waitFor: partial.waitFor ?? DEFAULT_RENDER_OPTIONS.waitFor,
|
|
1659
|
+
autoFit: partial.autoFit ?? DEFAULT_RENDER_OPTIONS.autoFit
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
};
|
|
1663
|
+
|
|
1664
|
+
// src/use-cases/screenshot.ts
|
|
1665
|
+
var ScreenshotUseCase = class {
|
|
1666
|
+
constructor(pool) {
|
|
1667
|
+
this.pool = pool;
|
|
1668
|
+
}
|
|
1669
|
+
pool;
|
|
1670
|
+
async screenshotUrl(url, options = {}) {
|
|
1671
|
+
const engines = options.engines ?? ["chromium"];
|
|
1672
|
+
const { waitForNetworkIdle = true } = options;
|
|
1673
|
+
return Promise.all(
|
|
1674
|
+
engines.map(async (engine) => {
|
|
1675
|
+
const { width = 1280, height = 800 } = options.viewport ?? {};
|
|
1676
|
+
const fullPage = options.fullPage ?? true;
|
|
1677
|
+
const waitFor = options.waitFor ?? 0;
|
|
1678
|
+
const page = await this.pool.getPage(engine);
|
|
1679
|
+
await page.setViewportSize({ width, height });
|
|
1680
|
+
const { errors, cleanup } = attachConsoleCapture(page);
|
|
1681
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 15e3 });
|
|
1682
|
+
if (waitForNetworkIdle) {
|
|
1683
|
+
await page.waitForLoadState("networkidle", { timeout: 5e3 }).catch(() => {
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
if (options.waitForSelector) {
|
|
1687
|
+
await page.waitForSelector(options.waitForSelector, {
|
|
1688
|
+
timeout: 1e4
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
if (waitFor > 0) {
|
|
1692
|
+
await page.waitForTimeout(waitFor);
|
|
1693
|
+
}
|
|
1694
|
+
const result = await takeScreenshot(
|
|
1695
|
+
page,
|
|
1696
|
+
engine,
|
|
1697
|
+
fullPage,
|
|
1698
|
+
errors,
|
|
1699
|
+
cleanup
|
|
1700
|
+
);
|
|
1701
|
+
this.pool.releasePage(engine, page);
|
|
1702
|
+
return result;
|
|
1703
|
+
})
|
|
1704
|
+
);
|
|
1705
|
+
}
|
|
1706
|
+
async screenshotUrlWithRetry(url, options = {}) {
|
|
1707
|
+
const { retryCount = 0, ...screenshotOpts } = options;
|
|
1708
|
+
const maxAttempts = retryCount + 1;
|
|
1709
|
+
let lastError;
|
|
1710
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1711
|
+
try {
|
|
1712
|
+
if (attempt > 0) {
|
|
1713
|
+
const delay = 300 * 2 ** (attempt - 1);
|
|
1714
|
+
await new Promise((resolve3) => setTimeout(resolve3, delay));
|
|
1715
|
+
}
|
|
1716
|
+
return await this.screenshotUrl(url, screenshotOpts);
|
|
1717
|
+
} catch (error) {
|
|
1718
|
+
lastError = error;
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
throw lastError;
|
|
1722
|
+
}
|
|
1723
|
+
};
|
|
1724
|
+
|
|
1725
|
+
// src/use-cases/snapshot.ts
|
|
1726
|
+
var SnapshotUseCase = class {
|
|
1727
|
+
constructor(store, renderUseCase, diffUseCase) {
|
|
1728
|
+
this.store = store;
|
|
1729
|
+
this.renderUseCase = renderUseCase;
|
|
1730
|
+
this.diffUseCase = diffUseCase;
|
|
1731
|
+
}
|
|
1732
|
+
store;
|
|
1733
|
+
renderUseCase;
|
|
1734
|
+
diffUseCase;
|
|
1735
|
+
async save(key, code, framework, options = {}) {
|
|
1736
|
+
const [result] = await this.renderUseCase.render(code, framework, {
|
|
1737
|
+
...options,
|
|
1738
|
+
engines: ["chromium"]
|
|
1739
|
+
});
|
|
1740
|
+
this.store.save(key, result.image);
|
|
1741
|
+
return { image: result.image, width: result.width, height: result.height };
|
|
1742
|
+
}
|
|
1743
|
+
async check(key, code, framework, options = {}) {
|
|
1744
|
+
const snapshot = this.store.get(key);
|
|
1745
|
+
if (!snapshot) return null;
|
|
1746
|
+
return this.diffUseCase.diffFromReference(
|
|
1747
|
+
code,
|
|
1748
|
+
framework,
|
|
1749
|
+
snapshot.image,
|
|
1750
|
+
options
|
|
1751
|
+
);
|
|
1752
|
+
}
|
|
1753
|
+
list() {
|
|
1754
|
+
return this.store.list();
|
|
1755
|
+
}
|
|
1756
|
+
};
|
|
1757
|
+
|
|
1758
|
+
export {
|
|
1759
|
+
__export,
|
|
1760
|
+
DEVICE_PRESETS,
|
|
1761
|
+
EXT_TO_FRAMEWORK,
|
|
1762
|
+
BrowserPool,
|
|
1763
|
+
HtmlBuilder,
|
|
1764
|
+
ImageComparator,
|
|
1765
|
+
SnapshotStore,
|
|
1766
|
+
ProjectDetector,
|
|
1767
|
+
ViteBundler,
|
|
1768
|
+
AuditUseCase,
|
|
1769
|
+
CatalogUseCase,
|
|
1770
|
+
DiffUseCase,
|
|
1771
|
+
RenderUseCase,
|
|
1772
|
+
ScreenshotUseCase,
|
|
1773
|
+
SnapshotUseCase
|
|
1774
|
+
};
|