@qontinui/ui-bridge-native 0.6.9 → 0.6.11
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/dist/control/action-executor.d.ts.map +1 -1
- package/dist/control/index.js +2 -1
- package/dist/control/index.js.map +1 -1
- package/dist/control/index.mjs +2 -1
- package/dist/control/index.mjs.map +1 -1
- package/dist/core/index.js +232 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +232 -2
- package/dist/core/index.mjs.map +1 -1
- package/dist/core/registry.d.ts +46 -1
- package/dist/core/registry.d.ts.map +1 -1
- package/dist/core/types.d.ts +114 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/vision-fields.d.ts +142 -0
- package/dist/core/vision-fields.d.ts.map +1 -0
- package/dist/debug/index.js.map +1 -1
- package/dist/debug/index.mjs.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +371 -20
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +365 -22
- package/dist/index.mjs.map +1 -1
- package/dist/react/UIBridgeNativeProvider.d.ts.map +1 -1
- package/dist/react/index.js +323 -20
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +324 -21
- package/dist/react/index.mjs.map +1 -1
- package/dist/react/useUIElement.d.ts.map +1 -1
- package/dist/server/bind-retry.d.ts +64 -0
- package/dist/server/bind-retry.d.ts.map +1 -0
- package/dist/server/handlers.d.ts.map +1 -1
- package/dist/server/http-server.d.ts +9 -1
- package/dist/server/http-server.d.ts.map +1 -1
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +275 -13
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +274 -14
- package/dist/server/index.mjs.map +1 -1
- package/dist/server/observability.d.ts +4 -3
- package/dist/server/observability.d.ts.map +1 -1
- package/dist/server/types.d.ts +10 -0
- package/dist/server/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -11,6 +11,179 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
11
11
|
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
+
// src/core/vision-fields.ts
|
|
15
|
+
var INTERACTIVE_HANDLER_NAMES = /* @__PURE__ */ new Set([
|
|
16
|
+
"onPress",
|
|
17
|
+
"onPressIn",
|
|
18
|
+
"onPressOut",
|
|
19
|
+
"onLongPress",
|
|
20
|
+
"onChangeText",
|
|
21
|
+
"onValueChange",
|
|
22
|
+
"onChange",
|
|
23
|
+
"onSubmitEditing",
|
|
24
|
+
"onClick",
|
|
25
|
+
"onTouchStart",
|
|
26
|
+
"onTouchEnd"
|
|
27
|
+
]);
|
|
28
|
+
var INTERACTIVE_TYPES = /* @__PURE__ */ new Set([
|
|
29
|
+
"button",
|
|
30
|
+
"input",
|
|
31
|
+
"switch",
|
|
32
|
+
"checkbox",
|
|
33
|
+
"radio",
|
|
34
|
+
"touchable",
|
|
35
|
+
"pressable",
|
|
36
|
+
"listItem"
|
|
37
|
+
]);
|
|
38
|
+
function roleForType(type) {
|
|
39
|
+
switch (type) {
|
|
40
|
+
case "button":
|
|
41
|
+
case "touchable":
|
|
42
|
+
case "pressable":
|
|
43
|
+
return "button";
|
|
44
|
+
case "input":
|
|
45
|
+
return "textbox";
|
|
46
|
+
case "text":
|
|
47
|
+
return "text";
|
|
48
|
+
case "switch":
|
|
49
|
+
return "switch";
|
|
50
|
+
case "checkbox":
|
|
51
|
+
return "checkbox";
|
|
52
|
+
case "radio":
|
|
53
|
+
return "radio";
|
|
54
|
+
case "image":
|
|
55
|
+
return "img";
|
|
56
|
+
case "list":
|
|
57
|
+
return "list";
|
|
58
|
+
case "listItem":
|
|
59
|
+
return "listitem";
|
|
60
|
+
case "modal":
|
|
61
|
+
return "dialog";
|
|
62
|
+
case "scroll":
|
|
63
|
+
case "view":
|
|
64
|
+
case "custom":
|
|
65
|
+
default:
|
|
66
|
+
return void 0;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function deriveInteractable(type, handlerNames) {
|
|
70
|
+
if (INTERACTIVE_TYPES.has(type)) return true;
|
|
71
|
+
if (handlerNames) {
|
|
72
|
+
for (const name of handlerNames) {
|
|
73
|
+
if (INTERACTIVE_HANDLER_NAMES.has(name)) return true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
function deriveText(state, label) {
|
|
79
|
+
const candidates = [state.value, state.textContent, label];
|
|
80
|
+
for (const c of candidates) {
|
|
81
|
+
if (typeof c === "string") {
|
|
82
|
+
const trimmed = c.trim();
|
|
83
|
+
if (trimmed.length > 0) return trimmed;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return void 0;
|
|
87
|
+
}
|
|
88
|
+
var NAMED_COLORS = {
|
|
89
|
+
black: { r: 0, g: 0, b: 0 },
|
|
90
|
+
white: { r: 255, g: 255, b: 255 },
|
|
91
|
+
red: { r: 255, g: 0, b: 0 },
|
|
92
|
+
green: { r: 0, g: 128, b: 0 },
|
|
93
|
+
blue: { r: 0, g: 0, b: 255 },
|
|
94
|
+
gray: { r: 128, g: 128, b: 128 },
|
|
95
|
+
grey: { r: 128, g: 128, b: 128 },
|
|
96
|
+
silver: { r: 192, g: 192, b: 192 },
|
|
97
|
+
transparent: { r: 0, g: 0, b: 0 },
|
|
98
|
+
orange: { r: 255, g: 165, b: 0 },
|
|
99
|
+
yellow: { r: 255, g: 255, b: 0 },
|
|
100
|
+
purple: { r: 128, g: 0, b: 128 },
|
|
101
|
+
navy: { r: 0, g: 0, b: 128 }
|
|
102
|
+
};
|
|
103
|
+
var clamp255 = (n) => Math.max(0, Math.min(255, Math.round(n)));
|
|
104
|
+
function parseColorToRgb(input) {
|
|
105
|
+
if (typeof input !== "string") return void 0;
|
|
106
|
+
const raw = input.trim().toLowerCase();
|
|
107
|
+
if (raw.length === 0) return void 0;
|
|
108
|
+
if (Object.prototype.hasOwnProperty.call(NAMED_COLORS, raw)) {
|
|
109
|
+
return NAMED_COLORS[raw];
|
|
110
|
+
}
|
|
111
|
+
if (raw.startsWith("#")) {
|
|
112
|
+
const hex = raw.slice(1);
|
|
113
|
+
if (hex.length === 3 || hex.length === 4) {
|
|
114
|
+
const r = parseInt(hex[0] + hex[0], 16);
|
|
115
|
+
const g = parseInt(hex[1] + hex[1], 16);
|
|
116
|
+
const b = parseInt(hex[2] + hex[2], 16);
|
|
117
|
+
if ([r, g, b].every((v) => Number.isFinite(v))) return { r, g, b };
|
|
118
|
+
return void 0;
|
|
119
|
+
}
|
|
120
|
+
if (hex.length === 6 || hex.length === 8) {
|
|
121
|
+
const r = parseInt(hex.slice(0, 2), 16);
|
|
122
|
+
const g = parseInt(hex.slice(2, 4), 16);
|
|
123
|
+
const b = parseInt(hex.slice(4, 6), 16);
|
|
124
|
+
if ([r, g, b].every((v) => Number.isFinite(v))) return { r, g, b };
|
|
125
|
+
return void 0;
|
|
126
|
+
}
|
|
127
|
+
return void 0;
|
|
128
|
+
}
|
|
129
|
+
const rgbMatch = raw.match(/^rgba?\(([^)]+)\)$/);
|
|
130
|
+
if (rgbMatch) {
|
|
131
|
+
const parts = rgbMatch[1].split(",").map((p) => p.trim());
|
|
132
|
+
if (parts.length < 3) return void 0;
|
|
133
|
+
const channel = (p) => {
|
|
134
|
+
if (p.endsWith("%")) {
|
|
135
|
+
const pct = parseFloat(p.slice(0, -1));
|
|
136
|
+
if (!Number.isFinite(pct)) return void 0;
|
|
137
|
+
return clamp255(pct / 100 * 255);
|
|
138
|
+
}
|
|
139
|
+
const n = parseFloat(p);
|
|
140
|
+
if (!Number.isFinite(n)) return void 0;
|
|
141
|
+
return clamp255(n);
|
|
142
|
+
};
|
|
143
|
+
const r = channel(parts[0]);
|
|
144
|
+
const g = channel(parts[1]);
|
|
145
|
+
const b = channel(parts[2]);
|
|
146
|
+
if (r === void 0 || g === void 0 || b === void 0) return void 0;
|
|
147
|
+
return { r, g, b };
|
|
148
|
+
}
|
|
149
|
+
return void 0;
|
|
150
|
+
}
|
|
151
|
+
function parseFontSizePx(input) {
|
|
152
|
+
if (typeof input === "number") {
|
|
153
|
+
return Number.isFinite(input) && input > 0 ? input : void 0;
|
|
154
|
+
}
|
|
155
|
+
if (typeof input === "string") {
|
|
156
|
+
const m = input.trim().match(/^(-?\d*\.?\d+)/);
|
|
157
|
+
if (m) {
|
|
158
|
+
const n = parseFloat(m[1]);
|
|
159
|
+
return Number.isFinite(n) && n > 0 ? n : void 0;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return void 0;
|
|
163
|
+
}
|
|
164
|
+
function projectVisionFields(args) {
|
|
165
|
+
const { type, label, handlerNames, state, flatStyle } = args;
|
|
166
|
+
const fields = {
|
|
167
|
+
interactable: deriveInteractable(type, handlerNames)
|
|
168
|
+
};
|
|
169
|
+
const text = deriveText(state, label);
|
|
170
|
+
if (text !== void 0) fields.text = text;
|
|
171
|
+
const role = roleForType(type);
|
|
172
|
+
if (role !== void 0) fields.role = role;
|
|
173
|
+
if (flatStyle) {
|
|
174
|
+
const fg = parseColorToRgb(flatStyle.color);
|
|
175
|
+
if (fg !== void 0) fields.fg_color = fg;
|
|
176
|
+
const bg = parseColorToRgb(flatStyle.backgroundColor);
|
|
177
|
+
if (bg !== void 0) fields.bg_color = bg;
|
|
178
|
+
const size = parseFontSizePx(flatStyle.fontSize);
|
|
179
|
+
if (size !== void 0) fields.font_size_px = size;
|
|
180
|
+
if (typeof flatStyle.fontFamily === "string" && flatStyle.fontFamily.trim().length > 0) {
|
|
181
|
+
fields.font_family = flatStyle.fontFamily;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return fields;
|
|
185
|
+
}
|
|
186
|
+
|
|
14
187
|
// src/core/registry.ts
|
|
15
188
|
function extractHandlerNames(props) {
|
|
16
189
|
if (!props) return [];
|
|
@@ -20,6 +193,25 @@ function matchesCurrentRoute(registrationRoute, currentRoute) {
|
|
|
20
193
|
if (registrationRoute == null || registrationRoute === "") return true;
|
|
21
194
|
return registrationRoute === currentRoute;
|
|
22
195
|
}
|
|
196
|
+
function projectBbox(state, visibility, pixelRatio = 1) {
|
|
197
|
+
if (visibility !== "visible") return void 0;
|
|
198
|
+
const layout = state.layout;
|
|
199
|
+
if (!layout) return void 0;
|
|
200
|
+
const ratio = pixelRatio > 0 && Number.isFinite(pixelRatio) ? pixelRatio : 1;
|
|
201
|
+
const toPx = (v) => Math.max(0, Math.round(v * ratio));
|
|
202
|
+
const hasPage = typeof layout.pageX === "number" && typeof layout.pageY === "number" && Number.isFinite(layout.pageX) && Number.isFinite(layout.pageY);
|
|
203
|
+
const originX = hasPage ? layout.pageX : layout.x;
|
|
204
|
+
const originY = hasPage ? layout.pageY : layout.y;
|
|
205
|
+
if (typeof originX !== "number" || typeof originY !== "number" || typeof layout.width !== "number" || typeof layout.height !== "number" || !Number.isFinite(originX) || !Number.isFinite(originY) || !Number.isFinite(layout.width) || !Number.isFinite(layout.height)) {
|
|
206
|
+
return void 0;
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
x: toPx(originX),
|
|
210
|
+
y: toPx(originY),
|
|
211
|
+
w: toPx(layout.width),
|
|
212
|
+
h: toPx(layout.height)
|
|
213
|
+
};
|
|
214
|
+
}
|
|
23
215
|
function inferActions(type) {
|
|
24
216
|
const baseActions = ["focus", "blur"];
|
|
25
217
|
switch (type) {
|
|
@@ -84,6 +276,15 @@ var NativeUIBridgeRegistry = class {
|
|
|
84
276
|
* already installs.
|
|
85
277
|
*/
|
|
86
278
|
this.routeProvider = null;
|
|
279
|
+
/**
|
|
280
|
+
* Device pixel ratio used to project `state.layout` (logical dp) into the
|
|
281
|
+
* physical-pixel `bbox` emitted on each snapshot element. Defaults to `1`
|
|
282
|
+
* (dp ≈ px) so callers/tests that never inject a ratio still get a usable
|
|
283
|
+
* box; the React provider wires the real `PixelRatio.get()` via
|
|
284
|
+
* {@link setPixelRatio}. Read from a non-React core module on purpose — see
|
|
285
|
+
* the `projectBbox` doc comment for why we don't import `react-native` here.
|
|
286
|
+
*/
|
|
287
|
+
this.pixelRatio = 1;
|
|
87
288
|
/**
|
|
88
289
|
* Sticky flag: flips `true` the first time any element is registered and
|
|
89
290
|
* stays `true` even after elements are unregistered. Lets agents distinguish
|
|
@@ -121,6 +322,21 @@ var NativeUIBridgeRegistry = class {
|
|
|
121
322
|
getRouteProvider() {
|
|
122
323
|
return this.routeProvider;
|
|
123
324
|
}
|
|
325
|
+
/**
|
|
326
|
+
* Set the device pixel ratio used to project per-element `bbox` geometry
|
|
327
|
+
* into physical pixels. The React provider passes `PixelRatio.get()` here
|
|
328
|
+
* once at startup. Ignores non-finite / non-positive values (keeps the
|
|
329
|
+
* previous ratio) so a bad call can't zero out every bbox.
|
|
330
|
+
*/
|
|
331
|
+
setPixelRatio(ratio) {
|
|
332
|
+
if (typeof ratio === "number" && Number.isFinite(ratio) && ratio > 0) {
|
|
333
|
+
this.pixelRatio = ratio;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/** Read-only accessor for the device pixel ratio used for bbox projection. */
|
|
337
|
+
getPixelRatio() {
|
|
338
|
+
return this.pixelRatio;
|
|
339
|
+
}
|
|
124
340
|
// ============================================================================
|
|
125
341
|
// Snapshot Enricher Slots
|
|
126
342
|
// ============================================================================
|
|
@@ -660,6 +876,18 @@ var NativeUIBridgeRegistry = class {
|
|
|
660
876
|
const handlers = extractHandlerNames(e.props);
|
|
661
877
|
const state = e.getState();
|
|
662
878
|
const visibility = !state.visible ? "hidden" : state.layout !== null ? "visible" : "likely-visible";
|
|
879
|
+
const bbox = projectBbox(state, visibility, this.pixelRatio);
|
|
880
|
+
const vision = projectVisionFields({
|
|
881
|
+
type: e.type,
|
|
882
|
+
label: e.label,
|
|
883
|
+
// Real interactivity signal: the names of props that are actually
|
|
884
|
+
// functions (onPress/onChangeText/...). NOT `e.actions`, which
|
|
885
|
+
// `inferActions` synthesizes press/click onto for every element —
|
|
886
|
+
// using it would mark plain text/view nodes interactable.
|
|
887
|
+
handlerNames: handlers,
|
|
888
|
+
state,
|
|
889
|
+
flatStyle: e.flatStyle
|
|
890
|
+
});
|
|
663
891
|
return {
|
|
664
892
|
id: e.id,
|
|
665
893
|
type: e.type,
|
|
@@ -670,7 +898,9 @@ var NativeUIBridgeRegistry = class {
|
|
|
670
898
|
customActions: e.customActions ? Object.keys(e.customActions) : void 0,
|
|
671
899
|
registeredHandlers: handlers.length > 0 ? handlers : void 0,
|
|
672
900
|
registrationRoute: e.registrationRoute,
|
|
673
|
-
visibility
|
|
901
|
+
visibility,
|
|
902
|
+
...bbox ? { bbox } : {},
|
|
903
|
+
...vision
|
|
674
904
|
};
|
|
675
905
|
}),
|
|
676
906
|
components: this.getAllComponents().map((c) => ({
|
|
@@ -987,9 +1217,10 @@ var DefaultNativeActionExecutor = class {
|
|
|
987
1217
|
requestId: request.requestId,
|
|
988
1218
|
durationMs: successTime - startTime
|
|
989
1219
|
});
|
|
1220
|
+
const fresh = this.registry.getElement(registered.id) ?? registered;
|
|
990
1221
|
return {
|
|
991
1222
|
success: true,
|
|
992
|
-
elementState:
|
|
1223
|
+
elementState: fresh.getState(),
|
|
993
1224
|
result,
|
|
994
1225
|
durationMs: successTime - startTime,
|
|
995
1226
|
timestamp: successTime,
|
|
@@ -1895,7 +2126,7 @@ function createDesignHandlers(registry, viewportProvider) {
|
|
|
1895
2126
|
var GRID_SIZE = 20;
|
|
1896
2127
|
var NAV_TYPES = /* @__PURE__ */ new Set(["button", "heading", "badge", "status-message"]);
|
|
1897
2128
|
var SKIP_TEXT_TYPES = /* @__PURE__ */ new Set(["button", "link", "tab", "menuitem"]);
|
|
1898
|
-
var
|
|
2129
|
+
var INTERACTIVE_TYPES2 = /* @__PURE__ */ new Set([
|
|
1899
2130
|
"button",
|
|
1900
2131
|
"input",
|
|
1901
2132
|
"switch",
|
|
@@ -2092,7 +2323,7 @@ function diagnosePageHealth(elements, viewport) {
|
|
|
2092
2323
|
let interactiveTotal = 0;
|
|
2093
2324
|
let interactiveDisabled = 0;
|
|
2094
2325
|
for (const el of elements) {
|
|
2095
|
-
if (!
|
|
2326
|
+
if (!INTERACTIVE_TYPES2.has(el.type)) continue;
|
|
2096
2327
|
interactiveTotal++;
|
|
2097
2328
|
if (el.state.enabled === false) interactiveDisabled++;
|
|
2098
2329
|
}
|
|
@@ -2160,6 +2391,13 @@ function matchElementsByText(registry, text, opts) {
|
|
|
2160
2391
|
scored.sort((a, b) => a.rank - b.rank);
|
|
2161
2392
|
return scored.map((s) => s.element);
|
|
2162
2393
|
}
|
|
2394
|
+
function resolveElementState(element) {
|
|
2395
|
+
const state = element.getState();
|
|
2396
|
+
if (element.type === "input" && state.value === void 0 && typeof element.props?.value === "string") {
|
|
2397
|
+
return { ...state, value: element.props.value };
|
|
2398
|
+
}
|
|
2399
|
+
return state;
|
|
2400
|
+
}
|
|
2163
2401
|
async function executeWorkflowStep(step, registry, executor) {
|
|
2164
2402
|
const startTime = Date.now();
|
|
2165
2403
|
try {
|
|
@@ -2368,6 +2606,16 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2368
2606
|
const handlers = extractHandlerNames(e.props);
|
|
2369
2607
|
const state = e.getState();
|
|
2370
2608
|
const visibility = !state.visible ? "hidden" : state.layout !== null ? "visible" : "likely-visible";
|
|
2609
|
+
const bbox = projectBbox(state, visibility, registry.getPixelRatio());
|
|
2610
|
+
const vision = projectVisionFields({
|
|
2611
|
+
type: e.type,
|
|
2612
|
+
label: e.label,
|
|
2613
|
+
// Real handler names (onPress/...), NOT inferred actions which
|
|
2614
|
+
// synthesize press/click onto every element. See projectVisionFields.
|
|
2615
|
+
handlerNames: handlers,
|
|
2616
|
+
state,
|
|
2617
|
+
flatStyle: e.flatStyle
|
|
2618
|
+
});
|
|
2371
2619
|
return {
|
|
2372
2620
|
id: e.id,
|
|
2373
2621
|
type: e.type,
|
|
@@ -2378,7 +2626,9 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2378
2626
|
customActions: e.customActions ? Object.keys(e.customActions) : void 0,
|
|
2379
2627
|
registeredHandlers: handlers.length > 0 ? handlers : void 0,
|
|
2380
2628
|
registrationRoute: e.registrationRoute,
|
|
2381
|
-
visibility
|
|
2629
|
+
visibility,
|
|
2630
|
+
...bbox ? { bbox } : {},
|
|
2631
|
+
...vision
|
|
2382
2632
|
};
|
|
2383
2633
|
});
|
|
2384
2634
|
return success2({ elements });
|
|
@@ -2396,7 +2646,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2396
2646
|
type: element.type,
|
|
2397
2647
|
label: element.label,
|
|
2398
2648
|
identifier: element.getIdentifier(),
|
|
2399
|
-
state: element
|
|
2649
|
+
state: resolveElementState(element),
|
|
2400
2650
|
actions: element.actions,
|
|
2401
2651
|
customActions: element.customActions ? Object.keys(element.customActions) : void 0,
|
|
2402
2652
|
registeredHandlers: handlers.length > 0 ? handlers : void 0,
|
|
@@ -2410,18 +2660,19 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2410
2660
|
if (!element) {
|
|
2411
2661
|
return error2(`Element not found: ${id}`, "ELEMENT_NOT_FOUND");
|
|
2412
2662
|
}
|
|
2413
|
-
return success2({ state: element
|
|
2663
|
+
return success2({ state: resolveElementState(element) });
|
|
2414
2664
|
},
|
|
2415
2665
|
executeAction: async (ctx) => {
|
|
2416
|
-
const
|
|
2417
|
-
const
|
|
2418
|
-
|
|
2666
|
+
const rawBody = ctx.body ?? {};
|
|
2667
|
+
const envelope = rawBody.request && typeof rawBody.request === "object" ? rawBody.request : rawBody;
|
|
2668
|
+
const id = ctx.params.id ?? rawBody.id ?? "";
|
|
2669
|
+
if (!envelope?.action) {
|
|
2419
2670
|
return error2("Action is required", "INVALID_REQUEST");
|
|
2420
2671
|
}
|
|
2421
2672
|
const response = await executor.executeAction(id, {
|
|
2422
|
-
action:
|
|
2423
|
-
params:
|
|
2424
|
-
waitOptions:
|
|
2673
|
+
action: envelope.action,
|
|
2674
|
+
params: envelope.params,
|
|
2675
|
+
waitOptions: envelope.waitOptions
|
|
2425
2676
|
});
|
|
2426
2677
|
if (!response.success) {
|
|
2427
2678
|
return error2(response.error || "Action failed", "ACTION_FAILED");
|
|
@@ -3060,7 +3311,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
3060
3311
|
};
|
|
3061
3312
|
if (config?.appInfo) {
|
|
3062
3313
|
response.uiBridge = {
|
|
3063
|
-
version: "0.
|
|
3314
|
+
version: "0.6.11",
|
|
3064
3315
|
...config.appInfo,
|
|
3065
3316
|
capabilities: ["elements", "components", "actions", "design"],
|
|
3066
3317
|
elementCount: stats.elements,
|
|
@@ -3652,6 +3903,14 @@ var NativeUIBridgeServer = class {
|
|
|
3652
3903
|
};
|
|
3653
3904
|
};
|
|
3654
3905
|
}
|
|
3906
|
+
/**
|
|
3907
|
+
* Whether the console/network capture should be installed. The dedicated
|
|
3908
|
+
* `observability` flag wins when set; otherwise falls back to `testHooks`
|
|
3909
|
+
* so consumers predating the flag keep their exact behavior.
|
|
3910
|
+
*/
|
|
3911
|
+
observabilityEnabled() {
|
|
3912
|
+
return this.config.observability ?? this.config.testHooks === true;
|
|
3913
|
+
}
|
|
3655
3914
|
/**
|
|
3656
3915
|
* Set the server adapter
|
|
3657
3916
|
*/
|
|
@@ -3865,7 +4124,7 @@ var NativeUIBridgeServer = class {
|
|
|
3865
4124
|
console.warn("[ui-bridge-native] See documentation for supported adapters.");
|
|
3866
4125
|
return;
|
|
3867
4126
|
}
|
|
3868
|
-
if (this.
|
|
4127
|
+
if (this.observabilityEnabled()) {
|
|
3869
4128
|
this.consoleErrorBuffer.install();
|
|
3870
4129
|
this.networkRequestBuffer.install();
|
|
3871
4130
|
}
|
|
@@ -5367,6 +5626,7 @@ function UIBridgeNativeProvider({
|
|
|
5367
5626
|
verbose: config.verbose,
|
|
5368
5627
|
onEvent
|
|
5369
5628
|
});
|
|
5629
|
+
registryRef.current.setPixelRatio(reactNative.PixelRatio.get());
|
|
5370
5630
|
setGlobalRegistry(registryRef.current);
|
|
5371
5631
|
}
|
|
5372
5632
|
const registry = registryRef.current;
|
|
@@ -5397,6 +5657,10 @@ function UIBridgeNativeProvider({
|
|
|
5397
5657
|
cors: true,
|
|
5398
5658
|
appInfo: config.appInfo,
|
|
5399
5659
|
testHooks: features.testHooks === true,
|
|
5660
|
+
// Dedicated console/network-capture flag; the server defaults it to
|
|
5661
|
+
// the `testHooks` value when undefined, so passing it through verbatim
|
|
5662
|
+
// keeps existing consumers unchanged.
|
|
5663
|
+
observability: features.observability,
|
|
5400
5664
|
// Injected device-viewport getter for POST /control/page-health. The
|
|
5401
5665
|
// server doesn't import `react-native` itself — the require/import
|
|
5402
5666
|
// pattern there crashed the host RN app in 0.6.3/0.6.4 (Metro/Hermes
|
|
@@ -5464,6 +5728,7 @@ function UIBridgeNativeProvider({
|
|
|
5464
5728
|
}, [
|
|
5465
5729
|
features.server,
|
|
5466
5730
|
features.testHooks,
|
|
5731
|
+
features.observability,
|
|
5467
5732
|
config.serverPort,
|
|
5468
5733
|
config.appInfo,
|
|
5469
5734
|
registry,
|
|
@@ -5502,6 +5767,7 @@ function UIBridgeNativeProvider({
|
|
|
5502
5767
|
cors: true,
|
|
5503
5768
|
appInfo: config.appInfo,
|
|
5504
5769
|
testHooks: features.testHooks === true,
|
|
5770
|
+
observability: features.observability,
|
|
5505
5771
|
viewportProvider: () => {
|
|
5506
5772
|
const win = reactNative.Dimensions.get("window");
|
|
5507
5773
|
return { width: win.width, height: win.height };
|
|
@@ -5705,12 +5971,41 @@ function scheduleDeadButtonWarning(bridge, id, type) {
|
|
|
5705
5971
|
}
|
|
5706
5972
|
function useUIElement(options) {
|
|
5707
5973
|
const bridge = useUIBridgeNativeOptional();
|
|
5708
|
-
const ref = react.useRef(null);
|
|
5709
5974
|
const [registered, setRegistered] = react.useState(false);
|
|
5710
5975
|
const [_layout, setLayout] = react.useState(null);
|
|
5711
5976
|
const propsRef = react.useRef({});
|
|
5712
5977
|
const registeredRef = react.useRef(false);
|
|
5713
5978
|
const registeredIdRef = react.useRef(null);
|
|
5979
|
+
const bridgeRef = react.useRef(bridge);
|
|
5980
|
+
bridgeRef.current = bridge;
|
|
5981
|
+
const nodeRef = react.useRef(null);
|
|
5982
|
+
const refInterceptorRef = react.useRef(null);
|
|
5983
|
+
if (refInterceptorRef.current === null) {
|
|
5984
|
+
refInterceptorRef.current = {
|
|
5985
|
+
get current() {
|
|
5986
|
+
return nodeRef.current;
|
|
5987
|
+
},
|
|
5988
|
+
set current(node) {
|
|
5989
|
+
const prev = nodeRef.current;
|
|
5990
|
+
nodeRef.current = node;
|
|
5991
|
+
const liveBridge = bridgeRef.current;
|
|
5992
|
+
const registeredId = registeredIdRef.current;
|
|
5993
|
+
if (!liveBridge || !registeredRef.current || registeredId === null) return;
|
|
5994
|
+
if (node == null && prev != null) {
|
|
5995
|
+
const state = liveBridge.registry.getElement(registeredId)?.getState();
|
|
5996
|
+
if (state && state.visible !== false) {
|
|
5997
|
+
liveBridge.registry.updateElementState(registeredId, { visible: false });
|
|
5998
|
+
}
|
|
5999
|
+
} else if (node != null && prev == null) {
|
|
6000
|
+
const state = liveBridge.registry.getElement(registeredId)?.getState();
|
|
6001
|
+
if (state && state.visible !== true) {
|
|
6002
|
+
liveBridge.registry.updateElementState(registeredId, { visible: true });
|
|
6003
|
+
}
|
|
6004
|
+
}
|
|
6005
|
+
}
|
|
6006
|
+
};
|
|
6007
|
+
}
|
|
6008
|
+
const ref = refInterceptorRef.current;
|
|
5714
6009
|
const {
|
|
5715
6010
|
id,
|
|
5716
6011
|
type = "custom",
|
|
@@ -5754,7 +6049,7 @@ function useUIElement(options) {
|
|
|
5754
6049
|
}
|
|
5755
6050
|
setRegistered(true);
|
|
5756
6051
|
scheduleDeadButtonWarning(bridge, id, type);
|
|
5757
|
-
}, [bridge, id, type, label, actions, customActions, treePath, style, stateStylesProp, valueProp]);
|
|
6052
|
+
}, [bridge, id, type, label, actions, customActions, treePath, style, stateStylesProp, valueProp, ref]);
|
|
5758
6053
|
const unregister = react.useCallback(() => {
|
|
5759
6054
|
if (!bridge || !registeredRef.current) return;
|
|
5760
6055
|
bridge.registry.unregisterElement(registeredIdRef.current ?? id);
|
|
@@ -5790,7 +6085,8 @@ function useUIElement(options) {
|
|
|
5790
6085
|
writeLayout({ x, y, width, height, pageX: x, pageY: y });
|
|
5791
6086
|
}
|
|
5792
6087
|
},
|
|
5793
|
-
|
|
6088
|
+
// `ref` is the once-per-hook interceptor object — identity-stable.
|
|
6089
|
+
[bridge, id, ref]
|
|
5794
6090
|
);
|
|
5795
6091
|
const registerRef = react.useRef(register);
|
|
5796
6092
|
const unregisterRef = react.useRef(unregister);
|
|
@@ -5811,7 +6107,14 @@ function useUIElement(options) {
|
|
|
5811
6107
|
react.useEffect(() => {
|
|
5812
6108
|
if (!bridge || !registered) return;
|
|
5813
6109
|
const node = ref.current;
|
|
5814
|
-
if (!node
|
|
6110
|
+
if (!node) {
|
|
6111
|
+
const seeded = bridge.registry.getElement(id)?.getState();
|
|
6112
|
+
if (seeded && seeded.visible !== false) {
|
|
6113
|
+
bridge.registry.updateElementState(id, { visible: false });
|
|
6114
|
+
}
|
|
6115
|
+
return;
|
|
6116
|
+
}
|
|
6117
|
+
if (typeof node.measureInWindow !== "function") return;
|
|
5815
6118
|
let cancelled = false;
|
|
5816
6119
|
let pendingTimer = null;
|
|
5817
6120
|
const backoffMs = [50, 200, 500, 1e3, 2500];
|
|
@@ -5850,7 +6153,7 @@ function useUIElement(options) {
|
|
|
5850
6153
|
cancelled = true;
|
|
5851
6154
|
if (pendingTimer != null) clearTimeout(pendingTimer);
|
|
5852
6155
|
};
|
|
5853
|
-
}, [bridge, registered, id]);
|
|
6156
|
+
}, [bridge, registered, id, ref]);
|
|
5854
6157
|
const registeredHandlerKeysRef = react.useRef([]);
|
|
5855
6158
|
react.useEffect(() => {
|
|
5856
6159
|
if (!bridge || !registered || !handlersProp) return;
|
|
@@ -6632,6 +6935,46 @@ function decodeTextPayload(payload) {
|
|
|
6632
6935
|
return new TextDecoder().decode(payload);
|
|
6633
6936
|
}
|
|
6634
6937
|
|
|
6938
|
+
// src/server/bind-retry.ts
|
|
6939
|
+
function isAddrInUseError(err) {
|
|
6940
|
+
const message = err instanceof Error ? err.message : typeof err === "string" ? err : String(err);
|
|
6941
|
+
const code = err?.code;
|
|
6942
|
+
return message.includes("EADDRINUSE") || message.toLowerCase().includes("address already in use") || code === "EADDRINUSE";
|
|
6943
|
+
}
|
|
6944
|
+
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
6945
|
+
async function bindWithRetry(attempt, options = {}) {
|
|
6946
|
+
const {
|
|
6947
|
+
minTotalMs = 3e4,
|
|
6948
|
+
initialDelayMs = 500,
|
|
6949
|
+
maxDelayMs = 5e3,
|
|
6950
|
+
backoffFactor = 2,
|
|
6951
|
+
logger = console,
|
|
6952
|
+
sleep: sleep2 = defaultSleep,
|
|
6953
|
+
now = Date.now
|
|
6954
|
+
} = options;
|
|
6955
|
+
const startedAt = now();
|
|
6956
|
+
let delay = initialDelayMs;
|
|
6957
|
+
let attemptIndex = 0;
|
|
6958
|
+
for (; ; ) {
|
|
6959
|
+
try {
|
|
6960
|
+
await attempt(attemptIndex);
|
|
6961
|
+
return;
|
|
6962
|
+
} catch (err) {
|
|
6963
|
+
const elapsed = now() - startedAt;
|
|
6964
|
+
const budgetExhausted = elapsed + delay >= minTotalMs;
|
|
6965
|
+
if (!isAddrInUseError(err) || budgetExhausted) {
|
|
6966
|
+
throw err;
|
|
6967
|
+
}
|
|
6968
|
+
logger.warn(
|
|
6969
|
+
`[ui-bridge-native] bind failed with EADDRINUSE (attempt ${attemptIndex + 1}); retrying in ${delay}ms (elapsed ${elapsed}ms / budget ${minTotalMs === Infinity ? "\u221E" : minTotalMs}ms)`
|
|
6970
|
+
);
|
|
6971
|
+
await sleep2(delay);
|
|
6972
|
+
delay = Math.min(delay * backoffFactor, maxDelayMs);
|
|
6973
|
+
attemptIndex += 1;
|
|
6974
|
+
}
|
|
6975
|
+
}
|
|
6976
|
+
}
|
|
6977
|
+
|
|
6635
6978
|
// src/server/ws-connection.ts
|
|
6636
6979
|
var WebSocketConnection = class {
|
|
6637
6980
|
constructor(id, socket, onMessage, onClose) {
|
|
@@ -7213,6 +7556,7 @@ exports.UI_BRIDGE_NATIVE_ROUTES = UI_BRIDGE_NATIVE_ROUTES;
|
|
|
7213
7556
|
exports.UndoTracker = UndoTracker;
|
|
7214
7557
|
exports.WebSocketConnection = WebSocketConnection;
|
|
7215
7558
|
exports.WebSocketEventBridge = WebSocketEventBridge;
|
|
7559
|
+
exports.bindWithRetry = bindWithRetry;
|
|
7216
7560
|
exports.buildTreePath = buildTreePath;
|
|
7217
7561
|
exports.buildUpgradeResponse = buildUpgradeResponse;
|
|
7218
7562
|
exports.captureNativeResponsiveSnapshot = captureNativeResponsiveSnapshot;
|
|
@@ -7223,15 +7567,22 @@ exports.createNativeActionExecutor = createNativeActionExecutor;
|
|
|
7223
7567
|
exports.createNativeElementIdentifier = createNativeElementIdentifier;
|
|
7224
7568
|
exports.createNativeServer = createNativeServer;
|
|
7225
7569
|
exports.createServerHandlers = createServerHandlers;
|
|
7570
|
+
exports.deriveInteractable = deriveInteractable;
|
|
7571
|
+
exports.deriveText = deriveText;
|
|
7226
7572
|
exports.extractHandlerNames = extractHandlerNames;
|
|
7227
7573
|
exports.findAllByPattern = findAllByPattern;
|
|
7228
7574
|
exports.findElementByIdentifier = findElementByIdentifier;
|
|
7229
7575
|
exports.getGlobalRegistry = getGlobalRegistry;
|
|
7230
7576
|
exports.getNativeElementDesignData = getNativeElementDesignData;
|
|
7577
|
+
exports.isAddrInUseError = isAddrInUseError;
|
|
7231
7578
|
exports.mapNativeStyleToExtended = mapNativeStyleToExtended;
|
|
7232
7579
|
exports.matchesIdentifier = matchesIdentifier;
|
|
7580
|
+
exports.parseColorToRgb = parseColorToRgb;
|
|
7581
|
+
exports.parseFontSizePx = parseFontSizePx;
|
|
7233
7582
|
exports.parseTreePath = parseTreePath;
|
|
7583
|
+
exports.projectVisionFields = projectVisionFields;
|
|
7234
7584
|
exports.resetGlobalRegistry = resetGlobalRegistry;
|
|
7585
|
+
exports.roleForType = roleForType;
|
|
7235
7586
|
exports.setGlobalRegistry = setGlobalRegistry;
|
|
7236
7587
|
exports.useToastRecorder = useToastRecorder;
|
|
7237
7588
|
exports.useUIBridge = useUIBridge;
|