@openclaw/diffs 2026.8.1-beta.2 → 2026.8.1-beta.3
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 +7 -7
- package/dist/assets/viewer-runtime.js +3 -3
- package/dist/browser.runtime-_hkQDewM.js +406 -0
- package/dist/index.js +20 -578
- package/dist/viewer-assets-DKR8WLAI.js +118 -0
- package/package.json +4 -4
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { a as getServedViewerAsset, i as getServedLanguagePackViewerAsset, n as VIEWER_ASSET_PREFIX, t as LANGUAGE_PACK_VIEWER_ASSET_PREFIX } from "./viewer-assets-DKR8WLAI.js";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
5
|
+
import fs$1 from "node:fs/promises";
|
|
6
|
+
import { writeExternalFileWithinRoot } from "openclaw/plugin-sdk/security-runtime";
|
|
7
|
+
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import { chromium } from "playwright-core";
|
|
10
|
+
//#region extensions/diffs/src/browser.runtime.ts
|
|
11
|
+
const DEFAULT_BROWSER_IDLE_MS = 3e4;
|
|
12
|
+
const SHARED_BROWSER_KEY = "__default__";
|
|
13
|
+
const IMAGE_SIZE_LIMIT_ERROR = "Diff frame did not render within image size limits.";
|
|
14
|
+
const PDF_REFERENCE_PAGE_HEIGHT_PX = 1056;
|
|
15
|
+
const MAX_PDF_PAGES = 50;
|
|
16
|
+
const LOCAL_VIEWER_BASE_HREF = "http://127.0.0.1/plugins/diffs/view/local/local";
|
|
17
|
+
let sharedBrowserState = null;
|
|
18
|
+
let executablePathCache = null;
|
|
19
|
+
var PlaywrightDiffScreenshotter = class {
|
|
20
|
+
constructor(params) {
|
|
21
|
+
this.config = params.config;
|
|
22
|
+
this.browserIdleMs = params.browserIdleMs ?? DEFAULT_BROWSER_IDLE_MS;
|
|
23
|
+
}
|
|
24
|
+
async screenshotHtml(params) {
|
|
25
|
+
let lease;
|
|
26
|
+
try {
|
|
27
|
+
lease = await acquireSharedBrowser({
|
|
28
|
+
config: this.config,
|
|
29
|
+
idleMs: this.browserIdleMs
|
|
30
|
+
});
|
|
31
|
+
} catch (error) {
|
|
32
|
+
throw buildBrowserUnavailableError(error);
|
|
33
|
+
}
|
|
34
|
+
let page;
|
|
35
|
+
let currentScale = params.image.scale;
|
|
36
|
+
const maxRetries = 2;
|
|
37
|
+
try {
|
|
38
|
+
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
|
|
39
|
+
try {
|
|
40
|
+
page = await lease.browser.newPage({
|
|
41
|
+
viewport: {
|
|
42
|
+
width: Math.max(Math.ceil(params.image.maxWidth + 240), 1200),
|
|
43
|
+
height: 900
|
|
44
|
+
},
|
|
45
|
+
deviceScaleFactor: currentScale,
|
|
46
|
+
colorScheme: params.theme
|
|
47
|
+
});
|
|
48
|
+
} catch (error) {
|
|
49
|
+
throw buildBrowserUnavailableError(error);
|
|
50
|
+
}
|
|
51
|
+
await page.route("**/*", async (route) => {
|
|
52
|
+
const requestUrl = route.request().url();
|
|
53
|
+
if (requestUrl === "about:blank" || requestUrl.startsWith("data:")) {
|
|
54
|
+
await route.continue();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
let parsed;
|
|
58
|
+
try {
|
|
59
|
+
parsed = new URL(requestUrl);
|
|
60
|
+
} catch {
|
|
61
|
+
await route.abort();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (parsed.protocol !== "http:" || parsed.hostname !== "127.0.0.1") {
|
|
65
|
+
await route.abort();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const isBaseViewerAsset = parsed.pathname.startsWith(VIEWER_ASSET_PREFIX);
|
|
69
|
+
const isLanguagePackViewerAsset = parsed.pathname.startsWith(LANGUAGE_PACK_VIEWER_ASSET_PREFIX);
|
|
70
|
+
if (!isBaseViewerAsset && !isLanguagePackViewerAsset) {
|
|
71
|
+
await route.abort();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const pathname = parsed.pathname;
|
|
75
|
+
const asset = isLanguagePackViewerAsset ? await getServedLanguagePackViewerAsset(pathname) : await getServedViewerAsset(pathname);
|
|
76
|
+
if (!asset) {
|
|
77
|
+
await route.abort();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
await route.fulfill({
|
|
81
|
+
status: 200,
|
|
82
|
+
contentType: asset.contentType,
|
|
83
|
+
body: asset.body
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
await page.setContent(injectBaseHref(params.html), { waitUntil: "load" });
|
|
87
|
+
await page.waitForFunction(() => {
|
|
88
|
+
if (document.documentElement.dataset.openclawDiffsReady === "true") return true;
|
|
89
|
+
return [...document.querySelectorAll("[data-openclaw-diff-host]")].every((element) => {
|
|
90
|
+
return element instanceof HTMLElement && element.shadowRoot?.querySelector("[data-diffs]");
|
|
91
|
+
});
|
|
92
|
+
}, { timeout: 1e4 });
|
|
93
|
+
await page.evaluate(async () => {
|
|
94
|
+
await document.fonts.ready;
|
|
95
|
+
});
|
|
96
|
+
await page.evaluate(() => {
|
|
97
|
+
const frame = document.querySelector(".oc-frame");
|
|
98
|
+
if (frame instanceof HTMLElement) frame.dataset.renderMode = "image";
|
|
99
|
+
});
|
|
100
|
+
const frame = page.locator(".oc-frame");
|
|
101
|
+
await frame.waitFor();
|
|
102
|
+
const initialBox = await frame.boundingBox();
|
|
103
|
+
if (!initialBox) throw new Error("Diff frame did not render.");
|
|
104
|
+
const isPdf = params.image.format === "pdf";
|
|
105
|
+
const padding = isPdf ? 0 : 20;
|
|
106
|
+
const clipWidth = Math.ceil(initialBox.width + padding * 2);
|
|
107
|
+
const clipHeight = Math.ceil(Math.max(initialBox.height + padding * 2, 320));
|
|
108
|
+
await page.setViewportSize({
|
|
109
|
+
width: Math.max(clipWidth + padding, 900),
|
|
110
|
+
height: Math.max(clipHeight + padding, 700)
|
|
111
|
+
});
|
|
112
|
+
const box = await frame.boundingBox();
|
|
113
|
+
if (!box) throw new Error("Diff frame was lost after resizing.");
|
|
114
|
+
if (isPdf) {
|
|
115
|
+
await page.emulateMedia({ media: "screen" });
|
|
116
|
+
await page.evaluate(() => {
|
|
117
|
+
const html = document.documentElement;
|
|
118
|
+
const body = document.body;
|
|
119
|
+
const frameLocal = document.querySelector(".oc-frame");
|
|
120
|
+
html.style.background = "transparent";
|
|
121
|
+
body.style.margin = "0";
|
|
122
|
+
body.style.padding = "0";
|
|
123
|
+
body.style.background = "transparent";
|
|
124
|
+
body.style.setProperty("-webkit-print-color-adjust", "exact");
|
|
125
|
+
if (frameLocal instanceof HTMLElement) frameLocal.style.margin = "0";
|
|
126
|
+
});
|
|
127
|
+
const pdfBox = await frame.boundingBox();
|
|
128
|
+
if (!pdfBox) throw new Error("Diff frame was lost before PDF render.");
|
|
129
|
+
const pdfWidth = Math.max(Math.ceil(pdfBox.width), 1);
|
|
130
|
+
const pdfHeight = Math.max(Math.ceil(pdfBox.height), 1);
|
|
131
|
+
const estimatedPixels = pdfWidth * pdfHeight;
|
|
132
|
+
const estimatedPages = Math.ceil(pdfHeight / PDF_REFERENCE_PAGE_HEIGHT_PX);
|
|
133
|
+
if (estimatedPixels > params.image.maxPixels || estimatedPages > MAX_PDF_PAGES) throw new Error(IMAGE_SIZE_LIMIT_ERROR);
|
|
134
|
+
const pageForPdf = page;
|
|
135
|
+
await writeExternalArtifactFile({
|
|
136
|
+
outputPath: params.outputPath,
|
|
137
|
+
write: async (tempPath) => {
|
|
138
|
+
await pageForPdf.pdf({
|
|
139
|
+
path: tempPath,
|
|
140
|
+
width: `${pdfWidth}px`,
|
|
141
|
+
height: `${pdfHeight}px`,
|
|
142
|
+
printBackground: true,
|
|
143
|
+
margin: {
|
|
144
|
+
top: "0",
|
|
145
|
+
right: "0",
|
|
146
|
+
bottom: "0",
|
|
147
|
+
left: "0"
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
return params.outputPath;
|
|
153
|
+
}
|
|
154
|
+
const dpr = await page.evaluate(() => window.devicePixelRatio || 1);
|
|
155
|
+
const rawX = Math.max(box.x - padding, 0);
|
|
156
|
+
const rawY = Math.max(box.y - padding, 0);
|
|
157
|
+
const rawRight = rawX + clipWidth;
|
|
158
|
+
const rawBottom = rawY + clipHeight;
|
|
159
|
+
const x = Math.floor(rawX * dpr) / dpr;
|
|
160
|
+
const y = Math.floor(rawY * dpr) / dpr;
|
|
161
|
+
const right = Math.ceil(rawRight * dpr) / dpr;
|
|
162
|
+
const bottom = Math.ceil(rawBottom * dpr) / dpr;
|
|
163
|
+
const cssWidth = Math.max(right - x, 1);
|
|
164
|
+
const cssHeight = Math.max(bottom - y, 1);
|
|
165
|
+
if (cssWidth * cssHeight * dpr * dpr > params.image.maxPixels) {
|
|
166
|
+
if (currentScale > 1) {
|
|
167
|
+
const maxScaleForPixels = Math.sqrt(params.image.maxPixels / (cssWidth * cssHeight));
|
|
168
|
+
const reducedScale = Math.max(1, Math.round(Math.min(currentScale, maxScaleForPixels) * 100) / 100);
|
|
169
|
+
if (reducedScale < currentScale - .01 && attempt < maxRetries) {
|
|
170
|
+
await page.close().catch(() => {});
|
|
171
|
+
page = void 0;
|
|
172
|
+
currentScale = reducedScale;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
throw new Error(IMAGE_SIZE_LIMIT_ERROR);
|
|
177
|
+
}
|
|
178
|
+
const pageForScreenshot = page;
|
|
179
|
+
await writeExternalArtifactFile({
|
|
180
|
+
outputPath: params.outputPath,
|
|
181
|
+
write: async (tempPath) => {
|
|
182
|
+
await pageForScreenshot.screenshot({
|
|
183
|
+
path: tempPath,
|
|
184
|
+
type: "png",
|
|
185
|
+
scale: "device",
|
|
186
|
+
clip: {
|
|
187
|
+
x,
|
|
188
|
+
y,
|
|
189
|
+
width: cssWidth,
|
|
190
|
+
height: cssHeight
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
return params.outputPath;
|
|
196
|
+
}
|
|
197
|
+
throw new Error(IMAGE_SIZE_LIMIT_ERROR);
|
|
198
|
+
} finally {
|
|
199
|
+
await page?.close().catch(() => {});
|
|
200
|
+
await lease.release();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
function buildBrowserUnavailableError(error) {
|
|
205
|
+
const reason = formatErrorMessage(error);
|
|
206
|
+
return new Error(`Diff PNG/PDF rendering requires a Chromium-compatible browser. Set browser.executablePath or install Chrome/Chromium. ${reason}`, { cause: error });
|
|
207
|
+
}
|
|
208
|
+
async function writeExternalArtifactFile(params) {
|
|
209
|
+
const rootDir = path.dirname(params.outputPath);
|
|
210
|
+
await fs$1.mkdir(rootDir, { recursive: true });
|
|
211
|
+
await writeExternalFileWithinRoot({
|
|
212
|
+
rootDir,
|
|
213
|
+
path: path.basename(params.outputPath),
|
|
214
|
+
write: params.write
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
function injectBaseHref(html) {
|
|
218
|
+
if (html.includes("<base ")) return html;
|
|
219
|
+
return html.replace("<head>", `<head><base href="${LOCAL_VIEWER_BASE_HREF}" />`);
|
|
220
|
+
}
|
|
221
|
+
async function resolveBrowserExecutablePath(config) {
|
|
222
|
+
const cacheKey = JSON.stringify({
|
|
223
|
+
configPath: config.browser?.executablePath?.trim() || "",
|
|
224
|
+
env: [
|
|
225
|
+
process.env.OPENCLAW_BROWSER_EXECUTABLE_PATH ?? "",
|
|
226
|
+
process.env.BROWSER_EXECUTABLE_PATH ?? "",
|
|
227
|
+
process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH ?? ""
|
|
228
|
+
],
|
|
229
|
+
path: process.env.PATH ?? ""
|
|
230
|
+
});
|
|
231
|
+
if (executablePathCache?.key === cacheKey) return await executablePathCache.valuePromise;
|
|
232
|
+
const valuePromise = resolveBrowserExecutablePathUncached(config).catch((error) => {
|
|
233
|
+
if (executablePathCache?.valuePromise === valuePromise) executablePathCache = null;
|
|
234
|
+
throw error;
|
|
235
|
+
});
|
|
236
|
+
executablePathCache = {
|
|
237
|
+
key: cacheKey,
|
|
238
|
+
valuePromise
|
|
239
|
+
};
|
|
240
|
+
return await valuePromise;
|
|
241
|
+
}
|
|
242
|
+
async function resolveBrowserExecutablePathUncached(config) {
|
|
243
|
+
const configPath = config.browser?.executablePath?.trim();
|
|
244
|
+
if (configPath) {
|
|
245
|
+
await assertExecutable(configPath, "browser.executablePath");
|
|
246
|
+
return configPath;
|
|
247
|
+
}
|
|
248
|
+
const envCandidates = [
|
|
249
|
+
process.env.OPENCLAW_BROWSER_EXECUTABLE_PATH,
|
|
250
|
+
process.env.BROWSER_EXECUTABLE_PATH,
|
|
251
|
+
process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
|
|
252
|
+
].map((value) => value?.trim()).filter((value) => Boolean(value));
|
|
253
|
+
for (const candidate of envCandidates) if (await isExecutable(candidate)) return candidate;
|
|
254
|
+
for (const candidate of await collectExecutableCandidates()) if (await isExecutable(candidate)) return candidate;
|
|
255
|
+
}
|
|
256
|
+
async function acquireSharedBrowser(params) {
|
|
257
|
+
const executablePath = await resolveBrowserExecutablePath(params.config);
|
|
258
|
+
const desiredKey = executablePath || SHARED_BROWSER_KEY;
|
|
259
|
+
if (sharedBrowserState && sharedBrowserState.key !== desiredKey) await closeSharedBrowser();
|
|
260
|
+
if (!sharedBrowserState) {
|
|
261
|
+
const browserPromise = chromium.launch({
|
|
262
|
+
headless: true,
|
|
263
|
+
...executablePath ? { executablePath } : {},
|
|
264
|
+
args: ["--disable-dev-shm-usage"]
|
|
265
|
+
}).then((browser) => {
|
|
266
|
+
if (sharedBrowserState?.browserPromise === browserPromise) {
|
|
267
|
+
sharedBrowserState.browser = browser;
|
|
268
|
+
browser.on("disconnected", () => {
|
|
269
|
+
if (sharedBrowserState?.browser === browser) {
|
|
270
|
+
clearIdleTimer(sharedBrowserState);
|
|
271
|
+
sharedBrowserState = null;
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
return browser;
|
|
276
|
+
}).catch((error) => {
|
|
277
|
+
if (sharedBrowserState?.browserPromise === browserPromise) sharedBrowserState = null;
|
|
278
|
+
throw error;
|
|
279
|
+
});
|
|
280
|
+
sharedBrowserState = {
|
|
281
|
+
browserPromise,
|
|
282
|
+
idleTimer: null,
|
|
283
|
+
key: desiredKey,
|
|
284
|
+
users: 0
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
clearIdleTimer(sharedBrowserState);
|
|
288
|
+
const state = sharedBrowserState;
|
|
289
|
+
const browser = await state.browserPromise;
|
|
290
|
+
state.users += 1;
|
|
291
|
+
let released = false;
|
|
292
|
+
return {
|
|
293
|
+
browser,
|
|
294
|
+
release: async () => {
|
|
295
|
+
if (released) return;
|
|
296
|
+
released = true;
|
|
297
|
+
state.users = Math.max(0, state.users - 1);
|
|
298
|
+
if (state.users === 0) scheduleIdleBrowserClose(state, params.idleMs);
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function scheduleIdleBrowserClose(state, idleMs) {
|
|
303
|
+
clearIdleTimer(state);
|
|
304
|
+
state.idleTimer = setTimeout(() => {
|
|
305
|
+
if (sharedBrowserState === state && state.users === 0) closeSharedBrowser();
|
|
306
|
+
}, idleMs);
|
|
307
|
+
state.idleTimer.unref();
|
|
308
|
+
}
|
|
309
|
+
function clearIdleTimer(state) {
|
|
310
|
+
if (!state.idleTimer) return;
|
|
311
|
+
clearTimeout(state.idleTimer);
|
|
312
|
+
state.idleTimer = null;
|
|
313
|
+
}
|
|
314
|
+
async function closeSharedBrowser() {
|
|
315
|
+
const state = sharedBrowserState;
|
|
316
|
+
if (!state) return;
|
|
317
|
+
sharedBrowserState = null;
|
|
318
|
+
clearIdleTimer(state);
|
|
319
|
+
await (state.browser ?? await state.browserPromise.catch(() => null))?.close().catch(() => {});
|
|
320
|
+
}
|
|
321
|
+
async function collectExecutableCandidates() {
|
|
322
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
323
|
+
for (const command of pathCommandsForPlatform()) {
|
|
324
|
+
const resolved = await findExecutableInPath(command);
|
|
325
|
+
if (resolved) candidates.add(resolved);
|
|
326
|
+
}
|
|
327
|
+
for (const candidate of commonExecutablePathsForPlatform()) candidates.add(candidate);
|
|
328
|
+
return [...candidates];
|
|
329
|
+
}
|
|
330
|
+
function pathCommandsForPlatform() {
|
|
331
|
+
if (process.platform === "win32") return [
|
|
332
|
+
"chrome.exe",
|
|
333
|
+
"msedge.exe",
|
|
334
|
+
"brave.exe"
|
|
335
|
+
];
|
|
336
|
+
if (process.platform === "darwin") return [
|
|
337
|
+
"google-chrome",
|
|
338
|
+
"chromium",
|
|
339
|
+
"msedge",
|
|
340
|
+
"brave-browser",
|
|
341
|
+
"brave"
|
|
342
|
+
];
|
|
343
|
+
return [
|
|
344
|
+
"chromium",
|
|
345
|
+
"chromium-browser",
|
|
346
|
+
"google-chrome",
|
|
347
|
+
"google-chrome-stable",
|
|
348
|
+
"msedge",
|
|
349
|
+
"brave-browser",
|
|
350
|
+
"brave"
|
|
351
|
+
];
|
|
352
|
+
}
|
|
353
|
+
function commonExecutablePathsForPlatform() {
|
|
354
|
+
if (process.platform === "darwin") return [
|
|
355
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
356
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
357
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
358
|
+
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
|
|
359
|
+
];
|
|
360
|
+
if (process.platform === "win32") {
|
|
361
|
+
const joinWindowsPath = path.win32.join;
|
|
362
|
+
const localAppData = normalizeOptionalString(process.env.LOCALAPPDATA) ?? joinWindowsPath(os.homedir(), "AppData", "Local");
|
|
363
|
+
const programFiles = normalizeOptionalString(process.env.ProgramFiles) ?? "C:\\Program Files";
|
|
364
|
+
const programFilesX86 = normalizeOptionalString(process.env["ProgramFiles(x86)"]) ?? "C:\\Program Files (x86)";
|
|
365
|
+
return [
|
|
366
|
+
joinWindowsPath(localAppData, "Google", "Chrome", "Application", "chrome.exe"),
|
|
367
|
+
joinWindowsPath(programFiles, "Google", "Chrome", "Application", "chrome.exe"),
|
|
368
|
+
joinWindowsPath(programFilesX86, "Google", "Chrome", "Application", "chrome.exe"),
|
|
369
|
+
joinWindowsPath(programFiles, "Microsoft", "Edge", "Application", "msedge.exe"),
|
|
370
|
+
joinWindowsPath(programFilesX86, "Microsoft", "Edge", "Application", "msedge.exe"),
|
|
371
|
+
joinWindowsPath(programFiles, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"),
|
|
372
|
+
joinWindowsPath(programFilesX86, "BraveSoftware", "Brave-Browser", "Application", "brave.exe")
|
|
373
|
+
];
|
|
374
|
+
}
|
|
375
|
+
return [
|
|
376
|
+
"/usr/bin/chromium",
|
|
377
|
+
"/usr/bin/chromium-browser",
|
|
378
|
+
"/usr/bin/google-chrome",
|
|
379
|
+
"/usr/bin/google-chrome-stable",
|
|
380
|
+
"/usr/bin/msedge",
|
|
381
|
+
"/usr/bin/brave-browser",
|
|
382
|
+
"/snap/bin/chromium"
|
|
383
|
+
];
|
|
384
|
+
}
|
|
385
|
+
async function findExecutableInPath(command) {
|
|
386
|
+
const pathValue = process.env.PATH;
|
|
387
|
+
if (!pathValue) return;
|
|
388
|
+
for (const directory of pathValue.split(path.delimiter)) {
|
|
389
|
+
if (!directory) continue;
|
|
390
|
+
const candidate = path.join(directory, command);
|
|
391
|
+
if (await isExecutable(candidate)) return candidate;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
async function assertExecutable(candidate, label) {
|
|
395
|
+
if (!await isExecutable(candidate)) throw new Error(`${label} not found or not executable: ${candidate}`);
|
|
396
|
+
}
|
|
397
|
+
async function isExecutable(candidate) {
|
|
398
|
+
try {
|
|
399
|
+
await fs$1.access(candidate, constants.X_OK);
|
|
400
|
+
return true;
|
|
401
|
+
} catch {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
//#endregion
|
|
406
|
+
export { PlaywrightDiffScreenshotter };
|