@gatesolve/playwright-plugin 0.1.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 +102 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +243 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# @gatesolve/playwright-plugin
|
|
2
|
+
|
|
3
|
+
Automatic CAPTCHA solving for Playwright. Detects Cloudflare Turnstile, reCAPTCHA, and hCaptcha challenges and solves them via [GateSolve](https://gatesolve.dev).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @gatesolve/playwright-plugin
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
### One-shot: Solve CAPTCHA on current page
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { chromium } from 'playwright';
|
|
17
|
+
import { solveOnPage } from '@gatesolve/playwright-plugin';
|
|
18
|
+
|
|
19
|
+
const browser = await chromium.launch();
|
|
20
|
+
const page = await browser.newPage();
|
|
21
|
+
await page.goto('https://example.com/login');
|
|
22
|
+
|
|
23
|
+
// Detect and solve any CAPTCHA on the page
|
|
24
|
+
const result = await solveOnPage(page, {
|
|
25
|
+
apiKey: 'gs_your_key_here', // Free at gatesolve.dev
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (result?.success) {
|
|
29
|
+
console.log(`Solved ${result.type} in ${result.solveTimeMs}ms`);
|
|
30
|
+
// Token is already injected into the page — submit the form
|
|
31
|
+
await page.click('button[type="submit"]');
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Auto-solve: Wrap browser context
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { chromium } from 'playwright';
|
|
39
|
+
import { withGateSolve } from '@gatesolve/playwright-plugin';
|
|
40
|
+
|
|
41
|
+
const browser = await chromium.launch();
|
|
42
|
+
const context = await browser.newContext();
|
|
43
|
+
|
|
44
|
+
// Every page in this context will auto-solve CAPTCHAs
|
|
45
|
+
withGateSolve(context, {
|
|
46
|
+
apiKey: 'gs_your_key_here',
|
|
47
|
+
debug: true,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const page = await context.newPage();
|
|
51
|
+
await page.goto('https://example.com/protected');
|
|
52
|
+
// CAPTCHA is solved automatically on page load
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Detect only (no solve)
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { detectCaptcha } from '@gatesolve/playwright-plugin';
|
|
59
|
+
|
|
60
|
+
const detection = await detectCaptcha(page);
|
|
61
|
+
if (detection) {
|
|
62
|
+
console.log(`Found ${detection.type} with siteKey ${detection.siteKey}`);
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## API
|
|
67
|
+
|
|
68
|
+
### `solveOnPage(page, options)`
|
|
69
|
+
|
|
70
|
+
Detect and solve a CAPTCHA on the current page. Returns `SolveResult | null`.
|
|
71
|
+
|
|
72
|
+
### `withGateSolve(context, options)`
|
|
73
|
+
|
|
74
|
+
Wrap a `BrowserContext` to auto-solve CAPTCHAs on every page load. Returns the same context.
|
|
75
|
+
|
|
76
|
+
### `detectCaptcha(page)`
|
|
77
|
+
|
|
78
|
+
Detect CAPTCHA type and siteKey without solving. Returns `CaptchaDetection | null`.
|
|
79
|
+
|
|
80
|
+
## Options
|
|
81
|
+
|
|
82
|
+
| Option | Type | Default | Description |
|
|
83
|
+
|--------|------|---------|-------------|
|
|
84
|
+
| `apiKey` | `string` | required | GateSolve API key |
|
|
85
|
+
| `baseUrl` | `string` | `https://gatesolve.dev` | API base URL |
|
|
86
|
+
| `timeout` | `number` | `30000` | Max solve wait (ms) |
|
|
87
|
+
| `pollInterval` | `number` | `2000` | Poll frequency (ms) |
|
|
88
|
+
| `debug` | `boolean` | `false` | Log debug info |
|
|
89
|
+
|
|
90
|
+
## Supported CAPTCHAs
|
|
91
|
+
|
|
92
|
+
- **Cloudflare Turnstile** — $0.02/solve
|
|
93
|
+
- **reCAPTCHA v2/v3** — $0.03/solve
|
|
94
|
+
- **hCaptcha** — $0.03/solve
|
|
95
|
+
|
|
96
|
+
## Free Tier
|
|
97
|
+
|
|
98
|
+
100 free solves per API key. No credit card required. Get your key at [gatesolve.dev](https://gatesolve.dev).
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @gatesolve/playwright-plugin
|
|
3
|
+
*
|
|
4
|
+
* Automatic CAPTCHA solving for Playwright.
|
|
5
|
+
* Detects Cloudflare Turnstile, reCAPTCHA, and hCaptcha on page navigation
|
|
6
|
+
* and solves them via GateSolve API.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* import { solveOnPage, withGateSolve } from '@gatesolve/playwright-plugin';
|
|
10
|
+
*
|
|
11
|
+
* // Option 1: One-shot — solve any CAPTCHA on current page
|
|
12
|
+
* const token = await solveOnPage(page, { apiKey: 'gs_...' });
|
|
13
|
+
*
|
|
14
|
+
* // Option 2: Auto-detect — wrap browser context, solves on every navigation
|
|
15
|
+
* const context = await withGateSolve(browserContext, { apiKey: 'gs_...' });
|
|
16
|
+
*/
|
|
17
|
+
export interface GateSolveOptions {
|
|
18
|
+
/** GateSolve API key. Get one free at gatesolve.dev */
|
|
19
|
+
apiKey: string;
|
|
20
|
+
/** API base URL (default: https://gatesolve.dev) */
|
|
21
|
+
baseUrl?: string;
|
|
22
|
+
/** Max time to wait for solve in ms (default: 30000) */
|
|
23
|
+
timeout?: number;
|
|
24
|
+
/** Poll interval in ms (default: 2000) */
|
|
25
|
+
pollInterval?: number;
|
|
26
|
+
/** Log debug info (default: false) */
|
|
27
|
+
debug?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface CaptchaDetection {
|
|
30
|
+
type: "turnstile" | "recaptcha" | "hcaptcha";
|
|
31
|
+
siteKey: string;
|
|
32
|
+
pageUrl: string;
|
|
33
|
+
}
|
|
34
|
+
export interface SolveResult {
|
|
35
|
+
success: boolean;
|
|
36
|
+
token?: string;
|
|
37
|
+
type?: string;
|
|
38
|
+
error?: string;
|
|
39
|
+
solveTimeMs?: number;
|
|
40
|
+
}
|
|
41
|
+
interface MinimalPage {
|
|
42
|
+
url(): string;
|
|
43
|
+
evaluate<T>(fn: string | ((...args: any[]) => T), ...args: any[]): Promise<T>;
|
|
44
|
+
waitForTimeout(ms: number): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
interface MinimalContext {
|
|
47
|
+
on(event: "page", handler: (page: MinimalPage) => void): void;
|
|
48
|
+
pages(): MinimalPage[];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Detect CAPTCHA challenges on a page.
|
|
52
|
+
*/
|
|
53
|
+
export declare function detectCaptcha(page: MinimalPage): Promise<CaptchaDetection | null>;
|
|
54
|
+
/**
|
|
55
|
+
* Detect and solve any CAPTCHA on the current page.
|
|
56
|
+
* Returns the solve result, or null if no CAPTCHA was found.
|
|
57
|
+
*/
|
|
58
|
+
export declare function solveOnPage(page: MinimalPage, opts: GateSolveOptions): Promise<SolveResult | null>;
|
|
59
|
+
/**
|
|
60
|
+
* Wrap a Playwright BrowserContext to automatically solve CAPTCHAs on every page load.
|
|
61
|
+
* Returns the same context (mutated with event listener).
|
|
62
|
+
*/
|
|
63
|
+
export declare function withGateSolve<T extends MinimalContext>(context: T, opts: GateSolveOptions): T;
|
|
64
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @gatesolve/playwright-plugin
|
|
4
|
+
*
|
|
5
|
+
* Automatic CAPTCHA solving for Playwright.
|
|
6
|
+
* Detects Cloudflare Turnstile, reCAPTCHA, and hCaptcha on page navigation
|
|
7
|
+
* and solves them via GateSolve API.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* import { solveOnPage, withGateSolve } from '@gatesolve/playwright-plugin';
|
|
11
|
+
*
|
|
12
|
+
* // Option 1: One-shot — solve any CAPTCHA on current page
|
|
13
|
+
* const token = await solveOnPage(page, { apiKey: 'gs_...' });
|
|
14
|
+
*
|
|
15
|
+
* // Option 2: Auto-detect — wrap browser context, solves on every navigation
|
|
16
|
+
* const context = await withGateSolve(browserContext, { apiKey: 'gs_...' });
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.detectCaptcha = detectCaptcha;
|
|
20
|
+
exports.solveOnPage = solveOnPage;
|
|
21
|
+
exports.withGateSolve = withGateSolve;
|
|
22
|
+
const DEFAULT_BASE_URL = "https://gatesolve.dev";
|
|
23
|
+
const DEFAULT_TIMEOUT = 30000;
|
|
24
|
+
const DEFAULT_POLL_INTERVAL = 2000;
|
|
25
|
+
function log(opts, ...args) {
|
|
26
|
+
if (opts.debug)
|
|
27
|
+
console.log("[gatesolve]", ...args);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Detect CAPTCHA challenges on a page.
|
|
31
|
+
*/
|
|
32
|
+
async function detectCaptcha(page) {
|
|
33
|
+
return page.evaluate(() => {
|
|
34
|
+
// Cloudflare Turnstile
|
|
35
|
+
const turnstileEl = document.querySelector("[data-sitekey].cf-turnstile") ||
|
|
36
|
+
document.querySelector('iframe[src*="challenges.cloudflare.com"]');
|
|
37
|
+
if (turnstileEl) {
|
|
38
|
+
const siteKey = turnstileEl.getAttribute("data-sitekey") ||
|
|
39
|
+
// Extract from iframe src
|
|
40
|
+
new URL(turnstileEl.src || "").searchParams.get("k") ||
|
|
41
|
+
"";
|
|
42
|
+
return {
|
|
43
|
+
type: "turnstile",
|
|
44
|
+
siteKey,
|
|
45
|
+
pageUrl: window.location.href,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
// reCAPTCHA v2/v3
|
|
49
|
+
const recaptchaEl = document.querySelector(".g-recaptcha[data-sitekey]") ||
|
|
50
|
+
document.querySelector('iframe[src*="google.com/recaptcha"]');
|
|
51
|
+
if (recaptchaEl) {
|
|
52
|
+
const siteKey = recaptchaEl.getAttribute("data-sitekey") ||
|
|
53
|
+
new URL(recaptchaEl.src || "").searchParams.get("k") ||
|
|
54
|
+
"";
|
|
55
|
+
return {
|
|
56
|
+
type: "recaptcha",
|
|
57
|
+
siteKey,
|
|
58
|
+
pageUrl: window.location.href,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// hCaptcha
|
|
62
|
+
const hcaptchaEl = document.querySelector(".h-captcha[data-sitekey]") ||
|
|
63
|
+
document.querySelector('iframe[src*="hcaptcha.com"]');
|
|
64
|
+
if (hcaptchaEl) {
|
|
65
|
+
const siteKey = hcaptchaEl.getAttribute("data-sitekey") ||
|
|
66
|
+
new URL(hcaptchaEl.src || "").searchParams.get("sitekey") ||
|
|
67
|
+
"";
|
|
68
|
+
return {
|
|
69
|
+
type: "hcaptcha",
|
|
70
|
+
siteKey,
|
|
71
|
+
pageUrl: window.location.href,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Submit a solve request to GateSolve and poll for result.
|
|
79
|
+
*/
|
|
80
|
+
async function submitAndPoll(detection, opts) {
|
|
81
|
+
const baseUrl = opts.baseUrl || DEFAULT_BASE_URL;
|
|
82
|
+
const timeout = opts.timeout || DEFAULT_TIMEOUT;
|
|
83
|
+
const pollInterval = opts.pollInterval || DEFAULT_POLL_INTERVAL;
|
|
84
|
+
const startTime = Date.now();
|
|
85
|
+
log(opts, `Solving ${detection.type} for ${detection.pageUrl}`);
|
|
86
|
+
// Submit
|
|
87
|
+
const submitRes = await fetch(`${baseUrl}/api/solve`, {
|
|
88
|
+
method: "POST",
|
|
89
|
+
headers: {
|
|
90
|
+
"Content-Type": "application/json",
|
|
91
|
+
"x-api-key": opts.apiKey,
|
|
92
|
+
},
|
|
93
|
+
body: JSON.stringify({
|
|
94
|
+
type: detection.type,
|
|
95
|
+
siteKey: detection.siteKey,
|
|
96
|
+
pageUrl: detection.pageUrl,
|
|
97
|
+
}),
|
|
98
|
+
});
|
|
99
|
+
if (!submitRes.ok) {
|
|
100
|
+
const err = await submitRes.text();
|
|
101
|
+
return { success: false, error: `Submit failed (${submitRes.status}): ${err}` };
|
|
102
|
+
}
|
|
103
|
+
const submitData = (await submitRes.json());
|
|
104
|
+
// If immediate result (unlikely but handle it)
|
|
105
|
+
if (submitData.token) {
|
|
106
|
+
return {
|
|
107
|
+
success: true,
|
|
108
|
+
token: submitData.token,
|
|
109
|
+
type: detection.type,
|
|
110
|
+
solveTimeMs: Date.now() - startTime,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (!submitData.id) {
|
|
114
|
+
return { success: false, error: submitData.error || "No job ID returned" };
|
|
115
|
+
}
|
|
116
|
+
// Poll
|
|
117
|
+
const jobId = submitData.id;
|
|
118
|
+
log(opts, `Job ${jobId} submitted, polling...`);
|
|
119
|
+
while (Date.now() - startTime < timeout) {
|
|
120
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
121
|
+
const pollRes = await fetch(`${baseUrl}/api/solve?id=${jobId}`, {
|
|
122
|
+
headers: { "x-api-key": opts.apiKey },
|
|
123
|
+
});
|
|
124
|
+
if (!pollRes.ok)
|
|
125
|
+
continue;
|
|
126
|
+
const pollData = (await pollRes.json());
|
|
127
|
+
if (pollData.status === "completed" && pollData.token) {
|
|
128
|
+
log(opts, `Solved in ${Date.now() - startTime}ms`);
|
|
129
|
+
return {
|
|
130
|
+
success: true,
|
|
131
|
+
token: pollData.token,
|
|
132
|
+
type: detection.type,
|
|
133
|
+
solveTimeMs: Date.now() - startTime,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (pollData.status === "failed") {
|
|
137
|
+
return { success: false, error: pollData.error || "Solve failed" };
|
|
138
|
+
}
|
|
139
|
+
log(opts, `Status: ${pollData.status}, waiting...`);
|
|
140
|
+
}
|
|
141
|
+
return { success: false, error: `Timeout after ${timeout}ms` };
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Inject a solved CAPTCHA token back into the page.
|
|
145
|
+
*/
|
|
146
|
+
async function injectToken(page, detection, token) {
|
|
147
|
+
await page.evaluate((args) => {
|
|
148
|
+
const { type, token } = args;
|
|
149
|
+
if (type === "turnstile") {
|
|
150
|
+
// Set the hidden input
|
|
151
|
+
const input = document.querySelector('input[name="cf-turnstile-response"]') ||
|
|
152
|
+
document.querySelector(".cf-turnstile input[type=hidden]");
|
|
153
|
+
if (input)
|
|
154
|
+
input.value = token;
|
|
155
|
+
// Call the callback if it exists
|
|
156
|
+
const widget = document.querySelector(".cf-turnstile");
|
|
157
|
+
const cbName = widget?.getAttribute("data-callback");
|
|
158
|
+
if (cbName && typeof window[cbName] === "function") {
|
|
159
|
+
window[cbName](token);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else if (type === "recaptcha") {
|
|
163
|
+
const textarea = document.querySelector("#g-recaptcha-response");
|
|
164
|
+
if (textarea) {
|
|
165
|
+
textarea.style.display = "block";
|
|
166
|
+
textarea.value = token;
|
|
167
|
+
}
|
|
168
|
+
// Trigger callback
|
|
169
|
+
if (typeof window.___grecaptcha_cfg !== "undefined") {
|
|
170
|
+
const clients = window.___grecaptcha_cfg?.clients;
|
|
171
|
+
if (clients) {
|
|
172
|
+
for (const client of Object.values(clients)) {
|
|
173
|
+
for (const val of Object.values(client)) {
|
|
174
|
+
if (val?.callback && typeof val.callback === "function") {
|
|
175
|
+
val.callback(token);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else if (type === "hcaptcha") {
|
|
183
|
+
const textarea = document.querySelector('[name="h-captcha-response"]');
|
|
184
|
+
if (textarea)
|
|
185
|
+
textarea.value = token;
|
|
186
|
+
const iframe = document.querySelector('iframe[src*="hcaptcha.com"]');
|
|
187
|
+
const cbName = iframe
|
|
188
|
+
?.closest(".h-captcha")
|
|
189
|
+
?.getAttribute("data-callback");
|
|
190
|
+
if (cbName && typeof window[cbName] === "function") {
|
|
191
|
+
window[cbName](token);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}, { type: detection.type, token });
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Detect and solve any CAPTCHA on the current page.
|
|
198
|
+
* Returns the solve result, or null if no CAPTCHA was found.
|
|
199
|
+
*/
|
|
200
|
+
async function solveOnPage(page, opts) {
|
|
201
|
+
const detection = await detectCaptcha(page);
|
|
202
|
+
if (!detection) {
|
|
203
|
+
log(opts, "No CAPTCHA detected");
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
log(opts, `Detected ${detection.type} (siteKey: ${detection.siteKey})`);
|
|
207
|
+
const result = await submitAndPoll(detection, opts);
|
|
208
|
+
if (result.success && result.token) {
|
|
209
|
+
await injectToken(page, detection, result.token);
|
|
210
|
+
log(opts, "Token injected into page");
|
|
211
|
+
}
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Wrap a Playwright BrowserContext to automatically solve CAPTCHAs on every page load.
|
|
216
|
+
* Returns the same context (mutated with event listener).
|
|
217
|
+
*/
|
|
218
|
+
function withGateSolve(context, opts) {
|
|
219
|
+
const handlePage = (page) => {
|
|
220
|
+
// We need to listen for navigation completion. Since we use minimal types,
|
|
221
|
+
// check if the page has a 'on' method for load events
|
|
222
|
+
const pageAny = page;
|
|
223
|
+
if (typeof pageAny.on === "function") {
|
|
224
|
+
pageAny.on("load", async () => {
|
|
225
|
+
try {
|
|
226
|
+
// Small delay to let JS render CAPTCHA widgets
|
|
227
|
+
await page.waitForTimeout(1000);
|
|
228
|
+
await solveOnPage(page, opts);
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
log(opts, "Auto-solve error:", err);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
// Attach to existing pages
|
|
237
|
+
for (const page of context.pages()) {
|
|
238
|
+
handlePage(page);
|
|
239
|
+
}
|
|
240
|
+
// Attach to future pages
|
|
241
|
+
context.on("page", handlePage);
|
|
242
|
+
return context;
|
|
243
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gatesolve/playwright-plugin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Automatic CAPTCHA solving for Playwright. Detects Cloudflare Turnstile, reCAPTCHA, and hCaptcha challenges and solves them via GateSolve.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"prepublishOnly": "npm run build"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"playwright",
|
|
13
|
+
"captcha",
|
|
14
|
+
"turnstile",
|
|
15
|
+
"cloudflare",
|
|
16
|
+
"recaptcha",
|
|
17
|
+
"hcaptcha",
|
|
18
|
+
"gatesolve",
|
|
19
|
+
"automation",
|
|
20
|
+
"ai-agent"
|
|
21
|
+
],
|
|
22
|
+
"author": "Arson <arson@agentmail.to>",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/arsonx-dev/gatesolve-playwright"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"playwright": ">=1.40.0",
|
|
30
|
+
"playwright-core": ">=1.40.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependenciesMeta": {
|
|
33
|
+
"playwright": {
|
|
34
|
+
"optional": true
|
|
35
|
+
},
|
|
36
|
+
"playwright-core": {
|
|
37
|
+
"optional": true
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"typescript": "^5.9.3"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"dist",
|
|
45
|
+
"README.md"
|
|
46
|
+
]
|
|
47
|
+
}
|