// Minimal CDP driver: node cdp_inspect.mjs [scriptFile] // Launches headless Chromium, connects over the DevTools WebSocket (Node's // built-in WebSocket — no dependencies), evaluates one async script, prints JSON. import { spawn } from 'node:child_process' import { mkdtempSync, readFileSync } from 'node:fs' import { readFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' const [chromeBin, targetUrl, scriptFile] = process.argv.slice(2) const userDataDir = mkdtempSync(join(tmpdir(), 'cdp-')) const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) // Optional --window-size=WxH, so responsive behaviour can be checked at real sizes. const windowSizeArg = process.argv.find((arg) => arg.startsWith('--window-size=')) const windowSize = windowSizeArg ? windowSizeArg.split('=')[1] : null const chrome = spawn(chromeBin, [ '--headless=new', '--no-sandbox', '--disable-dev-shm-usage', // Headless Chrome wants `--window-size=W,H`; a `WxH` string is accepted here too. ...(windowSize ? [`--window-size=${windowSize.replace('x', ',')}`] : []), // Software WebGL2: headless has no GPU, and SwiftShader is the only way to // exercise the renderer path in CI-like runs. '--use-angle=swiftshader', '--use-gl=angle', '--enable-unsafe-swiftshader', `--user-data-dir=${userDataDir}`, '--remote-debugging-port=0', targetUrl, ], { stdio: ['ignore', 'ignore', 'pipe'] }) let stderr = '' chrome.stderr.on('data', (d) => { stderr += d }) let port = null for (let i = 0; i < 100; i++) { await sleep(200) try { port = readFileSync(join(userDataDir, 'DevToolsActivePort'), 'utf8').trim().split('\n')[0] if (port) break } catch {} } if (!port) { console.error('NO PORT', stderr.slice(-500)); process.exit(1) } let page for (let i = 0; i < 50; i++) { await sleep(200) try { const list = await (await fetch(`http://127.0.0.1:${port}/json`)).json() page = list.find((x) => x.type === 'page') if (page) break } catch {} } if (!page) { console.error('NO PAGE', stderr.slice(-500)); process.exit(1) } const ws = new WebSocket(page.webSocketDebuggerUrl) await new Promise((resolve, reject) => { ws.onopen = resolve; ws.onerror = reject }) let msgId = 0 const pending = new Map() ws.onmessage = (event) => { const message = JSON.parse(event.data) if (message.id && pending.has(message.id)) { pending.get(message.id)(message); pending.delete(message.id) } } const send = (method, params = {}) => new Promise((resolve) => { const id = ++msgId pending.set(id, resolve) ws.send(JSON.stringify({ id, method, params })) }) await send('Runtime.enable') await send('Page.enable') await sleep(3000) const expression = scriptFile ? await readFile(scriptFile, 'utf8') : `({ title: document.title, text: document.body.innerText.slice(0, 2000) })` const result = await send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true }) const shotIndex = process.argv.indexOf('--screenshot') if (shotIndex !== -1) { const shotPath = process.argv[shotIndex + 1] const shot = await send('Page.captureScreenshot', { format: 'png' }) const data = shot.result?.data if (typeof data === 'string') { const { writeFileSync } = await import('node:fs') writeFileSync(shotPath, Buffer.from(data, 'base64')) console.error(`screenshot written: ${shotPath}`) } } if (result.result?.exceptionDetails) { console.error('EVAL ERROR', JSON.stringify(result.result.exceptionDetails).slice(0, 1500)) } else { console.log(JSON.stringify(result.result?.result?.value, null, 2)) } ws.close() chrome.kill() process.exit(0)