@feedthrough/playwright
Advanced tools
@@ -1,2 +0,2 @@ | ||
| export declare const bridgeBundle = "\"use strict\";\n(() => {\n // src/transport.ts\n var MAX_QUEUE = 1e3;\n var Transport = class {\n constructor(url, onMessage, onStatus, reconnectDelay) {\n this.url = url;\n this.onMessage = onMessage;\n this.onStatus = onStatus;\n this.reconnectDelay = reconnectDelay;\n this.ws = null;\n this.queue = [];\n this.reconnectTimer = null;\n this.destroyed = false;\n }\n connect() {\n if (this.destroyed) return;\n this.ws = new WebSocket(this.url);\n this.ws.onopen = () => {\n this.onStatus(true);\n for (const msg of this.queue) this.ws.send(msg);\n this.queue = [];\n };\n this.ws.onmessage = (event) => {\n try {\n this.onMessage(JSON.parse(event.data));\n } catch {\n }\n };\n this.ws.onclose = () => {\n this.onStatus(false);\n if (!this.destroyed) {\n this.reconnectTimer = setTimeout(() => this.connect(), this.reconnectDelay);\n }\n };\n this.ws.onerror = () => {\n };\n }\n send(msg) {\n const serialized = JSON.stringify(msg);\n if (this.ws?.readyState === WebSocket.OPEN) {\n this.ws.send(serialized);\n } else {\n this.queue.push(serialized);\n if (this.queue.length > MAX_QUEUE) this.queue.shift();\n }\n }\n destroy() {\n this.destroyed = true;\n if (this.reconnectTimer !== null) clearTimeout(this.reconnectTimer);\n this.ws?.close();\n }\n };\n\n // src/interceptors/console.ts\n var STD_LEVELS = [\"log\", \"warn\", \"error\", \"info\", \"debug\"];\n var MAX_LOGS = 1e3;\n var MAX_ARG_CHARS = 1e4;\n var ConsoleInterceptor = class {\n constructor() {\n this.originals = /* @__PURE__ */ new Map();\n this.logs = [];\n this.counts = /* @__PURE__ */ new Map();\n this.timers = /* @__PURE__ */ new Map();\n }\n install() {\n const c = console;\n const record = (msg) => {\n this.logs.push(msg);\n if (this.logs.length > MAX_LOGS) this.logs.shift();\n };\n const rich = (method, level, args, extras = {}) => {\n record({ type: \"console\", ts: Date.now(), level, method, args: args.map(serialize), ...extras });\n };\n const wrap = (name, fn) => {\n const orig = c[name];\n if (typeof orig !== \"function\") return;\n this.originals.set(name, orig.bind(console));\n c[name] = fn;\n };\n for (const level of STD_LEVELS) {\n const orig = c[level].bind(console);\n this.originals.set(level, orig);\n c[level] = (...args) => {\n orig(...args);\n record({ type: \"console\", ts: Date.now(), level, args: args.map(serialize) });\n };\n }\n wrap(\"dir\", (obj, options) => {\n this.originals.get(\"dir\")(obj, options);\n rich(\"dir\", \"log\", options === void 0 ? [obj] : [obj, options]);\n });\n wrap(\"table\", (data, columns) => {\n this.originals.get(\"table\")(data, columns);\n rich(\"table\", \"log\", columns === void 0 ? [data] : [data, columns]);\n });\n wrap(\"trace\", (...args) => {\n this.originals.get(\"trace\")(...args);\n rich(\"trace\", \"log\", args, { stack: captureStack() });\n });\n wrap(\"assert\", (condition, ...args) => {\n this.originals.get(\"assert\")(condition, ...args);\n if (condition) return;\n rich(\"assert\", \"error\", args.length ? args : [\"Assertion failed\"], { stack: captureStack() });\n });\n wrap(\"count\", (label) => {\n this.originals.get(\"count\")(label);\n const key = label == null ? \"default\" : String(label);\n const n = (this.counts.get(key) ?? 0) + 1;\n this.counts.set(key, n);\n rich(\"count\", \"log\", [`${key}: ${n}`]);\n });\n wrap(\"countReset\", (label) => {\n this.originals.get(\"countReset\")(label);\n const key = label == null ? \"default\" : String(label);\n this.counts.set(key, 0);\n rich(\"countReset\", \"log\", [`${key}: 0`]);\n });\n wrap(\"time\", (label) => {\n this.originals.get(\"time\")(label);\n const key = label == null ? \"default\" : String(label);\n this.timers.set(key, performance.now());\n });\n wrap(\"timeEnd\", (label) => {\n this.originals.get(\"timeEnd\")(label);\n const key = label == null ? \"default\" : String(label);\n const start = this.timers.get(key);\n if (start === void 0) {\n rich(\"timeEnd\", \"warn\", [`Timer \"${key}\" does not exist`]);\n return;\n }\n this.timers.delete(key);\n rich(\"timeEnd\", \"log\", [`${key}: ${(performance.now() - start).toFixed(3)}ms`]);\n });\n wrap(\"timeLog\", (label, ...args) => {\n this.originals.get(\"timeLog\")(label, ...args);\n const key = label == null ? \"default\" : String(label);\n const start = this.timers.get(key);\n if (start === void 0) {\n rich(\"timeLog\", \"warn\", [`Timer \"${key}\" does not exist`, ...args]);\n return;\n }\n rich(\"timeLog\", \"log\", [`${key}: ${(performance.now() - start).toFixed(3)}ms`, ...args]);\n });\n wrap(\"group\", (...args) => {\n this.originals.get(\"group\")(...args);\n rich(\"group\", \"log\", args);\n });\n wrap(\"groupCollapsed\", (...args) => {\n this.originals.get(\"groupCollapsed\")(...args);\n rich(\"groupCollapsed\", \"log\", args);\n });\n wrap(\"groupEnd\", () => {\n this.originals.get(\"groupEnd\")();\n rich(\"groupEnd\", \"log\", []);\n });\n wrap(\"clear\", () => {\n this.originals.get(\"clear\")();\n rich(\"clear\", \"log\", []);\n });\n this.onError = (e) => {\n record({\n type: \"console\",\n ts: Date.now(),\n level: \"error\",\n method: \"uncaught\",\n args: [serialize(e.message || \"Uncaught error\")],\n stack: e.error instanceof Error ? e.error.stack : void 0\n });\n };\n this.onRejection = (e) => {\n const reason = e.reason;\n record({\n type: \"console\",\n ts: Date.now(),\n level: \"error\",\n method: \"unhandledrejection\",\n args: [serialize(reason instanceof Error ? reason.message : reason)],\n stack: reason instanceof Error ? reason.stack : void 0\n });\n };\n window.addEventListener(\"error\", this.onError);\n window.addEventListener(\"unhandledrejection\", this.onRejection);\n }\n uninstall() {\n const c = console;\n for (const [name, original] of this.originals) {\n c[name] = original;\n }\n this.originals.clear();\n if (this.onError) window.removeEventListener(\"error\", this.onError);\n if (this.onRejection) window.removeEventListener(\"unhandledrejection\", this.onRejection);\n }\n getLogs(opts = {}) {\n let result = this.logs;\n if (opts.levels && opts.levels.length > 0) {\n const wanted = new Set(opts.levels);\n result = result.filter((m) => wanted.has(m.level));\n }\n if (opts.match) {\n const needle = opts.match.toLowerCase();\n result = result.filter((m) => JSON.stringify(m.args).toLowerCase().includes(needle));\n }\n if (opts.since !== void 0) {\n result = result.filter((m) => m.ts >= opts.since);\n }\n if (opts.limit !== void 0) {\n result = result.slice(-opts.limit);\n } else if (result === this.logs) {\n result = [...this.logs];\n }\n return result;\n }\n };\n function captureStack() {\n const raw = new Error().stack ?? \"\";\n const lines = raw.split(\"\\n\");\n return lines.length > 2 ? lines.slice(2).join(\"\\n\") : raw;\n }\n function serialize(v) {\n if (v === null || v === void 0) return String(v);\n if (typeof v === \"string\") return cap(v);\n if (typeof v !== \"object\") return v;\n if (v instanceof Error) return { name: v.name, message: v.message, stack: v.stack };\n try {\n const json = JSON.stringify(v);\n return json.length > MAX_ARG_CHARS ? cap(json) : JSON.parse(json);\n } catch {\n return String(v);\n }\n }\n function cap(text) {\n return text.length > MAX_ARG_CHARS ? text.slice(0, MAX_ARG_CHARS) + \"\\u2026[truncated]\" : text;\n }\n\n // src/interceptors/network.ts\n var MAX_REQUESTS = 1e3;\n var MAX_BODY_CHARS = 1e4;\n var MAX_BODY_BYTES = 64 * 1024;\n var NetworkInterceptor = class {\n constructor() {\n this.requests = [];\n this.origFetch = null;\n this.OrigXHR = null;\n }\n // Captured into a local buffer only \u2014 never streamed. An agent pulls requests\n // on demand via get_network_requests; pushing every request/response over the\n // WebSocket would be wasted traffic since nothing subscribes to it.\n install() {\n this.interceptFetch();\n this.interceptXHR();\n }\n interceptFetch() {\n this.origFetch = window.fetch.bind(window);\n const orig = this.origFetch;\n const requests = this.requests;\n window.fetch = async (input, init) => {\n const url = input instanceof Request ? input.url : String(input);\n const method = resolveMethod(input, init);\n const requestId = uid();\n const startTs = Date.now();\n const requestHeaders = mergeRequestHeaders(input, init);\n const requestBody = serializeRequestBody(init?.body);\n const pending = {\n type: \"network\",\n ts: startTs,\n requestId,\n method,\n url,\n requestHeaders,\n requestBody\n };\n pushRequest(requests, pending);\n try {\n const res = await orig(input, init);\n Object.assign(pending, {\n ts: Date.now(),\n status: res.status,\n duration: Date.now() - startTs,\n responseHeaders: headersToObject(res.headers)\n });\n captureResponseBody(res.clone(), pending);\n return res;\n } catch (e) {\n Object.assign(pending, {\n ts: Date.now(),\n duration: Date.now() - startTs,\n error: e instanceof Error ? e.message : String(e)\n });\n throw e;\n }\n };\n }\n interceptXHR() {\n this.OrigXHR = window.XMLHttpRequest;\n const OrigXHR = this.OrigXHR;\n const requests = this.requests;\n function FeedthroughXHR() {\n const xhr = new OrigXHR();\n let method = \"GET\";\n let url = \"\";\n const requestHeaders = {};\n const origOpen = xhr.open.bind(xhr);\n xhr.open = (m, u, async, user, password) => {\n method = m.toUpperCase();\n url = String(u);\n origOpen(m, String(u), async ?? true, user, password);\n };\n const origSetHeader = xhr.setRequestHeader.bind(xhr);\n xhr.setRequestHeader = (name, value) => {\n requestHeaders[name] = String(value);\n origSetHeader(name, value);\n };\n const origSend = xhr.send.bind(xhr);\n xhr.send = (body) => {\n const requestId = uid();\n const startTs = Date.now();\n const requestBody = serializeRequestBody(body ?? void 0);\n const pending = {\n type: \"network\",\n ts: startTs,\n requestId,\n method,\n url,\n requestHeaders: Object.keys(requestHeaders).length ? { ...requestHeaders } : void 0,\n requestBody\n };\n pushRequest(requests, pending);\n xhr.addEventListener(\"loadend\", () => {\n let responseBody;\n try {\n if (xhr.responseType === \"\" || xhr.responseType === \"text\") {\n responseBody = capText(xhr.responseText);\n } else {\n responseBody = `[XHR responseType=${xhr.responseType}]`;\n }\n } catch {\n }\n Object.assign(pending, {\n ts: Date.now(),\n status: xhr.status,\n duration: Date.now() - startTs,\n responseBody,\n responseHeaders: parseRawHeaders(xhr.getAllResponseHeaders())\n });\n });\n origSend(body);\n };\n return xhr;\n }\n FeedthroughXHR.prototype = OrigXHR.prototype;\n window.XMLHttpRequest = FeedthroughXHR;\n }\n uninstall() {\n if (this.origFetch) window.fetch = this.origFetch;\n if (this.OrigXHR) window.XMLHttpRequest = this.OrigXHR;\n }\n getRequests(filter, since) {\n let all = [...this.requests];\n if (since !== void 0) all = all.filter((r) => r.ts >= since);\n if (!filter) return all;\n const lower = filter.toLowerCase();\n return all.filter((r) => r.url.toLowerCase().includes(lower) || r.method.toLowerCase() === lower);\n }\n };\n function resolveMethod(input, init) {\n return (init?.method ?? (input instanceof Request ? input.method : null) ?? \"GET\").toUpperCase();\n }\n function pushRequest(requests, msg) {\n requests.push(msg);\n if (requests.length > MAX_REQUESTS) requests.shift();\n }\n function mergeRequestHeaders(input, init) {\n const source = init?.headers ?? (input instanceof Request ? input.headers : void 0);\n if (!source) return void 0;\n const out = {};\n if (source instanceof Headers) {\n source.forEach((v, k) => {\n out[k] = v;\n });\n } else if (Array.isArray(source)) {\n for (const [k, v] of source) out[k] = String(v);\n } else {\n for (const [k, v] of Object.entries(source)) out[k] = String(v);\n }\n return Object.keys(out).length ? out : void 0;\n }\n function headersToObject(headers) {\n const out = {};\n headers.forEach((v, k) => {\n out[k] = v;\n });\n return out;\n }\n function parseRawHeaders(raw) {\n if (!raw) return void 0;\n const out = {};\n for (const line of raw.split(\"\\r\\n\")) {\n const idx = line.indexOf(\":\");\n if (idx <= 0) continue;\n const k = line.slice(0, idx).trim();\n const v = line.slice(idx + 1).trim();\n if (k) out[k] = v;\n }\n return Object.keys(out).length ? out : void 0;\n }\n function serializeRequestBody(body) {\n if (body == null) return void 0;\n if (typeof body === \"string\") return capText(body);\n if (body instanceof URLSearchParams) return capText(body.toString());\n if (body instanceof FormData) {\n const obj = {};\n body.forEach((v, k) => {\n obj[k] = v instanceof File ? `[File: ${v.name}, ${v.size} bytes]` : v;\n });\n try {\n return capText(JSON.stringify(obj));\n } catch {\n return \"[FormData]\";\n }\n }\n if (body instanceof Blob) return `[Blob: ${body.size} bytes, ${body.type || \"unknown\"}]`;\n if (body instanceof ArrayBuffer) return `[ArrayBuffer: ${body.byteLength} bytes]`;\n if (ArrayBuffer.isView(body)) return `[${body.constructor.name}: ${body.byteLength} bytes]`;\n if (body instanceof ReadableStream) return \"[ReadableStream]\";\n try {\n return capText(JSON.stringify(body));\n } catch {\n return String(body);\n }\n }\n function capText(text) {\n if (text.length <= MAX_BODY_CHARS) return text;\n return text.slice(0, MAX_BODY_CHARS) + `\\u2026[truncated, ${text.length - MAX_BODY_CHARS} more chars]`;\n }\n function captureResponseBody(res, pending) {\n const ct = (res.headers.get(\"content-type\") ?? \"\").toLowerCase();\n if (isSkippable(ct)) {\n const len = res.headers.get(\"content-length\");\n pending.responseBody = `[${describe(ct)}${len ? `, ${len} bytes` : \"\"}, ${ct || \"no content-type\"}]`;\n return;\n }\n readBounded(res).then((body) => {\n pending.responseBody = body;\n }).catch(() => {\n });\n }\n async function readBounded(res) {\n if (!res.body) return capText(await res.text());\n const reader = res.body.getReader();\n const decoder = new TextDecoder();\n let text = \"\";\n let bytes = 0;\n let truncated = false;\n try {\n for (; ; ) {\n const { done, value } = await reader.read();\n if (done) break;\n bytes += value.byteLength;\n text += decoder.decode(value, { stream: true });\n if (bytes >= MAX_BODY_BYTES || text.length >= MAX_BODY_CHARS) {\n truncated = true;\n break;\n }\n }\n } finally {\n reader.cancel().catch(() => {\n });\n }\n if (!truncated) return capText(text);\n return text.slice(0, MAX_BODY_CHARS) + \"\\u2026[truncated]\";\n }\n function isSkippable(ct) {\n return /^(image|video|audio|font)\\//.test(ct) || ct.startsWith(\"application/octet-stream\") || ct.startsWith(\"application/pdf\") || ct.startsWith(\"application/zip\") || ct.startsWith(\"text/event-stream\") || // SSE \u2014 never-ending stream\n ct.startsWith(\"application/x-ndjson\");\n }\n function describe(ct) {\n if (ct.startsWith(\"text/event-stream\")) return \"event stream\";\n if (ct.startsWith(\"application/x-ndjson\")) return \"ndjson stream\";\n return \"binary\";\n }\n var counter = 0;\n function uid() {\n return `${Date.now()}-${++counter}`;\n }\n\n // src/commands.ts\n var CommandHandler = class {\n constructor(transport, console2, network) {\n this.transport = transport;\n this.console = console2;\n this.network = network;\n }\n handle(msg) {\n if (!isCommand(msg)) return;\n let value;\n let error;\n try {\n value = this.dispatch(msg);\n } catch (e) {\n error = e instanceof Error ? e.message : String(e);\n }\n const result = {\n type: \"result\",\n ts: Date.now(),\n commandId: msg.id,\n ok: error === void 0,\n value,\n error\n };\n this.transport.send(result);\n }\n dispatch(cmd) {\n switch (cmd.action) {\n case \"click\":\n return clickEl(cmd.selector);\n case \"fill\":\n return fillEl(cmd.selector, cmd.value);\n case \"hover\":\n return hoverEl(cmd.selector);\n case \"inspect\":\n return inspectEl(cmd.selector, cmd.properties);\n case \"query_dom\":\n return queryDom(cmd.selector);\n case \"get_console_logs\":\n return this.console.getLogs({ limit: cmd.limit, levels: cmd.levels, match: cmd.match, since: cmd.since });\n case \"get_network_requests\":\n return this.network.getRequests(cmd.filter, cmd.since);\n case \"press_key\":\n return pressKey(cmd.selector, cmd.key);\n case \"get_html\":\n return getHtml(cmd.selector);\n case \"get_page_info\":\n return getPageInfo();\n case \"set_style\":\n return setStyle(cmd.selector, cmd.properties);\n case \"set_attribute\":\n return setAttribute(cmd.selector, cmd.name, cmd.value);\n case \"set_text\":\n return setText(cmd.selector, cmd.text);\n case \"reset_overrides\":\n return resetOverrides();\n }\n }\n };\n function getEl(selector) {\n const el = document.querySelector(selector);\n if (!el) throw new Error(`no element matches \"${selector}\"`);\n return el;\n }\n function clickEl(selector) {\n const el = getEl(selector);\n el.click();\n return { tag: el.tagName.toLowerCase(), id: el.id || null };\n }\n function fillEl(selector, value) {\n const el = getEl(selector);\n el.focus();\n const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : el instanceof HTMLSelectElement ? HTMLSelectElement.prototype : HTMLInputElement.prototype;\n const nativeSetter = Object.getOwnPropertyDescriptor(proto, \"value\")?.set;\n if (nativeSetter) {\n nativeSetter.call(el, value);\n } else {\n el.value = value;\n }\n el.dispatchEvent(new Event(\"input\", { bubbles: true }));\n el.dispatchEvent(new Event(\"change\", { bubbles: true }));\n return { tag: el.tagName.toLowerCase(), value };\n }\n function hoverEl(selector) {\n const el = getEl(selector);\n el.dispatchEvent(new MouseEvent(\"mouseover\", { bubbles: true }));\n el.dispatchEvent(new MouseEvent(\"mouseenter\", { bubbles: true }));\n return { tag: el.tagName.toLowerCase() };\n }\n var DEFAULT_STYLE_PROPS = [\n \"display\",\n \"position\",\n \"visibility\",\n \"opacity\",\n \"z-index\",\n \"box-sizing\",\n \"top\",\n \"right\",\n \"bottom\",\n \"left\",\n \"width\",\n \"height\",\n \"margin\",\n \"padding\",\n \"border\",\n \"color\",\n \"background-color\",\n \"font-family\",\n \"font-size\",\n \"font-weight\",\n \"line-height\",\n \"text-align\",\n \"overflow\",\n \"cursor\",\n \"pointer-events\",\n \"flex\",\n \"flex-direction\",\n \"justify-content\",\n \"align-items\",\n \"gap\",\n \"grid-template-columns\",\n \"transform\"\n ];\n function inspectEl(selector, properties) {\n const el = getEl(selector);\n const rect = el.getBoundingClientRect();\n const cs = window.getComputedStyle(el);\n const result = {\n tag: el.tagName.toLowerCase(),\n id: el.id || null,\n classes: Array.from(el.classList),\n attributes: Object.fromEntries(Array.from(el.attributes).map((a) => [a.name, a.value])),\n textContent: el.textContent?.trim().slice(0, 200),\n rect: {\n top: rect.top,\n right: rect.right,\n bottom: rect.bottom,\n left: rect.left,\n width: rect.width,\n height: rect.height,\n x: rect.x,\n y: rect.y\n },\n scroll: { x: window.scrollX, y: window.scrollY },\n inViewport: rect.bottom > 0 && rect.right > 0 && rect.top < window.innerHeight && rect.left < window.innerWidth,\n styles: pickStyles(cs, DEFAULT_STYLE_PROPS)\n };\n const state = elementState(el);\n if (state) result.state = state;\n if (properties && properties.length > 0) {\n const requested = {};\n for (const p of properties) requested[p] = cs.getPropertyValue(p);\n result.requested = requested;\n }\n return result;\n }\n function pickStyles(cs, props) {\n const out = {};\n for (const p of props) {\n const v = cs.getPropertyValue(p);\n if (v) out[p] = v;\n }\n return out;\n }\n function elementState(el) {\n const s = {};\n if (el instanceof HTMLInputElement) {\n s.value = capValue(el.value);\n s.type = el.type;\n s.checked = el.checked;\n s.disabled = el.disabled;\n s.readOnly = el.readOnly;\n s.required = el.required;\n if (el.placeholder) s.placeholder = el.placeholder;\n if (el.validationMessage) s.validationMessage = el.validationMessage;\n } else if (el instanceof HTMLTextAreaElement) {\n s.value = capValue(el.value);\n s.disabled = el.disabled;\n s.readOnly = el.readOnly;\n } else if (el instanceof HTMLSelectElement) {\n s.value = el.value;\n s.selectedIndex = el.selectedIndex;\n s.disabled = el.disabled;\n } else if (el instanceof HTMLButtonElement) {\n s.type = el.type;\n s.disabled = el.disabled;\n } else if (el instanceof HTMLAnchorElement) {\n s.href = el.href;\n }\n if (el instanceof HTMLElement && Object.keys(el.dataset).length > 0) {\n s.dataset = { ...el.dataset };\n }\n return Object.keys(s).length > 0 ? s : void 0;\n }\n function capValue(v) {\n return v.length > 1e3 ? v.slice(0, 1e3) + \"\\u2026[truncated]\" : v;\n }\n function queryDom(selector) {\n return Array.from(document.querySelectorAll(selector)).map((el) => ({\n tag: el.tagName.toLowerCase(),\n id: el.id || null,\n classes: Array.from(el.classList),\n textContent: el.textContent?.trim().slice(0, 100)\n }));\n }\n var NAMED_KEYS = {\n Enter: { code: \"Enter\", keyCode: 13 },\n Tab: { code: \"Tab\", keyCode: 9 },\n Escape: { code: \"Escape\", keyCode: 27 },\n Backspace: { code: \"Backspace\", keyCode: 8 },\n Delete: { code: \"Delete\", keyCode: 46 },\n ArrowUp: { code: \"ArrowUp\", keyCode: 38 },\n ArrowDown: { code: \"ArrowDown\", keyCode: 40 },\n ArrowLeft: { code: \"ArrowLeft\", keyCode: 37 },\n ArrowRight: { code: \"ArrowRight\", keyCode: 39 },\n \" \": { code: \"Space\", keyCode: 32 }\n };\n function pressKey(selector, key) {\n const el = getEl(selector);\n el.focus?.();\n const named = NAMED_KEYS[key];\n const keyCode = named ? named.keyCode : key.length === 1 ? key.toUpperCase().charCodeAt(0) : 0;\n const code = named ? named.code : key.length === 1 ? `Key${key.toUpperCase()}` : key;\n const init = {\n key,\n code,\n keyCode,\n which: keyCode,\n bubbles: true,\n cancelable: true\n };\n el.dispatchEvent(new KeyboardEvent(\"keydown\", init));\n if (key.length === 1) el.dispatchEvent(new KeyboardEvent(\"keypress\", init));\n el.dispatchEvent(new KeyboardEvent(\"keyup\", init));\n return { tag: el.tagName.toLowerCase(), key };\n }\n var MAX_HTML_CHARS = 5e4;\n function getHtml(selector) {\n const el = getEl(selector);\n const html = el.outerHTML;\n const truncated = html.length > MAX_HTML_CHARS;\n return {\n tag: el.tagName.toLowerCase(),\n html: truncated ? html.slice(0, MAX_HTML_CHARS) + \"\\u2026[truncated]\" : html,\n truncated\n };\n }\n function getPageInfo() {\n return {\n url: window.location.href,\n title: document.title,\n readyState: document.readyState,\n viewport: { width: window.innerWidth, height: window.innerHeight },\n scroll: { x: window.scrollX, y: window.scrollY },\n userAgent: navigator.userAgent\n };\n }\n var PREVIEW_NOTE = \"Live preview only \\u2014 applied to the running DOM, not saved to source, and reset on reload/HMR. Once the user is happy, edit the actual source to make it permanent.\";\n var CLOBBER_WARNING = \"This may be reverted on the next framework render (React/Vue/etc. re-render this element from component state). If it snaps back, change it in the source instead of here.\";\n var FRAMEWORK_OWNED_ATTRS = /* @__PURE__ */ new Set([\"class\", \"style\", \"value\", \"checked\", \"disabled\", \"selected\"]);\n var overrides = [];\n function setStyle(selector, properties) {\n const el = getEl(selector);\n if (!el.style) throw new Error(`element \"${selector}\" has no style (not an HTMLElement)`);\n const applied = {};\n for (const [prop, value] of Object.entries(properties)) {\n const prevValue = el.style.getPropertyValue(prop);\n const prevPriority = el.style.getPropertyPriority(prop);\n overrides.push(() => {\n if (prevValue) el.style.setProperty(prop, prevValue, prevPriority);\n else el.style.removeProperty(prop);\n });\n el.style.setProperty(prop, value);\n applied[prop] = el.style.getPropertyValue(prop);\n }\n return { tag: el.tagName.toLowerCase(), applied, note: PREVIEW_NOTE };\n }\n function setAttribute(selector, name, value) {\n const el = getEl(selector);\n const had = el.hasAttribute(name);\n const prev = had ? el.getAttribute(name) : null;\n overrides.push(() => {\n if (had) el.setAttribute(name, prev ?? \"\");\n else el.removeAttribute(name);\n });\n if (value === null) el.removeAttribute(name);\n else el.setAttribute(name, value);\n const result = {\n tag: el.tagName.toLowerCase(),\n name,\n value: value === null ? null : el.getAttribute(name),\n removed: value === null,\n note: PREVIEW_NOTE\n };\n if (FRAMEWORK_OWNED_ATTRS.has(name.toLowerCase())) result.frameworkWarning = CLOBBER_WARNING;\n return result;\n }\n function setText(selector, text) {\n const el = getEl(selector);\n const prev = el.textContent;\n overrides.push(() => {\n el.textContent = prev;\n });\n el.textContent = text;\n return {\n tag: el.tagName.toLowerCase(),\n text,\n note: PREVIEW_NOTE,\n frameworkWarning: CLOBBER_WARNING\n };\n }\n function resetOverrides() {\n const count = overrides.length;\n while (overrides.length) overrides.pop()();\n return { reverted: count, note: \"All bridge-applied DOM changes since connect have been undone (best effort \\u2014 elements re-created by the framework since may not roll back).\" };\n }\n function isCommand(v) {\n return typeof v === \"object\" && v !== null && v.type === \"command\" && typeof v.id === \"string\";\n }\n\n // src/bridge.ts\n var DEFAULT_SERVER_URL = \"ws://localhost:8765\";\n var FeedthroughBridge = class {\n constructor(options = {}) {\n const url = options.serverUrl ?? DEFAULT_SERVER_URL;\n const reconnectDelay = options.reconnectDelay ?? 2e3;\n this.consoleInterceptor = new ConsoleInterceptor();\n this.networkInterceptor = new NetworkInterceptor();\n this.transport = new Transport(\n url,\n (msg) => this.commandHandler.handle(msg),\n (connected) => {\n if (connected) this.transport.send({ type: \"hello\", url: window.location.href });\n },\n reconnectDelay\n );\n this.commandHandler = new CommandHandler(this.transport, this.consoleInterceptor, this.networkInterceptor);\n }\n connect() {\n this.consoleInterceptor.install();\n this.networkInterceptor.install();\n this.transport.connect();\n }\n destroy() {\n this.transport.destroy();\n this.consoleInterceptor.uninstall();\n this.networkInterceptor.uninstall();\n }\n };\n\n // src/browser.ts\n window.__feedthrough = new FeedthroughBridge(window.__feedthroughOptions ?? {});\n window.__feedthrough.connect();\n})();\n"; | ||
| export declare const bridgeBundle = "\"use strict\";\n(() => {\n // src/commands.ts\n var CommandHandler = class {\n constructor(transport, console2, network) {\n this.transport = transport;\n this.console = console2;\n this.network = network;\n }\n handle(msg) {\n if (!isCommand(msg)) return;\n let value;\n let error;\n try {\n value = this.dispatch(msg);\n } catch (e) {\n error = e instanceof Error ? e.message : String(e);\n }\n const result = {\n type: \"result\",\n ts: Date.now(),\n commandId: msg.id,\n ok: error === void 0,\n value,\n error\n };\n this.transport.send(result);\n }\n dispatch(cmd) {\n switch (cmd.action) {\n case \"click\":\n return clickEl(cmd.selector);\n case \"fill\":\n return fillEl(cmd.selector, cmd.value);\n case \"hover\":\n return hoverEl(cmd.selector);\n case \"inspect\":\n return inspectEl(cmd.selector, cmd.properties);\n case \"query_dom\":\n return queryDom(cmd.selector);\n case \"get_console_logs\":\n return this.console.getLogs({\n limit: cmd.limit,\n levels: cmd.levels,\n match: cmd.match,\n since: cmd.since\n });\n case \"get_network_requests\":\n return this.network.getRequests(cmd.filter, cmd.since);\n case \"press_key\":\n return pressKey(cmd.selector, cmd.key);\n case \"get_html\":\n return getHtml(cmd.selector);\n case \"get_page_info\":\n return getPageInfo();\n case \"set_style\":\n return setStyle(cmd.selector, cmd.properties);\n case \"set_attribute\":\n return setAttribute(cmd.selector, cmd.name, cmd.value);\n case \"set_text\":\n return setText(cmd.selector, cmd.text);\n case \"reset_overrides\":\n return resetOverrides();\n }\n }\n };\n function getEl(selector) {\n const el = document.querySelector(selector);\n if (!el) throw new Error(`no element matches \"${selector}\"`);\n return el;\n }\n function clickEl(selector) {\n const el = getEl(selector);\n el.click();\n return { tag: el.tagName.toLowerCase(), id: el.id || null };\n }\n function fillEl(selector, value) {\n const el = getEl(selector);\n el.focus();\n const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : el instanceof HTMLSelectElement ? HTMLSelectElement.prototype : HTMLInputElement.prototype;\n const nativeSetter = Object.getOwnPropertyDescriptor(proto, \"value\")?.set;\n if (nativeSetter) {\n nativeSetter.call(el, value);\n } else {\n el.value = value;\n }\n el.dispatchEvent(new Event(\"input\", { bubbles: true }));\n el.dispatchEvent(new Event(\"change\", { bubbles: true }));\n return { tag: el.tagName.toLowerCase(), value };\n }\n function hoverEl(selector) {\n const el = getEl(selector);\n el.dispatchEvent(new MouseEvent(\"mouseover\", { bubbles: true }));\n el.dispatchEvent(new MouseEvent(\"mouseenter\", { bubbles: true }));\n return { tag: el.tagName.toLowerCase() };\n }\n var DEFAULT_STYLE_PROPS = [\n \"display\",\n \"position\",\n \"visibility\",\n \"opacity\",\n \"z-index\",\n \"box-sizing\",\n \"top\",\n \"right\",\n \"bottom\",\n \"left\",\n \"width\",\n \"height\",\n \"margin\",\n \"padding\",\n \"border\",\n \"color\",\n \"background-color\",\n \"font-family\",\n \"font-size\",\n \"font-weight\",\n \"line-height\",\n \"text-align\",\n \"overflow\",\n \"cursor\",\n \"pointer-events\",\n \"flex\",\n \"flex-direction\",\n \"justify-content\",\n \"align-items\",\n \"gap\",\n \"grid-template-columns\",\n \"transform\"\n ];\n function inspectEl(selector, properties) {\n const el = getEl(selector);\n const rect = el.getBoundingClientRect();\n const cs = window.getComputedStyle(el);\n const result = {\n tag: el.tagName.toLowerCase(),\n id: el.id || null,\n classes: Array.from(el.classList),\n attributes: Object.fromEntries(Array.from(el.attributes).map((a) => [a.name, a.value])),\n textContent: el.textContent?.trim().slice(0, 200),\n rect: {\n top: rect.top,\n right: rect.right,\n bottom: rect.bottom,\n left: rect.left,\n width: rect.width,\n height: rect.height,\n x: rect.x,\n y: rect.y\n },\n scroll: { x: window.scrollX, y: window.scrollY },\n inViewport: rect.bottom > 0 && rect.right > 0 && rect.top < window.innerHeight && rect.left < window.innerWidth,\n styles: pickStyles(cs, DEFAULT_STYLE_PROPS)\n };\n result.path = ancestorChain(el);\n const overflow = overflowInfo(el);\n if (overflow) result.overflow = overflow;\n const clipped = clippedByAncestor(el, rect, cs);\n if (clipped) result.clipped = clipped;\n const vis = effectiveVisibility(el, rect, cs);\n result.visible = vis.visible;\n if (!vis.visible) result.hiddenReason = vis.reason;\n const occ = occlusionInfo(el, rect);\n if (occ) {\n result.hittable = occ.hittable;\n if (occ.occludedBy) result.occludedBy = occ.occludedBy;\n }\n const a11y = accessibilityInfo(el);\n if (a11y) result.a11y = a11y;\n const pseudo = pseudoContent(el);\n if (pseudo) result.pseudo = pseudo;\n const state = elementState(el);\n if (state) result.state = state;\n if (properties && properties.length > 0) {\n const requested = {};\n for (const p of properties) requested[p] = cs.getPropertyValue(p);\n result.requested = requested;\n }\n return result;\n }\n function overflowInfo(el) {\n const { scrollWidth, clientWidth, scrollHeight, clientHeight } = el;\n const x = scrollWidth > clientWidth;\n const y = scrollHeight > clientHeight;\n if (!x && !y) return void 0;\n return { x, y, scrollWidth, clientWidth, scrollHeight, clientHeight };\n }\n function refString(el) {\n const tag = el.tagName.toLowerCase();\n const id = el.id ? `#${el.id}` : \"\";\n const all = Array.from(el.classList);\n const classes = all.slice(0, 3).map((c) => `.${c}`).join(\"\");\n const more = all.length > 3 ? \"\\u2026\" : \"\";\n return tag + id + classes + more;\n }\n function chainRef(el) {\n const tag = el.tagName.toLowerCase();\n if (el.id) return `${tag}#${el.id}`;\n const cls = el.classList[0];\n return cls ? `${tag}.${cls}` : tag;\n }\n function ancestorChain(el) {\n const maxDepth = 6;\n const parts = [];\n let node = el;\n while (node) {\n parts.unshift(chainRef(node));\n node = node.parentElement;\n if (parts.length >= maxDepth && node) {\n parts.unshift(\"\\u2026\");\n break;\n }\n }\n return parts.join(\" > \");\n }\n var MAX_ANCESTOR_WALK = 50;\n function establishesFixedContainingBlock(cs) {\n if (cs.transform !== \"none\") return true;\n if (cs.perspective !== \"none\") return true;\n if (cs.filter !== \"none\") return true;\n const backdrop = cs.getPropertyValue(\"backdrop-filter\");\n if (backdrop && backdrop !== \"none\") return true;\n if (/transform|perspective|filter/.test(cs.willChange)) return true;\n if (/paint|layout|strict|content/.test(cs.contain)) return true;\n return false;\n }\n function clippedByAncestor(el, rect, cs) {\n const tol = 1;\n let clipping = cs.position !== \"fixed\";\n let node = el.parentElement;\n for (let depth = 0; node && depth < MAX_ANCESTOR_WALK; node = node.parentElement, depth++) {\n const acs = window.getComputedStyle(node);\n if (!clipping) {\n if (establishesFixedContainingBlock(acs)) clipping = true;\n else continue;\n }\n const clipsX = acs.overflowX !== \"visible\";\n const clipsY = acs.overflowY !== \"visible\";\n if (!clipsX && !clipsY) continue;\n const ar = node.getBoundingClientRect();\n const edges = [];\n if (clipsY && rect.top < ar.top - tol) edges.push(\"top\");\n if (clipsX && rect.right > ar.right + tol) edges.push(\"right\");\n if (clipsY && rect.bottom > ar.bottom + tol) edges.push(\"bottom\");\n if (clipsX && rect.left < ar.left - tol) edges.push(\"left\");\n if (edges.length > 0) return { by: refString(node), edges };\n }\n return void 0;\n }\n function pseudoContent(el) {\n const out = {};\n for (const pseudo of [\"::before\", \"::after\"]) {\n const content = window.getComputedStyle(el, pseudo).content;\n if (content && content !== \"none\" && content !== \"normal\") {\n out[pseudo] = content.length > 200 ? `${content.slice(0, 200)}\\u2026` : content;\n }\n }\n return Object.keys(out).length > 0 ? out : void 0;\n }\n function occlusionInfo(el, rect) {\n if (rect.width === 0 || rect.height === 0) return void 0;\n const left = Math.max(rect.left, 0);\n const top0 = Math.max(rect.top, 0);\n const right = Math.min(rect.right, window.innerWidth - 1);\n const bottom = Math.min(rect.bottom, window.innerHeight - 1);\n if (right < left || bottom < top0) return void 0;\n const cx = (left + right) / 2;\n const cy = (top0 + bottom) / 2;\n const top = document.elementFromPoint(cx, cy);\n if (!top) return void 0;\n if (top === el || el.contains(top)) return { hittable: true };\n return {\n hittable: false,\n occludedBy: {\n tag: top.tagName.toLowerCase(),\n id: top.id || null,\n classes: Array.from(top.classList).slice(0, 3)\n }\n };\n }\n function effectiveVisibility(el, rect, cs) {\n if (cs.display === \"none\") return { visible: false, reason: \"display:none\" };\n let node = el.parentElement;\n for (let depth = 0; node && depth < MAX_ANCESTOR_WALK; node = node.parentElement, depth++) {\n const acs = window.getComputedStyle(node);\n if (acs.display === \"none\")\n return { visible: false, reason: `ancestor ${refString(node)} display:none` };\n if (parseFloat(acs.opacity) === 0)\n return { visible: false, reason: `ancestor ${refString(node)} opacity:0` };\n if (node.getAttribute(\"aria-hidden\") === \"true\")\n return { visible: false, reason: `ancestor ${refString(node)} aria-hidden` };\n }\n if (cs.visibility === \"hidden\" || cs.visibility === \"collapse\")\n return { visible: false, reason: `visibility:${cs.visibility}` };\n if (parseFloat(cs.opacity) === 0) return { visible: false, reason: \"opacity:0\" };\n if (el.getAttribute(\"aria-hidden\") === \"true\") return { visible: false, reason: \"aria-hidden\" };\n if (rect.width === 0 || rect.height === 0) return { visible: false, reason: \"zero-size\" };\n return { visible: true };\n }\n function pickStyles(cs, props) {\n const out = {};\n for (const p of props) {\n const v = cs.getPropertyValue(p);\n if (v) out[p] = v;\n }\n return out;\n }\n function hasNamingAttribute(el) {\n return !!el.getAttribute(\"aria-label\")?.trim() || !!el.getAttribute(\"aria-labelledby\")?.trim() || !!el.getAttribute(\"title\")?.trim();\n }\n function implicitRole(el) {\n const tag = el.tagName.toLowerCase();\n switch (tag) {\n case \"a\":\n case \"area\":\n return el.hasAttribute(\"href\") ? \"link\" : null;\n case \"button\":\n return \"button\";\n case \"input\": {\n const t = (el.getAttribute(\"type\") || \"text\").toLowerCase();\n if (el.hasAttribute(\"list\") && [\"text\", \"search\", \"email\", \"tel\", \"url\"].includes(t)) {\n return \"combobox\";\n }\n const map = {\n checkbox: \"checkbox\",\n radio: \"radio\",\n range: \"slider\",\n number: \"spinbutton\",\n button: \"button\",\n submit: \"button\",\n reset: \"button\",\n image: \"button\",\n search: \"searchbox\",\n email: \"textbox\",\n tel: \"textbox\",\n url: \"textbox\",\n text: \"textbox\"\n };\n return map[t] ?? null;\n }\n case \"select\": {\n const size = Number(el.getAttribute(\"size\") || \"0\");\n return el.hasAttribute(\"multiple\") || size > 1 ? \"listbox\" : \"combobox\";\n }\n case \"textarea\":\n return \"textbox\";\n case \"img\":\n return el.getAttribute(\"alt\") === \"\" ? \"presentation\" : \"img\";\n case \"nav\":\n return \"navigation\";\n case \"main\":\n return \"main\";\n case \"header\":\n return el.closest(\"article, aside, main, nav, section\") ? null : \"banner\";\n case \"footer\":\n return el.closest(\"article, aside, main, nav, section\") ? null : \"contentinfo\";\n case \"aside\": {\n const scoped = el.parentElement?.closest(\"article, aside, main, nav, section\");\n return !scoped || hasNamingAttribute(el) ? \"complementary\" : null;\n }\n case \"section\":\n return hasNamingAttribute(el) ? \"region\" : null;\n case \"article\":\n return \"article\";\n case \"dialog\":\n return \"dialog\";\n case \"form\":\n return hasNamingAttribute(el) ? \"form\" : null;\n case \"table\":\n return \"table\";\n case \"ul\":\n case \"ol\":\n return \"list\";\n case \"li\":\n return \"listitem\";\n case \"h1\":\n case \"h2\":\n case \"h3\":\n case \"h4\":\n case \"h5\":\n case \"h6\":\n return \"heading\";\n default:\n return null;\n }\n }\n var NAME_FROM_CONTENT = /* @__PURE__ */ new Set([\n \"button\",\n \"checkbox\",\n \"radio\",\n \"switch\",\n \"link\",\n \"menuitem\",\n \"menuitemcheckbox\",\n \"menuitemradio\",\n \"option\",\n \"tab\",\n \"treeitem\",\n \"heading\",\n \"cell\",\n \"gridcell\",\n \"columnheader\",\n \"rowheader\",\n \"row\",\n \"tooltip\"\n ]);\n function accessibleName(el, role) {\n const labelledby = el.getAttribute(\"aria-labelledby\");\n if (labelledby) {\n const txt = labelledby.split(/\\s+/).map((id) => document.getElementById(id)?.textContent?.trim()).filter(Boolean).join(\" \");\n if (txt) return txt.slice(0, 200);\n }\n const label = el.getAttribute(\"aria-label\")?.trim();\n if (label) return label.slice(0, 200);\n if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {\n if (el.id) {\n const forLabel = document.querySelector(`label[for=\"${CSS.escape(el.id)}\"]`);\n const txt = forLabel?.textContent?.trim();\n if (txt) return txt.slice(0, 200);\n }\n const wrapping = el.closest(\"label\")?.textContent?.trim();\n if (wrapping) return wrapping.slice(0, 200);\n if (el instanceof HTMLInputElement && el.placeholder) return el.placeholder.slice(0, 200);\n }\n if (el instanceof HTMLImageElement && el.alt) return el.alt.slice(0, 200);\n const title = el.getAttribute(\"title\")?.trim();\n if (title) return title.slice(0, 200);\n if (role && NAME_FROM_CONTENT.has(role.trim().split(/\\s+/)[0])) {\n const text = el.textContent?.trim();\n if (text) return text.slice(0, 200);\n }\n return void 0;\n }\n function accessibilityInfo(el) {\n const a11y = {};\n const role = el.getAttribute(\"role\") || implicitRole(el);\n if (role) a11y.role = role;\n const name = accessibleName(el, role);\n if (name) a11y.name = name;\n const states = {};\n for (const attr of [\n \"aria-expanded\",\n \"aria-checked\",\n \"aria-selected\",\n \"aria-pressed\",\n \"aria-current\",\n \"aria-disabled\"\n ]) {\n const v = el.getAttribute(attr);\n if (v !== null) states[attr.slice(5)] = v === \"true\" ? true : v === \"false\" ? false : v;\n }\n if (el.getAttribute(\"aria-hidden\") === \"true\") states.hidden = true;\n if (\"disabled\" in el && el.disabled) states.disabled = true;\n const tabindex = el.getAttribute(\"tabindex\");\n if (tabindex !== null) {\n const n = Number(tabindex);\n states.tabindex = Number.isNaN(n) ? tabindex : n;\n }\n if (Object.keys(states).length > 0) a11y.states = states;\n return Object.keys(a11y).length > 0 ? a11y : void 0;\n }\n function elementState(el) {\n const s = {};\n if (el instanceof HTMLInputElement) {\n s.value = capValue(el.value);\n s.type = el.type;\n s.checked = el.checked;\n s.disabled = el.disabled;\n s.readOnly = el.readOnly;\n s.required = el.required;\n if (el.placeholder) s.placeholder = el.placeholder;\n if (el.validationMessage) s.validationMessage = el.validationMessage;\n } else if (el instanceof HTMLTextAreaElement) {\n s.value = capValue(el.value);\n s.disabled = el.disabled;\n s.readOnly = el.readOnly;\n } else if (el instanceof HTMLSelectElement) {\n s.value = el.value;\n s.selectedIndex = el.selectedIndex;\n s.disabled = el.disabled;\n } else if (el instanceof HTMLButtonElement) {\n s.type = el.type;\n s.disabled = el.disabled;\n } else if (el instanceof HTMLAnchorElement) {\n s.href = el.href;\n }\n if (el instanceof HTMLElement && Object.keys(el.dataset).length > 0) {\n s.dataset = { ...el.dataset };\n }\n return Object.keys(s).length > 0 ? s : void 0;\n }\n function capValue(v) {\n return v.length > 1e3 ? `${v.slice(0, 1e3)}\\u2026[truncated]` : v;\n }\n function queryDom(selector) {\n return Array.from(document.querySelectorAll(selector)).map((el) => ({\n tag: el.tagName.toLowerCase(),\n id: el.id || null,\n classes: Array.from(el.classList),\n textContent: el.textContent?.trim().slice(0, 100)\n }));\n }\n var NAMED_KEYS = {\n Enter: { code: \"Enter\", keyCode: 13 },\n Tab: { code: \"Tab\", keyCode: 9 },\n Escape: { code: \"Escape\", keyCode: 27 },\n Backspace: { code: \"Backspace\", keyCode: 8 },\n Delete: { code: \"Delete\", keyCode: 46 },\n ArrowUp: { code: \"ArrowUp\", keyCode: 38 },\n ArrowDown: { code: \"ArrowDown\", keyCode: 40 },\n ArrowLeft: { code: \"ArrowLeft\", keyCode: 37 },\n ArrowRight: { code: \"ArrowRight\", keyCode: 39 },\n \" \": { code: \"Space\", keyCode: 32 }\n };\n function pressKey(selector, key) {\n const el = getEl(selector);\n el.focus?.();\n const named = NAMED_KEYS[key];\n const keyCode = named ? named.keyCode : key.length === 1 ? key.toUpperCase().charCodeAt(0) : 0;\n const code = named ? named.code : key.length === 1 ? `Key${key.toUpperCase()}` : key;\n const init = {\n key,\n code,\n keyCode,\n which: keyCode,\n bubbles: true,\n cancelable: true\n };\n el.dispatchEvent(new KeyboardEvent(\"keydown\", init));\n if (key.length === 1) el.dispatchEvent(new KeyboardEvent(\"keypress\", init));\n el.dispatchEvent(new KeyboardEvent(\"keyup\", init));\n return { tag: el.tagName.toLowerCase(), key };\n }\n var MAX_HTML_CHARS = 5e4;\n function getHtml(selector) {\n const el = getEl(selector);\n const html = el.outerHTML;\n const truncated = html.length > MAX_HTML_CHARS;\n return {\n tag: el.tagName.toLowerCase(),\n html: truncated ? `${html.slice(0, MAX_HTML_CHARS)}\\u2026[truncated]` : html,\n truncated\n };\n }\n function getPageInfo() {\n return {\n url: window.location.href,\n title: document.title,\n readyState: document.readyState,\n viewport: { width: window.innerWidth, height: window.innerHeight },\n scroll: { x: window.scrollX, y: window.scrollY },\n userAgent: navigator.userAgent\n };\n }\n var PREVIEW_NOTE = \"Live preview only \\u2014 applied to the running DOM, not saved to source, and reset on reload/HMR. Once the user is happy, edit the actual source to make it permanent.\";\n var CLOBBER_WARNING = \"This may be reverted on the next framework render (React/Vue/etc. re-render this element from component state). If it snaps back, change it in the source instead of here.\";\n var FRAMEWORK_OWNED_ATTRS = /* @__PURE__ */ new Set([\n \"class\",\n \"style\",\n \"value\",\n \"checked\",\n \"disabled\",\n \"selected\"\n ]);\n var overrides = [];\n function setStyle(selector, properties) {\n const el = getEl(selector);\n if (!el.style) throw new Error(`element \"${selector}\" has no style (not an HTMLElement)`);\n const applied = {};\n for (const [prop, value] of Object.entries(properties)) {\n const prevValue = el.style.getPropertyValue(prop);\n const prevPriority = el.style.getPropertyPriority(prop);\n overrides.push(() => {\n if (prevValue) el.style.setProperty(prop, prevValue, prevPriority);\n else el.style.removeProperty(prop);\n });\n el.style.setProperty(prop, value);\n applied[prop] = el.style.getPropertyValue(prop);\n }\n return { tag: el.tagName.toLowerCase(), applied, note: PREVIEW_NOTE };\n }\n function setAttribute(selector, name, value) {\n const el = getEl(selector);\n const had = el.hasAttribute(name);\n const prev = had ? el.getAttribute(name) : null;\n overrides.push(() => {\n if (had) el.setAttribute(name, prev ?? \"\");\n else el.removeAttribute(name);\n });\n if (value === null) el.removeAttribute(name);\n else el.setAttribute(name, value);\n const result = {\n tag: el.tagName.toLowerCase(),\n name,\n value: value === null ? null : el.getAttribute(name),\n removed: value === null,\n note: PREVIEW_NOTE\n };\n if (FRAMEWORK_OWNED_ATTRS.has(name.toLowerCase())) result.frameworkWarning = CLOBBER_WARNING;\n return result;\n }\n function setText(selector, text) {\n const el = getEl(selector);\n const prev = el.textContent;\n overrides.push(() => {\n el.textContent = prev;\n });\n el.textContent = text;\n return {\n tag: el.tagName.toLowerCase(),\n text,\n note: PREVIEW_NOTE,\n frameworkWarning: CLOBBER_WARNING\n };\n }\n function resetOverrides() {\n const count = overrides.length;\n while (overrides.length) overrides.pop()?.();\n return {\n reverted: count,\n note: \"All bridge-applied DOM changes since connect have been undone (best effort \\u2014 elements re-created by the framework since may not roll back).\"\n };\n }\n function isCommand(v) {\n return typeof v === \"object\" && v !== null && v.type === \"command\" && typeof v.id === \"string\";\n }\n\n // src/interceptors/console.ts\n var STD_LEVELS = [\"log\", \"warn\", \"error\", \"info\", \"debug\"];\n var MAX_LOGS = 1e3;\n var MAX_ARG_CHARS = 1e4;\n var ConsoleInterceptor = class {\n constructor() {\n this.originals = /* @__PURE__ */ new Map();\n this.logs = [];\n this.counts = /* @__PURE__ */ new Map();\n this.timers = /* @__PURE__ */ new Map();\n }\n install() {\n const c = console;\n const record = (msg) => {\n this.logs.push(msg);\n if (this.logs.length > MAX_LOGS) this.logs.shift();\n };\n const rich = (method, level, args, extras = {}) => {\n record({\n type: \"console\",\n ts: Date.now(),\n level,\n method,\n args: args.map(serialize),\n ...extras\n });\n };\n const wrap = (name, fn) => {\n const orig = c[name];\n if (typeof orig !== \"function\") return;\n this.originals.set(name, orig.bind(console));\n c[name] = fn;\n };\n for (const level of STD_LEVELS) {\n const orig = c[level].bind(console);\n this.originals.set(level, orig);\n c[level] = (...args) => {\n orig(...args);\n record({ type: \"console\", ts: Date.now(), level, args: args.map(serialize) });\n };\n }\n wrap(\"dir\", (obj, options) => {\n this.originals.get(\"dir\")?.(obj, options);\n rich(\"dir\", \"log\", options === void 0 ? [obj] : [obj, options]);\n });\n wrap(\"table\", (data, columns) => {\n this.originals.get(\"table\")?.(data, columns);\n rich(\"table\", \"log\", columns === void 0 ? [data] : [data, columns]);\n });\n wrap(\"trace\", (...args) => {\n this.originals.get(\"trace\")?.(...args);\n rich(\"trace\", \"log\", args, { stack: captureStack() });\n });\n wrap(\"assert\", (condition, ...args) => {\n this.originals.get(\"assert\")?.(condition, ...args);\n if (condition) return;\n rich(\"assert\", \"error\", args.length ? args : [\"Assertion failed\"], { stack: captureStack() });\n });\n wrap(\"count\", (label) => {\n this.originals.get(\"count\")?.(label);\n const key = label == null ? \"default\" : String(label);\n const n = (this.counts.get(key) ?? 0) + 1;\n this.counts.set(key, n);\n rich(\"count\", \"log\", [`${key}: ${n}`]);\n });\n wrap(\"countReset\", (label) => {\n this.originals.get(\"countReset\")?.(label);\n const key = label == null ? \"default\" : String(label);\n this.counts.set(key, 0);\n rich(\"countReset\", \"log\", [`${key}: 0`]);\n });\n wrap(\"time\", (label) => {\n this.originals.get(\"time\")?.(label);\n const key = label == null ? \"default\" : String(label);\n this.timers.set(key, performance.now());\n });\n wrap(\"timeEnd\", (label) => {\n this.originals.get(\"timeEnd\")?.(label);\n const key = label == null ? \"default\" : String(label);\n const start = this.timers.get(key);\n if (start === void 0) {\n rich(\"timeEnd\", \"warn\", [`Timer \"${key}\" does not exist`]);\n return;\n }\n this.timers.delete(key);\n rich(\"timeEnd\", \"log\", [`${key}: ${(performance.now() - start).toFixed(3)}ms`]);\n });\n wrap(\"timeLog\", (label, ...args) => {\n this.originals.get(\"timeLog\")?.(label, ...args);\n const key = label == null ? \"default\" : String(label);\n const start = this.timers.get(key);\n if (start === void 0) {\n rich(\"timeLog\", \"warn\", [`Timer \"${key}\" does not exist`, ...args]);\n return;\n }\n rich(\"timeLog\", \"log\", [`${key}: ${(performance.now() - start).toFixed(3)}ms`, ...args]);\n });\n wrap(\"group\", (...args) => {\n this.originals.get(\"group\")?.(...args);\n rich(\"group\", \"log\", args);\n });\n wrap(\"groupCollapsed\", (...args) => {\n this.originals.get(\"groupCollapsed\")?.(...args);\n rich(\"groupCollapsed\", \"log\", args);\n });\n wrap(\"groupEnd\", () => {\n this.originals.get(\"groupEnd\")?.();\n rich(\"groupEnd\", \"log\", []);\n });\n wrap(\"clear\", () => {\n this.originals.get(\"clear\")?.();\n rich(\"clear\", \"log\", []);\n });\n this.onError = (e) => {\n record({\n type: \"console\",\n ts: Date.now(),\n level: \"error\",\n method: \"uncaught\",\n args: [serialize(e.message || \"Uncaught error\")],\n stack: e.error instanceof Error ? e.error.stack : void 0\n });\n };\n this.onRejection = (e) => {\n const reason = e.reason;\n record({\n type: \"console\",\n ts: Date.now(),\n level: \"error\",\n method: \"unhandledrejection\",\n args: [serialize(reason instanceof Error ? reason.message : reason)],\n stack: reason instanceof Error ? reason.stack : void 0\n });\n };\n window.addEventListener(\"error\", this.onError);\n window.addEventListener(\"unhandledrejection\", this.onRejection);\n }\n uninstall() {\n const c = console;\n for (const [name, original] of this.originals) {\n c[name] = original;\n }\n this.originals.clear();\n if (this.onError) window.removeEventListener(\"error\", this.onError);\n if (this.onRejection) window.removeEventListener(\"unhandledrejection\", this.onRejection);\n }\n getLogs(opts = {}) {\n let result = this.logs;\n if (opts.levels && opts.levels.length > 0) {\n const wanted = new Set(opts.levels);\n result = result.filter((m) => wanted.has(m.level));\n }\n if (opts.match) {\n const needle = opts.match.toLowerCase();\n result = result.filter((m) => JSON.stringify(m.args).toLowerCase().includes(needle));\n }\n if (opts.since !== void 0) {\n result = result.filter((m) => m.ts >= opts.since);\n }\n if (opts.limit !== void 0) {\n result = result.slice(-opts.limit);\n } else if (result === this.logs) {\n result = [...this.logs];\n }\n return result;\n }\n };\n function captureStack() {\n const raw = new Error().stack ?? \"\";\n const lines = raw.split(\"\\n\");\n return lines.length > 2 ? lines.slice(2).join(\"\\n\") : raw;\n }\n function serialize(v) {\n if (v === null || v === void 0) return String(v);\n if (typeof v === \"string\") return cap(v);\n if (typeof v !== \"object\") return v;\n if (v instanceof Error) return { name: v.name, message: v.message, stack: v.stack };\n try {\n const json = JSON.stringify(v);\n return json.length > MAX_ARG_CHARS ? cap(json) : JSON.parse(json);\n } catch {\n return String(v);\n }\n }\n function cap(text) {\n return text.length > MAX_ARG_CHARS ? `${text.slice(0, MAX_ARG_CHARS)}\\u2026[truncated]` : text;\n }\n\n // src/interceptors/network.ts\n var MAX_REQUESTS = 1e3;\n var MAX_BODY_CHARS = 1e4;\n var MAX_BODY_BYTES = 64 * 1024;\n var NetworkInterceptor = class {\n constructor() {\n this.requests = [];\n this.origFetch = null;\n this.OrigXHR = null;\n }\n // Captured into a local buffer only \u2014 never streamed. An agent pulls requests\n // on demand via get_network_requests; pushing every request/response over the\n // WebSocket would be wasted traffic since nothing subscribes to it.\n install() {\n this.interceptFetch();\n this.interceptXHR();\n }\n interceptFetch() {\n this.origFetch = window.fetch.bind(window);\n const orig = this.origFetch;\n const requests = this.requests;\n window.fetch = async (input, init) => {\n const url = input instanceof Request ? input.url : String(input);\n const method = resolveMethod(input, init);\n const requestId = uid();\n const startTs = Date.now();\n const requestHeaders = mergeRequestHeaders(input, init);\n const requestBody = serializeRequestBody(init?.body);\n const pending = {\n type: \"network\",\n ts: startTs,\n requestId,\n method,\n url,\n requestHeaders,\n requestBody\n };\n pushRequest(requests, pending);\n try {\n const res = await orig(input, init);\n Object.assign(pending, {\n ts: Date.now(),\n status: res.status,\n duration: Date.now() - startTs,\n responseHeaders: headersToObject(res.headers)\n });\n captureResponseBody(res.clone(), pending);\n return res;\n } catch (e) {\n Object.assign(pending, {\n ts: Date.now(),\n duration: Date.now() - startTs,\n error: e instanceof Error ? e.message : String(e)\n });\n throw e;\n }\n };\n }\n interceptXHR() {\n this.OrigXHR = window.XMLHttpRequest;\n const OrigXHR = this.OrigXHR;\n const requests = this.requests;\n function FeedthroughXHR() {\n const xhr = new OrigXHR();\n let method = \"GET\";\n let url = \"\";\n const requestHeaders = {};\n const origOpen = xhr.open.bind(xhr);\n xhr.open = (m, u, async, user, password) => {\n method = m.toUpperCase();\n url = String(u);\n origOpen(m, String(u), async ?? true, user, password);\n };\n const origSetHeader = xhr.setRequestHeader.bind(xhr);\n xhr.setRequestHeader = (name, value) => {\n requestHeaders[name] = String(value);\n origSetHeader(name, value);\n };\n const origSend = xhr.send.bind(xhr);\n xhr.send = (body) => {\n const requestId = uid();\n const startTs = Date.now();\n const requestBody = serializeRequestBody(body ?? void 0);\n const pending = {\n type: \"network\",\n ts: startTs,\n requestId,\n method,\n url,\n requestHeaders: Object.keys(requestHeaders).length ? { ...requestHeaders } : void 0,\n requestBody\n };\n pushRequest(requests, pending);\n xhr.addEventListener(\"loadend\", () => {\n let responseBody;\n try {\n if (xhr.responseType === \"\" || xhr.responseType === \"text\") {\n responseBody = capText(xhr.responseText);\n } else {\n responseBody = `[XHR responseType=${xhr.responseType}]`;\n }\n } catch {\n }\n Object.assign(pending, {\n ts: Date.now(),\n status: xhr.status,\n duration: Date.now() - startTs,\n responseBody,\n responseHeaders: parseRawHeaders(xhr.getAllResponseHeaders())\n });\n });\n origSend(body);\n };\n return xhr;\n }\n FeedthroughXHR.prototype = OrigXHR.prototype;\n window.XMLHttpRequest = FeedthroughXHR;\n }\n uninstall() {\n if (this.origFetch) window.fetch = this.origFetch;\n if (this.OrigXHR) window.XMLHttpRequest = this.OrigXHR;\n }\n getRequests(filter, since) {\n let all = [...this.requests];\n if (since !== void 0) all = all.filter((r) => r.ts >= since);\n if (!filter) return all;\n const lower = filter.toLowerCase();\n return all.filter((r) => r.url.toLowerCase().includes(lower) || r.method.toLowerCase() === lower);\n }\n };\n function resolveMethod(input, init) {\n return (init?.method ?? (input instanceof Request ? input.method : null) ?? \"GET\").toUpperCase();\n }\n function pushRequest(requests, msg) {\n requests.push(msg);\n if (requests.length > MAX_REQUESTS) requests.shift();\n }\n function mergeRequestHeaders(input, init) {\n const source = init?.headers ?? (input instanceof Request ? input.headers : void 0);\n if (!source) return void 0;\n const out = {};\n if (source instanceof Headers) {\n source.forEach((v, k) => {\n out[k] = v;\n });\n } else if (Array.isArray(source)) {\n for (const [k, v] of source) out[k] = String(v);\n } else {\n for (const [k, v] of Object.entries(source)) out[k] = String(v);\n }\n return Object.keys(out).length ? out : void 0;\n }\n function headersToObject(headers) {\n const out = {};\n headers.forEach((v, k) => {\n out[k] = v;\n });\n return out;\n }\n function parseRawHeaders(raw) {\n if (!raw) return void 0;\n const out = {};\n for (const line of raw.split(\"\\r\\n\")) {\n const idx = line.indexOf(\":\");\n if (idx <= 0) continue;\n const k = line.slice(0, idx).trim();\n const v = line.slice(idx + 1).trim();\n if (k) out[k] = v;\n }\n return Object.keys(out).length ? out : void 0;\n }\n function serializeRequestBody(body) {\n if (body == null) return void 0;\n if (typeof body === \"string\") return capText(body);\n if (body instanceof URLSearchParams) return capText(body.toString());\n if (body instanceof FormData) {\n const obj = {};\n body.forEach((v, k) => {\n obj[k] = v instanceof File ? `[File: ${v.name}, ${v.size} bytes]` : v;\n });\n try {\n return capText(JSON.stringify(obj));\n } catch {\n return \"[FormData]\";\n }\n }\n if (body instanceof Blob) return `[Blob: ${body.size} bytes, ${body.type || \"unknown\"}]`;\n if (body instanceof ArrayBuffer) return `[ArrayBuffer: ${body.byteLength} bytes]`;\n if (ArrayBuffer.isView(body)) return `[${body.constructor.name}: ${body.byteLength} bytes]`;\n if (body instanceof ReadableStream) return \"[ReadableStream]\";\n try {\n return capText(JSON.stringify(body));\n } catch {\n return String(body);\n }\n }\n function capText(text) {\n if (text.length <= MAX_BODY_CHARS) return text;\n return `${text.slice(0, MAX_BODY_CHARS)}\\u2026[truncated, ${text.length - MAX_BODY_CHARS} more chars]`;\n }\n function captureResponseBody(res, pending) {\n const ct = (res.headers.get(\"content-type\") ?? \"\").toLowerCase();\n if (isSkippable(ct)) {\n const len = res.headers.get(\"content-length\");\n pending.responseBody = `[${describe(ct)}${len ? `, ${len} bytes` : \"\"}, ${ct || \"no content-type\"}]`;\n return;\n }\n readBounded(res).then((body) => {\n pending.responseBody = body;\n }).catch(() => {\n });\n }\n async function readBounded(res) {\n if (!res.body) return capText(await res.text());\n const reader = res.body.getReader();\n const decoder = new TextDecoder();\n let text = \"\";\n let bytes = 0;\n let truncated = false;\n try {\n for (; ; ) {\n const { done, value } = await reader.read();\n if (done) break;\n bytes += value.byteLength;\n text += decoder.decode(value, { stream: true });\n if (bytes >= MAX_BODY_BYTES || text.length >= MAX_BODY_CHARS) {\n truncated = true;\n break;\n }\n }\n } finally {\n reader.cancel().catch(() => {\n });\n }\n if (!truncated) return capText(text);\n return `${text.slice(0, MAX_BODY_CHARS)}\\u2026[truncated]`;\n }\n function isSkippable(ct) {\n return /^(image|video|audio|font)\\//.test(ct) || ct.startsWith(\"application/octet-stream\") || ct.startsWith(\"application/pdf\") || ct.startsWith(\"application/zip\") || ct.startsWith(\"text/event-stream\") || // SSE \u2014 never-ending stream\n ct.startsWith(\"application/x-ndjson\");\n }\n function describe(ct) {\n if (ct.startsWith(\"text/event-stream\")) return \"event stream\";\n if (ct.startsWith(\"application/x-ndjson\")) return \"ndjson stream\";\n return \"binary\";\n }\n var counter = 0;\n function uid() {\n return `${Date.now()}-${++counter}`;\n }\n\n // src/transport.ts\n var MAX_QUEUE = 1e3;\n var Transport = class {\n constructor(url, onMessage, onStatus, reconnectDelay) {\n this.url = url;\n this.onMessage = onMessage;\n this.onStatus = onStatus;\n this.reconnectDelay = reconnectDelay;\n this.ws = null;\n this.queue = [];\n this.reconnectTimer = null;\n this.destroyed = false;\n }\n connect() {\n if (this.destroyed) return;\n const ws = new WebSocket(this.url);\n this.ws = ws;\n const isCurrent = () => this.ws === ws;\n ws.onopen = () => {\n if (!isCurrent()) return;\n this.onStatus(true);\n for (const msg of this.queue) ws.send(msg);\n this.queue = [];\n };\n ws.onmessage = (event) => {\n if (!isCurrent()) return;\n try {\n this.onMessage(JSON.parse(event.data));\n } catch {\n }\n };\n ws.onclose = () => {\n if (!isCurrent()) return;\n this.onStatus(false);\n if (!this.destroyed) {\n this.reconnectTimer = setTimeout(() => this.connect(), this.reconnectDelay);\n }\n };\n ws.onerror = () => {\n };\n }\n send(msg) {\n const serialized = JSON.stringify(msg);\n if (this.ws?.readyState === WebSocket.OPEN) {\n this.ws.send(serialized);\n } else {\n this.queue.push(serialized);\n if (this.queue.length > MAX_QUEUE) this.queue.shift();\n }\n }\n destroy() {\n this.destroyed = true;\n if (this.reconnectTimer !== null) clearTimeout(this.reconnectTimer);\n this.ws?.close();\n }\n };\n\n // src/bridge.ts\n var DEFAULT_SERVER_URL = \"ws://localhost:8765\";\n var FeedthroughBridge = class {\n constructor(options = {}) {\n const url = options.serverUrl ?? DEFAULT_SERVER_URL;\n const reconnectDelay = options.reconnectDelay ?? 2e3;\n this.consoleInterceptor = new ConsoleInterceptor();\n this.networkInterceptor = new NetworkInterceptor();\n this.transport = new Transport(\n url,\n (msg) => this.commandHandler.handle(msg),\n (connected) => {\n if (connected) this.transport.send({ type: \"hello\", url: window.location.href });\n },\n reconnectDelay\n );\n this.commandHandler = new CommandHandler(\n this.transport,\n this.consoleInterceptor,\n this.networkInterceptor\n );\n }\n connect() {\n this.consoleInterceptor.install();\n this.networkInterceptor.install();\n this.transport.connect();\n }\n destroy() {\n this.transport.destroy();\n this.consoleInterceptor.uninstall();\n this.networkInterceptor.uninstall();\n }\n };\n\n // src/browser.ts\n window.__feedthrough = new FeedthroughBridge(window.__feedthroughOptions ?? {});\n window.__feedthrough.connect();\n})();\n"; | ||
| //# sourceMappingURL=bundle.d.ts.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../../src/generated/bundle.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,YAAY,k/6BAAw+6B,CAAC"} | ||
| {"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../../src/generated/bundle.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,YAAY,u4xCAA63xC,CAAC"} |
| // AUTO-GENERATED by scripts/embed-bundle.mjs — do not edit | ||
| export const bridgeBundle = "\"use strict\";\n(() => {\n // src/transport.ts\n var MAX_QUEUE = 1e3;\n var Transport = class {\n constructor(url, onMessage, onStatus, reconnectDelay) {\n this.url = url;\n this.onMessage = onMessage;\n this.onStatus = onStatus;\n this.reconnectDelay = reconnectDelay;\n this.ws = null;\n this.queue = [];\n this.reconnectTimer = null;\n this.destroyed = false;\n }\n connect() {\n if (this.destroyed) return;\n this.ws = new WebSocket(this.url);\n this.ws.onopen = () => {\n this.onStatus(true);\n for (const msg of this.queue) this.ws.send(msg);\n this.queue = [];\n };\n this.ws.onmessage = (event) => {\n try {\n this.onMessage(JSON.parse(event.data));\n } catch {\n }\n };\n this.ws.onclose = () => {\n this.onStatus(false);\n if (!this.destroyed) {\n this.reconnectTimer = setTimeout(() => this.connect(), this.reconnectDelay);\n }\n };\n this.ws.onerror = () => {\n };\n }\n send(msg) {\n const serialized = JSON.stringify(msg);\n if (this.ws?.readyState === WebSocket.OPEN) {\n this.ws.send(serialized);\n } else {\n this.queue.push(serialized);\n if (this.queue.length > MAX_QUEUE) this.queue.shift();\n }\n }\n destroy() {\n this.destroyed = true;\n if (this.reconnectTimer !== null) clearTimeout(this.reconnectTimer);\n this.ws?.close();\n }\n };\n\n // src/interceptors/console.ts\n var STD_LEVELS = [\"log\", \"warn\", \"error\", \"info\", \"debug\"];\n var MAX_LOGS = 1e3;\n var MAX_ARG_CHARS = 1e4;\n var ConsoleInterceptor = class {\n constructor() {\n this.originals = /* @__PURE__ */ new Map();\n this.logs = [];\n this.counts = /* @__PURE__ */ new Map();\n this.timers = /* @__PURE__ */ new Map();\n }\n install() {\n const c = console;\n const record = (msg) => {\n this.logs.push(msg);\n if (this.logs.length > MAX_LOGS) this.logs.shift();\n };\n const rich = (method, level, args, extras = {}) => {\n record({ type: \"console\", ts: Date.now(), level, method, args: args.map(serialize), ...extras });\n };\n const wrap = (name, fn) => {\n const orig = c[name];\n if (typeof orig !== \"function\") return;\n this.originals.set(name, orig.bind(console));\n c[name] = fn;\n };\n for (const level of STD_LEVELS) {\n const orig = c[level].bind(console);\n this.originals.set(level, orig);\n c[level] = (...args) => {\n orig(...args);\n record({ type: \"console\", ts: Date.now(), level, args: args.map(serialize) });\n };\n }\n wrap(\"dir\", (obj, options) => {\n this.originals.get(\"dir\")(obj, options);\n rich(\"dir\", \"log\", options === void 0 ? [obj] : [obj, options]);\n });\n wrap(\"table\", (data, columns) => {\n this.originals.get(\"table\")(data, columns);\n rich(\"table\", \"log\", columns === void 0 ? [data] : [data, columns]);\n });\n wrap(\"trace\", (...args) => {\n this.originals.get(\"trace\")(...args);\n rich(\"trace\", \"log\", args, { stack: captureStack() });\n });\n wrap(\"assert\", (condition, ...args) => {\n this.originals.get(\"assert\")(condition, ...args);\n if (condition) return;\n rich(\"assert\", \"error\", args.length ? args : [\"Assertion failed\"], { stack: captureStack() });\n });\n wrap(\"count\", (label) => {\n this.originals.get(\"count\")(label);\n const key = label == null ? \"default\" : String(label);\n const n = (this.counts.get(key) ?? 0) + 1;\n this.counts.set(key, n);\n rich(\"count\", \"log\", [`${key}: ${n}`]);\n });\n wrap(\"countReset\", (label) => {\n this.originals.get(\"countReset\")(label);\n const key = label == null ? \"default\" : String(label);\n this.counts.set(key, 0);\n rich(\"countReset\", \"log\", [`${key}: 0`]);\n });\n wrap(\"time\", (label) => {\n this.originals.get(\"time\")(label);\n const key = label == null ? \"default\" : String(label);\n this.timers.set(key, performance.now());\n });\n wrap(\"timeEnd\", (label) => {\n this.originals.get(\"timeEnd\")(label);\n const key = label == null ? \"default\" : String(label);\n const start = this.timers.get(key);\n if (start === void 0) {\n rich(\"timeEnd\", \"warn\", [`Timer \"${key}\" does not exist`]);\n return;\n }\n this.timers.delete(key);\n rich(\"timeEnd\", \"log\", [`${key}: ${(performance.now() - start).toFixed(3)}ms`]);\n });\n wrap(\"timeLog\", (label, ...args) => {\n this.originals.get(\"timeLog\")(label, ...args);\n const key = label == null ? \"default\" : String(label);\n const start = this.timers.get(key);\n if (start === void 0) {\n rich(\"timeLog\", \"warn\", [`Timer \"${key}\" does not exist`, ...args]);\n return;\n }\n rich(\"timeLog\", \"log\", [`${key}: ${(performance.now() - start).toFixed(3)}ms`, ...args]);\n });\n wrap(\"group\", (...args) => {\n this.originals.get(\"group\")(...args);\n rich(\"group\", \"log\", args);\n });\n wrap(\"groupCollapsed\", (...args) => {\n this.originals.get(\"groupCollapsed\")(...args);\n rich(\"groupCollapsed\", \"log\", args);\n });\n wrap(\"groupEnd\", () => {\n this.originals.get(\"groupEnd\")();\n rich(\"groupEnd\", \"log\", []);\n });\n wrap(\"clear\", () => {\n this.originals.get(\"clear\")();\n rich(\"clear\", \"log\", []);\n });\n this.onError = (e) => {\n record({\n type: \"console\",\n ts: Date.now(),\n level: \"error\",\n method: \"uncaught\",\n args: [serialize(e.message || \"Uncaught error\")],\n stack: e.error instanceof Error ? e.error.stack : void 0\n });\n };\n this.onRejection = (e) => {\n const reason = e.reason;\n record({\n type: \"console\",\n ts: Date.now(),\n level: \"error\",\n method: \"unhandledrejection\",\n args: [serialize(reason instanceof Error ? reason.message : reason)],\n stack: reason instanceof Error ? reason.stack : void 0\n });\n };\n window.addEventListener(\"error\", this.onError);\n window.addEventListener(\"unhandledrejection\", this.onRejection);\n }\n uninstall() {\n const c = console;\n for (const [name, original] of this.originals) {\n c[name] = original;\n }\n this.originals.clear();\n if (this.onError) window.removeEventListener(\"error\", this.onError);\n if (this.onRejection) window.removeEventListener(\"unhandledrejection\", this.onRejection);\n }\n getLogs(opts = {}) {\n let result = this.logs;\n if (opts.levels && opts.levels.length > 0) {\n const wanted = new Set(opts.levels);\n result = result.filter((m) => wanted.has(m.level));\n }\n if (opts.match) {\n const needle = opts.match.toLowerCase();\n result = result.filter((m) => JSON.stringify(m.args).toLowerCase().includes(needle));\n }\n if (opts.since !== void 0) {\n result = result.filter((m) => m.ts >= opts.since);\n }\n if (opts.limit !== void 0) {\n result = result.slice(-opts.limit);\n } else if (result === this.logs) {\n result = [...this.logs];\n }\n return result;\n }\n };\n function captureStack() {\n const raw = new Error().stack ?? \"\";\n const lines = raw.split(\"\\n\");\n return lines.length > 2 ? lines.slice(2).join(\"\\n\") : raw;\n }\n function serialize(v) {\n if (v === null || v === void 0) return String(v);\n if (typeof v === \"string\") return cap(v);\n if (typeof v !== \"object\") return v;\n if (v instanceof Error) return { name: v.name, message: v.message, stack: v.stack };\n try {\n const json = JSON.stringify(v);\n return json.length > MAX_ARG_CHARS ? cap(json) : JSON.parse(json);\n } catch {\n return String(v);\n }\n }\n function cap(text) {\n return text.length > MAX_ARG_CHARS ? text.slice(0, MAX_ARG_CHARS) + \"\\u2026[truncated]\" : text;\n }\n\n // src/interceptors/network.ts\n var MAX_REQUESTS = 1e3;\n var MAX_BODY_CHARS = 1e4;\n var MAX_BODY_BYTES = 64 * 1024;\n var NetworkInterceptor = class {\n constructor() {\n this.requests = [];\n this.origFetch = null;\n this.OrigXHR = null;\n }\n // Captured into a local buffer only — never streamed. An agent pulls requests\n // on demand via get_network_requests; pushing every request/response over the\n // WebSocket would be wasted traffic since nothing subscribes to it.\n install() {\n this.interceptFetch();\n this.interceptXHR();\n }\n interceptFetch() {\n this.origFetch = window.fetch.bind(window);\n const orig = this.origFetch;\n const requests = this.requests;\n window.fetch = async (input, init) => {\n const url = input instanceof Request ? input.url : String(input);\n const method = resolveMethod(input, init);\n const requestId = uid();\n const startTs = Date.now();\n const requestHeaders = mergeRequestHeaders(input, init);\n const requestBody = serializeRequestBody(init?.body);\n const pending = {\n type: \"network\",\n ts: startTs,\n requestId,\n method,\n url,\n requestHeaders,\n requestBody\n };\n pushRequest(requests, pending);\n try {\n const res = await orig(input, init);\n Object.assign(pending, {\n ts: Date.now(),\n status: res.status,\n duration: Date.now() - startTs,\n responseHeaders: headersToObject(res.headers)\n });\n captureResponseBody(res.clone(), pending);\n return res;\n } catch (e) {\n Object.assign(pending, {\n ts: Date.now(),\n duration: Date.now() - startTs,\n error: e instanceof Error ? e.message : String(e)\n });\n throw e;\n }\n };\n }\n interceptXHR() {\n this.OrigXHR = window.XMLHttpRequest;\n const OrigXHR = this.OrigXHR;\n const requests = this.requests;\n function FeedthroughXHR() {\n const xhr = new OrigXHR();\n let method = \"GET\";\n let url = \"\";\n const requestHeaders = {};\n const origOpen = xhr.open.bind(xhr);\n xhr.open = (m, u, async, user, password) => {\n method = m.toUpperCase();\n url = String(u);\n origOpen(m, String(u), async ?? true, user, password);\n };\n const origSetHeader = xhr.setRequestHeader.bind(xhr);\n xhr.setRequestHeader = (name, value) => {\n requestHeaders[name] = String(value);\n origSetHeader(name, value);\n };\n const origSend = xhr.send.bind(xhr);\n xhr.send = (body) => {\n const requestId = uid();\n const startTs = Date.now();\n const requestBody = serializeRequestBody(body ?? void 0);\n const pending = {\n type: \"network\",\n ts: startTs,\n requestId,\n method,\n url,\n requestHeaders: Object.keys(requestHeaders).length ? { ...requestHeaders } : void 0,\n requestBody\n };\n pushRequest(requests, pending);\n xhr.addEventListener(\"loadend\", () => {\n let responseBody;\n try {\n if (xhr.responseType === \"\" || xhr.responseType === \"text\") {\n responseBody = capText(xhr.responseText);\n } else {\n responseBody = `[XHR responseType=${xhr.responseType}]`;\n }\n } catch {\n }\n Object.assign(pending, {\n ts: Date.now(),\n status: xhr.status,\n duration: Date.now() - startTs,\n responseBody,\n responseHeaders: parseRawHeaders(xhr.getAllResponseHeaders())\n });\n });\n origSend(body);\n };\n return xhr;\n }\n FeedthroughXHR.prototype = OrigXHR.prototype;\n window.XMLHttpRequest = FeedthroughXHR;\n }\n uninstall() {\n if (this.origFetch) window.fetch = this.origFetch;\n if (this.OrigXHR) window.XMLHttpRequest = this.OrigXHR;\n }\n getRequests(filter, since) {\n let all = [...this.requests];\n if (since !== void 0) all = all.filter((r) => r.ts >= since);\n if (!filter) return all;\n const lower = filter.toLowerCase();\n return all.filter((r) => r.url.toLowerCase().includes(lower) || r.method.toLowerCase() === lower);\n }\n };\n function resolveMethod(input, init) {\n return (init?.method ?? (input instanceof Request ? input.method : null) ?? \"GET\").toUpperCase();\n }\n function pushRequest(requests, msg) {\n requests.push(msg);\n if (requests.length > MAX_REQUESTS) requests.shift();\n }\n function mergeRequestHeaders(input, init) {\n const source = init?.headers ?? (input instanceof Request ? input.headers : void 0);\n if (!source) return void 0;\n const out = {};\n if (source instanceof Headers) {\n source.forEach((v, k) => {\n out[k] = v;\n });\n } else if (Array.isArray(source)) {\n for (const [k, v] of source) out[k] = String(v);\n } else {\n for (const [k, v] of Object.entries(source)) out[k] = String(v);\n }\n return Object.keys(out).length ? out : void 0;\n }\n function headersToObject(headers) {\n const out = {};\n headers.forEach((v, k) => {\n out[k] = v;\n });\n return out;\n }\n function parseRawHeaders(raw) {\n if (!raw) return void 0;\n const out = {};\n for (const line of raw.split(\"\\r\\n\")) {\n const idx = line.indexOf(\":\");\n if (idx <= 0) continue;\n const k = line.slice(0, idx).trim();\n const v = line.slice(idx + 1).trim();\n if (k) out[k] = v;\n }\n return Object.keys(out).length ? out : void 0;\n }\n function serializeRequestBody(body) {\n if (body == null) return void 0;\n if (typeof body === \"string\") return capText(body);\n if (body instanceof URLSearchParams) return capText(body.toString());\n if (body instanceof FormData) {\n const obj = {};\n body.forEach((v, k) => {\n obj[k] = v instanceof File ? `[File: ${v.name}, ${v.size} bytes]` : v;\n });\n try {\n return capText(JSON.stringify(obj));\n } catch {\n return \"[FormData]\";\n }\n }\n if (body instanceof Blob) return `[Blob: ${body.size} bytes, ${body.type || \"unknown\"}]`;\n if (body instanceof ArrayBuffer) return `[ArrayBuffer: ${body.byteLength} bytes]`;\n if (ArrayBuffer.isView(body)) return `[${body.constructor.name}: ${body.byteLength} bytes]`;\n if (body instanceof ReadableStream) return \"[ReadableStream]\";\n try {\n return capText(JSON.stringify(body));\n } catch {\n return String(body);\n }\n }\n function capText(text) {\n if (text.length <= MAX_BODY_CHARS) return text;\n return text.slice(0, MAX_BODY_CHARS) + `\\u2026[truncated, ${text.length - MAX_BODY_CHARS} more chars]`;\n }\n function captureResponseBody(res, pending) {\n const ct = (res.headers.get(\"content-type\") ?? \"\").toLowerCase();\n if (isSkippable(ct)) {\n const len = res.headers.get(\"content-length\");\n pending.responseBody = `[${describe(ct)}${len ? `, ${len} bytes` : \"\"}, ${ct || \"no content-type\"}]`;\n return;\n }\n readBounded(res).then((body) => {\n pending.responseBody = body;\n }).catch(() => {\n });\n }\n async function readBounded(res) {\n if (!res.body) return capText(await res.text());\n const reader = res.body.getReader();\n const decoder = new TextDecoder();\n let text = \"\";\n let bytes = 0;\n let truncated = false;\n try {\n for (; ; ) {\n const { done, value } = await reader.read();\n if (done) break;\n bytes += value.byteLength;\n text += decoder.decode(value, { stream: true });\n if (bytes >= MAX_BODY_BYTES || text.length >= MAX_BODY_CHARS) {\n truncated = true;\n break;\n }\n }\n } finally {\n reader.cancel().catch(() => {\n });\n }\n if (!truncated) return capText(text);\n return text.slice(0, MAX_BODY_CHARS) + \"\\u2026[truncated]\";\n }\n function isSkippable(ct) {\n return /^(image|video|audio|font)\\//.test(ct) || ct.startsWith(\"application/octet-stream\") || ct.startsWith(\"application/pdf\") || ct.startsWith(\"application/zip\") || ct.startsWith(\"text/event-stream\") || // SSE — never-ending stream\n ct.startsWith(\"application/x-ndjson\");\n }\n function describe(ct) {\n if (ct.startsWith(\"text/event-stream\")) return \"event stream\";\n if (ct.startsWith(\"application/x-ndjson\")) return \"ndjson stream\";\n return \"binary\";\n }\n var counter = 0;\n function uid() {\n return `${Date.now()}-${++counter}`;\n }\n\n // src/commands.ts\n var CommandHandler = class {\n constructor(transport, console2, network) {\n this.transport = transport;\n this.console = console2;\n this.network = network;\n }\n handle(msg) {\n if (!isCommand(msg)) return;\n let value;\n let error;\n try {\n value = this.dispatch(msg);\n } catch (e) {\n error = e instanceof Error ? e.message : String(e);\n }\n const result = {\n type: \"result\",\n ts: Date.now(),\n commandId: msg.id,\n ok: error === void 0,\n value,\n error\n };\n this.transport.send(result);\n }\n dispatch(cmd) {\n switch (cmd.action) {\n case \"click\":\n return clickEl(cmd.selector);\n case \"fill\":\n return fillEl(cmd.selector, cmd.value);\n case \"hover\":\n return hoverEl(cmd.selector);\n case \"inspect\":\n return inspectEl(cmd.selector, cmd.properties);\n case \"query_dom\":\n return queryDom(cmd.selector);\n case \"get_console_logs\":\n return this.console.getLogs({ limit: cmd.limit, levels: cmd.levels, match: cmd.match, since: cmd.since });\n case \"get_network_requests\":\n return this.network.getRequests(cmd.filter, cmd.since);\n case \"press_key\":\n return pressKey(cmd.selector, cmd.key);\n case \"get_html\":\n return getHtml(cmd.selector);\n case \"get_page_info\":\n return getPageInfo();\n case \"set_style\":\n return setStyle(cmd.selector, cmd.properties);\n case \"set_attribute\":\n return setAttribute(cmd.selector, cmd.name, cmd.value);\n case \"set_text\":\n return setText(cmd.selector, cmd.text);\n case \"reset_overrides\":\n return resetOverrides();\n }\n }\n };\n function getEl(selector) {\n const el = document.querySelector(selector);\n if (!el) throw new Error(`no element matches \"${selector}\"`);\n return el;\n }\n function clickEl(selector) {\n const el = getEl(selector);\n el.click();\n return { tag: el.tagName.toLowerCase(), id: el.id || null };\n }\n function fillEl(selector, value) {\n const el = getEl(selector);\n el.focus();\n const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : el instanceof HTMLSelectElement ? HTMLSelectElement.prototype : HTMLInputElement.prototype;\n const nativeSetter = Object.getOwnPropertyDescriptor(proto, \"value\")?.set;\n if (nativeSetter) {\n nativeSetter.call(el, value);\n } else {\n el.value = value;\n }\n el.dispatchEvent(new Event(\"input\", { bubbles: true }));\n el.dispatchEvent(new Event(\"change\", { bubbles: true }));\n return { tag: el.tagName.toLowerCase(), value };\n }\n function hoverEl(selector) {\n const el = getEl(selector);\n el.dispatchEvent(new MouseEvent(\"mouseover\", { bubbles: true }));\n el.dispatchEvent(new MouseEvent(\"mouseenter\", { bubbles: true }));\n return { tag: el.tagName.toLowerCase() };\n }\n var DEFAULT_STYLE_PROPS = [\n \"display\",\n \"position\",\n \"visibility\",\n \"opacity\",\n \"z-index\",\n \"box-sizing\",\n \"top\",\n \"right\",\n \"bottom\",\n \"left\",\n \"width\",\n \"height\",\n \"margin\",\n \"padding\",\n \"border\",\n \"color\",\n \"background-color\",\n \"font-family\",\n \"font-size\",\n \"font-weight\",\n \"line-height\",\n \"text-align\",\n \"overflow\",\n \"cursor\",\n \"pointer-events\",\n \"flex\",\n \"flex-direction\",\n \"justify-content\",\n \"align-items\",\n \"gap\",\n \"grid-template-columns\",\n \"transform\"\n ];\n function inspectEl(selector, properties) {\n const el = getEl(selector);\n const rect = el.getBoundingClientRect();\n const cs = window.getComputedStyle(el);\n const result = {\n tag: el.tagName.toLowerCase(),\n id: el.id || null,\n classes: Array.from(el.classList),\n attributes: Object.fromEntries(Array.from(el.attributes).map((a) => [a.name, a.value])),\n textContent: el.textContent?.trim().slice(0, 200),\n rect: {\n top: rect.top,\n right: rect.right,\n bottom: rect.bottom,\n left: rect.left,\n width: rect.width,\n height: rect.height,\n x: rect.x,\n y: rect.y\n },\n scroll: { x: window.scrollX, y: window.scrollY },\n inViewport: rect.bottom > 0 && rect.right > 0 && rect.top < window.innerHeight && rect.left < window.innerWidth,\n styles: pickStyles(cs, DEFAULT_STYLE_PROPS)\n };\n const state = elementState(el);\n if (state) result.state = state;\n if (properties && properties.length > 0) {\n const requested = {};\n for (const p of properties) requested[p] = cs.getPropertyValue(p);\n result.requested = requested;\n }\n return result;\n }\n function pickStyles(cs, props) {\n const out = {};\n for (const p of props) {\n const v = cs.getPropertyValue(p);\n if (v) out[p] = v;\n }\n return out;\n }\n function elementState(el) {\n const s = {};\n if (el instanceof HTMLInputElement) {\n s.value = capValue(el.value);\n s.type = el.type;\n s.checked = el.checked;\n s.disabled = el.disabled;\n s.readOnly = el.readOnly;\n s.required = el.required;\n if (el.placeholder) s.placeholder = el.placeholder;\n if (el.validationMessage) s.validationMessage = el.validationMessage;\n } else if (el instanceof HTMLTextAreaElement) {\n s.value = capValue(el.value);\n s.disabled = el.disabled;\n s.readOnly = el.readOnly;\n } else if (el instanceof HTMLSelectElement) {\n s.value = el.value;\n s.selectedIndex = el.selectedIndex;\n s.disabled = el.disabled;\n } else if (el instanceof HTMLButtonElement) {\n s.type = el.type;\n s.disabled = el.disabled;\n } else if (el instanceof HTMLAnchorElement) {\n s.href = el.href;\n }\n if (el instanceof HTMLElement && Object.keys(el.dataset).length > 0) {\n s.dataset = { ...el.dataset };\n }\n return Object.keys(s).length > 0 ? s : void 0;\n }\n function capValue(v) {\n return v.length > 1e3 ? v.slice(0, 1e3) + \"\\u2026[truncated]\" : v;\n }\n function queryDom(selector) {\n return Array.from(document.querySelectorAll(selector)).map((el) => ({\n tag: el.tagName.toLowerCase(),\n id: el.id || null,\n classes: Array.from(el.classList),\n textContent: el.textContent?.trim().slice(0, 100)\n }));\n }\n var NAMED_KEYS = {\n Enter: { code: \"Enter\", keyCode: 13 },\n Tab: { code: \"Tab\", keyCode: 9 },\n Escape: { code: \"Escape\", keyCode: 27 },\n Backspace: { code: \"Backspace\", keyCode: 8 },\n Delete: { code: \"Delete\", keyCode: 46 },\n ArrowUp: { code: \"ArrowUp\", keyCode: 38 },\n ArrowDown: { code: \"ArrowDown\", keyCode: 40 },\n ArrowLeft: { code: \"ArrowLeft\", keyCode: 37 },\n ArrowRight: { code: \"ArrowRight\", keyCode: 39 },\n \" \": { code: \"Space\", keyCode: 32 }\n };\n function pressKey(selector, key) {\n const el = getEl(selector);\n el.focus?.();\n const named = NAMED_KEYS[key];\n const keyCode = named ? named.keyCode : key.length === 1 ? key.toUpperCase().charCodeAt(0) : 0;\n const code = named ? named.code : key.length === 1 ? `Key${key.toUpperCase()}` : key;\n const init = {\n key,\n code,\n keyCode,\n which: keyCode,\n bubbles: true,\n cancelable: true\n };\n el.dispatchEvent(new KeyboardEvent(\"keydown\", init));\n if (key.length === 1) el.dispatchEvent(new KeyboardEvent(\"keypress\", init));\n el.dispatchEvent(new KeyboardEvent(\"keyup\", init));\n return { tag: el.tagName.toLowerCase(), key };\n }\n var MAX_HTML_CHARS = 5e4;\n function getHtml(selector) {\n const el = getEl(selector);\n const html = el.outerHTML;\n const truncated = html.length > MAX_HTML_CHARS;\n return {\n tag: el.tagName.toLowerCase(),\n html: truncated ? html.slice(0, MAX_HTML_CHARS) + \"\\u2026[truncated]\" : html,\n truncated\n };\n }\n function getPageInfo() {\n return {\n url: window.location.href,\n title: document.title,\n readyState: document.readyState,\n viewport: { width: window.innerWidth, height: window.innerHeight },\n scroll: { x: window.scrollX, y: window.scrollY },\n userAgent: navigator.userAgent\n };\n }\n var PREVIEW_NOTE = \"Live preview only \\u2014 applied to the running DOM, not saved to source, and reset on reload/HMR. Once the user is happy, edit the actual source to make it permanent.\";\n var CLOBBER_WARNING = \"This may be reverted on the next framework render (React/Vue/etc. re-render this element from component state). If it snaps back, change it in the source instead of here.\";\n var FRAMEWORK_OWNED_ATTRS = /* @__PURE__ */ new Set([\"class\", \"style\", \"value\", \"checked\", \"disabled\", \"selected\"]);\n var overrides = [];\n function setStyle(selector, properties) {\n const el = getEl(selector);\n if (!el.style) throw new Error(`element \"${selector}\" has no style (not an HTMLElement)`);\n const applied = {};\n for (const [prop, value] of Object.entries(properties)) {\n const prevValue = el.style.getPropertyValue(prop);\n const prevPriority = el.style.getPropertyPriority(prop);\n overrides.push(() => {\n if (prevValue) el.style.setProperty(prop, prevValue, prevPriority);\n else el.style.removeProperty(prop);\n });\n el.style.setProperty(prop, value);\n applied[prop] = el.style.getPropertyValue(prop);\n }\n return { tag: el.tagName.toLowerCase(), applied, note: PREVIEW_NOTE };\n }\n function setAttribute(selector, name, value) {\n const el = getEl(selector);\n const had = el.hasAttribute(name);\n const prev = had ? el.getAttribute(name) : null;\n overrides.push(() => {\n if (had) el.setAttribute(name, prev ?? \"\");\n else el.removeAttribute(name);\n });\n if (value === null) el.removeAttribute(name);\n else el.setAttribute(name, value);\n const result = {\n tag: el.tagName.toLowerCase(),\n name,\n value: value === null ? null : el.getAttribute(name),\n removed: value === null,\n note: PREVIEW_NOTE\n };\n if (FRAMEWORK_OWNED_ATTRS.has(name.toLowerCase())) result.frameworkWarning = CLOBBER_WARNING;\n return result;\n }\n function setText(selector, text) {\n const el = getEl(selector);\n const prev = el.textContent;\n overrides.push(() => {\n el.textContent = prev;\n });\n el.textContent = text;\n return {\n tag: el.tagName.toLowerCase(),\n text,\n note: PREVIEW_NOTE,\n frameworkWarning: CLOBBER_WARNING\n };\n }\n function resetOverrides() {\n const count = overrides.length;\n while (overrides.length) overrides.pop()();\n return { reverted: count, note: \"All bridge-applied DOM changes since connect have been undone (best effort \\u2014 elements re-created by the framework since may not roll back).\" };\n }\n function isCommand(v) {\n return typeof v === \"object\" && v !== null && v.type === \"command\" && typeof v.id === \"string\";\n }\n\n // src/bridge.ts\n var DEFAULT_SERVER_URL = \"ws://localhost:8765\";\n var FeedthroughBridge = class {\n constructor(options = {}) {\n const url = options.serverUrl ?? DEFAULT_SERVER_URL;\n const reconnectDelay = options.reconnectDelay ?? 2e3;\n this.consoleInterceptor = new ConsoleInterceptor();\n this.networkInterceptor = new NetworkInterceptor();\n this.transport = new Transport(\n url,\n (msg) => this.commandHandler.handle(msg),\n (connected) => {\n if (connected) this.transport.send({ type: \"hello\", url: window.location.href });\n },\n reconnectDelay\n );\n this.commandHandler = new CommandHandler(this.transport, this.consoleInterceptor, this.networkInterceptor);\n }\n connect() {\n this.consoleInterceptor.install();\n this.networkInterceptor.install();\n this.transport.connect();\n }\n destroy() {\n this.transport.destroy();\n this.consoleInterceptor.uninstall();\n this.networkInterceptor.uninstall();\n }\n };\n\n // src/browser.ts\n window.__feedthrough = new FeedthroughBridge(window.__feedthroughOptions ?? {});\n window.__feedthrough.connect();\n})();\n"; | ||
| export const bridgeBundle = "\"use strict\";\n(() => {\n // src/commands.ts\n var CommandHandler = class {\n constructor(transport, console2, network) {\n this.transport = transport;\n this.console = console2;\n this.network = network;\n }\n handle(msg) {\n if (!isCommand(msg)) return;\n let value;\n let error;\n try {\n value = this.dispatch(msg);\n } catch (e) {\n error = e instanceof Error ? e.message : String(e);\n }\n const result = {\n type: \"result\",\n ts: Date.now(),\n commandId: msg.id,\n ok: error === void 0,\n value,\n error\n };\n this.transport.send(result);\n }\n dispatch(cmd) {\n switch (cmd.action) {\n case \"click\":\n return clickEl(cmd.selector);\n case \"fill\":\n return fillEl(cmd.selector, cmd.value);\n case \"hover\":\n return hoverEl(cmd.selector);\n case \"inspect\":\n return inspectEl(cmd.selector, cmd.properties);\n case \"query_dom\":\n return queryDom(cmd.selector);\n case \"get_console_logs\":\n return this.console.getLogs({\n limit: cmd.limit,\n levels: cmd.levels,\n match: cmd.match,\n since: cmd.since\n });\n case \"get_network_requests\":\n return this.network.getRequests(cmd.filter, cmd.since);\n case \"press_key\":\n return pressKey(cmd.selector, cmd.key);\n case \"get_html\":\n return getHtml(cmd.selector);\n case \"get_page_info\":\n return getPageInfo();\n case \"set_style\":\n return setStyle(cmd.selector, cmd.properties);\n case \"set_attribute\":\n return setAttribute(cmd.selector, cmd.name, cmd.value);\n case \"set_text\":\n return setText(cmd.selector, cmd.text);\n case \"reset_overrides\":\n return resetOverrides();\n }\n }\n };\n function getEl(selector) {\n const el = document.querySelector(selector);\n if (!el) throw new Error(`no element matches \"${selector}\"`);\n return el;\n }\n function clickEl(selector) {\n const el = getEl(selector);\n el.click();\n return { tag: el.tagName.toLowerCase(), id: el.id || null };\n }\n function fillEl(selector, value) {\n const el = getEl(selector);\n el.focus();\n const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : el instanceof HTMLSelectElement ? HTMLSelectElement.prototype : HTMLInputElement.prototype;\n const nativeSetter = Object.getOwnPropertyDescriptor(proto, \"value\")?.set;\n if (nativeSetter) {\n nativeSetter.call(el, value);\n } else {\n el.value = value;\n }\n el.dispatchEvent(new Event(\"input\", { bubbles: true }));\n el.dispatchEvent(new Event(\"change\", { bubbles: true }));\n return { tag: el.tagName.toLowerCase(), value };\n }\n function hoverEl(selector) {\n const el = getEl(selector);\n el.dispatchEvent(new MouseEvent(\"mouseover\", { bubbles: true }));\n el.dispatchEvent(new MouseEvent(\"mouseenter\", { bubbles: true }));\n return { tag: el.tagName.toLowerCase() };\n }\n var DEFAULT_STYLE_PROPS = [\n \"display\",\n \"position\",\n \"visibility\",\n \"opacity\",\n \"z-index\",\n \"box-sizing\",\n \"top\",\n \"right\",\n \"bottom\",\n \"left\",\n \"width\",\n \"height\",\n \"margin\",\n \"padding\",\n \"border\",\n \"color\",\n \"background-color\",\n \"font-family\",\n \"font-size\",\n \"font-weight\",\n \"line-height\",\n \"text-align\",\n \"overflow\",\n \"cursor\",\n \"pointer-events\",\n \"flex\",\n \"flex-direction\",\n \"justify-content\",\n \"align-items\",\n \"gap\",\n \"grid-template-columns\",\n \"transform\"\n ];\n function inspectEl(selector, properties) {\n const el = getEl(selector);\n const rect = el.getBoundingClientRect();\n const cs = window.getComputedStyle(el);\n const result = {\n tag: el.tagName.toLowerCase(),\n id: el.id || null,\n classes: Array.from(el.classList),\n attributes: Object.fromEntries(Array.from(el.attributes).map((a) => [a.name, a.value])),\n textContent: el.textContent?.trim().slice(0, 200),\n rect: {\n top: rect.top,\n right: rect.right,\n bottom: rect.bottom,\n left: rect.left,\n width: rect.width,\n height: rect.height,\n x: rect.x,\n y: rect.y\n },\n scroll: { x: window.scrollX, y: window.scrollY },\n inViewport: rect.bottom > 0 && rect.right > 0 && rect.top < window.innerHeight && rect.left < window.innerWidth,\n styles: pickStyles(cs, DEFAULT_STYLE_PROPS)\n };\n result.path = ancestorChain(el);\n const overflow = overflowInfo(el);\n if (overflow) result.overflow = overflow;\n const clipped = clippedByAncestor(el, rect, cs);\n if (clipped) result.clipped = clipped;\n const vis = effectiveVisibility(el, rect, cs);\n result.visible = vis.visible;\n if (!vis.visible) result.hiddenReason = vis.reason;\n const occ = occlusionInfo(el, rect);\n if (occ) {\n result.hittable = occ.hittable;\n if (occ.occludedBy) result.occludedBy = occ.occludedBy;\n }\n const a11y = accessibilityInfo(el);\n if (a11y) result.a11y = a11y;\n const pseudo = pseudoContent(el);\n if (pseudo) result.pseudo = pseudo;\n const state = elementState(el);\n if (state) result.state = state;\n if (properties && properties.length > 0) {\n const requested = {};\n for (const p of properties) requested[p] = cs.getPropertyValue(p);\n result.requested = requested;\n }\n return result;\n }\n function overflowInfo(el) {\n const { scrollWidth, clientWidth, scrollHeight, clientHeight } = el;\n const x = scrollWidth > clientWidth;\n const y = scrollHeight > clientHeight;\n if (!x && !y) return void 0;\n return { x, y, scrollWidth, clientWidth, scrollHeight, clientHeight };\n }\n function refString(el) {\n const tag = el.tagName.toLowerCase();\n const id = el.id ? `#${el.id}` : \"\";\n const all = Array.from(el.classList);\n const classes = all.slice(0, 3).map((c) => `.${c}`).join(\"\");\n const more = all.length > 3 ? \"\\u2026\" : \"\";\n return tag + id + classes + more;\n }\n function chainRef(el) {\n const tag = el.tagName.toLowerCase();\n if (el.id) return `${tag}#${el.id}`;\n const cls = el.classList[0];\n return cls ? `${tag}.${cls}` : tag;\n }\n function ancestorChain(el) {\n const maxDepth = 6;\n const parts = [];\n let node = el;\n while (node) {\n parts.unshift(chainRef(node));\n node = node.parentElement;\n if (parts.length >= maxDepth && node) {\n parts.unshift(\"\\u2026\");\n break;\n }\n }\n return parts.join(\" > \");\n }\n var MAX_ANCESTOR_WALK = 50;\n function establishesFixedContainingBlock(cs) {\n if (cs.transform !== \"none\") return true;\n if (cs.perspective !== \"none\") return true;\n if (cs.filter !== \"none\") return true;\n const backdrop = cs.getPropertyValue(\"backdrop-filter\");\n if (backdrop && backdrop !== \"none\") return true;\n if (/transform|perspective|filter/.test(cs.willChange)) return true;\n if (/paint|layout|strict|content/.test(cs.contain)) return true;\n return false;\n }\n function clippedByAncestor(el, rect, cs) {\n const tol = 1;\n let clipping = cs.position !== \"fixed\";\n let node = el.parentElement;\n for (let depth = 0; node && depth < MAX_ANCESTOR_WALK; node = node.parentElement, depth++) {\n const acs = window.getComputedStyle(node);\n if (!clipping) {\n if (establishesFixedContainingBlock(acs)) clipping = true;\n else continue;\n }\n const clipsX = acs.overflowX !== \"visible\";\n const clipsY = acs.overflowY !== \"visible\";\n if (!clipsX && !clipsY) continue;\n const ar = node.getBoundingClientRect();\n const edges = [];\n if (clipsY && rect.top < ar.top - tol) edges.push(\"top\");\n if (clipsX && rect.right > ar.right + tol) edges.push(\"right\");\n if (clipsY && rect.bottom > ar.bottom + tol) edges.push(\"bottom\");\n if (clipsX && rect.left < ar.left - tol) edges.push(\"left\");\n if (edges.length > 0) return { by: refString(node), edges };\n }\n return void 0;\n }\n function pseudoContent(el) {\n const out = {};\n for (const pseudo of [\"::before\", \"::after\"]) {\n const content = window.getComputedStyle(el, pseudo).content;\n if (content && content !== \"none\" && content !== \"normal\") {\n out[pseudo] = content.length > 200 ? `${content.slice(0, 200)}\\u2026` : content;\n }\n }\n return Object.keys(out).length > 0 ? out : void 0;\n }\n function occlusionInfo(el, rect) {\n if (rect.width === 0 || rect.height === 0) return void 0;\n const left = Math.max(rect.left, 0);\n const top0 = Math.max(rect.top, 0);\n const right = Math.min(rect.right, window.innerWidth - 1);\n const bottom = Math.min(rect.bottom, window.innerHeight - 1);\n if (right < left || bottom < top0) return void 0;\n const cx = (left + right) / 2;\n const cy = (top0 + bottom) / 2;\n const top = document.elementFromPoint(cx, cy);\n if (!top) return void 0;\n if (top === el || el.contains(top)) return { hittable: true };\n return {\n hittable: false,\n occludedBy: {\n tag: top.tagName.toLowerCase(),\n id: top.id || null,\n classes: Array.from(top.classList).slice(0, 3)\n }\n };\n }\n function effectiveVisibility(el, rect, cs) {\n if (cs.display === \"none\") return { visible: false, reason: \"display:none\" };\n let node = el.parentElement;\n for (let depth = 0; node && depth < MAX_ANCESTOR_WALK; node = node.parentElement, depth++) {\n const acs = window.getComputedStyle(node);\n if (acs.display === \"none\")\n return { visible: false, reason: `ancestor ${refString(node)} display:none` };\n if (parseFloat(acs.opacity) === 0)\n return { visible: false, reason: `ancestor ${refString(node)} opacity:0` };\n if (node.getAttribute(\"aria-hidden\") === \"true\")\n return { visible: false, reason: `ancestor ${refString(node)} aria-hidden` };\n }\n if (cs.visibility === \"hidden\" || cs.visibility === \"collapse\")\n return { visible: false, reason: `visibility:${cs.visibility}` };\n if (parseFloat(cs.opacity) === 0) return { visible: false, reason: \"opacity:0\" };\n if (el.getAttribute(\"aria-hidden\") === \"true\") return { visible: false, reason: \"aria-hidden\" };\n if (rect.width === 0 || rect.height === 0) return { visible: false, reason: \"zero-size\" };\n return { visible: true };\n }\n function pickStyles(cs, props) {\n const out = {};\n for (const p of props) {\n const v = cs.getPropertyValue(p);\n if (v) out[p] = v;\n }\n return out;\n }\n function hasNamingAttribute(el) {\n return !!el.getAttribute(\"aria-label\")?.trim() || !!el.getAttribute(\"aria-labelledby\")?.trim() || !!el.getAttribute(\"title\")?.trim();\n }\n function implicitRole(el) {\n const tag = el.tagName.toLowerCase();\n switch (tag) {\n case \"a\":\n case \"area\":\n return el.hasAttribute(\"href\") ? \"link\" : null;\n case \"button\":\n return \"button\";\n case \"input\": {\n const t = (el.getAttribute(\"type\") || \"text\").toLowerCase();\n if (el.hasAttribute(\"list\") && [\"text\", \"search\", \"email\", \"tel\", \"url\"].includes(t)) {\n return \"combobox\";\n }\n const map = {\n checkbox: \"checkbox\",\n radio: \"radio\",\n range: \"slider\",\n number: \"spinbutton\",\n button: \"button\",\n submit: \"button\",\n reset: \"button\",\n image: \"button\",\n search: \"searchbox\",\n email: \"textbox\",\n tel: \"textbox\",\n url: \"textbox\",\n text: \"textbox\"\n };\n return map[t] ?? null;\n }\n case \"select\": {\n const size = Number(el.getAttribute(\"size\") || \"0\");\n return el.hasAttribute(\"multiple\") || size > 1 ? \"listbox\" : \"combobox\";\n }\n case \"textarea\":\n return \"textbox\";\n case \"img\":\n return el.getAttribute(\"alt\") === \"\" ? \"presentation\" : \"img\";\n case \"nav\":\n return \"navigation\";\n case \"main\":\n return \"main\";\n case \"header\":\n return el.closest(\"article, aside, main, nav, section\") ? null : \"banner\";\n case \"footer\":\n return el.closest(\"article, aside, main, nav, section\") ? null : \"contentinfo\";\n case \"aside\": {\n const scoped = el.parentElement?.closest(\"article, aside, main, nav, section\");\n return !scoped || hasNamingAttribute(el) ? \"complementary\" : null;\n }\n case \"section\":\n return hasNamingAttribute(el) ? \"region\" : null;\n case \"article\":\n return \"article\";\n case \"dialog\":\n return \"dialog\";\n case \"form\":\n return hasNamingAttribute(el) ? \"form\" : null;\n case \"table\":\n return \"table\";\n case \"ul\":\n case \"ol\":\n return \"list\";\n case \"li\":\n return \"listitem\";\n case \"h1\":\n case \"h2\":\n case \"h3\":\n case \"h4\":\n case \"h5\":\n case \"h6\":\n return \"heading\";\n default:\n return null;\n }\n }\n var NAME_FROM_CONTENT = /* @__PURE__ */ new Set([\n \"button\",\n \"checkbox\",\n \"radio\",\n \"switch\",\n \"link\",\n \"menuitem\",\n \"menuitemcheckbox\",\n \"menuitemradio\",\n \"option\",\n \"tab\",\n \"treeitem\",\n \"heading\",\n \"cell\",\n \"gridcell\",\n \"columnheader\",\n \"rowheader\",\n \"row\",\n \"tooltip\"\n ]);\n function accessibleName(el, role) {\n const labelledby = el.getAttribute(\"aria-labelledby\");\n if (labelledby) {\n const txt = labelledby.split(/\\s+/).map((id) => document.getElementById(id)?.textContent?.trim()).filter(Boolean).join(\" \");\n if (txt) return txt.slice(0, 200);\n }\n const label = el.getAttribute(\"aria-label\")?.trim();\n if (label) return label.slice(0, 200);\n if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {\n if (el.id) {\n const forLabel = document.querySelector(`label[for=\"${CSS.escape(el.id)}\"]`);\n const txt = forLabel?.textContent?.trim();\n if (txt) return txt.slice(0, 200);\n }\n const wrapping = el.closest(\"label\")?.textContent?.trim();\n if (wrapping) return wrapping.slice(0, 200);\n if (el instanceof HTMLInputElement && el.placeholder) return el.placeholder.slice(0, 200);\n }\n if (el instanceof HTMLImageElement && el.alt) return el.alt.slice(0, 200);\n const title = el.getAttribute(\"title\")?.trim();\n if (title) return title.slice(0, 200);\n if (role && NAME_FROM_CONTENT.has(role.trim().split(/\\s+/)[0])) {\n const text = el.textContent?.trim();\n if (text) return text.slice(0, 200);\n }\n return void 0;\n }\n function accessibilityInfo(el) {\n const a11y = {};\n const role = el.getAttribute(\"role\") || implicitRole(el);\n if (role) a11y.role = role;\n const name = accessibleName(el, role);\n if (name) a11y.name = name;\n const states = {};\n for (const attr of [\n \"aria-expanded\",\n \"aria-checked\",\n \"aria-selected\",\n \"aria-pressed\",\n \"aria-current\",\n \"aria-disabled\"\n ]) {\n const v = el.getAttribute(attr);\n if (v !== null) states[attr.slice(5)] = v === \"true\" ? true : v === \"false\" ? false : v;\n }\n if (el.getAttribute(\"aria-hidden\") === \"true\") states.hidden = true;\n if (\"disabled\" in el && el.disabled) states.disabled = true;\n const tabindex = el.getAttribute(\"tabindex\");\n if (tabindex !== null) {\n const n = Number(tabindex);\n states.tabindex = Number.isNaN(n) ? tabindex : n;\n }\n if (Object.keys(states).length > 0) a11y.states = states;\n return Object.keys(a11y).length > 0 ? a11y : void 0;\n }\n function elementState(el) {\n const s = {};\n if (el instanceof HTMLInputElement) {\n s.value = capValue(el.value);\n s.type = el.type;\n s.checked = el.checked;\n s.disabled = el.disabled;\n s.readOnly = el.readOnly;\n s.required = el.required;\n if (el.placeholder) s.placeholder = el.placeholder;\n if (el.validationMessage) s.validationMessage = el.validationMessage;\n } else if (el instanceof HTMLTextAreaElement) {\n s.value = capValue(el.value);\n s.disabled = el.disabled;\n s.readOnly = el.readOnly;\n } else if (el instanceof HTMLSelectElement) {\n s.value = el.value;\n s.selectedIndex = el.selectedIndex;\n s.disabled = el.disabled;\n } else if (el instanceof HTMLButtonElement) {\n s.type = el.type;\n s.disabled = el.disabled;\n } else if (el instanceof HTMLAnchorElement) {\n s.href = el.href;\n }\n if (el instanceof HTMLElement && Object.keys(el.dataset).length > 0) {\n s.dataset = { ...el.dataset };\n }\n return Object.keys(s).length > 0 ? s : void 0;\n }\n function capValue(v) {\n return v.length > 1e3 ? `${v.slice(0, 1e3)}\\u2026[truncated]` : v;\n }\n function queryDom(selector) {\n return Array.from(document.querySelectorAll(selector)).map((el) => ({\n tag: el.tagName.toLowerCase(),\n id: el.id || null,\n classes: Array.from(el.classList),\n textContent: el.textContent?.trim().slice(0, 100)\n }));\n }\n var NAMED_KEYS = {\n Enter: { code: \"Enter\", keyCode: 13 },\n Tab: { code: \"Tab\", keyCode: 9 },\n Escape: { code: \"Escape\", keyCode: 27 },\n Backspace: { code: \"Backspace\", keyCode: 8 },\n Delete: { code: \"Delete\", keyCode: 46 },\n ArrowUp: { code: \"ArrowUp\", keyCode: 38 },\n ArrowDown: { code: \"ArrowDown\", keyCode: 40 },\n ArrowLeft: { code: \"ArrowLeft\", keyCode: 37 },\n ArrowRight: { code: \"ArrowRight\", keyCode: 39 },\n \" \": { code: \"Space\", keyCode: 32 }\n };\n function pressKey(selector, key) {\n const el = getEl(selector);\n el.focus?.();\n const named = NAMED_KEYS[key];\n const keyCode = named ? named.keyCode : key.length === 1 ? key.toUpperCase().charCodeAt(0) : 0;\n const code = named ? named.code : key.length === 1 ? `Key${key.toUpperCase()}` : key;\n const init = {\n key,\n code,\n keyCode,\n which: keyCode,\n bubbles: true,\n cancelable: true\n };\n el.dispatchEvent(new KeyboardEvent(\"keydown\", init));\n if (key.length === 1) el.dispatchEvent(new KeyboardEvent(\"keypress\", init));\n el.dispatchEvent(new KeyboardEvent(\"keyup\", init));\n return { tag: el.tagName.toLowerCase(), key };\n }\n var MAX_HTML_CHARS = 5e4;\n function getHtml(selector) {\n const el = getEl(selector);\n const html = el.outerHTML;\n const truncated = html.length > MAX_HTML_CHARS;\n return {\n tag: el.tagName.toLowerCase(),\n html: truncated ? `${html.slice(0, MAX_HTML_CHARS)}\\u2026[truncated]` : html,\n truncated\n };\n }\n function getPageInfo() {\n return {\n url: window.location.href,\n title: document.title,\n readyState: document.readyState,\n viewport: { width: window.innerWidth, height: window.innerHeight },\n scroll: { x: window.scrollX, y: window.scrollY },\n userAgent: navigator.userAgent\n };\n }\n var PREVIEW_NOTE = \"Live preview only \\u2014 applied to the running DOM, not saved to source, and reset on reload/HMR. Once the user is happy, edit the actual source to make it permanent.\";\n var CLOBBER_WARNING = \"This may be reverted on the next framework render (React/Vue/etc. re-render this element from component state). If it snaps back, change it in the source instead of here.\";\n var FRAMEWORK_OWNED_ATTRS = /* @__PURE__ */ new Set([\n \"class\",\n \"style\",\n \"value\",\n \"checked\",\n \"disabled\",\n \"selected\"\n ]);\n var overrides = [];\n function setStyle(selector, properties) {\n const el = getEl(selector);\n if (!el.style) throw new Error(`element \"${selector}\" has no style (not an HTMLElement)`);\n const applied = {};\n for (const [prop, value] of Object.entries(properties)) {\n const prevValue = el.style.getPropertyValue(prop);\n const prevPriority = el.style.getPropertyPriority(prop);\n overrides.push(() => {\n if (prevValue) el.style.setProperty(prop, prevValue, prevPriority);\n else el.style.removeProperty(prop);\n });\n el.style.setProperty(prop, value);\n applied[prop] = el.style.getPropertyValue(prop);\n }\n return { tag: el.tagName.toLowerCase(), applied, note: PREVIEW_NOTE };\n }\n function setAttribute(selector, name, value) {\n const el = getEl(selector);\n const had = el.hasAttribute(name);\n const prev = had ? el.getAttribute(name) : null;\n overrides.push(() => {\n if (had) el.setAttribute(name, prev ?? \"\");\n else el.removeAttribute(name);\n });\n if (value === null) el.removeAttribute(name);\n else el.setAttribute(name, value);\n const result = {\n tag: el.tagName.toLowerCase(),\n name,\n value: value === null ? null : el.getAttribute(name),\n removed: value === null,\n note: PREVIEW_NOTE\n };\n if (FRAMEWORK_OWNED_ATTRS.has(name.toLowerCase())) result.frameworkWarning = CLOBBER_WARNING;\n return result;\n }\n function setText(selector, text) {\n const el = getEl(selector);\n const prev = el.textContent;\n overrides.push(() => {\n el.textContent = prev;\n });\n el.textContent = text;\n return {\n tag: el.tagName.toLowerCase(),\n text,\n note: PREVIEW_NOTE,\n frameworkWarning: CLOBBER_WARNING\n };\n }\n function resetOverrides() {\n const count = overrides.length;\n while (overrides.length) overrides.pop()?.();\n return {\n reverted: count,\n note: \"All bridge-applied DOM changes since connect have been undone (best effort \\u2014 elements re-created by the framework since may not roll back).\"\n };\n }\n function isCommand(v) {\n return typeof v === \"object\" && v !== null && v.type === \"command\" && typeof v.id === \"string\";\n }\n\n // src/interceptors/console.ts\n var STD_LEVELS = [\"log\", \"warn\", \"error\", \"info\", \"debug\"];\n var MAX_LOGS = 1e3;\n var MAX_ARG_CHARS = 1e4;\n var ConsoleInterceptor = class {\n constructor() {\n this.originals = /* @__PURE__ */ new Map();\n this.logs = [];\n this.counts = /* @__PURE__ */ new Map();\n this.timers = /* @__PURE__ */ new Map();\n }\n install() {\n const c = console;\n const record = (msg) => {\n this.logs.push(msg);\n if (this.logs.length > MAX_LOGS) this.logs.shift();\n };\n const rich = (method, level, args, extras = {}) => {\n record({\n type: \"console\",\n ts: Date.now(),\n level,\n method,\n args: args.map(serialize),\n ...extras\n });\n };\n const wrap = (name, fn) => {\n const orig = c[name];\n if (typeof orig !== \"function\") return;\n this.originals.set(name, orig.bind(console));\n c[name] = fn;\n };\n for (const level of STD_LEVELS) {\n const orig = c[level].bind(console);\n this.originals.set(level, orig);\n c[level] = (...args) => {\n orig(...args);\n record({ type: \"console\", ts: Date.now(), level, args: args.map(serialize) });\n };\n }\n wrap(\"dir\", (obj, options) => {\n this.originals.get(\"dir\")?.(obj, options);\n rich(\"dir\", \"log\", options === void 0 ? [obj] : [obj, options]);\n });\n wrap(\"table\", (data, columns) => {\n this.originals.get(\"table\")?.(data, columns);\n rich(\"table\", \"log\", columns === void 0 ? [data] : [data, columns]);\n });\n wrap(\"trace\", (...args) => {\n this.originals.get(\"trace\")?.(...args);\n rich(\"trace\", \"log\", args, { stack: captureStack() });\n });\n wrap(\"assert\", (condition, ...args) => {\n this.originals.get(\"assert\")?.(condition, ...args);\n if (condition) return;\n rich(\"assert\", \"error\", args.length ? args : [\"Assertion failed\"], { stack: captureStack() });\n });\n wrap(\"count\", (label) => {\n this.originals.get(\"count\")?.(label);\n const key = label == null ? \"default\" : String(label);\n const n = (this.counts.get(key) ?? 0) + 1;\n this.counts.set(key, n);\n rich(\"count\", \"log\", [`${key}: ${n}`]);\n });\n wrap(\"countReset\", (label) => {\n this.originals.get(\"countReset\")?.(label);\n const key = label == null ? \"default\" : String(label);\n this.counts.set(key, 0);\n rich(\"countReset\", \"log\", [`${key}: 0`]);\n });\n wrap(\"time\", (label) => {\n this.originals.get(\"time\")?.(label);\n const key = label == null ? \"default\" : String(label);\n this.timers.set(key, performance.now());\n });\n wrap(\"timeEnd\", (label) => {\n this.originals.get(\"timeEnd\")?.(label);\n const key = label == null ? \"default\" : String(label);\n const start = this.timers.get(key);\n if (start === void 0) {\n rich(\"timeEnd\", \"warn\", [`Timer \"${key}\" does not exist`]);\n return;\n }\n this.timers.delete(key);\n rich(\"timeEnd\", \"log\", [`${key}: ${(performance.now() - start).toFixed(3)}ms`]);\n });\n wrap(\"timeLog\", (label, ...args) => {\n this.originals.get(\"timeLog\")?.(label, ...args);\n const key = label == null ? \"default\" : String(label);\n const start = this.timers.get(key);\n if (start === void 0) {\n rich(\"timeLog\", \"warn\", [`Timer \"${key}\" does not exist`, ...args]);\n return;\n }\n rich(\"timeLog\", \"log\", [`${key}: ${(performance.now() - start).toFixed(3)}ms`, ...args]);\n });\n wrap(\"group\", (...args) => {\n this.originals.get(\"group\")?.(...args);\n rich(\"group\", \"log\", args);\n });\n wrap(\"groupCollapsed\", (...args) => {\n this.originals.get(\"groupCollapsed\")?.(...args);\n rich(\"groupCollapsed\", \"log\", args);\n });\n wrap(\"groupEnd\", () => {\n this.originals.get(\"groupEnd\")?.();\n rich(\"groupEnd\", \"log\", []);\n });\n wrap(\"clear\", () => {\n this.originals.get(\"clear\")?.();\n rich(\"clear\", \"log\", []);\n });\n this.onError = (e) => {\n record({\n type: \"console\",\n ts: Date.now(),\n level: \"error\",\n method: \"uncaught\",\n args: [serialize(e.message || \"Uncaught error\")],\n stack: e.error instanceof Error ? e.error.stack : void 0\n });\n };\n this.onRejection = (e) => {\n const reason = e.reason;\n record({\n type: \"console\",\n ts: Date.now(),\n level: \"error\",\n method: \"unhandledrejection\",\n args: [serialize(reason instanceof Error ? reason.message : reason)],\n stack: reason instanceof Error ? reason.stack : void 0\n });\n };\n window.addEventListener(\"error\", this.onError);\n window.addEventListener(\"unhandledrejection\", this.onRejection);\n }\n uninstall() {\n const c = console;\n for (const [name, original] of this.originals) {\n c[name] = original;\n }\n this.originals.clear();\n if (this.onError) window.removeEventListener(\"error\", this.onError);\n if (this.onRejection) window.removeEventListener(\"unhandledrejection\", this.onRejection);\n }\n getLogs(opts = {}) {\n let result = this.logs;\n if (opts.levels && opts.levels.length > 0) {\n const wanted = new Set(opts.levels);\n result = result.filter((m) => wanted.has(m.level));\n }\n if (opts.match) {\n const needle = opts.match.toLowerCase();\n result = result.filter((m) => JSON.stringify(m.args).toLowerCase().includes(needle));\n }\n if (opts.since !== void 0) {\n result = result.filter((m) => m.ts >= opts.since);\n }\n if (opts.limit !== void 0) {\n result = result.slice(-opts.limit);\n } else if (result === this.logs) {\n result = [...this.logs];\n }\n return result;\n }\n };\n function captureStack() {\n const raw = new Error().stack ?? \"\";\n const lines = raw.split(\"\\n\");\n return lines.length > 2 ? lines.slice(2).join(\"\\n\") : raw;\n }\n function serialize(v) {\n if (v === null || v === void 0) return String(v);\n if (typeof v === \"string\") return cap(v);\n if (typeof v !== \"object\") return v;\n if (v instanceof Error) return { name: v.name, message: v.message, stack: v.stack };\n try {\n const json = JSON.stringify(v);\n return json.length > MAX_ARG_CHARS ? cap(json) : JSON.parse(json);\n } catch {\n return String(v);\n }\n }\n function cap(text) {\n return text.length > MAX_ARG_CHARS ? `${text.slice(0, MAX_ARG_CHARS)}\\u2026[truncated]` : text;\n }\n\n // src/interceptors/network.ts\n var MAX_REQUESTS = 1e3;\n var MAX_BODY_CHARS = 1e4;\n var MAX_BODY_BYTES = 64 * 1024;\n var NetworkInterceptor = class {\n constructor() {\n this.requests = [];\n this.origFetch = null;\n this.OrigXHR = null;\n }\n // Captured into a local buffer only — never streamed. An agent pulls requests\n // on demand via get_network_requests; pushing every request/response over the\n // WebSocket would be wasted traffic since nothing subscribes to it.\n install() {\n this.interceptFetch();\n this.interceptXHR();\n }\n interceptFetch() {\n this.origFetch = window.fetch.bind(window);\n const orig = this.origFetch;\n const requests = this.requests;\n window.fetch = async (input, init) => {\n const url = input instanceof Request ? input.url : String(input);\n const method = resolveMethod(input, init);\n const requestId = uid();\n const startTs = Date.now();\n const requestHeaders = mergeRequestHeaders(input, init);\n const requestBody = serializeRequestBody(init?.body);\n const pending = {\n type: \"network\",\n ts: startTs,\n requestId,\n method,\n url,\n requestHeaders,\n requestBody\n };\n pushRequest(requests, pending);\n try {\n const res = await orig(input, init);\n Object.assign(pending, {\n ts: Date.now(),\n status: res.status,\n duration: Date.now() - startTs,\n responseHeaders: headersToObject(res.headers)\n });\n captureResponseBody(res.clone(), pending);\n return res;\n } catch (e) {\n Object.assign(pending, {\n ts: Date.now(),\n duration: Date.now() - startTs,\n error: e instanceof Error ? e.message : String(e)\n });\n throw e;\n }\n };\n }\n interceptXHR() {\n this.OrigXHR = window.XMLHttpRequest;\n const OrigXHR = this.OrigXHR;\n const requests = this.requests;\n function FeedthroughXHR() {\n const xhr = new OrigXHR();\n let method = \"GET\";\n let url = \"\";\n const requestHeaders = {};\n const origOpen = xhr.open.bind(xhr);\n xhr.open = (m, u, async, user, password) => {\n method = m.toUpperCase();\n url = String(u);\n origOpen(m, String(u), async ?? true, user, password);\n };\n const origSetHeader = xhr.setRequestHeader.bind(xhr);\n xhr.setRequestHeader = (name, value) => {\n requestHeaders[name] = String(value);\n origSetHeader(name, value);\n };\n const origSend = xhr.send.bind(xhr);\n xhr.send = (body) => {\n const requestId = uid();\n const startTs = Date.now();\n const requestBody = serializeRequestBody(body ?? void 0);\n const pending = {\n type: \"network\",\n ts: startTs,\n requestId,\n method,\n url,\n requestHeaders: Object.keys(requestHeaders).length ? { ...requestHeaders } : void 0,\n requestBody\n };\n pushRequest(requests, pending);\n xhr.addEventListener(\"loadend\", () => {\n let responseBody;\n try {\n if (xhr.responseType === \"\" || xhr.responseType === \"text\") {\n responseBody = capText(xhr.responseText);\n } else {\n responseBody = `[XHR responseType=${xhr.responseType}]`;\n }\n } catch {\n }\n Object.assign(pending, {\n ts: Date.now(),\n status: xhr.status,\n duration: Date.now() - startTs,\n responseBody,\n responseHeaders: parseRawHeaders(xhr.getAllResponseHeaders())\n });\n });\n origSend(body);\n };\n return xhr;\n }\n FeedthroughXHR.prototype = OrigXHR.prototype;\n window.XMLHttpRequest = FeedthroughXHR;\n }\n uninstall() {\n if (this.origFetch) window.fetch = this.origFetch;\n if (this.OrigXHR) window.XMLHttpRequest = this.OrigXHR;\n }\n getRequests(filter, since) {\n let all = [...this.requests];\n if (since !== void 0) all = all.filter((r) => r.ts >= since);\n if (!filter) return all;\n const lower = filter.toLowerCase();\n return all.filter((r) => r.url.toLowerCase().includes(lower) || r.method.toLowerCase() === lower);\n }\n };\n function resolveMethod(input, init) {\n return (init?.method ?? (input instanceof Request ? input.method : null) ?? \"GET\").toUpperCase();\n }\n function pushRequest(requests, msg) {\n requests.push(msg);\n if (requests.length > MAX_REQUESTS) requests.shift();\n }\n function mergeRequestHeaders(input, init) {\n const source = init?.headers ?? (input instanceof Request ? input.headers : void 0);\n if (!source) return void 0;\n const out = {};\n if (source instanceof Headers) {\n source.forEach((v, k) => {\n out[k] = v;\n });\n } else if (Array.isArray(source)) {\n for (const [k, v] of source) out[k] = String(v);\n } else {\n for (const [k, v] of Object.entries(source)) out[k] = String(v);\n }\n return Object.keys(out).length ? out : void 0;\n }\n function headersToObject(headers) {\n const out = {};\n headers.forEach((v, k) => {\n out[k] = v;\n });\n return out;\n }\n function parseRawHeaders(raw) {\n if (!raw) return void 0;\n const out = {};\n for (const line of raw.split(\"\\r\\n\")) {\n const idx = line.indexOf(\":\");\n if (idx <= 0) continue;\n const k = line.slice(0, idx).trim();\n const v = line.slice(idx + 1).trim();\n if (k) out[k] = v;\n }\n return Object.keys(out).length ? out : void 0;\n }\n function serializeRequestBody(body) {\n if (body == null) return void 0;\n if (typeof body === \"string\") return capText(body);\n if (body instanceof URLSearchParams) return capText(body.toString());\n if (body instanceof FormData) {\n const obj = {};\n body.forEach((v, k) => {\n obj[k] = v instanceof File ? `[File: ${v.name}, ${v.size} bytes]` : v;\n });\n try {\n return capText(JSON.stringify(obj));\n } catch {\n return \"[FormData]\";\n }\n }\n if (body instanceof Blob) return `[Blob: ${body.size} bytes, ${body.type || \"unknown\"}]`;\n if (body instanceof ArrayBuffer) return `[ArrayBuffer: ${body.byteLength} bytes]`;\n if (ArrayBuffer.isView(body)) return `[${body.constructor.name}: ${body.byteLength} bytes]`;\n if (body instanceof ReadableStream) return \"[ReadableStream]\";\n try {\n return capText(JSON.stringify(body));\n } catch {\n return String(body);\n }\n }\n function capText(text) {\n if (text.length <= MAX_BODY_CHARS) return text;\n return `${text.slice(0, MAX_BODY_CHARS)}\\u2026[truncated, ${text.length - MAX_BODY_CHARS} more chars]`;\n }\n function captureResponseBody(res, pending) {\n const ct = (res.headers.get(\"content-type\") ?? \"\").toLowerCase();\n if (isSkippable(ct)) {\n const len = res.headers.get(\"content-length\");\n pending.responseBody = `[${describe(ct)}${len ? `, ${len} bytes` : \"\"}, ${ct || \"no content-type\"}]`;\n return;\n }\n readBounded(res).then((body) => {\n pending.responseBody = body;\n }).catch(() => {\n });\n }\n async function readBounded(res) {\n if (!res.body) return capText(await res.text());\n const reader = res.body.getReader();\n const decoder = new TextDecoder();\n let text = \"\";\n let bytes = 0;\n let truncated = false;\n try {\n for (; ; ) {\n const { done, value } = await reader.read();\n if (done) break;\n bytes += value.byteLength;\n text += decoder.decode(value, { stream: true });\n if (bytes >= MAX_BODY_BYTES || text.length >= MAX_BODY_CHARS) {\n truncated = true;\n break;\n }\n }\n } finally {\n reader.cancel().catch(() => {\n });\n }\n if (!truncated) return capText(text);\n return `${text.slice(0, MAX_BODY_CHARS)}\\u2026[truncated]`;\n }\n function isSkippable(ct) {\n return /^(image|video|audio|font)\\//.test(ct) || ct.startsWith(\"application/octet-stream\") || ct.startsWith(\"application/pdf\") || ct.startsWith(\"application/zip\") || ct.startsWith(\"text/event-stream\") || // SSE — never-ending stream\n ct.startsWith(\"application/x-ndjson\");\n }\n function describe(ct) {\n if (ct.startsWith(\"text/event-stream\")) return \"event stream\";\n if (ct.startsWith(\"application/x-ndjson\")) return \"ndjson stream\";\n return \"binary\";\n }\n var counter = 0;\n function uid() {\n return `${Date.now()}-${++counter}`;\n }\n\n // src/transport.ts\n var MAX_QUEUE = 1e3;\n var Transport = class {\n constructor(url, onMessage, onStatus, reconnectDelay) {\n this.url = url;\n this.onMessage = onMessage;\n this.onStatus = onStatus;\n this.reconnectDelay = reconnectDelay;\n this.ws = null;\n this.queue = [];\n this.reconnectTimer = null;\n this.destroyed = false;\n }\n connect() {\n if (this.destroyed) return;\n const ws = new WebSocket(this.url);\n this.ws = ws;\n const isCurrent = () => this.ws === ws;\n ws.onopen = () => {\n if (!isCurrent()) return;\n this.onStatus(true);\n for (const msg of this.queue) ws.send(msg);\n this.queue = [];\n };\n ws.onmessage = (event) => {\n if (!isCurrent()) return;\n try {\n this.onMessage(JSON.parse(event.data));\n } catch {\n }\n };\n ws.onclose = () => {\n if (!isCurrent()) return;\n this.onStatus(false);\n if (!this.destroyed) {\n this.reconnectTimer = setTimeout(() => this.connect(), this.reconnectDelay);\n }\n };\n ws.onerror = () => {\n };\n }\n send(msg) {\n const serialized = JSON.stringify(msg);\n if (this.ws?.readyState === WebSocket.OPEN) {\n this.ws.send(serialized);\n } else {\n this.queue.push(serialized);\n if (this.queue.length > MAX_QUEUE) this.queue.shift();\n }\n }\n destroy() {\n this.destroyed = true;\n if (this.reconnectTimer !== null) clearTimeout(this.reconnectTimer);\n this.ws?.close();\n }\n };\n\n // src/bridge.ts\n var DEFAULT_SERVER_URL = \"ws://localhost:8765\";\n var FeedthroughBridge = class {\n constructor(options = {}) {\n const url = options.serverUrl ?? DEFAULT_SERVER_URL;\n const reconnectDelay = options.reconnectDelay ?? 2e3;\n this.consoleInterceptor = new ConsoleInterceptor();\n this.networkInterceptor = new NetworkInterceptor();\n this.transport = new Transport(\n url,\n (msg) => this.commandHandler.handle(msg),\n (connected) => {\n if (connected) this.transport.send({ type: \"hello\", url: window.location.href });\n },\n reconnectDelay\n );\n this.commandHandler = new CommandHandler(\n this.transport,\n this.consoleInterceptor,\n this.networkInterceptor\n );\n }\n connect() {\n this.consoleInterceptor.install();\n this.networkInterceptor.install();\n this.transport.connect();\n }\n destroy() {\n this.transport.destroy();\n this.consoleInterceptor.uninstall();\n this.networkInterceptor.uninstall();\n }\n };\n\n // src/browser.ts\n window.__feedthrough = new FeedthroughBridge(window.__feedthroughOptions ?? {});\n window.__feedthrough.connect();\n})();\n"; | ||
| //# sourceMappingURL=bundle.js.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"bundle.js","sourceRoot":"","sources":["../../src/generated/bundle.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,MAAM,CAAC,MAAM,YAAY,GAAG,q+6BAAq+6B,CAAC"} | ||
| {"version":3,"file":"bundle.js","sourceRoot":"","sources":["../../src/generated/bundle.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,MAAM,CAAC,MAAM,YAAY,GAAG,03xCAA03xC,CAAC"} |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE1C;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,aAAkB,+OAS3D;AAED,eAAO,MAAM,IAAI,6OAAqB,CAAC"} | ||
| {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAIvD,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE1C;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,aAAkB,+OAS3D;AAED,eAAO,MAAM,IAAI,6OAAqB,CAAC"} |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,IAAI,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAGrD,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE1C;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAAC,UAAyB,EAAE;IAC1D,OAAO,QAAQ,CAAC,MAAM,CAAC;QACrB,IAAI,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,EAAE;YAC5B,MAAM,IAAI,CAAC,aAAa,CAAC;gBACvB,OAAO,EAAE,iCAAiC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,YAAY,EAAE;aACtF,CAAC,CAAC;YACH,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC;QAClB,CAAC;KACF,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,MAAM,IAAI,GAAG,gBAAgB,EAAE,CAAC"} | ||
| {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,IAAI,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAErD,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE1C;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAAC,UAAyB,EAAE;IAC1D,OAAO,QAAQ,CAAC,MAAM,CAAC;QACrB,IAAI,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,EAAE;YAC5B,MAAM,IAAI,CAAC,aAAa,CAAC;gBACvB,OAAO,EAAE,iCAAiC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,YAAY,EAAE;aACtF,CAAC,CAAC;YACH,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC;QAClB,CAAC;KACF,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,MAAM,IAAI,GAAG,gBAAgB,EAAE,CAAC"} |
+7
-2
| { | ||
| "name": "@feedthrough/playwright", | ||
| "version": "0.1.0", | ||
| "version": "0.3.0", | ||
| "license": "MIT", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/feedthrough/feedthrough.git", | ||
| "directory": "packages/playwright" | ||
| }, | ||
| "type": "module", | ||
@@ -19,3 +24,3 @@ "main": "./dist/index.js", | ||
| "dependencies": { | ||
| "@feedthrough/core": "0.1.0" | ||
| "@feedthrough/core": "0.3.0" | ||
| }, | ||
@@ -22,0 +27,0 @@ "devDependencies": { |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
90809
34.88%292
32.73%2
-33.33%1
Infinity%+ Added
- Removed
Updated