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
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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')
|
||||
+12
-73
@@ -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',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<void>;
|
||||
/**
|
||||
* 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<string>;
|
||||
/**
|
||||
* 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<void>;
|
||||
/**
|
||||
* 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<Record<string, unknown>>;
|
||||
/**
|
||||
* Returns global lexical scope variable names.
|
||||
*
|
||||
* @returns Array of global variable names
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const globals = await dbg.inspectGlobalVariables()
|
||||
* // ['myGlobal', 'CONFIG']
|
||||
* ```
|
||||
*/
|
||||
inspectGlobalVariables(): Promise<string[]>;
|
||||
/**
|
||||
* 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<EvaluateResult>;
|
||||
/**
|
||||
* 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<LocationInfo>;
|
||||
/**
|
||||
* 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<void>;
|
||||
/**
|
||||
* 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<void>;
|
||||
/**
|
||||
* 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<void>;
|
||||
/**
|
||||
* 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<void>;
|
||||
/**
|
||||
* 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<void>;
|
||||
/**
|
||||
* 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<ScriptInfo[]>;
|
||||
setXHRBreakpoint({ url }: {
|
||||
url: string;
|
||||
}): Promise<void>;
|
||||
removeXHRBreakpoint({ url }: {
|
||||
url: string;
|
||||
}): Promise<void>;
|
||||
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<void>;
|
||||
/**
|
||||
* 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<void>;
|
||||
/**
|
||||
* Removes a pattern from the blackbox list.
|
||||
*
|
||||
* @param options - Options
|
||||
* @param options.pattern - The exact pattern string to remove
|
||||
*/
|
||||
removeBlackboxPattern({ pattern }: {
|
||||
pattern: string;
|
||||
}): Promise<void>;
|
||||
/**
|
||||
* 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 }
|
||||
|
||||
```
|
||||
@@ -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<void>;
|
||||
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<string[]>;
|
||||
/**
|
||||
* 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<ReadResult>;
|
||||
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<EditResult>;
|
||||
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<SearchMatch[]>;
|
||||
/**
|
||||
* 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<EditResult>;
|
||||
}
|
||||
```
|
||||
|
||||
## 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 }
|
||||
|
||||
```
|
||||
@@ -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<string, string>;
|
||||
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<StylesResult>;
|
||||
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 }
|
||||
|
||||
```
|
||||
Reference in New Issue
Block a user