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.
Files changed (47) hide show
  1. package/dist/cdp-session.d.ts +21 -0
  2. package/dist/cdp-session.d.ts.map +1 -0
  3. package/dist/cdp-session.js +131 -0
  4. package/dist/cdp-session.js.map +1 -0
  5. package/dist/cdp-types.d.ts +15 -0
  6. package/dist/cdp-types.d.ts.map +1 -1
  7. package/dist/cdp-types.js.map +1 -1
  8. package/dist/create-logger.d.ts +9 -0
  9. package/dist/create-logger.d.ts.map +1 -0
  10. package/dist/create-logger.js +43 -0
  11. package/dist/create-logger.js.map +1 -0
  12. package/dist/extension/cdp-relay.d.ts +7 -3
  13. package/dist/extension/cdp-relay.d.ts.map +1 -1
  14. package/dist/extension/cdp-relay.js +22 -12
  15. package/dist/extension/cdp-relay.js.map +1 -1
  16. package/dist/mcp.js +86 -44
  17. package/dist/mcp.js.map +1 -1
  18. package/dist/mcp.test.d.ts.map +1 -1
  19. package/dist/mcp.test.js +669 -183
  20. package/dist/mcp.test.js.map +1 -1
  21. package/dist/prompt.md +38 -8
  22. package/dist/selector-generator.js +331 -0
  23. package/dist/start-relay-server.d.ts +1 -3
  24. package/dist/start-relay-server.d.ts.map +1 -1
  25. package/dist/start-relay-server.js +3 -16
  26. package/dist/start-relay-server.js.map +1 -1
  27. package/dist/utils.d.ts +3 -0
  28. package/dist/utils.d.ts.map +1 -1
  29. package/dist/utils.js +36 -0
  30. package/dist/utils.js.map +1 -1
  31. package/dist/wait-for-page-load.d.ts +16 -0
  32. package/dist/wait-for-page-load.d.ts.map +1 -0
  33. package/dist/wait-for-page-load.js +126 -0
  34. package/dist/wait-for-page-load.js.map +1 -0
  35. package/package.json +16 -12
  36. package/src/cdp-session.ts +156 -0
  37. package/src/cdp-types.ts +6 -0
  38. package/src/create-logger.ts +56 -0
  39. package/src/debugger.md +453 -0
  40. package/src/extension/cdp-relay.ts +32 -14
  41. package/src/mcp.test.ts +795 -189
  42. package/src/mcp.ts +101 -47
  43. package/src/prompt.md +38 -8
  44. package/src/snapshots/shadcn-ui-accessibility.md +94 -91
  45. package/src/start-relay-server.ts +3 -20
  46. package/src/utils.ts +45 -0
  47. 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 typeof value === 'object' && value !== null && typeof value.test === 'function' && typeof value.exec === 'function'
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('Failed to start CDP relay server after 5 seconds')
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
- try {
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
- const logEntry = `[${msg.type()}] ${msg.text()}`
276
+ try {
277
+ let logEntry = `[${msg.type()}] ${msg.text()}`
245
278
 
246
- // Get or create logs array for this page targetId
247
- if (!browserLogs.has(targetId)) {
248
- browserLogs.set(targetId, [])
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
- pageLogs.push(logEntry)
253
- if (pageLogs.length > MAX_LOGS_PER_PAGE) {
254
- pageLogs.shift()
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.waitForEvent('load', { timeout })
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 = fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), 'prompt.md'), 'utf-8')
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
- return await element.evaluate(async (el: any) => {
468
- const WIN = globalThis as any
469
- if (!WIN.__selectorGenerator) {
470
- const module: SelectorGenerator = await import(
471
- // @ts-ignore
472
- 'https://unpkg.com/@mizchi/selector-generator@1.50.0-next/dist/index.js'
473
- )
474
- WIN.__selectorGenerator = {
475
- createSelectorGenerator: module.createSelectorGenerator,
476
- toLocator: module.toLocator,
477
- }
478
- }
479
- const { createSelectorGenerator, toLocator } = WIN.__selectorGenerator as SelectorGenerator
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(console.error)
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, 1000)));`
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
- To bring a tab to front and focus it, use the standard Playwright method `await page.bringToFront()`
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 a locator identified by snapshot aria-ref
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
- in some cases you want to get a selector for a locator you just identified using `const element = page.locator('aria-ref=${ref}')`. To do so you can use `await getLocatorStringForElement(element)`. This is useful if you need to find other elements of the same type in a list for example. If you know the selector you can usually change a bit the selector to find the other elements of the same type in the list or table
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=123');
117
- console.log(await getLocatorStringForElement(loc));
118
- // => "getByRole('button', { name: 'Save' })" or similar
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 "Themes" [ref=e14] [cursor=pointer]:
22
- - /url: /themes
23
- - link "Colors" [ref=e15] [cursor=pointer]:
24
- - /url: /colors
25
- - generic [ref=e16]:
26
- - button "Search documentation... ⌘ K" [ref=e18]:
27
- - generic [ref=e19]: Search documentation...
28
- - generic [ref=e21]:
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=e23]: 100.5k
35
- - button "Toggle theme" [ref=e24]:
31
+ - generic [ref=e21]: 103k
32
+ - button "Toggle theme" [ref=e22]:
36
33
  - img
37
- - generic [ref=e25]: Toggle theme
38
- - main [ref=e26]:
39
- - generic [ref=e27]:
40
- - generic [ref=e30]:
41
- - 'link "New New Components: Field, Input Group, Item and more" [ref=e31] [cursor=pointer]':
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=e32]
44
- - text: "New Components: Field, Input Group, Item and more"
44
+ - generic "New" [ref=e31]
45
+ - text: npx shadcn create
45
46
  - img
46
- - heading "The Foundation for your Design System" [level=1] [ref=e33]
47
- - paragraph [ref=e34]: 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.
48
- - generic [ref=e35]:
49
- - link "Get Started" [ref=e36] [cursor=pointer]:
50
- - /url: /docs/installation
51
- - link "View Components" [ref=e37] [cursor=pointer]:
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=e39]:
54
- - generic [ref=e44]:
55
- - link "Examples" [ref=e45] [cursor=pointer]:
56
+ - generic [ref=e38]:
57
+ - generic [ref=e43]:
58
+ - link "Examples" [ref=e44] [cursor=pointer]:
56
59
  - /url: /
57
- - link "Dashboard" [ref=e46] [cursor=pointer]:
60
+ - link "Dashboard" [ref=e45] [cursor=pointer]:
58
61
  - /url: /examples/dashboard
59
- - link "Tasks" [ref=e47] [cursor=pointer]:
62
+ - link "Tasks" [ref=e46] [cursor=pointer]:
60
63
  - /url: /examples/tasks
61
- - link "Playground" [ref=e48] [cursor=pointer]:
64
+ - link "Playground" [ref=e47] [cursor=pointer]:
62
65
  - /url: /examples/playground
63
- - link "Authentication" [ref=e49] [cursor=pointer]:
66
+ - link "Authentication" [ref=e48] [cursor=pointer]:
64
67
  - /url: /examples/authentication
65
- - generic [ref=e50]:
66
- - generic [ref=e51]: Theme
67
- - combobox "Theme" [ref=e52]:
68
- - generic [ref=e53]: "Theme:"
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=e54]:
74
+ - button "Copy Code" [ref=e53]:
72
75
  - img
73
- - generic [ref=e55]: Copy Code
74
- - generic [ref=e59]:
75
- - generic [ref=e63]:
76
- - group "Payment Method" [ref=e64]:
77
- - generic [ref=e65]: Payment Method
78
- - paragraph [ref=e66]: All transactions are secure and encrypted
79
- - generic [ref=e67]:
80
- - group [ref=e68]:
81
- - generic [ref=e69]: Name on Card
82
- - textbox "Name on Card" [ref=e70]:
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=e71]:
85
- - group [ref=e72]:
86
- - generic [ref=e73]: Card Number
87
- - textbox "Card Number" [ref=e74]:
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=e75]: Enter your 16-digit number.
90
- - group [ref=e76]:
91
- - generic [ref=e77]: CVV
92
- - textbox "CVV" [ref=e78]:
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=e79]:
95
- - group [ref=e80]:
96
- - generic [ref=e81]: Month
97
- - combobox "Month" [ref=e82]:
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=e83]
101
- - group [ref=e84]:
102
- - generic [ref=e85]: Year
103
- - combobox "Year" [ref=e86]:
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=e87]
107
- - group "Billing Address" [ref=e89]:
108
- - generic [ref=e90]: Billing Address
109
- - paragraph [ref=e91]: The billing address associated with your payment method
110
- - group [ref=e93]:
111
- - checkbox "Same as shipping address" [checked] [ref=e94]:
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=e95]: Same as shipping address
116
- - group [ref=e97]:
117
- - group [ref=e99]:
118
- - generic [ref=e100]: Comments
119
- - textbox "Comments" [ref=e101]:
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=e102]:
122
- - button "Submit" [ref=e103]
123
- - button "Cancel" [ref=e104]
124
- - generic [ref=e105]:
125
- - generic [ref=e106]:
126
- - generic [ref=e107]:
127
- - generic [ref=e109]:
128
- - img "@shadcn" [ref=e111]
129
- - img "@maxleiter" [ref=e113]
130
- - img "@evilrabbit" [ref=e115]
131
- - generic [ref=e116]: No Team Members
132
- - generic [ref=e117]: Invite your team to collaborate on this project.
133
- - button "Invite Members" [ref=e119]:
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=e120]:
137
- - generic [ref=e121]:
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 fs from 'node:fs'
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
+ // }