playwriter 0.0.16 → 0.0.21
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/dist/cdp-session.d.ts +21 -0
- package/dist/cdp-session.d.ts.map +1 -0
- package/dist/cdp-session.js +131 -0
- package/dist/cdp-session.js.map +1 -0
- package/dist/cdp-types.d.ts +15 -0
- package/dist/cdp-types.d.ts.map +1 -1
- package/dist/cdp-types.js.map +1 -1
- package/dist/create-logger.d.ts +9 -0
- package/dist/create-logger.d.ts.map +1 -0
- package/dist/create-logger.js +43 -0
- package/dist/create-logger.js.map +1 -0
- package/dist/extension/cdp-relay.d.ts +7 -3
- package/dist/extension/cdp-relay.d.ts.map +1 -1
- package/dist/extension/cdp-relay.js +22 -12
- package/dist/extension/cdp-relay.js.map +1 -1
- package/dist/mcp.js +86 -44
- package/dist/mcp.js.map +1 -1
- package/dist/mcp.test.d.ts.map +1 -1
- package/dist/mcp.test.js +669 -183
- package/dist/mcp.test.js.map +1 -1
- package/dist/prompt.md +38 -8
- package/dist/selector-generator.js +331 -0
- package/dist/start-relay-server.d.ts +1 -3
- package/dist/start-relay-server.d.ts.map +1 -1
- package/dist/start-relay-server.js +3 -16
- package/dist/start-relay-server.js.map +1 -1
- package/dist/utils.d.ts +3 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +36 -0
- package/dist/utils.js.map +1 -1
- package/dist/wait-for-page-load.d.ts +16 -0
- package/dist/wait-for-page-load.d.ts.map +1 -0
- package/dist/wait-for-page-load.js +126 -0
- package/dist/wait-for-page-load.js.map +1 -0
- package/package.json +16 -12
- package/src/cdp-session.ts +156 -0
- package/src/cdp-types.ts +6 -0
- package/src/create-logger.ts +56 -0
- package/src/debugger.md +453 -0
- package/src/extension/cdp-relay.ts +32 -14
- package/src/mcp.test.ts +795 -189
- package/src/mcp.ts +101 -47
- package/src/prompt.md +38 -8
- package/src/snapshots/shadcn-ui-accessibility.md +94 -91
- package/src/start-relay-server.ts +3 -20
- package/src/utils.ts +45 -0
- package/src/wait-for-page-load.ts +173 -0
package/src/mcp.ts
CHANGED
|
@@ -4,13 +4,16 @@ import { z } from 'zod'
|
|
|
4
4
|
import { Page, Browser, BrowserContext, chromium } from 'playwright-core'
|
|
5
5
|
import fs from 'node:fs'
|
|
6
6
|
import path from 'node:path'
|
|
7
|
+
import os from 'node:os'
|
|
7
8
|
import { spawn } from 'node:child_process'
|
|
8
9
|
import { createRequire } from 'node:module'
|
|
9
10
|
import { fileURLToPath } from 'node:url'
|
|
10
11
|
import vm from 'node:vm'
|
|
11
12
|
import dedent from 'string-dedent'
|
|
12
13
|
import { createPatch } from 'diff'
|
|
13
|
-
import { getCdpUrl } from './utils.js'
|
|
14
|
+
import { getCdpUrl, LOG_FILE_PATH } from './utils.js'
|
|
15
|
+
import { waitForPageLoad, WaitForPageLoadOptions, WaitForPageLoadResult } from './wait-for-page-load.js'
|
|
16
|
+
import { getCDPSessionForPage, CDPSession } from './cdp-session.js'
|
|
14
17
|
|
|
15
18
|
const require = createRequire(import.meta.url)
|
|
16
19
|
|
|
@@ -59,6 +62,8 @@ interface VMContext {
|
|
|
59
62
|
resetPlaywright: () => Promise<{ page: Page; context: BrowserContext }>
|
|
60
63
|
getLatestLogs: (options?: { page?: Page; count?: number; search?: string | RegExp }) => Promise<string[]>
|
|
61
64
|
clearAllLogs: () => void
|
|
65
|
+
waitForPageLoad: (options: WaitForPageLoadOptions) => Promise<WaitForPageLoadResult>
|
|
66
|
+
getCDPSession: (options: { page: Page }) => Promise<CDPSession>
|
|
62
67
|
require: NodeRequire
|
|
63
68
|
import: (specifier: string) => Promise<any>
|
|
64
69
|
}
|
|
@@ -83,11 +88,48 @@ const MAX_LOGS_PER_PAGE = 5000
|
|
|
83
88
|
// Store last accessibility snapshot per page for diff feature
|
|
84
89
|
const lastSnapshots: WeakMap<Page, string> = new WeakMap()
|
|
85
90
|
|
|
91
|
+
// Cache CDP sessions per page
|
|
92
|
+
const cdpSessionCache: WeakMap<Page, CDPSession> = new WeakMap()
|
|
93
|
+
|
|
86
94
|
const RELAY_PORT = 19988
|
|
87
95
|
const NO_TABS_ERROR = `No browser tabs are connected. Please install and enable the Playwriter extension on at least one tab: https://chromewebstore.google.com/detail/playwriter-mcp/jfeammnjpkecdekppnclgkkffahnhfhe`
|
|
88
96
|
|
|
97
|
+
async function setDeviceScaleFactorForMacOS(context: BrowserContext): Promise<void> {
|
|
98
|
+
if (os.platform() !== 'darwin') {
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
const options = (context as any)._options
|
|
102
|
+
if (!options || options.deviceScaleFactor === 2) {
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
options.deviceScaleFactor = 2
|
|
106
|
+
for (const page of context.pages()) {
|
|
107
|
+
const delegate = (page as any)._delegate
|
|
108
|
+
if (delegate?.updateEmulatedViewportSize) {
|
|
109
|
+
await delegate.updateEmulatedViewportSize().catch(() => {})
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function preserveSystemColorScheme(context: BrowserContext): Promise<void> {
|
|
115
|
+
const options = (context as any)._options
|
|
116
|
+
if (!options) {
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
options.colorScheme = 'no-override'
|
|
120
|
+
options.reducedMotion = 'no-override'
|
|
121
|
+
options.forcedColors = 'no-override'
|
|
122
|
+
await Promise.all(
|
|
123
|
+
context.pages().map((page) => {
|
|
124
|
+
return page.emulateMedia({ colorScheme: null, reducedMotion: null, forcedColors: null }).catch(() => {})
|
|
125
|
+
}),
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
89
129
|
function isRegExp(value: any): value is RegExp {
|
|
90
|
-
return
|
|
130
|
+
return (
|
|
131
|
+
typeof value === 'object' && value !== null && typeof value.test === 'function' && typeof value.exec === 'function'
|
|
132
|
+
)
|
|
91
133
|
}
|
|
92
134
|
|
|
93
135
|
function clearUserState() {
|
|
@@ -152,7 +194,7 @@ async function ensureRelayServer(): Promise<void> {
|
|
|
152
194
|
}
|
|
153
195
|
}
|
|
154
196
|
|
|
155
|
-
throw new Error(
|
|
197
|
+
throw new Error(`Failed to start CDP relay server after 5 seconds. Check logs at: ${LOG_FILE_PATH}`)
|
|
156
198
|
}
|
|
157
199
|
|
|
158
200
|
async function ensureConnection(): Promise<{ browser: Browser; page: Page }> {
|
|
@@ -182,6 +224,9 @@ async function ensureConnection(): Promise<{ browser: Browser; page: Page }> {
|
|
|
182
224
|
// Set up console listener for all existing pages
|
|
183
225
|
context.pages().forEach((p) => setupPageConsoleListener(p))
|
|
184
226
|
|
|
227
|
+
await preserveSystemColorScheme(context)
|
|
228
|
+
await setDeviceScaleFactorForMacOS(context)
|
|
229
|
+
|
|
185
230
|
state.browser = browser
|
|
186
231
|
state.page = page
|
|
187
232
|
state.context = context
|
|
@@ -195,22 +240,12 @@ async function getPageTargetId(page: Page): Promise<string> {
|
|
|
195
240
|
throw new Error('Page is null or undefined')
|
|
196
241
|
}
|
|
197
242
|
|
|
198
|
-
// Always use internal _guid for consistency and speed
|
|
199
243
|
const guid = (page as any)._guid
|
|
200
244
|
if (guid) {
|
|
201
245
|
return guid
|
|
202
246
|
}
|
|
203
247
|
|
|
204
|
-
|
|
205
|
-
// Fallback to CDP if _guid is not available
|
|
206
|
-
const client = await page.context().newCDPSession(page)
|
|
207
|
-
const { targetInfo } = await client.send('Target.getTargetInfo')
|
|
208
|
-
await client.detach()
|
|
209
|
-
|
|
210
|
-
return targetInfo.targetId
|
|
211
|
-
} catch (e) {
|
|
212
|
-
throw new Error(`Could not get page identifier: ${e}`)
|
|
213
|
-
}
|
|
248
|
+
throw new Error('Could not get page identifier: _guid not available')
|
|
214
249
|
}
|
|
215
250
|
|
|
216
251
|
function setupPageConsoleListener(page: Page) {
|
|
@@ -227,31 +262,32 @@ function setupPageConsoleListener(page: Page) {
|
|
|
227
262
|
browserLogs.set(targetId, [])
|
|
228
263
|
}
|
|
229
264
|
|
|
230
|
-
// Clear logs on navigation/reload
|
|
231
265
|
page.on('framenavigated', (frame) => {
|
|
232
|
-
// Only clear if it's the main frame navigating (page reload/navigation)
|
|
233
266
|
if (frame === page.mainFrame()) {
|
|
234
267
|
browserLogs.set(targetId, [])
|
|
235
268
|
}
|
|
236
269
|
})
|
|
237
270
|
|
|
238
|
-
// Delete logs when page is closed
|
|
239
271
|
page.on('close', () => {
|
|
240
272
|
browserLogs.delete(targetId)
|
|
241
273
|
})
|
|
242
274
|
|
|
243
275
|
page.on('console', (msg) => {
|
|
244
|
-
|
|
276
|
+
try {
|
|
277
|
+
let logEntry = `[${msg.type()}] ${msg.text()}`
|
|
245
278
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const pageLogs = browserLogs.get(targetId)!
|
|
279
|
+
if (!browserLogs.has(targetId)) {
|
|
280
|
+
browserLogs.set(targetId, [])
|
|
281
|
+
}
|
|
282
|
+
const pageLogs = browserLogs.get(targetId)!
|
|
251
283
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
284
|
+
pageLogs.push(logEntry)
|
|
285
|
+
if (pageLogs.length > MAX_LOGS_PER_PAGE) {
|
|
286
|
+
pageLogs.shift()
|
|
287
|
+
}
|
|
288
|
+
} catch (e) {
|
|
289
|
+
console.error('[MCP] Failed to get console message text:', e)
|
|
290
|
+
return
|
|
255
291
|
}
|
|
256
292
|
})
|
|
257
293
|
}
|
|
@@ -268,8 +304,7 @@ async function getCurrentPage(timeout = 5000) {
|
|
|
268
304
|
|
|
269
305
|
if (pages.length > 0) {
|
|
270
306
|
const page = pages[0]
|
|
271
|
-
page.
|
|
272
|
-
await page.emulateMedia({ colorScheme: null })
|
|
307
|
+
await page.waitForLoadState('load', { timeout }).catch(() => {})
|
|
273
308
|
return page
|
|
274
309
|
}
|
|
275
310
|
}
|
|
@@ -315,6 +350,9 @@ async function resetConnection(): Promise<{ browser: Browser; page: Page; contex
|
|
|
315
350
|
// Set up console listener for all existing pages
|
|
316
351
|
context.pages().forEach((p) => setupPageConsoleListener(p))
|
|
317
352
|
|
|
353
|
+
await preserveSystemColorScheme(context)
|
|
354
|
+
await setDeviceScaleFactorForMacOS(context)
|
|
355
|
+
|
|
318
356
|
state.browser = browser
|
|
319
357
|
state.page = page
|
|
320
358
|
state.context = context
|
|
@@ -329,7 +367,9 @@ const server = new McpServer({
|
|
|
329
367
|
version: '1.0.0',
|
|
330
368
|
})
|
|
331
369
|
|
|
332
|
-
const promptContent =
|
|
370
|
+
const promptContent =
|
|
371
|
+
fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), 'prompt.md'), 'utf-8') +
|
|
372
|
+
`\n\nfor debugging internal playwriter errors, check playwriter relay server logs at: ${LOG_FILE_PATH}`
|
|
333
373
|
|
|
334
374
|
server.tool(
|
|
335
375
|
'execute',
|
|
@@ -464,20 +504,19 @@ server.tool(
|
|
|
464
504
|
throw new Error('getLocatorStringForElement: argument must be a Playwright Locator or ElementHandle')
|
|
465
505
|
}
|
|
466
506
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
479
|
-
const
|
|
480
|
-
const generator = createSelectorGenerator(WIN)
|
|
507
|
+
const elementPage = element.page ? element.page() : page
|
|
508
|
+
const hasGenerator = await elementPage.evaluate(() => !!(globalThis as any).__selectorGenerator)
|
|
509
|
+
|
|
510
|
+
if (!hasGenerator) {
|
|
511
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
|
512
|
+
const scriptPath = path.join(currentDir, '..', 'dist', 'selector-generator.js')
|
|
513
|
+
const scriptContent = fs.readFileSync(scriptPath, 'utf-8')
|
|
514
|
+
await elementPage.addScriptTag({ content: scriptContent })
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return await element.evaluate((el: any) => {
|
|
518
|
+
const { createSelectorGenerator, toLocator } = (globalThis as any).__selectorGenerator
|
|
519
|
+
const generator = createSelectorGenerator(globalThis)
|
|
481
520
|
const result = generator(el)
|
|
482
521
|
return toLocator(result.selector, 'javascript')
|
|
483
522
|
})
|
|
@@ -516,6 +555,17 @@ server.tool(
|
|
|
516
555
|
browserLogs.clear()
|
|
517
556
|
}
|
|
518
557
|
|
|
558
|
+
const getCDPSession = async (options: { page: Page }) => {
|
|
559
|
+
const cached = cdpSessionCache.get(options.page)
|
|
560
|
+
if (cached) {
|
|
561
|
+
return cached
|
|
562
|
+
}
|
|
563
|
+
const wsUrl = getCdpUrl({ port: RELAY_PORT })
|
|
564
|
+
const session = await getCDPSessionForPage({ page: options.page, wsUrl })
|
|
565
|
+
cdpSessionCache.set(options.page, session)
|
|
566
|
+
return session
|
|
567
|
+
}
|
|
568
|
+
|
|
519
569
|
let vmContextObj: VMContextWithGlobals = {
|
|
520
570
|
page,
|
|
521
571
|
context,
|
|
@@ -525,6 +575,8 @@ server.tool(
|
|
|
525
575
|
getLocatorStringForElement,
|
|
526
576
|
getLatestLogs,
|
|
527
577
|
clearAllLogs,
|
|
578
|
+
waitForPageLoad,
|
|
579
|
+
getCDPSession,
|
|
528
580
|
resetPlaywright: async () => {
|
|
529
581
|
const { page: newPage, context: newContext } = await resetConnection()
|
|
530
582
|
|
|
@@ -537,6 +589,8 @@ server.tool(
|
|
|
537
589
|
getLocatorStringForElement,
|
|
538
590
|
getLatestLogs,
|
|
539
591
|
clearAllLogs,
|
|
592
|
+
waitForPageLoad,
|
|
593
|
+
getCDPSession,
|
|
540
594
|
resetPlaywright: vmContextObj.resetPlaywright,
|
|
541
595
|
require,
|
|
542
596
|
// TODO --experimental-vm-modules is needed to make import work in vm
|
|
@@ -659,13 +713,13 @@ server.tool(
|
|
|
659
713
|
},
|
|
660
714
|
)
|
|
661
715
|
|
|
662
|
-
// Start the server
|
|
663
716
|
async function main() {
|
|
664
717
|
await ensureRelayServer()
|
|
665
718
|
const transport = new StdioServerTransport()
|
|
666
719
|
await server.connect(transport)
|
|
667
|
-
// console.error('Playwright MCP server running on stdio')
|
|
668
720
|
}
|
|
669
721
|
|
|
670
|
-
main().catch(
|
|
671
|
-
|
|
722
|
+
main().catch((error) => {
|
|
723
|
+
console.error('Fatal error starting MCP server:', error)
|
|
724
|
+
process.exit(1)
|
|
725
|
+
})
|
package/src/prompt.md
CHANGED
|
@@ -51,13 +51,16 @@ IMPORTANT! never call bringToFront unless specifically asked by the user. It is
|
|
|
51
51
|
|
|
52
52
|
- only call `page.close()` if the user asks you so or if you previously created this page yourself with `newPage`. do not close user created pages unless asked
|
|
53
53
|
- try to never sleep or run `page.waitForTimeout` unless you have to. there are better ways to wait for an element
|
|
54
|
+
- use `page.waitForLoadState('load')` instead of `page.waitForEvent('load')`. `waitForEvent` waits for a future event and will timeout if the page is already loaded, while `waitForLoadState` resolves immediately if already in that state
|
|
54
55
|
- never close browser or context. NEVER call `browser.close()`
|
|
56
|
+
- NEVER use `page.context().newCDPSession()` or `browser.newCDPSession()` - these do not work through the playwriter relay. If you need to send raw CDP commands, use the `getCDPSession` utility function instead.
|
|
57
|
+
|
|
55
58
|
|
|
56
59
|
## always check the current page state after an action
|
|
57
60
|
|
|
58
61
|
after you click a button or submit a form you ALWAYS have to then check what is the current state of the page. you cannot assume what happened after doing an action. instead run the following code to know what happened after the action:
|
|
59
62
|
|
|
60
|
-
`console.log('url:', page.url()); console.log(await accessibilitySnapshot({ page }).then(x => x.slice(0,
|
|
63
|
+
`console.log('url:', page.url()); console.log(await accessibilitySnapshot({ page }).then(x => x.split('\n').slice(0, 30).join('\n')));`
|
|
61
64
|
|
|
62
65
|
if nothing happened you may need to wait before the action completes, using something like `page.waitForNavigation({timeout: 3000})` or `await page.waitForLoadState('networkidle', {timeout: 3000})`
|
|
63
66
|
|
|
@@ -81,8 +84,25 @@ you have access to some functions in addition to playwright methods:
|
|
|
81
84
|
- `page`: (optional) filter logs by a specific page instance. Only returns logs from that page
|
|
82
85
|
- `count`: (optional) limit number of logs to return. If not specified, returns all available logs
|
|
83
86
|
- `search`: (optional) string or regex to filter logs. Only returns logs that match
|
|
84
|
-
|
|
85
|
-
|
|
87
|
+
- `waitForPageLoad({ page, timeout, pollInterval, minWait })`: smart network-aware page load detection. Playwright's `networkidle` waits for ALL requests to finish, which often times out on sites with analytics/ads. This function ignores those and returns when meaningful content is loaded.
|
|
88
|
+
- `page`: the page object to wait on
|
|
89
|
+
- `timeout`: (optional) max wait time in ms (default: 30000)
|
|
90
|
+
- `pollInterval`: (optional) how often to check in ms (default: 100)
|
|
91
|
+
- `minWait`: (optional) minimum wait before checking in ms (default: 500)
|
|
92
|
+
- Returns: `{ success, readyState, pendingRequests, waitTimeMs, timedOut }`
|
|
93
|
+
- Filters out: ad networks (doubleclick, googlesyndication), analytics (google-analytics, mixpanel, segment), social (facebook.net, twitter), support widgets (intercom, zendesk), and slow fonts/images
|
|
94
|
+
- `getCDPSession({ page })`: creates a CDP session to send raw Chrome DevTools Protocol commands. Use this instead of `page.context().newCDPSession()` which does not work through the playwriter relay. Sessions are cached per page.
|
|
95
|
+
- `page`: the page object to create the session for
|
|
96
|
+
- Returns: `{ send(method, params?), on(event, callback), off(event, callback) }`
|
|
97
|
+
- Example: `const cdp = await getCDPSession({ page }); const metrics = await cdp.send('Page.getLayoutMetrics');`
|
|
98
|
+
- Example listening for events:
|
|
99
|
+
```js
|
|
100
|
+
const cdp = await getCDPSession({ page });
|
|
101
|
+
await cdp.send('Debugger.enable');
|
|
102
|
+
const pausedEvent = await new Promise((resolve) => { cdp.on('Debugger.paused', resolve); });
|
|
103
|
+
console.log('Paused at:', pausedEvent.callFrames[0].location);
|
|
104
|
+
await cdp.send('Debugger.resume');
|
|
105
|
+
```
|
|
86
106
|
|
|
87
107
|
example:
|
|
88
108
|
|
|
@@ -108,14 +128,24 @@ Then you can use `page.locator(`aria-ref=${ref}`)` to get an element with a spec
|
|
|
108
128
|
|
|
109
129
|
IMPORTANT: notice that we do not add any quotes in `aria-ref`! it MUST be called without quotes
|
|
110
130
|
|
|
111
|
-
## getting selector for
|
|
131
|
+
## getting a stable selector for an element (getLocatorStringForElement)
|
|
132
|
+
|
|
133
|
+
The `aria-ref` values from accessibility snapshots are ephemeral - they change on page reload and when components remount. Use `getLocatorStringForElement(element)` to get a stable Playwright locator string that you can reuse programmatically.
|
|
112
134
|
|
|
113
|
-
|
|
135
|
+
This is useful for:
|
|
136
|
+
- Getting a selector you can store and reuse across page reloads
|
|
137
|
+
- Finding similar elements in a list (modify the selector pattern)
|
|
138
|
+
- Debugging which selector Playwright would use for an element
|
|
114
139
|
|
|
115
140
|
```js
|
|
116
|
-
const loc = page.locator('aria-ref=
|
|
117
|
-
|
|
118
|
-
|
|
141
|
+
const loc = page.locator('aria-ref=e14');
|
|
142
|
+
const selector = await getLocatorStringForElement(loc);
|
|
143
|
+
console.log(selector);
|
|
144
|
+
// => "getByRole('button', { name: 'Save' })"
|
|
145
|
+
|
|
146
|
+
// use the selector programmatically with eval:
|
|
147
|
+
const stableLocator = page.getByRole('button', { name: 'Save' })
|
|
148
|
+
await stableLocator.click();
|
|
119
149
|
```
|
|
120
150
|
|
|
121
151
|
## finding specific elements with snapshot
|
|
@@ -18,124 +18,127 @@ Return value:
|
|
|
18
18
|
- /url: /charts/area
|
|
19
19
|
- link "Directory" [ref=e13] [cursor=pointer]:
|
|
20
20
|
- /url: /docs/directory
|
|
21
|
-
- link "
|
|
22
|
-
- /url: /
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
- generic: ⌘
|
|
30
|
-
- generic: K
|
|
31
|
-
- link "100.5k" [ref=e22] [cursor=pointer]:
|
|
21
|
+
- link "Create" [ref=e14] [cursor=pointer]:
|
|
22
|
+
- /url: /create
|
|
23
|
+
- generic [ref=e15]:
|
|
24
|
+
- button "Search documentation... ⌘K" [ref=e17]:
|
|
25
|
+
- generic [ref=e18]: Search documentation...
|
|
26
|
+
- generic [ref=e19]:
|
|
27
|
+
- generic: ⌘K
|
|
28
|
+
- link "103k" [ref=e20] [cursor=pointer]:
|
|
32
29
|
- /url: https://github.com/shadcn-ui/ui
|
|
33
30
|
- img
|
|
34
|
-
- generic [ref=
|
|
35
|
-
- button "Toggle theme" [ref=
|
|
31
|
+
- generic [ref=e21]: 103k
|
|
32
|
+
- button "Toggle theme" [ref=e22]:
|
|
36
33
|
- img
|
|
37
|
-
- generic [ref=
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
- generic [ref=e23]: Toggle theme
|
|
35
|
+
- link "New Project" [ref=e24] [cursor=pointer]:
|
|
36
|
+
- /url: /create
|
|
37
|
+
- img
|
|
38
|
+
- text: New Project
|
|
39
|
+
- main [ref=e25]:
|
|
40
|
+
- generic [ref=e26]:
|
|
41
|
+
- generic [ref=e29]:
|
|
42
|
+
- link "New npx shadcn create" [ref=e30] [cursor=pointer]:
|
|
42
43
|
- /url: /docs/changelog
|
|
43
|
-
- generic "New" [ref=
|
|
44
|
-
- text:
|
|
44
|
+
- generic "New" [ref=e31]
|
|
45
|
+
- text: npx shadcn create
|
|
45
46
|
- img
|
|
46
|
-
- heading "The Foundation for your Design System" [level=1] [ref=
|
|
47
|
-
- paragraph [ref=
|
|
48
|
-
- generic [ref=
|
|
49
|
-
- link "
|
|
50
|
-
- /url: /
|
|
51
|
-
|
|
47
|
+
- heading "The Foundation for your Design System" [level=1] [ref=e32]
|
|
48
|
+
- paragraph [ref=e33]: A set of beautifully designed components that you can customize, extend, and build on. Start here then make it your own. Open Source. Open Code.
|
|
49
|
+
- generic [ref=e34]:
|
|
50
|
+
- link "New Project" [ref=e35] [cursor=pointer]:
|
|
51
|
+
- /url: /create
|
|
52
|
+
- img
|
|
53
|
+
- text: New Project
|
|
54
|
+
- link "View Components" [ref=e36] [cursor=pointer]:
|
|
52
55
|
- /url: /docs/components
|
|
53
|
-
- generic [ref=
|
|
54
|
-
- generic [ref=
|
|
55
|
-
- link "Examples" [ref=
|
|
56
|
+
- generic [ref=e38]:
|
|
57
|
+
- generic [ref=e43]:
|
|
58
|
+
- link "Examples" [ref=e44] [cursor=pointer]:
|
|
56
59
|
- /url: /
|
|
57
|
-
- link "Dashboard" [ref=
|
|
60
|
+
- link "Dashboard" [ref=e45] [cursor=pointer]:
|
|
58
61
|
- /url: /examples/dashboard
|
|
59
|
-
- link "Tasks" [ref=
|
|
62
|
+
- link "Tasks" [ref=e46] [cursor=pointer]:
|
|
60
63
|
- /url: /examples/tasks
|
|
61
|
-
- link "Playground" [ref=
|
|
64
|
+
- link "Playground" [ref=e47] [cursor=pointer]:
|
|
62
65
|
- /url: /examples/playground
|
|
63
|
-
- link "Authentication" [ref=
|
|
66
|
+
- link "Authentication" [ref=e48] [cursor=pointer]:
|
|
64
67
|
- /url: /examples/authentication
|
|
65
|
-
- generic [ref=
|
|
66
|
-
- generic [ref=
|
|
67
|
-
- combobox "Theme" [ref=
|
|
68
|
-
- generic [ref=
|
|
68
|
+
- generic [ref=e49]:
|
|
69
|
+
- generic [ref=e50]: Theme
|
|
70
|
+
- combobox "Theme" [ref=e51]:
|
|
71
|
+
- generic [ref=e52]: "Theme:"
|
|
69
72
|
- generic: Neutral
|
|
70
73
|
- img
|
|
71
|
-
- button "Copy Code" [ref=
|
|
74
|
+
- button "Copy Code" [ref=e53]:
|
|
72
75
|
- img
|
|
73
|
-
- generic [ref=
|
|
74
|
-
- generic [ref=
|
|
75
|
-
- generic [ref=
|
|
76
|
-
- group "Payment Method" [ref=
|
|
77
|
-
- generic [ref=
|
|
78
|
-
- paragraph [ref=
|
|
79
|
-
- generic [ref=
|
|
80
|
-
- group [ref=
|
|
81
|
-
- generic [ref=
|
|
82
|
-
- textbox "Name on Card" [ref=
|
|
76
|
+
- generic [ref=e54]: Copy Code
|
|
77
|
+
- generic [ref=e58]:
|
|
78
|
+
- generic [ref=e62]:
|
|
79
|
+
- group "Payment Method" [ref=e63]:
|
|
80
|
+
- generic [ref=e64]: Payment Method
|
|
81
|
+
- paragraph [ref=e65]: All transactions are secure and encrypted
|
|
82
|
+
- generic [ref=e66]:
|
|
83
|
+
- group [ref=e67]:
|
|
84
|
+
- generic [ref=e68]: Name on Card
|
|
85
|
+
- textbox "Name on Card" [ref=e69]:
|
|
83
86
|
- /placeholder: John Doe
|
|
84
|
-
- generic [ref=
|
|
85
|
-
- group [ref=
|
|
86
|
-
- generic [ref=
|
|
87
|
-
- textbox "Card Number" [ref=
|
|
87
|
+
- generic [ref=e70]:
|
|
88
|
+
- group [ref=e71]:
|
|
89
|
+
- generic [ref=e72]: Card Number
|
|
90
|
+
- textbox "Card Number" [ref=e73]:
|
|
88
91
|
- /placeholder: 1234 5678 9012 3456
|
|
89
|
-
- paragraph [ref=
|
|
90
|
-
- group [ref=
|
|
91
|
-
- generic [ref=
|
|
92
|
-
- textbox "CVV" [ref=
|
|
92
|
+
- paragraph [ref=e74]: Enter your 16-digit number.
|
|
93
|
+
- group [ref=e75]:
|
|
94
|
+
- generic [ref=e76]: CVV
|
|
95
|
+
- textbox "CVV" [ref=e77]:
|
|
93
96
|
- /placeholder: "123"
|
|
94
|
-
- generic [ref=
|
|
95
|
-
- group [ref=
|
|
96
|
-
- generic [ref=
|
|
97
|
-
- combobox "Month" [ref=
|
|
97
|
+
- generic [ref=e78]:
|
|
98
|
+
- group [ref=e79]:
|
|
99
|
+
- generic [ref=e80]: Month
|
|
100
|
+
- combobox "Month" [ref=e81]:
|
|
98
101
|
- generic: MM
|
|
99
102
|
- img
|
|
100
|
-
- combobox [ref=
|
|
101
|
-
- group [ref=
|
|
102
|
-
- generic [ref=
|
|
103
|
-
- combobox "Year" [ref=
|
|
103
|
+
- combobox [ref=e82]
|
|
104
|
+
- group [ref=e83]:
|
|
105
|
+
- generic [ref=e84]: Year
|
|
106
|
+
- combobox "Year" [ref=e85]:
|
|
104
107
|
- generic: YYYY
|
|
105
108
|
- img
|
|
106
|
-
- combobox [ref=
|
|
107
|
-
- group "Billing Address" [ref=
|
|
108
|
-
- generic [ref=
|
|
109
|
-
- paragraph [ref=
|
|
110
|
-
- group [ref=
|
|
111
|
-
- checkbox "Same as shipping address" [checked] [ref=
|
|
109
|
+
- combobox [ref=e86]
|
|
110
|
+
- group "Billing Address" [ref=e88]:
|
|
111
|
+
- generic [ref=e89]: Billing Address
|
|
112
|
+
- paragraph [ref=e90]: The billing address associated with your payment method
|
|
113
|
+
- group [ref=e92]:
|
|
114
|
+
- checkbox "Same as shipping address" [checked] [ref=e93]:
|
|
112
115
|
- generic:
|
|
113
116
|
- img
|
|
114
117
|
- checkbox [checked]
|
|
115
|
-
- generic [ref=
|
|
116
|
-
- group [ref=
|
|
117
|
-
- group [ref=
|
|
118
|
-
- generic [ref=
|
|
119
|
-
- textbox "Comments" [ref=
|
|
118
|
+
- generic [ref=e94]: Same as shipping address
|
|
119
|
+
- group [ref=e96]:
|
|
120
|
+
- group [ref=e98]:
|
|
121
|
+
- generic [ref=e99]: Comments
|
|
122
|
+
- textbox "Comments" [ref=e100]:
|
|
120
123
|
- /placeholder: Add any additional comments
|
|
121
|
-
- group [ref=
|
|
122
|
-
- button "Submit" [ref=
|
|
123
|
-
- button "Cancel" [ref=
|
|
124
|
-
- generic [ref=
|
|
125
|
-
- generic [ref=
|
|
126
|
-
- generic [ref=
|
|
127
|
-
- generic [ref=
|
|
128
|
-
- img "@shadcn" [ref=
|
|
129
|
-
- img "@maxleiter" [ref=
|
|
130
|
-
- img "@evilrabbit" [ref=
|
|
131
|
-
- generic [ref=
|
|
132
|
-
- generic [ref=
|
|
133
|
-
- button "Invite Members" [ref=
|
|
124
|
+
- group [ref=e101]:
|
|
125
|
+
- button "Submit" [ref=e102]
|
|
126
|
+
- button "Cancel" [ref=e103]
|
|
127
|
+
- generic [ref=e104]:
|
|
128
|
+
- generic [ref=e105]:
|
|
129
|
+
- generic [ref=e106]:
|
|
130
|
+
- generic [ref=e108]:
|
|
131
|
+
- img "@shadcn" [ref=e110]
|
|
132
|
+
- img "@maxleiter" [ref=e112]
|
|
133
|
+
- img "@evilrabbit" [ref=e114]
|
|
134
|
+
- generic [ref=e115]: No Team Members
|
|
135
|
+
- generic [ref=e116]: Invite your team to collaborate on this project.
|
|
136
|
+
- button "Invite Members" [ref=e118]:
|
|
134
137
|
- img
|
|
135
138
|
- text: Invite Members
|
|
136
|
-
- generic [ref=
|
|
137
|
-
- generic [ref=
|
|
139
|
+
- generic [ref=e119]:
|
|
140
|
+
- generic [ref=e120]:
|
|
138
141
|
- status "Loading"
|
|
139
|
-
|
|
142
|
+
|
|
140
143
|
|
|
141
144
|
[Truncated to 6000 characters. Better manage your logs or paginate them to read the full logs]
|
|
@@ -1,26 +1,9 @@
|
|
|
1
1
|
import { startPlayWriterCDPRelayServer } from './extension/cdp-relay.js'
|
|
2
|
-
import
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import { fileURLToPath } from 'node:url'
|
|
5
|
-
import util from 'node:util'
|
|
6
|
-
|
|
7
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
8
|
-
const logFilePath = path.join(__dirname, '..', 'relay-server.log')
|
|
2
|
+
import { createFileLogger } from './create-logger.js'
|
|
9
3
|
|
|
10
4
|
process.title = 'playwriter-ws-server'
|
|
11
|
-
fs.writeFileSync(logFilePath, '')
|
|
12
|
-
|
|
13
|
-
const log = (...args: any[]) => {
|
|
14
|
-
const message = args.map(arg =>
|
|
15
|
-
typeof arg === 'string' ? arg : util.inspect(arg, { depth: null, colors: false })
|
|
16
|
-
).join(' ')
|
|
17
|
-
return fs.promises.appendFile(logFilePath, message + '\n')
|
|
18
|
-
}
|
|
19
5
|
|
|
20
|
-
const logger =
|
|
21
|
-
log,
|
|
22
|
-
error: log
|
|
23
|
-
}
|
|
6
|
+
const logger = createFileLogger()
|
|
24
7
|
|
|
25
8
|
process.on('uncaughtException', async (err) => {
|
|
26
9
|
await logger.error('Uncaught Exception:', err);
|
|
@@ -41,7 +24,7 @@ export async function startServer({ port = 19988 }: { port?: number } = {}) {
|
|
|
41
24
|
const server = await startPlayWriterCDPRelayServer({ port, logger })
|
|
42
25
|
|
|
43
26
|
console.log('CDP Relay Server running. Press Ctrl+C to stop.')
|
|
44
|
-
console.log('Logs are being written to:', logFilePath)
|
|
27
|
+
console.log('Logs are being written to:', logger.logFilePath)
|
|
45
28
|
|
|
46
29
|
process.on('SIGINT', () => {
|
|
47
30
|
console.log('\nShutting down...')
|
package/src/utils.ts
CHANGED
|
@@ -1,4 +1,49 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { xdgData } from 'xdg-basedir'
|
|
5
|
+
|
|
1
6
|
export function getCdpUrl({ port = 19988, host = '127.0.0.1' }: { port?: number; host?: string } = {}) {
|
|
2
7
|
const id = `${Math.random().toString(36).substring(2, 15)}_${Date.now()}`
|
|
3
8
|
return `ws://${host}:${port}/cdp/${id}`
|
|
4
9
|
}
|
|
10
|
+
|
|
11
|
+
export function getDataDir(): string {
|
|
12
|
+
const dataDir = xdgData || path.join(os.homedir(), '.local', 'share')
|
|
13
|
+
return path.join(dataDir, 'playwriter')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ensureDataDir(): string {
|
|
17
|
+
const dataDir = getDataDir()
|
|
18
|
+
if (!fs.existsSync(dataDir)) {
|
|
19
|
+
fs.mkdirSync(dataDir, { recursive: true })
|
|
20
|
+
}
|
|
21
|
+
return dataDir
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getLogsDir(): string {
|
|
25
|
+
return path.join(getDataDir(), 'logs')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getLogFilePath(): string {
|
|
29
|
+
if (process.env.PLAYWRITER_LOG_PATH) {
|
|
30
|
+
return process.env.PLAYWRITER_LOG_PATH
|
|
31
|
+
}
|
|
32
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
|
|
33
|
+
return path.join(getLogsDir(), `relay-server-${timestamp}.log`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const LOG_FILE_PATH = getLogFilePath()
|
|
37
|
+
|
|
38
|
+
// export function getDidPromptReviewPath(): string {
|
|
39
|
+
// return path.join(getDataDir(), 'did-prompt-review')
|
|
40
|
+
// }
|
|
41
|
+
|
|
42
|
+
// export function hasReviewedPrompt(): boolean {
|
|
43
|
+
// return fs.existsSync(getDidPromptReviewPath())
|
|
44
|
+
// }
|
|
45
|
+
|
|
46
|
+
// export function markPromptReviewed(): void {
|
|
47
|
+
// ensureDataDir()
|
|
48
|
+
// fs.writeFileSync(getDidPromptReviewPath(), new Date().toISOString())
|
|
49
|
+
// }
|