From 237f24d3f8ec845f1359fbeb1e31aa13df23027a Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Tue, 30 Dec 2025 14:30:30 +0100 Subject: [PATCH] Generate MCP resources at build time and host on playwriter.dev - Add build-resources.ts script to generate markdown API docs - Resources now use https://playwriter.dev/resources/*.md URLs - Output to both playwriter/dist/ and website/public/resources/ - Simplify mcp.ts resource handlers to read pre-built files --- PLAYWRITER_AGENTS.md | 2 + playwriter/package.json | 2 +- playwriter/scripts/build-resources.ts | 126 +++++++ playwriter/src/mcp.ts | 85 +---- playwriter/src/prompt.md | 6 +- website/public/resources/debugger-api.md | 457 +++++++++++++++++++++++ website/public/resources/editor-api.md | 364 ++++++++++++++++++ website/public/resources/styles-api.md | 117 ++++++ 8 files changed, 1082 insertions(+), 77 deletions(-) create mode 100644 playwriter/scripts/build-resources.ts create mode 100644 website/public/resources/debugger-api.md create mode 100644 website/public/resources/editor-api.md create mode 100644 website/public/resources/styles-api.md diff --git a/PLAYWRITER_AGENTS.md b/PLAYWRITER_AGENTS.md index caa46be..d86bcb5 100644 --- a/PLAYWRITER_AGENTS.md +++ b/PLAYWRITER_AGENTS.md @@ -36,6 +36,8 @@ playwriter contains the ws server and MCP code. also the tests for the mcp are t playwriter/src/resource.md is for more generic knowledge about playwright that the agent can use when necessary, for things like best practices for selecting locators on the page +website/public/resources/ is auto-generated by `playwriter/scripts/build-resources.ts` during `pnpm build`. DO NOT edit these files manually - edit the source files in `playwriter/src/` instead (e.g. `debugger-examples.ts`, `editor-examples.ts`, `styles-examples.ts`) + ## CDP docs here are some commands you can run to fetch does about CDP various domains (events and commands namespaces) diff --git a/playwriter/package.json b/playwriter/package.json index d623445..43ffdad 100644 --- a/playwriter/package.json +++ b/playwriter/package.json @@ -7,7 +7,7 @@ "types": "dist/index.d.ts", "repository": "https://github.com/remorses/playwriter", "scripts": { - "build": "rm -rf dist *.tsbuildinfo && mkdir dist && bun scripts/build-selector-generator.ts && bun scripts/build-bippy.ts && tsc", + "build": "rm -rf dist *.tsbuildinfo && mkdir dist && bun scripts/build-selector-generator.ts && bun scripts/build-bippy.ts && tsc && bun scripts/build-resources.ts", "prepublishOnly": "pnpm build", "watch": "tsc -w", "typecheck": "tsc --noEmit", diff --git a/playwriter/scripts/build-resources.ts b/playwriter/scripts/build-resources.ts new file mode 100644 index 0000000..91cd68e --- /dev/null +++ b/playwriter/scripts/build-resources.ts @@ -0,0 +1,126 @@ +/** + * Generates markdown resource files for the MCP at build time. + * + * These files are written to: + * - playwriter/dist/ - for the MCP to read at runtime + * - website/public/ - for hosting on playwriter.dev + */ + +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import dedent from 'string-dedent' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const playwriterDir = path.join(__dirname, '..') +const distDir = path.join(playwriterDir, 'dist') +const websitePublicDir = path.join(playwriterDir, '..', 'website', 'public', 'resources') + +function ensureDir(dir: string) { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }) + } +} + +function readFile(relativePath: string): string { + return fs.readFileSync(path.join(playwriterDir, relativePath), 'utf-8') +} + +function writeToDestinations(filename: string, content: string) { + ensureDir(distDir) + ensureDir(websitePublicDir) + + const distPath = path.join(distDir, filename) + const websitePath = path.join(websitePublicDir, filename) + + fs.writeFileSync(distPath, content, 'utf-8') + fs.writeFileSync(websitePath, content, 'utf-8') + + console.log(`Generated ${filename}`) +} + +function cleanTypes(typesContent: string): string { + return typesContent + .replace(/\/\/# sourceMappingURL=.*$/gm, '') + .trim() +} + +function buildDebuggerApi() { + const debuggerTypes = cleanTypes(readFile('dist/debugger.d.ts')) + const debuggerExamples = readFile('src/debugger-examples.ts') + + const content = dedent` + # Debugger API Reference + + ## Types + + \`\`\`ts + ${debuggerTypes} + \`\`\` + + ## Examples + + \`\`\`ts + ${debuggerExamples} + \`\`\` + ` + + writeToDestinations('debugger-api.md', content) +} + +function buildEditorApi() { + const editorTypes = cleanTypes(readFile('dist/editor.d.ts')) + const editorExamples = readFile('src/editor-examples.ts') + + const content = dedent` + # Editor API Reference + + The Editor class provides a Claude Code-like interface for viewing and editing web page scripts at runtime. + + ## Types + + \`\`\`ts + ${editorTypes} + \`\`\` + + ## Examples + + \`\`\`ts + ${editorExamples} + \`\`\` + ` + + writeToDestinations('editor-api.md', content) +} + +function buildStylesApi() { + const stylesTypes = cleanTypes(readFile('dist/styles.d.ts')) + const stylesExamples = readFile('src/styles-examples.ts') + + const content = dedent` + # Styles API Reference + + The getStylesForLocator function inspects CSS styles applied to an element, similar to browser DevTools "Styles" panel. + + ## Types + + \`\`\`ts + ${stylesTypes} + \`\`\` + + ## Examples + + \`\`\`ts + ${stylesExamples} + \`\`\` + ` + + writeToDestinations('styles-api.md', content) +} + +// Run all builds +buildDebuggerApi() +buildEditorApi() +buildStylesApi() + +console.log('Resource files generated successfully') diff --git a/playwriter/src/mcp.ts b/playwriter/src/mcp.ts index fe0ea20..75a0525 100644 --- a/playwriter/src/mcp.ts +++ b/playwriter/src/mcp.ts @@ -431,109 +431,48 @@ const promptContent = fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), 'prompt.md'), 'utf-8') + `\n\nfor debugging internal playwriter errors, check playwriter relay server logs at: ${LOG_FILE_PATH}` -server.resource('debugger-api', 'playwriter://debugger-api', { mimeType: 'text/plain' }, async () => { +server.resource('debugger-api', 'https://playwriter.dev/resources/debugger-api.md', { mimeType: 'text/plain' }, async () => { const packageJsonPath = require.resolve('playwriter/package.json') const packageDir = path.dirname(packageJsonPath) - - const debuggerTypes = fs - .readFileSync(path.join(packageDir, 'dist', 'debugger.d.ts'), 'utf-8') - .replace(/\/\/# sourceMappingURL=.*$/gm, '') - .trim() - const debuggerExamples = fs.readFileSync(path.join(packageDir, 'src', 'debugger-examples.ts'), 'utf-8') + const content = fs.readFileSync(path.join(packageDir, 'dist', 'debugger-api.md'), 'utf-8') return { contents: [ { - uri: 'playwriter://debugger-api', - text: dedent` - # Debugger API Reference - - ## Types - - \`\`\`ts - ${debuggerTypes} - \`\`\` - - ## Examples - - \`\`\`ts - ${debuggerExamples} - \`\`\` - `, + uri: 'https://playwriter.dev/resources/debugger-api.md', + text: content, mimeType: 'text/plain', }, ], } }) -server.resource('editor-api', 'playwriter://editor-api', { mimeType: 'text/plain' }, async () => { +server.resource('editor-api', 'https://playwriter.dev/resources/editor-api.md', { mimeType: 'text/plain' }, async () => { const packageJsonPath = require.resolve('playwriter/package.json') const packageDir = path.dirname(packageJsonPath) - - const editorTypes = fs - .readFileSync(path.join(packageDir, 'dist', 'editor.d.ts'), 'utf-8') - .replace(/\/\/# sourceMappingURL=.*$/gm, '') - .trim() - const editorExamples = fs.readFileSync(path.join(packageDir, 'src', 'editor-examples.ts'), 'utf-8') + const content = fs.readFileSync(path.join(packageDir, 'dist', 'editor-api.md'), 'utf-8') return { contents: [ { - uri: 'playwriter://editor-api', - text: dedent` - # Editor API Reference - - The Editor class provides a Claude Code-like interface for viewing and editing web page scripts at runtime. - - ## Types - - \`\`\`ts - ${editorTypes} - \`\`\` - - ## Examples - - \`\`\`ts - ${editorExamples} - \`\`\` - `, + uri: 'https://playwriter.dev/resources/editor-api.md', + text: content, mimeType: 'text/plain', }, ], } }) -server.resource('styles-api', 'playwriter://styles-api', { mimeType: 'text/plain' }, async () => { +server.resource('styles-api', 'https://playwriter.dev/resources/styles-api.md', { mimeType: 'text/plain' }, async () => { const packageJsonPath = require.resolve('playwriter/package.json') const packageDir = path.dirname(packageJsonPath) - - const stylesTypes = fs - .readFileSync(path.join(packageDir, 'dist', 'styles.d.ts'), 'utf-8') - .replace(/\/\/# sourceMappingURL=.*$/gm, '') - .trim() - const stylesExamples = fs.readFileSync(path.join(packageDir, 'src', 'styles-examples.ts'), 'utf-8') + const content = fs.readFileSync(path.join(packageDir, 'dist', 'styles-api.md'), 'utf-8') return { contents: [ { - uri: 'playwriter://styles-api', - text: dedent` - # Styles API Reference - - The getStylesForLocator function inspects CSS styles applied to an element, similar to browser DevTools "Styles" panel. - - ## Types - - \`\`\`ts - ${stylesTypes} - \`\`\` - - ## Examples - - \`\`\`ts - ${stylesExamples} - \`\`\` - `, + uri: 'https://playwriter.dev/resources/styles-api.md', + text: content, mimeType: 'text/plain', }, ], diff --git a/playwriter/src/prompt.md b/playwriter/src/prompt.md index 0fcee62..99a7f27 100644 --- a/playwriter/src/prompt.md +++ b/playwriter/src/prompt.md @@ -95,9 +95,9 @@ you have access to some functions in addition to playwright methods: - `page`: the page object to create the session for - Returns: `{ send(method, params?), on(event, callback), off(event, callback) }` - Example: `const cdp = await getCDPSession({ page }); const metrics = await cdp.send('Page.getLayoutMetrics');` -- `createDebugger({ cdp })`: creates a Debugger instance for setting breakpoints, stepping, and inspecting variables. Read the `playwriter://debugger-api` resource for full API docs and examples. -- `createEditor({ cdp })`: creates an Editor instance for viewing and live-editing page scripts and CSS stylesheets. Read the `playwriter://editor-api` resource for full API docs and examples. -- `getStylesForLocator({ locator })`: gets the CSS styles applied to an element, similar to browser DevTools "Styles" panel. Read the `playwriter://styles-api` resource for full API docs and examples. +- `createDebugger({ cdp })`: creates a Debugger instance for setting breakpoints, stepping, and inspecting variables. Read the `https://playwriter.dev/resources/debugger-api.md` resource for full API docs and examples. +- `createEditor({ cdp })`: creates an Editor instance for viewing and live-editing page scripts and CSS stylesheets. Read the `https://playwriter.dev/resources/editor-api.md` resource for full API docs and examples. +- `getStylesForLocator({ locator })`: gets the CSS styles applied to an element, similar to browser DevTools "Styles" panel. Read the `https://playwriter.dev/resources/styles-api.md` resource for full API docs and examples. - `getReactSource({ locator })`: gets the React component source location (file, line, column) for an element. - `locator`: a Playwright Locator or ElementHandle for the element to inspect - Returns: `{ fileName, lineNumber, columnNumber, componentName }` or `null` if not found diff --git a/website/public/resources/debugger-api.md b/website/public/resources/debugger-api.md new file mode 100644 index 0000000..0ff1315 --- /dev/null +++ b/website/public/resources/debugger-api.md @@ -0,0 +1,457 @@ +# Debugger API Reference + +## Types + +```ts +import type { CDPSession } from './cdp-session.js'; +export interface BreakpointInfo { + id: string; + file: string; + line: number; +} +export interface LocationInfo { + url: string; + lineNumber: number; + columnNumber: number; + callstack: Array<{ + functionName: string; + url: string; + lineNumber: number; + columnNumber: number; + }>; + sourceContext: string; +} +export interface EvaluateResult { + value: unknown; +} +export interface ScriptInfo { + scriptId: string; + url: string; +} +/** + * A class for debugging JavaScript code via Chrome DevTools Protocol. + * Works with both Node.js (--inspect) and browser debugging. + * + * @example + * ```ts + * const cdp = await getCDPSessionForPage({ page, wsUrl }) + * const dbg = new Debugger({ cdp }) + * + * await dbg.setBreakpoint({ file: 'https://example.com/app.js', line: 42 }) + * // trigger the code path, then: + * const location = await dbg.getLocation() + * const vars = await dbg.inspectLocalVariables() + * await dbg.resume() + * ``` + */ +export declare class Debugger { + private cdp; + private debuggerEnabled; + private paused; + private currentCallFrames; + private breakpoints; + private scripts; + private xhrBreakpoints; + private blackboxPatterns; + /** + * Creates a new Debugger instance. + * + * @param options - Configuration options + * @param options.cdp - A CDPSession instance for sending CDP commands + * + * @example + * ```ts + * const cdp = await getCDPSessionForPage({ page, wsUrl }) + * const dbg = new Debugger({ cdp }) + * ``` + */ + constructor({ cdp }: { + cdp: CDPSession; + }); + private setupEventListeners; + /** + * Enables the debugger and runtime domains. Called automatically by other methods. + * Also resumes execution if the target was started with --inspect-brk. + * + * @example + * ```ts + * await dbg.enable() + * ``` + */ + enable(): Promise; + /** + * Sets a breakpoint at a specified URL and line number. + * Use the URL from listScripts() to find available scripts. + * + * @param options - Breakpoint options + * @param options.file - Script URL (e.g. https://example.com/app.js) + * @param options.line - Line number (1-based) + * @param options.condition - Optional JS expression; only pause when it evaluates to true + * @returns The breakpoint ID for later removal + * + * @example + * ```ts + * const id = await dbg.setBreakpoint({ file: 'https://example.com/app.js', line: 42 }) + * // later: + * await dbg.deleteBreakpoint({ breakpointId: id }) + * + * // Conditional breakpoint - only pause when userId is 123 + * await dbg.setBreakpoint({ + * file: 'https://example.com/app.js', + * line: 42, + * condition: 'userId === 123' + * }) + * ``` + */ + setBreakpoint({ file, line, condition }: { + file: string; + line: number; + condition?: string; + }): Promise; + /** + * Removes a breakpoint by its ID. + * + * @param options - Options + * @param options.breakpointId - The breakpoint ID returned by setBreakpoint + * + * @example + * ```ts + * await dbg.deleteBreakpoint({ breakpointId: 'bp-123' }) + * ``` + */ + deleteBreakpoint({ breakpointId }: { + breakpointId: string; + }): Promise; + /** + * Returns a list of all active breakpoints set by this debugger instance. + * + * @returns Array of breakpoint info objects + * + * @example + * ```ts + * const breakpoints = dbg.listBreakpoints() + * // [{ id: 'bp-123', file: 'https://example.com/index.js', line: 42 }] + * ``` + */ + listBreakpoints(): BreakpointInfo[]; + /** + * Inspects local variables in the current call frame. + * Must be paused at a breakpoint. String values over 1000 chars are truncated. + * Use evaluate() for full control over reading specific values. + * + * @returns Record of variable names to values + * @throws Error if not paused or no active call frames + * + * @example + * ```ts + * const vars = await dbg.inspectLocalVariables() + * // { myVar: 'hello', count: 42 } + * ``` + */ + inspectLocalVariables(): Promise>; + /** + * Returns global lexical scope variable names. + * + * @returns Array of global variable names + * + * @example + * ```ts + * const globals = await dbg.inspectGlobalVariables() + * // ['myGlobal', 'CONFIG'] + * ``` + */ + inspectGlobalVariables(): Promise; + /** + * Evaluates a JavaScript expression and returns the result. + * When paused at a breakpoint, evaluates in the current stack frame scope, + * allowing access to local variables. Otherwise evaluates in global scope. + * Values are not truncated, use this for full control over reading specific variables. + * + * @param options - Options + * @param options.expression - JavaScript expression to evaluate + * @returns The result value + * + * @example + * ```ts + * // When paused, can access local variables: + * const result = await dbg.evaluate({ expression: 'localVar + 1' }) + * + * // Read a large string that would be truncated in inspectLocalVariables: + * const full = await dbg.evaluate({ expression: 'largeStringVar' }) + * ``` + */ + evaluate({ expression }: { + expression: string; + }): Promise; + /** + * Gets the current execution location when paused at a breakpoint. + * Includes the call stack and surrounding source code for context. + * + * @returns Location info with URL, line number, call stack, and source context + * @throws Error if debugger is not paused + * + * @example + * ```ts + * const location = await dbg.getLocation() + * console.log(location.url) // 'https://example.com/src/index.js' + * console.log(location.lineNumber) // 42 + * console.log(location.callstack) // [{ functionName: 'handleRequest', ... }] + * console.log(location.sourceContext) + * // ' 40: function handleRequest(req) { + * // 41: const data = req.body + * // > 42: processData(data) + * // 43: }' + * ``` + */ + getLocation(): Promise; + /** + * Steps over to the next line of code, not entering function calls. + * + * @throws Error if debugger is not paused + * + * @example + * ```ts + * await dbg.stepOver() + * const newLocation = await dbg.getLocation() + * ``` + */ + stepOver(): Promise; + /** + * Steps into a function call on the current line. + * + * @throws Error if debugger is not paused + * + * @example + * ```ts + * await dbg.stepInto() + * const location = await dbg.getLocation() + * // now inside the called function + * ``` + */ + stepInto(): Promise; + /** + * Steps out of the current function, returning to the caller. + * + * @throws Error if debugger is not paused + * + * @example + * ```ts + * await dbg.stepOut() + * const location = await dbg.getLocation() + * // back in the calling function + * ``` + */ + stepOut(): Promise; + /** + * Resumes code execution until the next breakpoint or completion. + * + * @throws Error if debugger is not paused + * + * @example + * ```ts + * await dbg.resume() + * // execution continues + * ``` + */ + resume(): Promise; + /** + * Returns whether the debugger is currently paused at a breakpoint. + * + * @returns true if paused, false otherwise + * + * @example + * ```ts + * if (dbg.isPaused()) { + * const vars = await dbg.inspectLocalVariables() + * } + * ``` + */ + isPaused(): boolean; + /** + * Configures the debugger to pause on exceptions. + * + * @param options - Options + * @param options.state - When to pause: 'none' (never), 'uncaught' (only uncaught), or 'all' (all exceptions) + * + * @example + * ```ts + * // Pause only on uncaught exceptions + * await dbg.setPauseOnExceptions({ state: 'uncaught' }) + * + * // Pause on all exceptions (caught and uncaught) + * await dbg.setPauseOnExceptions({ state: 'all' }) + * + * // Disable pausing on exceptions + * await dbg.setPauseOnExceptions({ state: 'none' }) + * ``` + */ + setPauseOnExceptions({ state }: { + state: 'none' | 'uncaught' | 'all'; + }): Promise; + /** + * Lists available scripts where breakpoints can be set. + * Automatically enables the debugger if not already enabled. + * + * @param options - Options + * @param options.search - Optional string to filter scripts by URL (case-insensitive) + * @returns Array of up to 20 matching scripts with scriptId and url + * + * @example + * ```ts + * // List all scripts + * const scripts = await dbg.listScripts() + * // [{ scriptId: '1', url: 'https://example.com/app.js' }, ...] + * + * // Search for specific files + * const handlers = await dbg.listScripts({ search: 'handler' }) + * // [{ scriptId: '5', url: 'https://example.com/handlers.js' }] + * ``` + */ + listScripts({ search }?: { + search?: string; + }): Promise; + setXHRBreakpoint({ url }: { + url: string; + }): Promise; + removeXHRBreakpoint({ url }: { + url: string; + }): Promise; + listXHRBreakpoints(): string[]; + /** + * Sets regex patterns for scripts to blackbox (skip when stepping). + * Blackboxed scripts are hidden from the call stack and stepped over automatically. + * Useful for ignoring framework/library code during debugging. + * + * @param options - Options + * @param options.patterns - Array of regex patterns to match script URLs + * + * @example + * ```ts + * // Skip all node_modules + * await dbg.setBlackboxPatterns({ patterns: ['node_modules'] }) + * + * // Skip React and other frameworks + * await dbg.setBlackboxPatterns({ + * patterns: [ + * 'node_modules/react', + * 'node_modules/react-dom', + * 'node_modules/next', + * 'webpack://', + * ] + * }) + * + * // Skip all third-party scripts + * await dbg.setBlackboxPatterns({ patterns: ['^https://cdn\\.'] }) + * + * // Clear all blackbox patterns + * await dbg.setBlackboxPatterns({ patterns: [] }) + * ``` + */ + setBlackboxPatterns({ patterns }: { + patterns: string[]; + }): Promise; + /** + * Adds a single regex pattern to the blackbox list. + * + * @param options - Options + * @param options.pattern - Regex pattern to match script URLs + * + * @example + * ```ts + * await dbg.addBlackboxPattern({ pattern: 'node_modules/lodash' }) + * await dbg.addBlackboxPattern({ pattern: 'node_modules/axios' }) + * ``` + */ + addBlackboxPattern({ pattern }: { + pattern: string; + }): Promise; + /** + * Removes a pattern from the blackbox list. + * + * @param options - Options + * @param options.pattern - The exact pattern string to remove + */ + removeBlackboxPattern({ pattern }: { + pattern: string; + }): Promise; + /** + * Returns the current list of blackbox patterns. + */ + listBlackboxPatterns(): string[]; + private truncateValue; + private formatPropertyValue; + private processRemoteObject; +} +``` + +## Examples + +```ts +import { page, getCDPSession, createDebugger, console } from './debugger-examples-types.js' + +// Example: List available scripts and set a breakpoint +async function listScriptsAndSetBreakpoint() { + const cdp = await getCDPSession({ page }) + const dbg = createDebugger({ cdp }) + await dbg.enable() + + const scripts = await dbg.listScripts({ search: 'app' }) + console.log(scripts) + + if (scripts.length > 0) { + const bpId = await dbg.setBreakpoint({ file: scripts[0].url, line: 100 }) + console.log('Breakpoint set:', bpId) + } +} + +// Example: Inspect state when paused at a breakpoint +async function inspectWhenPaused() { + const cdp = await getCDPSession({ page }) + const dbg = createDebugger({ cdp }) + await dbg.enable() + + if (dbg.isPaused()) { + const loc = await dbg.getLocation() + console.log('Paused at:', loc.url, 'line', loc.lineNumber) + console.log('Source:', loc.sourceContext) + + const vars = await dbg.inspectLocalVariables() + console.log('Variables:', vars) + + const result = await dbg.evaluate({ expression: 'myVar.length' }) + console.log('myVar.length =', result.value) + + await dbg.stepOver() + } +} + +// Example: Step through code +async function stepThroughCode() { + const cdp = await getCDPSession({ page }) + const dbg = createDebugger({ cdp }) + await dbg.enable() + + await dbg.setBreakpoint({ file: 'https://example.com/app.js', line: 42 }) + + if (dbg.isPaused()) { + await dbg.stepOver() + await dbg.stepInto() + await dbg.stepOut() + await dbg.resume() + } +} + +// Example: Cleanup all breakpoints +async function cleanupBreakpoints() { + const cdp = await getCDPSession({ page }) + const dbg = createDebugger({ cdp }) + + const breakpoints = dbg.listBreakpoints() + for (const bp of breakpoints) { + await dbg.deleteBreakpoint({ breakpointId: bp.id }) + } +} + +export { listScriptsAndSetBreakpoint, inspectWhenPaused, stepThroughCode, cleanupBreakpoints } + +``` \ No newline at end of file diff --git a/website/public/resources/editor-api.md b/website/public/resources/editor-api.md new file mode 100644 index 0000000..043227f --- /dev/null +++ b/website/public/resources/editor-api.md @@ -0,0 +1,364 @@ +# Editor API Reference + +The Editor class provides a Claude Code-like interface for viewing and editing web page scripts at runtime. + +## Types + +```ts +import type { CDPSession } from './cdp-session.js'; +export interface ReadResult { + content: string; + totalLines: number; + startLine: number; + endLine: number; +} +export interface SearchMatch { + url: string; + lineNumber: number; + lineContent: string; +} +export interface EditResult { + success: boolean; + stackChanged?: boolean; +} +/** + * A class for viewing and editing web page scripts via Chrome DevTools Protocol. + * Provides a Claude Code-like interface: list, read, edit, grep. + * + * Edits are in-memory only and persist until page reload. They modify the running + * V8 instance but are not saved to disk or server. + * + * @example + * ```ts + * const cdp = await getCDPSession({ page }) + * const editor = new Editor({ cdp }) + * await editor.enable() + * + * // List available scripts + * const scripts = editor.list({ search: 'app' }) + * + * // Read a script + * const { content } = await editor.read({ url: 'https://example.com/app.js' }) + * + * // Edit a script + * await editor.edit({ + * url: 'https://example.com/app.js', + * oldString: 'console.log("old")', + * newString: 'console.log("new")' + * }) + * ``` + */ +export declare class Editor { + private cdp; + private enabled; + private scripts; + private stylesheets; + private sourceCache; + constructor({ cdp }: { + cdp: CDPSession; + }); + private setupEventListeners; + /** + * Enables the editor. Must be called before other methods. + * Scripts are collected from Debugger.scriptParsed events. + * Reload the page after enabling to capture all scripts. + */ + enable(): Promise; + private getIdByUrl; + /** + * Lists available script and stylesheet URLs. Use pattern to filter by regex. + * Automatically enables the editor if not already enabled. + * + * @param options - Options + * @param options.pattern - Optional regex to filter URLs + * @returns Array of URLs + * + * @example + * ```ts + * // List all scripts and stylesheets + * const urls = await editor.list() + * + * // List only JS files + * const jsFiles = await editor.list({ pattern: /\.js/ }) + * + * // List only CSS files + * const cssFiles = await editor.list({ pattern: /\.css/ }) + * + * // Search for specific scripts + * const appScripts = await editor.list({ pattern: /app/ }) + * ``` + */ + list({ pattern }?: { + pattern?: RegExp; + }): Promise; + /** + * Reads a script or stylesheet's source code by URL. + * Returns line-numbered content like Claude Code's Read tool. + * For inline scripts, use the `inline://` URL from list() or grep(). + * + * @param options - Options + * @param options.url - Script or stylesheet URL (inline scripts have `inline://{id}` URLs) + * @param options.offset - Line number to start from (0-based, default 0) + * @param options.limit - Number of lines to return (default 2000) + * @returns Content with line numbers, total lines, and range info + * + * @example + * ```ts + * // Read by URL + * const { content, totalLines } = await editor.read({ + * url: 'https://example.com/app.js' + * }) + * + * // Read a CSS file + * const { content } = await editor.read({ url: 'https://example.com/styles.css' }) + * + * // Read lines 100-200 + * const { content } = await editor.read({ + * url: 'https://example.com/app.js', + * offset: 100, + * limit: 100 + * }) + * ``` + */ + read({ url, offset, limit }: { + url: string; + offset?: number; + limit?: number; + }): Promise; + private getSource; + /** + * Edits a script or stylesheet by replacing oldString with newString. + * Like Claude Code's Edit tool - performs exact string replacement. + * + * @param options - Options + * @param options.url - Script or stylesheet URL (inline scripts have `inline://{id}` URLs) + * @param options.oldString - Exact string to find and replace + * @param options.newString - Replacement string + * @param options.dryRun - If true, validate without applying (default false) + * @returns Result with success status + * + * @example + * ```ts + * // Replace a string in JS + * await editor.edit({ + * url: 'https://example.com/app.js', + * oldString: 'const DEBUG = false', + * newString: 'const DEBUG = true' + * }) + * + * // Edit CSS + * await editor.edit({ + * url: 'https://example.com/styles.css', + * oldString: 'color: red', + * newString: 'color: blue' + * }) + * ``` + */ + edit({ url, oldString, newString, dryRun, }: { + url: string; + oldString: string; + newString: string; + dryRun?: boolean; + }): Promise; + private setSource; + /** + * Searches for a regex across all scripts and stylesheets. + * Like Claude Code's Grep tool - returns matching lines with context. + * + * @param options - Options + * @param options.regex - Regular expression to search for in file contents + * @param options.pattern - Optional regex to filter which URLs to search + * @returns Array of matches with url, line number, and line content + * + * @example + * ```ts + * // Search all scripts and stylesheets for "color" + * const matches = await editor.grep({ regex: /color/ }) + * + * // Search only CSS files + * const matches = await editor.grep({ + * regex: /background-color/, + * pattern: /\.css/ + * }) + * + * // Regex search for console methods in JS + * const matches = await editor.grep({ + * regex: /console\.(log|error|warn)/, + * pattern: /\.js/ + * }) + * ``` + */ + grep({ regex, pattern }: { + regex: RegExp; + pattern?: RegExp; + }): Promise; + /** + * Writes entire content to a script or stylesheet, replacing all existing code. + * Use with caution - prefer edit() for targeted changes. + * + * @param options - Options + * @param options.url - Script or stylesheet URL (inline scripts have `inline://{id}` URLs) + * @param options.content - New content + * @param options.dryRun - If true, validate without applying (default false, only works for JS) + */ + write({ url, content, dryRun }: { + url: string; + content: string; + dryRun?: boolean; + }): Promise; +} +``` + +## Examples + +```ts +import { page, getCDPSession, createEditor, console } from './debugger-examples-types.js' + +// Example: List available scripts +async function listScripts() { + const cdp = await getCDPSession({ page }) + const editor = createEditor({ cdp }) + await editor.enable() + + const scripts = editor.list({ pattern: /app/ }) + console.log(scripts) +} + +// Example: Read a script with line numbers +async function readScript() { + const cdp = await getCDPSession({ page }) + const editor = createEditor({ cdp }) + await editor.enable() + + const { content, totalLines } = await editor.read({ + url: 'https://example.com/app.js', + }) + console.log('Total lines:', totalLines) + console.log(content) + + const { content: partial } = await editor.read({ + url: 'https://example.com/app.js', + offset: 100, + limit: 50, + }) + console.log(partial) +} + +// Example: Edit a script (exact string replacement) +async function editScript() { + const cdp = await getCDPSession({ page }) + const editor = createEditor({ cdp }) + await editor.enable() + + await editor.edit({ + url: 'https://example.com/app.js', + oldString: 'const DEBUG = false', + newString: 'const DEBUG = true', + }) + + const dryRunResult = await editor.edit({ + url: 'https://example.com/app.js', + oldString: 'old code', + newString: 'new code', + dryRun: true, + }) + console.log('Dry run result:', dryRunResult) +} + +// Example: Search across all scripts +async function searchScripts() { + const cdp = await getCDPSession({ page }) + const editor = createEditor({ cdp }) + await editor.enable() + + const matches = await editor.grep({ regex: /console\.log/ }) + console.log(matches) + + const todoMatches = await editor.grep({ + regex: /TODO|FIXME/i, + pattern: /app/, + }) + console.log(todoMatches) +} + +// Example: Write entire script content +async function writeScript() { + const cdp = await getCDPSession({ page }) + const editor = createEditor({ cdp }) + await editor.enable() + + const { content } = await editor.read({ url: 'https://example.com/app.js' }) + const newContent = content.replace(/console\.log/g, 'console.debug') + + await editor.write({ + url: 'https://example.com/app.js', + content: newContent, + }) +} + +// Example: Edit an inline script (scripts without URL get inline://{id} URLs) +async function editInlineScript() { + const cdp = await getCDPSession({ page }) + const editor = createEditor({ cdp }) + await editor.enable() + + const matches = await editor.grep({ regex: /myFunction/ }) + if (matches.length > 0) { + const { url } = matches[0] + console.log('Found in:', url) + + await editor.edit({ + url, + oldString: 'return false', + newString: 'return true', + }) + } +} + +// Example: List and read CSS stylesheets +async function readStylesheet() { + const cdp = await getCDPSession({ page }) + const editor = createEditor({ cdp }) + await editor.enable() + + const stylesheets = await editor.list({ pattern: /\.css/ }) + console.log('Stylesheets:', stylesheets) + + if (stylesheets.length > 0) { + const { content, totalLines } = await editor.read({ + url: stylesheets[0], + }) + console.log('Total lines:', totalLines) + console.log(content) + } +} + +// Example: Edit a CSS stylesheet +async function editStylesheet() { + const cdp = await getCDPSession({ page }) + const editor = createEditor({ cdp }) + await editor.enable() + + await editor.edit({ + url: 'https://example.com/styles.css', + oldString: 'color: red', + newString: 'color: blue', + }) +} + +// Example: Search CSS for specific properties +async function searchStyles() { + const cdp = await getCDPSession({ page }) + const editor = createEditor({ cdp }) + await editor.enable() + + const matches = await editor.grep({ + regex: /background-color/, + pattern: /\.css/, + }) + console.log(matches) +} + +export { listScripts, readScript, editScript, searchScripts, writeScript, editInlineScript, readStylesheet, editStylesheet, searchStyles } + +``` \ No newline at end of file diff --git a/website/public/resources/styles-api.md b/website/public/resources/styles-api.md new file mode 100644 index 0000000..5acf7ce --- /dev/null +++ b/website/public/resources/styles-api.md @@ -0,0 +1,117 @@ +# Styles API Reference + +The getStylesForLocator function inspects CSS styles applied to an element, similar to browser DevTools "Styles" panel. + +## Types + +```ts +import type { CDPSession } from './cdp-session.js'; +import type { Locator } from 'playwright-core'; +export interface StyleSource { + url: string; + line: number; + column: number; +} +export type StyleDeclarations = Record; +export interface StyleRule { + selector: string; + source: StyleSource | null; + origin: 'regular' | 'user-agent' | 'injected' | 'inspector'; + declarations: StyleDeclarations; + inheritedFrom: string | null; +} +export interface StylesResult { + element: string; + inlineStyle: StyleDeclarations | null; + rules: StyleRule[]; +} +export declare function getStylesForLocator({ locator, cdp, includeUserAgentStyles, }: { + locator: Locator; + cdp: CDPSession; + includeUserAgentStyles?: boolean; +}): Promise; +export declare function formatStylesAsText(styles: StylesResult): string; +``` + +## Examples + +```ts +import { page, getStylesForLocator, formatStylesAsText, console } from './debugger-examples-types.js' + +// Example: Get styles for an element and display them +async function getElementStyles() { + const loc = page.locator('.my-button') + const styles = await getStylesForLocator({ locator: loc }) + console.log(formatStylesAsText(styles)) +} + +// Example: Inspect computed styles for a specific element +async function inspectButtonStyles() { + const button = page.getByRole('button', { name: 'Submit' }) + const styles = await getStylesForLocator({ locator: button }) + + console.log('Element:', styles.element) + + if (styles.inlineStyle) { + console.log('Inline styles:', styles.inlineStyle) + } + + for (const rule of styles.rules) { + console.log(`${rule.selector}: ${JSON.stringify(rule.declarations)}`) + if (rule.source) { + console.log(` Source: ${rule.source.url}:${rule.source.line}`) + } + } +} + +// Example: Include browser default (user-agent) styles +async function getStylesWithUserAgent() { + const loc = page.locator('input[type="text"]') + const styles = await getStylesForLocator({ + locator: loc, + includeUserAgentStyles: true, + }) + console.log(formatStylesAsText(styles)) +} + +// Example: Find where a CSS property is defined +async function findPropertySource() { + const loc = page.locator('.card') + const styles = await getStylesForLocator({ locator: loc }) + + const backgroundRule = styles.rules.find((r) => 'background-color' in r.declarations) + if (backgroundRule) { + console.log('background-color defined by:', backgroundRule.selector) + if (backgroundRule.source) { + console.log(` at ${backgroundRule.source.url}:${backgroundRule.source.line}`) + } + } +} + +// Example: Check inherited styles +async function checkInheritedStyles() { + const loc = page.locator('.nested-text') + const styles = await getStylesForLocator({ locator: loc }) + + const inheritedRules = styles.rules.filter((r) => r.inheritedFrom) + for (const rule of inheritedRules) { + console.log(`Inherited from ${rule.inheritedFrom}: ${rule.selector}`) + console.log(' Properties:', rule.declarations) + } +} + +// Example: Compare styles between two elements +async function compareStyles() { + const primary = await getStylesForLocator({ locator: page.locator('.btn-primary') }) + const secondary = await getStylesForLocator({ locator: page.locator('.btn-secondary') }) + + console.log('Primary button:') + console.log(formatStylesAsText(primary)) + + console.log('Secondary button:') + console.log(formatStylesAsText(secondary)) +} + +export { getElementStyles, inspectButtonStyles, getStylesWithUserAgent, findPropertySource, checkInheritedStyles, compareStyles } + +``` \ No newline at end of file