@avasapp/agent-bridge 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -225
- package/dist/adapters/ably.cjs +22 -4
- package/dist/adapters/ably.d.cts +8 -1
- package/dist/adapters/expo-router.cjs +22 -1
- package/dist/adapters/expo-router.d.cts +5 -1
- package/dist/adapters/react-native-mmkv.cjs +1 -0
- package/dist/adapters/react-native-mmkv.d.cts +1 -1
- package/dist/adapters/react-native-webview.cjs +672 -0
- package/dist/adapters/react-native-webview.d.cts +79 -0
- package/dist/adapters/socket-io.cjs +3 -1
- package/dist/adapters/socket-io.d.cts +1 -1
- package/dist/adapters/tanstack-query.cjs +256 -17
- package/dist/adapters/tanstack-query.d.cts +1 -1
- package/dist/adapters/zustand.cjs +152 -12
- package/dist/adapters/zustand.d.cts +19 -3
- package/dist/{chunk-S2FYN2R5.js → chunk-AJD3XTNT.js} +223 -57
- package/dist/cli.js +817 -56
- package/dist/client/index.cjs +218 -56
- package/dist/client/index.d.ts +20 -2
- package/dist/client/index.js +1 -1
- package/dist/expo/index.d.cts +1 -1
- package/dist/network/index.cjs +254 -51
- package/dist/network/index.d.cts +9 -2
- package/dist/noop/index.cjs +3 -0
- package/dist/noop/react-native-webview.cjs +38 -0
- package/dist/realtime/index.cjs +2 -1
- package/dist/realtime/index.d.cts +3 -1
- package/dist/runtime/index.cjs +860 -100
- package/dist/runtime/index.d.cts +31 -5
- package/dist/{types-6kKQsLC1.d.cts → types-BmpykP1P.d.cts} +23 -1
- package/entries/react-native-webview.cjs +9 -0
- package/package.json +17 -6
- package/skills/agent-bridge/SKILL.md +196 -5
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/adapters/react-native-webview.ts
|
|
21
|
+
var react_native_webview_exports = {};
|
|
22
|
+
__export(react_native_webview_exports, {
|
|
23
|
+
useWebViewTools: () => useWebViewTools,
|
|
24
|
+
webviewTools: () => webviewTools
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(react_native_webview_exports);
|
|
27
|
+
var import_react = require("react");
|
|
28
|
+
|
|
29
|
+
// src/runtime/screen/settle.ts
|
|
30
|
+
var STATE = /* @__PURE__ */ Symbol.for("@avasapp/agent-bridge/commits");
|
|
31
|
+
var UPDATE_LANES = (1 << 22) - 1;
|
|
32
|
+
var hook = () => globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
33
|
+
var now = () => performance.now();
|
|
34
|
+
function install() {
|
|
35
|
+
const h = hook();
|
|
36
|
+
if (!h) return null;
|
|
37
|
+
const current = h.onCommitFiberRoot;
|
|
38
|
+
const existing = current?.[STATE];
|
|
39
|
+
if (existing) return existing;
|
|
40
|
+
const state = { commits: 0, listeners: /* @__PURE__ */ new Set() };
|
|
41
|
+
const wrapped = function(...args) {
|
|
42
|
+
state.commits += 1;
|
|
43
|
+
for (const listener of state.listeners) {
|
|
44
|
+
try {
|
|
45
|
+
listener();
|
|
46
|
+
} catch {
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return current?.apply(this, args);
|
|
50
|
+
};
|
|
51
|
+
Object.defineProperty(wrapped, STATE, { value: state });
|
|
52
|
+
h.onCommitFiberRoot = wrapped;
|
|
53
|
+
return state;
|
|
54
|
+
}
|
|
55
|
+
function busy() {
|
|
56
|
+
const h = hook();
|
|
57
|
+
if (!h?.renderers || !h.getFiberRoots) return false;
|
|
58
|
+
for (const id of h.renderers.keys()) {
|
|
59
|
+
for (const root of h.getFiberRoots(id)) {
|
|
60
|
+
const pending = root.pendingLanes;
|
|
61
|
+
if (typeof pending !== "number") continue;
|
|
62
|
+
if (pending & ~(root.suspendedLanes ?? 0) & UPDATE_LANES) return true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
var frame = () => new Promise((resolve) => {
|
|
68
|
+
const raf = globalThis.requestAnimationFrame;
|
|
69
|
+
if (raf) raf(() => resolve());
|
|
70
|
+
else setTimeout(resolve, 16);
|
|
71
|
+
});
|
|
72
|
+
async function settle(options = {}) {
|
|
73
|
+
const maxMs = options.maxMs ?? 500;
|
|
74
|
+
const t0 = now();
|
|
75
|
+
const state = install();
|
|
76
|
+
const start = state?.commits ?? 0;
|
|
77
|
+
const result = () => ({
|
|
78
|
+
commits: (state?.commits ?? 0) - start,
|
|
79
|
+
ms: Math.round((now() - t0) * 100) / 100
|
|
80
|
+
});
|
|
81
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
82
|
+
if (!state) return result();
|
|
83
|
+
let seen = state.commits;
|
|
84
|
+
if (seen === start && !busy()) return result();
|
|
85
|
+
while (now() - t0 < maxMs) {
|
|
86
|
+
await frame();
|
|
87
|
+
if (state.commits === seen && !busy()) break;
|
|
88
|
+
seen = state.commits;
|
|
89
|
+
}
|
|
90
|
+
return result();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/runtime/logs.ts
|
|
94
|
+
var STATE2 = /* @__PURE__ */ Symbol.for("@avasapp/agent-bridge/logs");
|
|
95
|
+
function logToBridge(level, message, env = globalThis) {
|
|
96
|
+
const state = env[STATE2];
|
|
97
|
+
state?.capture.record(level, [message]);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/runtime/screen/webview-mark.ts
|
|
101
|
+
var escape = (name) => encodeURIComponent(name).replace(/\*/g, "%2A");
|
|
102
|
+
var webViewMark = (name) => `/*agent-bridge:webview:${escape(name)}*/`;
|
|
103
|
+
|
|
104
|
+
// src/adapters/webview-page.generated.ts
|
|
105
|
+
var PAGE_BUNDLE = '(function(){"use strict";var __agentBridgePage=(()=>{var F=Object.defineProperty;var ee=Object.getOwnPropertyDescriptor;var te=Object.getOwnPropertyNames;var ne=Object.prototype.hasOwnProperty;var oe=(s,E)=>{for(var S in E)F(s,S,{get:E[S],enumerable:!0})},re=(s,E,S,f)=>{if(E&&typeof E=="object"||typeof E=="function")for(let y of te(E))!ne.call(s,y)&&y!==S&&F(s,y,{get:()=>E[y],enumerable:!(f=ee(E,y))||f.enumerable});return s};var ie=s=>re(F({},"__esModule",{value:!0}),s);var ce={};oe(ce,{bootMain:()=>se,pageMain:()=>ae});function se(s,E,S){if(s.__agentBridgeBoot)return;s.__agentBridgeBoot=!0;let f=s.JSON.stringify.bind(s.JSON),y=[],k=()=>{try{if(!s.ReactNativeWebView)return!1;for(;y.length;)E(y.shift());return!0}catch{return!0}},$=l=>{y.push(f({__agentBridge:1,t:S,...l})),k()};if(!k()){let l=0,m=s.setInterval(()=>{(k()||++l>400)&&s.clearInterval(m)},25)}s.document.addEventListener("DOMContentLoaded",k);let N=()=>({url:String(s.location.href),origin:String(s.location.origin)}),v=l=>$({kind:"state",state:l,...N()}),L=l=>{if(typeof l=="string")return l;if(l&&typeof l=="object"&&"message"in l){let m=l,x=`${m.name||"Error"}: ${m.message}`;return m.stack&&m.stack.startsWith(x)?m.stack:m.stack?`${x}\n${m.stack}`:x}try{let m=f(l);return m===void 0?String(l):m}catch{return String(l)}},A=l=>$({kind:"log",level:"error",message:l.slice(0,2e3)}),R=s.console.error;s.console.error=function(...l){return A(l.map(L).join(" ")),R.apply(this,l)},s.addEventListener("error",l=>{l.error?A(`Uncaught ${L(l.error)}`):A(`Uncaught ${l.message} (${l.filename}:${l.lineno})`)}),s.addEventListener("unhandledrejection",l=>A(`Unhandled promise rejection: ${L(l.reason)}`)),v("loading"),s.addEventListener("load",()=>v("loaded")),s.addEventListener("popstate",()=>v("navigated")),s.addEventListener("hashchange",()=>v("navigated"));for(let l of["pushState","replaceState"]){let m=s.history[l];s.history[l]=function(...x){let T=m.apply(this,x);return v("navigated"),T}}}function ae(s,E,S){let f=JSON.parse(E),y=String(s.location.origin),k=(e,t)=>{try{S(JSON.stringify(e?{__agentBridge:1,t:f.token,kind:"reply",nonce:f.nonce,origin:y,ok:e,result:t}:{__agentBridge:1,t:f.token,kind:"reply",nonce:f.nonce,origin:y,ok:e,error:t}))}catch{}},$=e=>k(!1,e instanceof Error?e.message:String(e));if(!f.allowed.includes(y)){k(!1,`Origin ${y} is not allowed (allowed: ${f.allowed.join(", ")||"none"})`);return}let N=s.document,v=["data-testid","data-test-id","data-cy"],L=["SCRIPT","STYLE","NOSCRIPT","TEMPLATE","HEAD","META","LINK","DATALIST"],A=["SPAN","B","I","EM","STRONG","SMALL","CODE","MARK","SUB","SUP","U","S","ABBR","CITE","Q","TIME"],R=["button","link","menuitem","tab","checkbox","switch","radio","option","menuitemcheckbox","menuitemradio"],l=["textbox","searchbox","combobox"],m=["button","submit","reset","image"],x=["testID","label","placeholder","text","role","index"],T=e=>(e||"").replace(/\\s+/g," ").trim(),b=(e,t)=>e.getAttribute(t),_=e=>{if(e.hidden||b(e,"aria-hidden")==="true")return!0;try{let t=s.getComputedStyle(e);return t.display==="none"||t.visibility==="hidden"}catch{return!1}},M=e=>{for(let t of v){let n=b(e,t);if(n)return n}},K=e=>Array.from(e.childNodes).some(t=>t.nodeType===3&&T(t.nodeValue)),B=new WeakSet,J=(e,t)=>{let n=[];for(let o of Array.from(e.childNodes))o.nodeType===3?n.push(o.nodeValue):o.nodeType===1&&A.includes(o.tagName)&&!M(o)&&!b(o,"aria-label")&&!b(o,"role")&&!_(o)&&(t&&B.add(o),n.push(J(o,t)));return T(n.join(" "))},X=e=>{let t=e.tagName,n=b(e,"role");if(t==="TEXTAREA"||t==="SELECT")return"input";if(t==="INPUT"){let r=String(e.type||"text").toLowerCase();return r==="hidden"?null:m.includes(r)||r==="checkbox"||r==="radio"?"button":"input"}let o=b(e,"contenteditable");return o===""||o==="true"||n&&l.includes(n)?"input":t==="BUTTON"||t==="SUMMARY"||t==="A"&&b(e,"href")!==null||n&&R.includes(n)?"button":null},W=e=>{let t=b(e,"role");if(t)return t;let n=e.tagName;if(n==="A")return"link";if(n==="BUTTON")return"button";if(n==="SELECT")return"combobox";if(n==="TEXTAREA")return"textbox";if(n==="INPUT"){let o=String(e.type||"text").toLowerCase();return o==="checkbox"||o==="radio"?o:m.includes(o)?"button":"textbox"}},Y=e=>{let t=b(e,"aria-label");if(t)return t;let n=e.tagName;if(n!=="INPUT"&&n!=="TEXTAREA"&&n!=="SELECT")return;let o="";try{if(o=T(e.labels&&e.labels[0]?e.labels[0].textContent:""),!o&&e.id){let r=N.querySelector(`label[for="${String(e.id).replace(/"/g,\'\\\\"\')}"]`);o=T(r?r.textContent:"")}}catch{}return o||void 0},D=e=>e==="true"?!0:e==="false"?!1:e==="mixed"?"mixed":void 0,P=(e,t,n,o)=>{let r={kind:t,rect:null},i=M(e);i&&(r.testID=i),n&&(r.text=n);let a=Y(e);a&&(r.label=a);let g=b(e,"placeholder");g&&(r.placeholder=g);let d=t==="text"?b(e,"role")||void 0:W(e);d&&(r.role=d);let u=e.tagName,h=String(e.type||"").toLowerCase();if(u==="SELECT"&&(r.options=Array.from(e.options).map(p=>{let H={value:String(p.value),label:T(p.label||p.textContent)};return p.selected&&(H.selected=!0),H})),t==="input"&&(u==="INPUT"||u==="TEXTAREA"||u==="SELECT"?(h==="password"?e.value&&(r.value="[hidden]"):r.value=String(e.value),r.editable=!(e.readOnly||e.disabled)):r.value=T(e.textContent),r.editable===void 0&&(r.editable=!0)),u==="INPUT"&&(h==="checkbox"||h==="radio"))r.checked=!!e.checked;else{let p=D(b(e,"aria-checked"));p!==void 0&&(r.checked=p)}let c=D(b(e,"aria-selected"));c!==void 0&&c!=="mixed"&&(r.selected=c);let w=D(b(e,"aria-expanded"));w!==void 0&&w!=="mixed"&&(r.expanded=w),(e.disabled===!0||b(e,"aria-disabled")==="true")&&(r.disabled=!0),o&&(r.frame=o);try{let p=e.getBoundingClientRect();r.rect={x:Math.round(p.x),y:Math.round(p.y),width:Math.round(p.width),height:Math.round(p.height)}}catch{}return r},O=()=>{let e=[],t=(n,o,r)=>{for(let i of Array.from(n.children)){if(L.includes(i.tagName)||_(i))continue;let a=i.tagName;if(a==="IFRAME"||a==="FRAME"){let c=null;try{c=i.contentDocument,c&&c.location.href}catch{c=null}let w=M(i)||b(i,"name")||b(i,"id")||String(e.length);if(c&&c.documentElement)t(c.documentElement,!1,w);else{let p=P(i,"iframe",void 0,r);p.note="iframe (cross-origin, not reachable)",e.push({info:p,el:i,hasText:!1})}continue}let g=X(i),d=B.has(i),u=!1;if(g){let c=g==="button"?T(a==="INPUT"?i.value:i.textContent):void 0;e.push({info:P(i,g,c||void 0,r),el:i,hasText:!!c}),u=!0}else if(!o&&!d&&K(i)){let c=J(i,!0);c&&(e.push({info:P(i,"text",c,r),el:i,hasText:!0}),u=!0)}if(!u&&!d&&(M(i)||b(i,"aria-label"))&&e.push({info:P(i,"view",void 0,r),el:i,hasText:!1}),a==="SELECT")continue;let h=o||g==="button";i.shadowRoot&&t(i.shadowRoot,h,r),t(i,h,r)}};return t(N.documentElement,!1),e},I=e=>{let t=o=>o.length>40?`${o.slice(0,39)}\\u2026`:o;if(e.note)return e.note;let n=[(e.checked!==void 0||e.selected!==void 0||e.expanded!==void 0)&&e.role||e.kind];return e.testID&&n.push(`#${e.testID}`),e.text&&n.push(JSON.stringify(t(e.text))),e.label&&e.label!==e.text&&n.push(`label=${JSON.stringify(t(e.label))}`),e.placeholder&&n.push(`placeholder=${JSON.stringify(t(e.placeholder))}`),e.value!==void 0&&n.push(`value=${JSON.stringify(t(e.value))}`),e.checked!==void 0&&n.push(e.checked===!0?"checked":e.checked===!1?"unchecked":"mixed"),e.selected!==void 0&&n.push(e.selected?"selected":"unselected"),e.expanded!==void 0&&n.push(e.expanded?"expanded":"collapsed"),e.disabled&&n.push("disabled"),n.join(" ")},C=e=>JSON.stringify(e),q=(e,t)=>{if(typeof t!="string"){if(t===null||typeof t!="object"||Array.isArray(t))throw new Error(`A target is a string or an object with ${x.join(", ")}; got ${JSON.stringify(t)}`);let i=Object.keys(t).filter(a=>!x.includes(a));if(i.length)throw new Error(`Unknown target key ${i.map(a=>JSON.stringify(a)).join(", ")}. Valid keys: ${x.join(", ")}`)}let n=i=>i.info.kind==="view"&&!i.info.testID&&!i.info.text&&!i.info.placeholder,o;if(typeof t=="string"){let i=t.toLowerCase();o=[a=>a.testID===t,a=>a.label===t,a=>a.placeholder===t,a=>a.text===t,a=>!!a.text&&a.text.includes(t),a=>!!a.role&&a.role.toLowerCase()===i]}else{let{testID:i,label:a,placeholder:g,text:d,role:u}=t,h=c=>(i===void 0||c.testID===i)&&(a===void 0||c.label===a)&&(g===void 0||c.placeholder===g)&&(u===void 0||!!c.role&&c.role.toLowerCase()===u.toLowerCase());o=d===void 0?[h]:[c=>h(c)&&c.text===d,c=>h(c)&&!!c.text&&c.text.includes(d)]}let r=e.filter(i=>i.info.kind!=="iframe");for(let i of[r.filter(a=>!n(a)),r.filter(n)])for(let a of o){let g=i.filter(d=>a(d.info));if(g.length)return g}return[]},U=(e,t=15)=>{let n=e.slice(0,t).map(o=>I(o.info));return e.length>t&&n.push(`+${e.length-t} more`),n.length?n.join("; "):"nothing"},V=(e,t,n)=>{let o=O(),r=q(o,e);if(!r.length)throw new Error(`Nothing in the page matches ${C(e)}. On the page: ${U(o)}`);let i=r.filter(t);i.length&&(r=i);let a=n??(typeof e=="object"?e.index:void 0);if(a!==void 0){let g=r[a];if(!g)throw new Error(`${C(e)} has ${r.length} match(es); index ${a} is out of range`);return g}if(r.length>1){let g=r.slice(0,10).map((d,u)=>`${u}: ${I(d.info)}`).join("; ");throw new Error(`${C(e)} matches ${r.length} elements; pick one with an index, e.g. {"index":1} as the last argument, or use a narrower target. ${g}`)}return r[0]},j=()=>new Promise(e=>{let t=Date.now(),n=!1,o=!1,r,i,a=null,g=u=>{n||(n=!0,clearTimeout(r),clearTimeout(i),a&&a.disconnect(),e({ms:Date.now()-t,capped:u}))};i=setTimeout(()=>g(!0),2e3);let d=()=>{if(o||n)return;o=!0;let u=()=>{clearTimeout(r),r=setTimeout(()=>g(!1),100)};try{a=new s.MutationObserver(u);let h={subtree:!0,childList:!0,attributes:!0,characterData:!0},c=w=>{a.observe(w,h);for(let p of Array.from(w.querySelectorAll("*")))if(p.shadowRoot&&c(p.shadowRoot),p.tagName==="IFRAME"||p.tagName==="FRAME")try{p.contentDocument&&p.contentDocument.documentElement&&c(p.contentDocument)}catch{}};c(N)}catch{}u()};typeof s.requestAnimationFrame=="function"&&s.requestAnimationFrame(d),setTimeout(d,100)}),G=e=>new Promise(t=>setTimeout(t,e)),Q=e=>{try{e.scrollIntoView&&e.scrollIntoView({block:"center",inline:"center"})}catch{}let t={bubbles:!0,cancelable:!0,composed:!0,view:s};typeof s.PointerEvent=="function"&&e.dispatchEvent(new s.PointerEvent("pointerdown",t)),e.dispatchEvent(new s.MouseEvent("mousedown",t)),typeof e.focus=="function"&&e.focus(),typeof s.PointerEvent=="function"&&e.dispatchEvent(new s.PointerEvent("pointerup",t)),e.dispatchEvent(new s.MouseEvent("mouseup",t)),e.click()},z=(e,t)=>{let n=e.tagName;if(n==="INPUT"||n==="TEXTAREA"||n==="SELECT"){let o=n==="INPUT"?s.HTMLInputElement:n==="TEXTAREA"?s.HTMLTextAreaElement:s.HTMLSelectElement,r=Object.getOwnPropertyDescriptor(o.prototype,"value")?.set;r?r.call(e,t):e.value=t}else e.textContent=t},Z=async()=>{let e=f.options||{};if(f.op==="snapshot"){let t=O().map(n=>n.info);return{url:String(s.location.href),title:String(N.title||""),elements:t}}if(f.op==="press"){let t=V(f.target,o=>o.info.kind==="button",e.index);if(t.info.disabled&&!e.force)throw new Error(`${I(t.info)} is disabled; pass {"force":true} to press it anyway`);Q(t.el);let n=await j();return{element:t.info,settled:n}}if(f.op==="fill"){let t=V(f.target,d=>d.info.kind==="input",e.index);if(t.info.kind!=="input")throw new Error(`${I(t.info)} is not an input; webview.press it instead`);if(t.info.editable===!1)throw new Error(`${I(t.info)} is not editable`);let n=String(f.text),o=t.el;if(o.tagName==="SELECT"){let d=Array.from(o.options),u=d.find(h=>String(h.value)===n)||d.find(h=>T(h.label||h.textContent)===n);if(!u)throw new Error(`${JSON.stringify(n)} is not an option of ${I(t.info)}. Options: ${d.map(h=>`${JSON.stringify(String(h.value))} (${JSON.stringify(T(h.label||h.textContent))})`).join(", ")}`);n=String(u.value)}let r=o.tagName==="INPUT"&&String(o.type||"").toLowerCase()==="password";if(typeof o.focus=="function"&&o.focus(),z(o,n),o.tagName==="SELECT"?o.dispatchEvent(new s.Event("input",{bubbles:!0})):o.dispatchEvent(new s.InputEvent("input",{bubbles:!0,inputType:"insertText",data:n})),o.dispatchEvent(new s.Event("change",{bubbles:!0})),e.submit){let d={key:"Enter",code:"Enter",bubbles:!0,cancelable:!0},u=new s.KeyboardEvent("keydown",d);o.dispatchEvent(u),o.dispatchEvent(new s.KeyboardEvent("keyup",d)),!u.defaultPrevented&&o.form&&typeof o.form.requestSubmit=="function"&&o.form.requestSubmit()}let a=await j(),g=O().find(d=>d.el===o);return{filled:r?"[redacted]":n,element:(g||t).info,settled:a}}if(f.op==="waitFor"){let t=e.timeoutMs??5e3,n=Date.now();for(;q(O(),f.target).length>0===!!e.gone;){if(Date.now()-n>=t){let i=O();throw new Error(e.gone?`${C(f.target)} is still on the page after ${t} ms. On the page: ${U(i)}`:`Timed out after ${t} ms waiting for ${C(f.target)}. On the page: ${U(i)}`)}await G(50)}let o=await j();return{found:!e.gone,waitedMs:Date.now()-n,settled:o}}throw new Error(`Unknown operation ${String(f.op)}`)};try{Z().then(e=>k(!0,e),$)}catch(e){$(e)}}return ie(ce);})();;return __agentBridgePage})()';
|
|
106
|
+
|
|
107
|
+
// src/adapters/webview-host.ts
|
|
108
|
+
var LOG_CAPACITY = 100;
|
|
109
|
+
var BODY_CHARS = 2048;
|
|
110
|
+
var CALL_TIMEOUT_MS = 1e4;
|
|
111
|
+
var registry = /* @__PURE__ */ new Map();
|
|
112
|
+
var nextId = 1;
|
|
113
|
+
var entries = () => [...registry.values()];
|
|
114
|
+
function entryNamed(name) {
|
|
115
|
+
if (name === void 0) {
|
|
116
|
+
if (registry.size === 1) return registry.values().next().value;
|
|
117
|
+
if (!registry.size) throw new Error("No WebView is registered (useWebViewTools)");
|
|
118
|
+
throw new Error(
|
|
119
|
+
`Several WebViews are registered; say which: ${[...registry.keys()].join(", ")}`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
const entry = registry.get(name);
|
|
123
|
+
if (!entry)
|
|
124
|
+
throw new Error(
|
|
125
|
+
`Unknown WebView "${name}". Registered: ${[...registry.keys()].join(", ") || "none"}`
|
|
126
|
+
);
|
|
127
|
+
return entry;
|
|
128
|
+
}
|
|
129
|
+
var DEFAULT_PORTS = { "http:": "80", "https:": "443", "ws:": "80", "wss:": "443" };
|
|
130
|
+
function originOf(value) {
|
|
131
|
+
const found = /^([a-z][a-z0-9+.-]*:)\/\/([^/?#]*)/i.exec(value.trim());
|
|
132
|
+
if (!found) return "null";
|
|
133
|
+
const scheme = found[1].toLowerCase();
|
|
134
|
+
if (!(scheme in DEFAULT_PORTS)) return "null";
|
|
135
|
+
const authority = found[2].slice(found[2].lastIndexOf("@") + 1).toLowerCase();
|
|
136
|
+
const port = /:(\d*)$/.exec(authority);
|
|
137
|
+
const host = port ? authority.slice(0, -port[0].length) : authority;
|
|
138
|
+
if (!host) return "null";
|
|
139
|
+
const keep = port?.[1] && port[1] !== DEFAULT_PORTS[scheme] ? `:${port[1]}` : "";
|
|
140
|
+
return `${scheme}//${host}${keep}`;
|
|
141
|
+
}
|
|
142
|
+
var allowedOrigins = (entry) => {
|
|
143
|
+
const explicit = (entry.options.allowedOrigins ?? []).map(
|
|
144
|
+
(o) => o.trim().toLowerCase() === "null" ? "null" : originOf(o)
|
|
145
|
+
);
|
|
146
|
+
const initial = entry.initialOrigin && entry.initialOrigin !== "null" ? [entry.initialOrigin] : [];
|
|
147
|
+
return [.../* @__PURE__ */ new Set([...initial, ...explicit])];
|
|
148
|
+
};
|
|
149
|
+
var isAllowed = (entry, origin) => origin !== void 0 && allowedOrigins(entry).includes(origin);
|
|
150
|
+
var REDACTED = "[redacted]";
|
|
151
|
+
function redactValue(redact, name, value, path, seen = []) {
|
|
152
|
+
if (typeof value === "function") return value;
|
|
153
|
+
if (typeof redact === "function") {
|
|
154
|
+
if (path) value = redact(name, path, value);
|
|
155
|
+
} else if (path && redact.some((p) => path === p || path.startsWith(`${p}.`))) {
|
|
156
|
+
return REDACTED;
|
|
157
|
+
}
|
|
158
|
+
if (typeof value !== "object" || value === null || seen.includes(value)) return value;
|
|
159
|
+
const next = [...seen, value];
|
|
160
|
+
const items = Object.entries(value).map(
|
|
161
|
+
([k, v]) => [k, redactValue(redact, name, v, path ? `${path}.${k}` : k, next)]
|
|
162
|
+
);
|
|
163
|
+
return Array.isArray(value) ? items.map(([, v]) => v) : Object.fromEntries(items);
|
|
164
|
+
}
|
|
165
|
+
function redactBody(entry, body) {
|
|
166
|
+
const { redact, name } = entry.options;
|
|
167
|
+
if (!redact) return body;
|
|
168
|
+
let parsed;
|
|
169
|
+
try {
|
|
170
|
+
parsed = JSON.parse(body);
|
|
171
|
+
} catch {
|
|
172
|
+
if (typeof redact !== "function") return body;
|
|
173
|
+
const out = redact(name, "", body);
|
|
174
|
+
return typeof out === "string" ? out : JSON.stringify(out);
|
|
175
|
+
}
|
|
176
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
177
|
+
if (typeof redact !== "function") return body;
|
|
178
|
+
const out = redact(name, "", parsed);
|
|
179
|
+
return typeof out === "string" ? out : JSON.stringify(out);
|
|
180
|
+
}
|
|
181
|
+
return JSON.stringify(redactValue(redact, name, parsed, ""));
|
|
182
|
+
}
|
|
183
|
+
function record(entry, direction, via, body) {
|
|
184
|
+
const text = typeof body === "string" ? body : safeStringify(body);
|
|
185
|
+
entry.log.push({
|
|
186
|
+
id: nextId++,
|
|
187
|
+
at: Date.now(),
|
|
188
|
+
webview: entry.options.name,
|
|
189
|
+
direction,
|
|
190
|
+
via,
|
|
191
|
+
body: redactBody(entry, text)
|
|
192
|
+
});
|
|
193
|
+
if (entry.log.length > LOG_CAPACITY) entry.log.shift();
|
|
194
|
+
}
|
|
195
|
+
var safeStringify = (value) => {
|
|
196
|
+
try {
|
|
197
|
+
return JSON.stringify(value) ?? String(value);
|
|
198
|
+
} catch {
|
|
199
|
+
return String(value);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
function shown(message) {
|
|
203
|
+
const { body, ...rest } = message;
|
|
204
|
+
if (body.length <= BODY_CHARS) return { ...rest, body };
|
|
205
|
+
return { ...rest, body: body.slice(0, BODY_CHARS), truncated: true, size: body.length };
|
|
206
|
+
}
|
|
207
|
+
function messageById(id) {
|
|
208
|
+
for (const entry of registry.values()) {
|
|
209
|
+
const found = entry.log.find((m) => m.id === id);
|
|
210
|
+
if (found) return found;
|
|
211
|
+
}
|
|
212
|
+
throw new Error(`No message ${id} in the log (it keeps the last ${LOG_CAPACITY} per WebView)`);
|
|
213
|
+
}
|
|
214
|
+
var randomNonce = () => {
|
|
215
|
+
const c = globalThis.crypto;
|
|
216
|
+
const bytes = new Uint8Array(16);
|
|
217
|
+
if (c?.getRandomValues) c.getRandomValues(bytes);
|
|
218
|
+
else for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
219
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
220
|
+
};
|
|
221
|
+
var bootScript = (token) => `(function(){var rn=window.ReactNativeWebView,p=rn&&rn.postMessage&&rn.postMessage.bind(rn);${PAGE_BUNDLE}.bootMain(window,function(s){(p||window.ReactNativeWebView.postMessage.bind(window.ReactNativeWebView))(s)},${JSON.stringify(token)})})();true;`;
|
|
222
|
+
var literal = (value) => JSON.stringify(value).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
223
|
+
var callScript = (argsJson) => `${PAGE_BUNDLE}.pageMain(window,${literal(argsJson)},function(s){window.ReactNativeWebView.postMessage(s)});true;`;
|
|
224
|
+
function attach(entry) {
|
|
225
|
+
const instance = entry.ref.current;
|
|
226
|
+
if (entry.patched && entry.patched.instance === instance) return;
|
|
227
|
+
entry.patched?.undo();
|
|
228
|
+
entry.patched = null;
|
|
229
|
+
if (!instance) return;
|
|
230
|
+
const { postMessage, injectJavaScript } = instance;
|
|
231
|
+
const inject = injectJavaScript.bind(instance);
|
|
232
|
+
const post = postMessage.bind(instance);
|
|
233
|
+
instance.postMessage = (data) => {
|
|
234
|
+
record(entry, "app\u2192page", "postMessage", data);
|
|
235
|
+
return post(data);
|
|
236
|
+
};
|
|
237
|
+
instance.injectJavaScript = (script) => {
|
|
238
|
+
record(entry, "app\u2192page", "injectJavaScript", script);
|
|
239
|
+
return inject(script);
|
|
240
|
+
};
|
|
241
|
+
entry.patched = {
|
|
242
|
+
instance,
|
|
243
|
+
undo: () => {
|
|
244
|
+
instance.postMessage = postMessage;
|
|
245
|
+
instance.injectJavaScript = injectJavaScript;
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
originals.set(entry, { inject, post });
|
|
249
|
+
}
|
|
250
|
+
var originals = /* @__PURE__ */ new WeakMap();
|
|
251
|
+
var timing = { handshakeMs: void 0 };
|
|
252
|
+
var DEFAULT_LOAD_WAIT_MS = 1e4;
|
|
253
|
+
var PAGE_LOG_CHARS = 500;
|
|
254
|
+
var asRecord = (value) => {
|
|
255
|
+
if (typeof value !== "string" || !value.startsWith("{") || !value.includes("__agentBridge")) return null;
|
|
256
|
+
try {
|
|
257
|
+
const parsed = JSON.parse(value);
|
|
258
|
+
return parsed && parsed.__agentBridge === 1 ? parsed : null;
|
|
259
|
+
} catch {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
function navigated(entry, url) {
|
|
264
|
+
entry.handshake = false;
|
|
265
|
+
entry.nativeLoaded = false;
|
|
266
|
+
entry.loaded = false;
|
|
267
|
+
if (url !== void 0) {
|
|
268
|
+
entry.url = url;
|
|
269
|
+
entry.origin = originOf(url);
|
|
270
|
+
entry.initialOrigin ?? (entry.initialOrigin = entry.origin);
|
|
271
|
+
}
|
|
272
|
+
for (const [nonce, pending] of [...entry.pending]) {
|
|
273
|
+
entry.pending.delete(nonce);
|
|
274
|
+
clearTimeout(pending.timer);
|
|
275
|
+
if (pending.op === "press" || pending.op === "fill")
|
|
276
|
+
pending.resolve({ navigated: true, url: entry.url ?? null });
|
|
277
|
+
else
|
|
278
|
+
pending.reject(
|
|
279
|
+
new Error(
|
|
280
|
+
`The page navigated to ${entry.url ?? "another page"} during webview.${pending.op}; call it again on the new page`
|
|
281
|
+
)
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function rotateToken(entry) {
|
|
286
|
+
entry.token = randomNonce();
|
|
287
|
+
entry.onRotate?.();
|
|
288
|
+
}
|
|
289
|
+
function seeNative(entry, event, kind) {
|
|
290
|
+
const info = event?.nativeEvent;
|
|
291
|
+
if (info?.isTopFrame === false || typeof info?.url !== "string") return false;
|
|
292
|
+
const origin = originOf(info.url);
|
|
293
|
+
entry.initialOrigin ?? (entry.initialOrigin = origin);
|
|
294
|
+
const orphan = kind === "start" && entry.orphanLoading === info.url;
|
|
295
|
+
if (kind === "start") entry.orphanLoading = void 0;
|
|
296
|
+
if (orphan) {
|
|
297
|
+
entry.url = info.url;
|
|
298
|
+
entry.origin = origin;
|
|
299
|
+
} else if (kind === "start" || origin !== entry.origin) {
|
|
300
|
+
navigated(entry, info.url);
|
|
301
|
+
if (kind === "start") entry.startPending = true;
|
|
302
|
+
}
|
|
303
|
+
if (!isAllowed(entry, origin) && kind !== "message") rotateToken(entry);
|
|
304
|
+
else entry.url = info.url;
|
|
305
|
+
if (kind === "end") {
|
|
306
|
+
entry.nativeLoaded = true;
|
|
307
|
+
entry.loaded = true;
|
|
308
|
+
for (const listener of [...entry.onLoaded]) listener();
|
|
309
|
+
}
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
function onOwnMessage(entry, message, event) {
|
|
313
|
+
if (message.t !== entry.token) return;
|
|
314
|
+
if (!seeNative(entry, event, "message")) return;
|
|
315
|
+
if (!isAllowed(entry, entry.origin)) return;
|
|
316
|
+
const url = event.nativeEvent?.url;
|
|
317
|
+
if (message.kind === "state") {
|
|
318
|
+
if (message.state === "loading") {
|
|
319
|
+
const started = entry.startPending;
|
|
320
|
+
entry.startPending = false;
|
|
321
|
+
navigated(entry, url);
|
|
322
|
+
entry.orphanLoading = started ? void 0 : url;
|
|
323
|
+
}
|
|
324
|
+
if (message.state === "loaded") {
|
|
325
|
+
entry.loaded = true;
|
|
326
|
+
for (const listener of [...entry.onLoaded]) listener();
|
|
327
|
+
}
|
|
328
|
+
entry.handshake = true;
|
|
329
|
+
for (const listener of [...entry.onHandshake]) listener();
|
|
330
|
+
} else if (message.kind === "log") {
|
|
331
|
+
const text = String(message.message).replace(/\s*[\r\n]+\s*/g, " \u23CE ").slice(0, PAGE_LOG_CHARS);
|
|
332
|
+
logToBridge("error", `[webview ${entry.options.name}, page output] ${text}`);
|
|
333
|
+
} else if (message.kind === "reply" && typeof message.nonce === "string") {
|
|
334
|
+
const pending = entry.pending.get(message.nonce);
|
|
335
|
+
if (!pending) return;
|
|
336
|
+
entry.pending.delete(message.nonce);
|
|
337
|
+
clearTimeout(pending.timer);
|
|
338
|
+
if (message.ok === true) pending.resolve(message.result);
|
|
339
|
+
else pending.reject(new Error(String(message.error)));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
function register(ref, options) {
|
|
343
|
+
const entry = {
|
|
344
|
+
options,
|
|
345
|
+
ref,
|
|
346
|
+
url: void 0,
|
|
347
|
+
origin: void 0,
|
|
348
|
+
initialOrigin: void 0,
|
|
349
|
+
loaded: false,
|
|
350
|
+
handshake: false,
|
|
351
|
+
nativeLoaded: false,
|
|
352
|
+
warnedDuplicate: false,
|
|
353
|
+
orphanLoading: void 0,
|
|
354
|
+
startPending: false,
|
|
355
|
+
token: randomNonce(),
|
|
356
|
+
onRotate: void 0,
|
|
357
|
+
onHandshake: /* @__PURE__ */ new Set(),
|
|
358
|
+
log: [],
|
|
359
|
+
pending: /* @__PURE__ */ new Map(),
|
|
360
|
+
handler: void 0,
|
|
361
|
+
patched: null,
|
|
362
|
+
onLoaded: /* @__PURE__ */ new Set()
|
|
363
|
+
};
|
|
364
|
+
const wrap = (onMessage) => {
|
|
365
|
+
entry.handler = onMessage;
|
|
366
|
+
return (event) => {
|
|
367
|
+
const data = event?.nativeEvent?.data;
|
|
368
|
+
const own = asRecord(data);
|
|
369
|
+
if (own) return onOwnMessage(entry, own, event);
|
|
370
|
+
record(entry, "page\u2192app", "onMessage", typeof data === "string" ? data : safeStringify(data));
|
|
371
|
+
return entry.handler?.(event);
|
|
372
|
+
};
|
|
373
|
+
};
|
|
374
|
+
const props = () => ({
|
|
375
|
+
injectedJavaScriptBeforeContentLoaded: `${webViewMark(entry.options.name)}${bootScript(entry.token)}${entry.options.injectedJavaScriptBeforeContentLoaded ?? ""}`,
|
|
376
|
+
// Forced: in a sub-frame the script's token would be in reach of a page that isn't the app's.
|
|
377
|
+
injectedJavaScriptBeforeContentLoadedForMainFrameOnly: true,
|
|
378
|
+
onLoadStart: (event) => {
|
|
379
|
+
seeNative(entry, event, "start");
|
|
380
|
+
entry.options.onLoadStart?.(event);
|
|
381
|
+
},
|
|
382
|
+
onLoadEnd: (event) => {
|
|
383
|
+
seeNative(entry, event, "end");
|
|
384
|
+
entry.options.onLoadEnd?.(event);
|
|
385
|
+
},
|
|
386
|
+
onNavigationStateChange: (state) => {
|
|
387
|
+
seeNative(entry, { nativeEvent: state }, "nav");
|
|
388
|
+
entry.options.onNavigationStateChange?.(state);
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
return {
|
|
392
|
+
entry,
|
|
393
|
+
wrap,
|
|
394
|
+
props,
|
|
395
|
+
mount: () => {
|
|
396
|
+
const name = entry.options.name;
|
|
397
|
+
const existing = registry.get(name);
|
|
398
|
+
if (existing && existing !== entry) {
|
|
399
|
+
if (!entry.warnedDuplicate) {
|
|
400
|
+
entry.warnedDuplicate = true;
|
|
401
|
+
console.error(
|
|
402
|
+
`agent-bridge: a WebView named "${name}" is already registered; this one is not reachable. Give each WebView its own name.`
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
entry.warnedDuplicate = false;
|
|
408
|
+
registry.set(name, entry);
|
|
409
|
+
attach(entry);
|
|
410
|
+
},
|
|
411
|
+
dispose: () => {
|
|
412
|
+
entry.patched?.undo();
|
|
413
|
+
entry.patched = null;
|
|
414
|
+
dropPending(entry, "The WebView was unmounted");
|
|
415
|
+
if (registry.get(entry.options.name) === entry) registry.delete(entry.options.name);
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
function dropPending(entry, why) {
|
|
420
|
+
for (const [nonce, pending] of entry.pending) {
|
|
421
|
+
clearTimeout(pending.timer);
|
|
422
|
+
pending.reject(new Error(why));
|
|
423
|
+
entry.pending.delete(nonce);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
var notAllowed = (entry) => new Error(
|
|
427
|
+
`WebView "${entry.options.name}" is on ${entry.origin ?? "an unknown origin"}, which is not allowed (allowed: ${allowedOrigins(entry).join(", ") || "none"}). Add it to allowedOrigins to read or drive it`
|
|
428
|
+
);
|
|
429
|
+
var handshakeError = (entry) => new Error(
|
|
430
|
+
entry.nativeLoaded ? `WebView "${entry.options.name}" loaded but its page script did not check in. Spread useWebViewTools' props and pass wrap(onMessage) as onMessage; if you set your own injectedJavaScriptBeforeContentLoaded, pass it through useWebViewTools options instead` : `WebView "${entry.options.name}" is still loading`
|
|
431
|
+
);
|
|
432
|
+
function refusal(entry) {
|
|
433
|
+
if (entry.origin === void 0)
|
|
434
|
+
return new Error(`WebView "${entry.options.name}" has not loaded a page yet`);
|
|
435
|
+
if (!isAllowed(entry, entry.origin)) return notAllowed(entry);
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
function requireVerified(entry) {
|
|
439
|
+
const why = refusal(entry) ?? (entry.handshake ? null : handshakeError(entry));
|
|
440
|
+
if (why) throw why;
|
|
441
|
+
}
|
|
442
|
+
var isCurrentAllowed = (entry) => entry.origin === void 0 ? void 0 : isAllowed(entry, entry.origin);
|
|
443
|
+
function awaitHandshake(entry) {
|
|
444
|
+
return new Promise((resolve, reject) => {
|
|
445
|
+
const settleNow = () => {
|
|
446
|
+
const why = entry.origin === void 0 ? null : refusal(entry);
|
|
447
|
+
if (why) return done(why);
|
|
448
|
+
if (entry.handshake) return done();
|
|
449
|
+
};
|
|
450
|
+
const timer = setTimeout(() => done(refusal(entry) ?? handshakeError(entry)), timing.handshakeMs ?? entry.options.firstLoadTimeoutMs ?? DEFAULT_LOAD_WAIT_MS);
|
|
451
|
+
const done = (error) => {
|
|
452
|
+
clearTimeout(timer);
|
|
453
|
+
entry.onHandshake.delete(settleNow);
|
|
454
|
+
if (error) reject(error);
|
|
455
|
+
else resolve();
|
|
456
|
+
};
|
|
457
|
+
entry.onHandshake.add(settleNow);
|
|
458
|
+
settleNow();
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
async function callPage(entry, op, args, timeoutMs = CALL_TIMEOUT_MS) {
|
|
462
|
+
const { name } = entry.options;
|
|
463
|
+
if (!entry.ref.current) throw new Error(`WebView "${name}" is not mounted`);
|
|
464
|
+
attach(entry);
|
|
465
|
+
await awaitHandshake(entry);
|
|
466
|
+
const webview = entry.ref.current;
|
|
467
|
+
if (!webview) throw new Error(`WebView "${name}" is not mounted`);
|
|
468
|
+
requireVerified(entry);
|
|
469
|
+
const allowed = allowedOrigins(entry);
|
|
470
|
+
const nonce = randomNonce();
|
|
471
|
+
const script = callScript(JSON.stringify({ ...args, op, nonce, allowed, token: entry.token }));
|
|
472
|
+
return new Promise((resolve, reject) => {
|
|
473
|
+
const timer = setTimeout(() => {
|
|
474
|
+
entry.pending.delete(nonce);
|
|
475
|
+
reject(
|
|
476
|
+
new Error(
|
|
477
|
+
`No reply from WebView "${name}" in ${Math.round(timeoutMs / 1e3)} s. Is a page loaded${entry.loaded ? "" : " (it is still loading)"}?`
|
|
478
|
+
)
|
|
479
|
+
);
|
|
480
|
+
}, timeoutMs);
|
|
481
|
+
entry.pending.set(nonce, { op, resolve, reject, timer });
|
|
482
|
+
try {
|
|
483
|
+
;
|
|
484
|
+
(originals.get(entry)?.inject ?? webview.injectJavaScript.bind(webview))(script);
|
|
485
|
+
} catch (error) {
|
|
486
|
+
entry.pending.delete(nonce);
|
|
487
|
+
clearTimeout(timer);
|
|
488
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
function sendToPage(entry, body) {
|
|
493
|
+
const webview = entry.ref.current;
|
|
494
|
+
if (!webview) throw new Error(`WebView "${entry.options.name}" is not mounted`);
|
|
495
|
+
attach(entry);
|
|
496
|
+
requireVerified(entry);
|
|
497
|
+
(originals.get(entry)?.post ?? webview.postMessage.bind(webview))(body);
|
|
498
|
+
record(entry, "app\u2192page", "webview.send", body);
|
|
499
|
+
}
|
|
500
|
+
function restoreAll() {
|
|
501
|
+
let cleared = 0;
|
|
502
|
+
for (const entry of registry.values()) {
|
|
503
|
+
cleared += entry.log.length;
|
|
504
|
+
entry.log = [];
|
|
505
|
+
dropPending(entry, "Cleared by webview.restore");
|
|
506
|
+
}
|
|
507
|
+
return { cleared };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// src/adapters/react-native-webview.ts
|
|
511
|
+
function useWebViewTools(ref, options) {
|
|
512
|
+
const host = (0, import_react.useMemo)(() => register(ref, options), [ref, options.name]);
|
|
513
|
+
host.entry.options = options;
|
|
514
|
+
const [version, setVersion] = (0, import_react.useState)(0);
|
|
515
|
+
host.entry.onRotate = () => setVersion((v) => v + 1);
|
|
516
|
+
(0, import_react.useEffect)(() => {
|
|
517
|
+
host.mount();
|
|
518
|
+
return host.dispose;
|
|
519
|
+
}, [host]);
|
|
520
|
+
(0, import_react.useEffect)(() => host.mount());
|
|
521
|
+
const props = (0, import_react.useMemo)(
|
|
522
|
+
() => host.props(),
|
|
523
|
+
[host, version, options.injectedJavaScriptBeforeContentLoaded]
|
|
524
|
+
);
|
|
525
|
+
return { props, wrap: host.wrap };
|
|
526
|
+
}
|
|
527
|
+
function nameAndData(args, tool) {
|
|
528
|
+
if (args.length === 0 || args.length > 2)
|
|
529
|
+
throw new Error(`${tool} takes [name] <json>`);
|
|
530
|
+
return args.length === 2 ? [entryNamed(String(args[0])), args[1]] : [entryNamed(), args[0]];
|
|
531
|
+
}
|
|
532
|
+
var bodyOf = (data) => typeof data === "string" ? data : JSON.stringify(data);
|
|
533
|
+
var TARGET_HELP = "target is a string (testID, label, placeholder, text, then role) or an object that must match every key it sets: {testID, label, placeholder, text, role, index}. testID is data-testid, data-test-id or data-cy. Several matches: add an index, in the target or as {index:n}.";
|
|
534
|
+
var WHICH = "Give {webview: name} as the last argument when more than one WebView is registered.";
|
|
535
|
+
function webviewTools() {
|
|
536
|
+
return {
|
|
537
|
+
"webview.list": {
|
|
538
|
+
maxArgs: 0,
|
|
539
|
+
description: "The registered WebViews: name, current url, whether the page has loaded, whether its origin is allowed (a page outside the allowed origins is never read or driven), and the allowed origins.",
|
|
540
|
+
run: () => entries().map((entry) => ({
|
|
541
|
+
name: entry.options.name,
|
|
542
|
+
mounted: !!entry.ref.current,
|
|
543
|
+
url: entry.url ?? null,
|
|
544
|
+
loaded: entry.loaded,
|
|
545
|
+
ready: entry.handshake,
|
|
546
|
+
allowed: isCurrentAllowed(entry) ?? null,
|
|
547
|
+
origins: allowedOrigins(entry)
|
|
548
|
+
}))
|
|
549
|
+
},
|
|
550
|
+
"webview.snapshot": {
|
|
551
|
+
maxArgs: 1,
|
|
552
|
+
description: 'Buttons, links, inputs, text and testID/aria-label elements in a WebView page, in the same shape as screen.snapshot (kind, text, testID, label, placeholder, value, role, checked, disabled, rect). Pierces open shadow roots and same-origin iframes; a cross-origin iframe shows as "iframe (cross-origin, not reachable)". Arguments: [name].',
|
|
553
|
+
run: (name) => callPage(entryNamed(name), "snapshot", {})
|
|
554
|
+
},
|
|
555
|
+
"webview.press": {
|
|
556
|
+
maxArgs: 2,
|
|
557
|
+
description: `Click a button, link or control in a WebView page, then wait for the page to settle (next frame, then 100 ms without DOM changes, at most 2 s). Not undone by webview.restore. ${TARGET_HELP} Disabled elements are refused; {force:true} presses anyway. ${WHICH}`,
|
|
558
|
+
run: (target, options = {}) => callPage(entryNamed(options.webview), "press", {
|
|
559
|
+
target,
|
|
560
|
+
options: { force: options.force, index: options.index }
|
|
561
|
+
})
|
|
562
|
+
},
|
|
563
|
+
"webview.fill": {
|
|
564
|
+
maxArgs: 3,
|
|
565
|
+
description: `Set an input's value in a WebView page through the native value setter and dispatch input and change events, so React and Vue inputs pick it up, then wait for the page to settle. Not undone by webview.restore. ${TARGET_HELP} {submit:true} also presses Enter. ${WHICH}`,
|
|
566
|
+
run: (target, text, options = {}) => callPage(entryNamed(options.webview), "fill", {
|
|
567
|
+
target,
|
|
568
|
+
text,
|
|
569
|
+
options: { submit: options.submit, index: options.index }
|
|
570
|
+
})
|
|
571
|
+
},
|
|
572
|
+
"webview.waitFor": {
|
|
573
|
+
maxArgs: 2,
|
|
574
|
+
description: `Wait until a target is on the WebView page, or gone with {gone:true}, then wait for the page to settle. Default timeout 5000 ms; a timeout lists what is on the page. ${TARGET_HELP} ${WHICH}`,
|
|
575
|
+
run: (target, options = {}) => callPage(
|
|
576
|
+
entryNamed(options.webview),
|
|
577
|
+
"waitFor",
|
|
578
|
+
{ target, options: { gone: options.gone, timeoutMs: options.timeoutMs } },
|
|
579
|
+
(options.timeoutMs ?? 5e3) + 5e3
|
|
580
|
+
)
|
|
581
|
+
},
|
|
582
|
+
"webview.url": {
|
|
583
|
+
maxArgs: 1,
|
|
584
|
+
description: "The URL a WebView is on, as it last reported it. Arguments: [name].",
|
|
585
|
+
run: (name) => {
|
|
586
|
+
const entry = entryNamed(name);
|
|
587
|
+
return { url: entry.url ?? null, allowed: isCurrentAllowed(entry) ?? null };
|
|
588
|
+
}
|
|
589
|
+
},
|
|
590
|
+
"webview.reload": {
|
|
591
|
+
maxArgs: 1,
|
|
592
|
+
description: "Reload the page, and wait up to 10 s for it to load again. Arguments: [name].",
|
|
593
|
+
run: async (name) => {
|
|
594
|
+
const entry = entryNamed(name);
|
|
595
|
+
const webview = entry.ref.current;
|
|
596
|
+
if (!webview) throw new Error(`WebView "${entry.options.name}" is not mounted`);
|
|
597
|
+
const loaded = new Promise((resolve) => {
|
|
598
|
+
const done = () => {
|
|
599
|
+
clearTimeout(timer);
|
|
600
|
+
entry.onLoaded.delete(done);
|
|
601
|
+
resolve(true);
|
|
602
|
+
};
|
|
603
|
+
const timer = setTimeout(() => {
|
|
604
|
+
entry.onLoaded.delete(done);
|
|
605
|
+
resolve(false);
|
|
606
|
+
}, 1e4);
|
|
607
|
+
entry.onLoaded.add(done);
|
|
608
|
+
});
|
|
609
|
+
entry.loaded = false;
|
|
610
|
+
webview.reload();
|
|
611
|
+
return { reloaded: true, loaded: await loaded, url: entry.url ?? null };
|
|
612
|
+
}
|
|
613
|
+
},
|
|
614
|
+
"webview.messages": {
|
|
615
|
+
maxArgs: 2,
|
|
616
|
+
description: `The last 100 messages in both directions, oldest first, with timestamps: page \u2192 app (onMessage) and app \u2192 page (postMessage, injectJavaScript, and what webview.send delivered). Bodies are cut at ${BODY_CHARS / 1024} KB (truncated:true, size); webview.message <id> returns one in full. Bodies are redacted by the WebView's redact option. Arguments: [name], {limit}. Without a name, every WebView's, merged by time.`,
|
|
617
|
+
run: (first, second) => {
|
|
618
|
+
const name = typeof first === "string" ? first : void 0;
|
|
619
|
+
const options = (typeof first === "string" ? second : first) ?? {};
|
|
620
|
+
const all = (name === void 0 ? entries() : [entryNamed(name)]).flatMap((entry) => entry.log).sort((a, b) => a.id - b.id);
|
|
621
|
+
return (options.limit !== void 0 ? all.slice(-options.limit) : all).map(shown);
|
|
622
|
+
}
|
|
623
|
+
},
|
|
624
|
+
"webview.message": {
|
|
625
|
+
maxArgs: 1,
|
|
626
|
+
description: "One logged message in full, by the id webview.messages shows (redacted like the log).",
|
|
627
|
+
run: (id) => messageById(Number(id))
|
|
628
|
+
},
|
|
629
|
+
"webview.send": {
|
|
630
|
+
maxArgs: 2,
|
|
631
|
+
description: "Deliver a message to the page as if the app had sent it (WebView postMessage): a string as is, anything else as JSON. Arguments: [name] <json>. Refused when the page is outside the allowed origins. It is logged, and not undone.",
|
|
632
|
+
run: async (...args) => {
|
|
633
|
+
const [entry, data] = nameAndData(args, "webview.send");
|
|
634
|
+
sendToPage(entry, bodyOf(data));
|
|
635
|
+
return { sent: true };
|
|
636
|
+
}
|
|
637
|
+
},
|
|
638
|
+
"webview.receive": {
|
|
639
|
+
maxArgs: 2,
|
|
640
|
+
description: "Run the app's onMessage handler with a fake page message, without the page sending it: a string as is, anything else as JSON. Arguments: [name] <json>. Then waits for the render. Errors from the handler come back as errors. Not undone: mock what the handler does with onRestore.",
|
|
641
|
+
run: async (...args) => {
|
|
642
|
+
const [entry, data] = nameAndData(args, "webview.receive");
|
|
643
|
+
if (!entry.handler)
|
|
644
|
+
throw new Error(
|
|
645
|
+
`WebView "${entry.options.name}" has no onMessage handler: pass wrap(onMessage) to the WebView`
|
|
646
|
+
);
|
|
647
|
+
requireVerified(entry);
|
|
648
|
+
const body = bodyOf(data);
|
|
649
|
+
record(entry, "page\u2192app", "webview.receive", body);
|
|
650
|
+
const result = await entry.handler({
|
|
651
|
+
nativeEvent: {
|
|
652
|
+
data: body,
|
|
653
|
+
url: entry.url ?? "",
|
|
654
|
+
title: "",
|
|
655
|
+
loading: false,
|
|
656
|
+
canGoBack: false,
|
|
657
|
+
canGoForward: false,
|
|
658
|
+
lockIdentifier: 0,
|
|
659
|
+
target: 0
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
await settle();
|
|
663
|
+
return { delivered: true, ...result === void 0 ? {} : { result } };
|
|
664
|
+
}
|
|
665
|
+
},
|
|
666
|
+
"webview.restore": {
|
|
667
|
+
maxArgs: 0,
|
|
668
|
+
description: "Clear the message logs and drop any call still waiting for a reply. webview.press, fill and send change the page and the app and are not undone: reload the page, or mock host handlers with onRestore.",
|
|
669
|
+
run: () => restoreAll()
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
}
|