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:
Tommy D. Rossi
2025-12-30 14:30:30 +01:00
parent aca26896b4
commit 237f24d3f8
8 changed files with 1082 additions and 77 deletions
+2
View File
@@ -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 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 ## CDP docs
here are some commands you can run to fetch does about CDP various domains (events and commands namespaces) here are some commands you can run to fetch does about CDP various domains (events and commands namespaces)
+1 -1
View File
@@ -7,7 +7,7 @@
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"repository": "https://github.com/remorses/playwriter", "repository": "https://github.com/remorses/playwriter",
"scripts": { "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", "prepublishOnly": "pnpm build",
"watch": "tsc -w", "watch": "tsc -w",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
+126
View File
@@ -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
View File
@@ -431,109 +431,48 @@ const promptContent =
fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), 'prompt.md'), 'utf-8') + 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}` `\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 packageJsonPath = require.resolve('playwriter/package.json')
const packageDir = path.dirname(packageJsonPath) const packageDir = path.dirname(packageJsonPath)
const content = fs.readFileSync(path.join(packageDir, 'dist', 'debugger-api.md'), 'utf-8')
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')
return { return {
contents: [ contents: [
{ {
uri: 'playwriter://debugger-api', uri: 'https://playwriter.dev/resources/debugger-api.md',
text: dedent` text: content,
# Debugger API Reference
## Types
\`\`\`ts
${debuggerTypes}
\`\`\`
## Examples
\`\`\`ts
${debuggerExamples}
\`\`\`
`,
mimeType: 'text/plain', 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 packageJsonPath = require.resolve('playwriter/package.json')
const packageDir = path.dirname(packageJsonPath) const packageDir = path.dirname(packageJsonPath)
const content = fs.readFileSync(path.join(packageDir, 'dist', 'editor-api.md'), 'utf-8')
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')
return { return {
contents: [ contents: [
{ {
uri: 'playwriter://editor-api', uri: 'https://playwriter.dev/resources/editor-api.md',
text: dedent` text: content,
# 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}
\`\`\`
`,
mimeType: 'text/plain', 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 packageJsonPath = require.resolve('playwriter/package.json')
const packageDir = path.dirname(packageJsonPath) const packageDir = path.dirname(packageJsonPath)
const content = fs.readFileSync(path.join(packageDir, 'dist', 'styles-api.md'), 'utf-8')
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')
return { return {
contents: [ contents: [
{ {
uri: 'playwriter://styles-api', uri: 'https://playwriter.dev/resources/styles-api.md',
text: dedent` text: content,
# 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}
\`\`\`
`,
mimeType: 'text/plain', mimeType: 'text/plain',
}, },
], ],
+3 -3
View File
@@ -95,9 +95,9 @@ you have access to some functions in addition to playwright methods:
- `page`: the page object to create the session for - `page`: the page object to create the session for
- Returns: `{ send(method, params?), on(event, callback), off(event, callback) }` - Returns: `{ send(method, params?), on(event, callback), off(event, callback) }`
- Example: `const cdp = await getCDPSession({ page }); const metrics = await cdp.send('Page.getLayoutMetrics');` - 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. - `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 `playwriter://editor-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 `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 `playwriter://styles-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 `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. - `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 - `locator`: a Playwright Locator or ElementHandle for the element to inspect
- Returns: `{ fileName, lineNumber, columnNumber, componentName }` or `null` if not found - Returns: `{ fileName, lineNumber, columnNumber, componentName }` or `null` if not found
+457
View File
@@ -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 }
```
+364
View File
@@ -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 }
```
+117
View File
@@ -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 }
```