@qontinui/ui-bridge-native 0.2.0 → 0.3.0
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/{useUIBridgeToast-GmT1TzUe.d.mts → StateAnnotation-D8reaRcS.d.mts} +42 -7
- package/dist/{useUIBridgeToast-D32KXyL1.d.ts → StateAnnotation-WmK1YjwP.d.ts} +42 -7
- package/dist/control/index.d.mts +4 -4
- package/dist/control/index.d.ts +4 -4
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.d.ts +3 -3
- package/dist/core/index.js +39 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +39 -1
- package/dist/core/index.mjs.map +1 -1
- package/dist/debug/index.js +2 -2
- package/dist/debug/index.js.map +1 -1
- package/dist/debug/index.mjs +2 -2
- package/dist/debug/index.mjs.map +1 -1
- package/dist/design/index.d.mts +1 -1
- package/dist/design/index.d.ts +1 -1
- package/dist/{http-server-DCdR8WlL.d.mts → http-server-D2U1k48d.d.mts} +89 -8
- package/dist/{http-server-Dj3grQW4.d.ts → http-server-ck8clUsZ.d.ts} +89 -8
- package/dist/index.d.mts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +372 -39
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +372 -41
- package/dist/index.mjs.map +1 -1
- package/dist/react/index.d.mts +5 -5
- package/dist/react/index.d.ts +5 -5
- package/dist/react/index.js +354 -39
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +354 -41
- package/dist/react/index.mjs.map +1 -1
- package/dist/{registry-Cahd8Gn2.d.mts → registry-BxXaxmsT.d.ts} +24 -1
- package/dist/{registry-z-3WJsxp.d.ts → registry-KdUW_koV.d.mts} +24 -1
- package/dist/server/index.d.mts +5 -5
- package/dist/server/index.d.ts +5 -5
- package/dist/server/index.js +272 -16
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +272 -16
- package/dist/server/index.mjs.map +1 -1
- package/dist/{types-Oz13YLZq.d.mts → types-BwdPNpII.d.mts} +52 -4
- package/dist/{types-Oz13YLZq.d.ts → types-BwdPNpII.d.ts} +52 -4
- package/dist/{types-ajakyp-l.d.ts → types-DCEv5qb6.d.ts} +1 -1
- package/dist/{types-DAfCeWXc.d.mts → types-DjB0zJJH.d.mts} +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -57,6 +57,13 @@ var NativeUIBridgeRegistry = class {
|
|
|
57
57
|
this.eventListeners = /* @__PURE__ */ new Map();
|
|
58
58
|
this.enrichers = {};
|
|
59
59
|
this.snapshotExtras = /* @__PURE__ */ new Map();
|
|
60
|
+
/**
|
|
61
|
+
* Sticky flag: flips `true` the first time any element is registered and
|
|
62
|
+
* stays `true` even after elements are unregistered. Lets agents distinguish
|
|
63
|
+
* "this route never wired any elements" from "this route registered then
|
|
64
|
+
* unmounted".
|
|
65
|
+
*/
|
|
66
|
+
this.everHadRegistrations = false;
|
|
60
67
|
this.config = config;
|
|
61
68
|
}
|
|
62
69
|
// ============================================================================
|
|
@@ -69,6 +76,16 @@ var NativeUIBridgeRegistry = class {
|
|
|
69
76
|
setEnrichers(e) {
|
|
70
77
|
this.enrichers = { ...this.enrichers, ...e };
|
|
71
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Read-only accessor for the canonical enrichers slot. Returned object is
|
|
81
|
+
* the live reference, so callers can detect "no detector wired" via
|
|
82
|
+
* `getEnrichers().modalDetector === undefined`. Used by the test-hook HTTP
|
|
83
|
+
* handlers (`control/modal/push`, `control/modal/dismiss/:id`) which need
|
|
84
|
+
* to drive the ModalDetector directly without re-instantiating it.
|
|
85
|
+
*/
|
|
86
|
+
getEnrichers() {
|
|
87
|
+
return this.enrichers;
|
|
88
|
+
}
|
|
72
89
|
/**
|
|
73
90
|
* Register a custom snapshot enricher. The returned object will be
|
|
74
91
|
* `Object.assign`ed onto the snapshot, so use unique top-level keys to avoid
|
|
@@ -148,6 +165,7 @@ var NativeUIBridgeRegistry = class {
|
|
|
148
165
|
stateStyles
|
|
149
166
|
};
|
|
150
167
|
this.elements.set(id, registered);
|
|
168
|
+
this.everHadRegistrations = true;
|
|
151
169
|
this.emit("element:registered", { id, type, label });
|
|
152
170
|
if (this.config.verbose) {
|
|
153
171
|
console.log(`[ui-bridge-native] Registered element: ${id} (${type})`);
|
|
@@ -475,7 +493,8 @@ var NativeUIBridgeRegistry = class {
|
|
|
475
493
|
stepCount: w.steps.length
|
|
476
494
|
})),
|
|
477
495
|
currentRoute: routeInfo?.currentRoute ?? null,
|
|
478
|
-
segments: routeInfo?.segments
|
|
496
|
+
segments: routeInfo?.segments,
|
|
497
|
+
registration: this.getRegistrationCoverage()
|
|
479
498
|
};
|
|
480
499
|
if (this.enrichers.modalDetector) {
|
|
481
500
|
try {
|
|
@@ -531,6 +550,25 @@ var NativeUIBridgeRegistry = class {
|
|
|
531
550
|
workflows: this.workflows.size
|
|
532
551
|
};
|
|
533
552
|
}
|
|
553
|
+
/**
|
|
554
|
+
* Compute registration coverage metadata for the current registry state.
|
|
555
|
+
*
|
|
556
|
+
* Groups currently-registered elements by `registrationRoute`, bucketing
|
|
557
|
+
* elements without a route under `'?'`. `everHadRegistrations` is sticky —
|
|
558
|
+
* once true, it stays true for the lifetime of the registry instance.
|
|
559
|
+
*/
|
|
560
|
+
getRegistrationCoverage() {
|
|
561
|
+
const byRoute = {};
|
|
562
|
+
for (const element of this.elements.values()) {
|
|
563
|
+
const key = element.registrationRoute == null || element.registrationRoute === "" ? "?" : element.registrationRoute;
|
|
564
|
+
byRoute[key] = (byRoute[key] ?? 0) + 1;
|
|
565
|
+
}
|
|
566
|
+
return {
|
|
567
|
+
totalRegistered: this.elements.size,
|
|
568
|
+
everHadRegistrations: this.everHadRegistrations,
|
|
569
|
+
byRoute
|
|
570
|
+
};
|
|
571
|
+
}
|
|
534
572
|
/**
|
|
535
573
|
* Clear all registrations
|
|
536
574
|
*/
|
|
@@ -1938,6 +1976,20 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1938
1976
|
return success2(response);
|
|
1939
1977
|
},
|
|
1940
1978
|
// Discovery
|
|
1979
|
+
//
|
|
1980
|
+
// Native parity with the runner's `POST /ui-bridge/ai/find`. The runner
|
|
1981
|
+
// matches a free-text `query` against label > text > value > aria-label >
|
|
1982
|
+
// placeholder > name. We replicate that here against the native registry's
|
|
1983
|
+
// analogues:
|
|
1984
|
+
// 1. `label` (the registered display label)
|
|
1985
|
+
// 2. identifier fields: `testId`, `accessibilityLabel`, `treePath`, `uiId`
|
|
1986
|
+
// 3. registered handler names (e.g. `onPress`, `onChangeText`)
|
|
1987
|
+
//
|
|
1988
|
+
// Match is case-insensitive substring. Empty/missing query falls back to
|
|
1989
|
+
// the structured filters (types/visibleOnly/etc.) so callers that don't
|
|
1990
|
+
// pass `query` still get the legacy behaviour. Once we've narrowed by
|
|
1991
|
+
// query the response is shaped through the executor's NativeFindResponse
|
|
1992
|
+
// envelope so the wire format stays stable.
|
|
1941
1993
|
find: async (ctx) => {
|
|
1942
1994
|
const body = ctx.body;
|
|
1943
1995
|
const response = await executor.find({
|
|
@@ -1945,9 +1997,49 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1945
1997
|
testIdPattern: body?.testIdPattern,
|
|
1946
1998
|
accessibilityLabelPattern: body?.accessibilityLabelPattern,
|
|
1947
1999
|
visibleOnly: body?.visibleOnly,
|
|
1948
|
-
limit
|
|
2000
|
+
// When `query` is supplied we apply our own limit *after* ranking so
|
|
2001
|
+
// the highest-priority matches survive the cap; defer slicing to us
|
|
2002
|
+
// by widening here.
|
|
2003
|
+
limit: body?.query && body.query.length > 0 ? void 0 : body?.limit
|
|
2004
|
+
});
|
|
2005
|
+
const rawQuery = typeof body?.query === "string" ? body.query.trim() : "";
|
|
2006
|
+
if (rawQuery.length === 0) {
|
|
2007
|
+
return success2(response);
|
|
2008
|
+
}
|
|
2009
|
+
const needle = rawQuery.toLowerCase();
|
|
2010
|
+
const containsNeedle = (value) => typeof value === "string" && value.toLowerCase().includes(needle);
|
|
2011
|
+
const scored = [];
|
|
2012
|
+
for (const el of response.elements) {
|
|
2013
|
+
let priorityRank = Number.POSITIVE_INFINITY;
|
|
2014
|
+
if (containsNeedle(el.label)) {
|
|
2015
|
+
priorityRank = Math.min(priorityRank, 0);
|
|
2016
|
+
}
|
|
2017
|
+
const ident = el.identifier;
|
|
2018
|
+
if (ident) {
|
|
2019
|
+
if (containsNeedle(ident.testId)) priorityRank = Math.min(priorityRank, 1);
|
|
2020
|
+
if (containsNeedle(ident.accessibilityLabel)) priorityRank = Math.min(priorityRank, 1);
|
|
2021
|
+
if (containsNeedle(ident.treePath)) priorityRank = Math.min(priorityRank, 1);
|
|
2022
|
+
if (containsNeedle(ident.uiId)) priorityRank = Math.min(priorityRank, 1);
|
|
2023
|
+
}
|
|
2024
|
+
if (priorityRank === Number.POSITIVE_INFINITY) {
|
|
2025
|
+
const registered = registry.getElement(el.id);
|
|
2026
|
+
const handlers = extractHandlerNames(registered?.props);
|
|
2027
|
+
if (handlers.some((h) => h.toLowerCase().includes(needle))) {
|
|
2028
|
+
priorityRank = 2;
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
if (priorityRank !== Number.POSITIVE_INFINITY) {
|
|
2032
|
+
scored.push({ priorityRank, element: el });
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
scored.sort((a, b) => a.priorityRank - b.priorityRank);
|
|
2036
|
+
const limit = body?.limit && body.limit > 0 ? body.limit : void 0;
|
|
2037
|
+
const filtered = limit ? scored.slice(0, limit) : scored;
|
|
2038
|
+
return success2({
|
|
2039
|
+
...response,
|
|
2040
|
+
elements: filtered.map((s) => s.element),
|
|
2041
|
+
total: filtered.length
|
|
1949
2042
|
});
|
|
1950
|
-
return success2(response);
|
|
1951
2043
|
},
|
|
1952
2044
|
getSnapshot: async (ctx) => {
|
|
1953
2045
|
const visibleOnly = ctx.query?.visibleOnly === "true" || ctx.body?.visibleOnly === true;
|
|
@@ -2048,15 +2140,162 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2048
2140
|
getMethods: async () => {
|
|
2049
2141
|
return error2("Methods introspection not configured", "NOT_SUPPORTED");
|
|
2050
2142
|
},
|
|
2143
|
+
// AI helpers
|
|
2144
|
+
//
|
|
2145
|
+
// Mobile-side parity with the runner's `POST /ui-bridge/ai/fill-form`.
|
|
2146
|
+
// Each entry in `fields` is dispatched through the existing executor as a
|
|
2147
|
+
// `type` action with `clear: true` so the input value is replaced
|
|
2148
|
+
// atomically (the closest native equivalent to the web bridge's
|
|
2149
|
+
// "setValue" semantics — there is no `setValue` action on native).
|
|
2150
|
+
//
|
|
2151
|
+
// Failures do not short-circuit: every field is attempted, and the
|
|
2152
|
+
// response carries per-field success/error plus aggregate counts.
|
|
2153
|
+
fillForm: async (ctx) => {
|
|
2154
|
+
const body = ctx.body;
|
|
2155
|
+
const rawFields = body?.fields;
|
|
2156
|
+
if (!Array.isArray(rawFields)) {
|
|
2157
|
+
return error2('Request body must include "fields" array', "INVALID_REQUEST");
|
|
2158
|
+
}
|
|
2159
|
+
const results = [];
|
|
2160
|
+
for (let i = 0; i < rawFields.length; i++) {
|
|
2161
|
+
const field = rawFields[i];
|
|
2162
|
+
if (!field || typeof field !== "object") {
|
|
2163
|
+
results.push({
|
|
2164
|
+
elementId: "",
|
|
2165
|
+
success: false,
|
|
2166
|
+
error: `fields[${i}] must be an object with "elementId" and "value"`
|
|
2167
|
+
});
|
|
2168
|
+
continue;
|
|
2169
|
+
}
|
|
2170
|
+
const { elementId, value } = field;
|
|
2171
|
+
if (typeof elementId !== "string" || elementId.length === 0) {
|
|
2172
|
+
results.push({
|
|
2173
|
+
elementId: typeof elementId === "string" ? elementId : "",
|
|
2174
|
+
success: false,
|
|
2175
|
+
error: `fields[${i}] is missing a non-empty "elementId"`
|
|
2176
|
+
});
|
|
2177
|
+
continue;
|
|
2178
|
+
}
|
|
2179
|
+
if (typeof value !== "string") {
|
|
2180
|
+
results.push({
|
|
2181
|
+
elementId,
|
|
2182
|
+
success: false,
|
|
2183
|
+
error: `fields[${i}] is missing a string "value"`
|
|
2184
|
+
});
|
|
2185
|
+
continue;
|
|
2186
|
+
}
|
|
2187
|
+
try {
|
|
2188
|
+
const response = await executor.executeAction(elementId, {
|
|
2189
|
+
action: "type",
|
|
2190
|
+
params: { text: value, clear: true }
|
|
2191
|
+
});
|
|
2192
|
+
if (response.success) {
|
|
2193
|
+
results.push({ elementId, success: true });
|
|
2194
|
+
} else {
|
|
2195
|
+
results.push({
|
|
2196
|
+
elementId,
|
|
2197
|
+
success: false,
|
|
2198
|
+
error: response.error ?? "Action failed"
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
2201
|
+
} catch (err) {
|
|
2202
|
+
results.push({
|
|
2203
|
+
elementId,
|
|
2204
|
+
success: false,
|
|
2205
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2206
|
+
});
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
const succeededCount = results.filter((r) => r.success).length;
|
|
2210
|
+
const failedCount = results.length - succeededCount;
|
|
2211
|
+
return success2({ results, succeededCount, failedCount });
|
|
2212
|
+
},
|
|
2213
|
+
// Test hooks — modal stack manipulation
|
|
2214
|
+
//
|
|
2215
|
+
// These two endpoints exist so external automation runners can drive
|
|
2216
|
+
// ModalDetector state via HTTP, mirroring the in-tree `pushModal` /
|
|
2217
|
+
// `dismissModal` calls that components normally make. They're gated by
|
|
2218
|
+
// `features.testHooks` at the route layer (see http-server.ts); the
|
|
2219
|
+
// handlers themselves still defend against a missing modalDetector
|
|
2220
|
+
// because the bare server path used by CloudRelayClient may run before
|
|
2221
|
+
// enrichers are wired.
|
|
2222
|
+
pushModal: async (ctx) => {
|
|
2223
|
+
const detector = registry.getEnrichers().modalDetector;
|
|
2224
|
+
if (!detector) {
|
|
2225
|
+
return error2(
|
|
2226
|
+
"modal detector not registered",
|
|
2227
|
+
"ENRICHER_UNAVAILABLE"
|
|
2228
|
+
);
|
|
2229
|
+
}
|
|
2230
|
+
const body = ctx.body;
|
|
2231
|
+
const id = body?.id;
|
|
2232
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
2233
|
+
return error2(
|
|
2234
|
+
'pushModal requires a non-empty "id"',
|
|
2235
|
+
"INVALID_REQUEST"
|
|
2236
|
+
);
|
|
2237
|
+
}
|
|
2238
|
+
const allowedTypes = ["modal", "sheet", "drawer", "popover", "alertdialog", "dialog"];
|
|
2239
|
+
const requestedType = body?.type;
|
|
2240
|
+
if (requestedType !== void 0 && (typeof requestedType !== "string" || !allowedTypes.includes(requestedType))) {
|
|
2241
|
+
return error2(
|
|
2242
|
+
`pushModal "type" must be one of: ${allowedTypes.join(", ")}`,
|
|
2243
|
+
"INVALID_REQUEST"
|
|
2244
|
+
);
|
|
2245
|
+
}
|
|
2246
|
+
detector.pushModal({
|
|
2247
|
+
id,
|
|
2248
|
+
title: body?.title,
|
|
2249
|
+
// requestedType is narrowed by the allowedTypes guard above; the cast
|
|
2250
|
+
// converts the validated string to the NativeModalInfo['type'] union.
|
|
2251
|
+
type: requestedType,
|
|
2252
|
+
blocking: body?.blocking,
|
|
2253
|
+
dismissible: body?.dismissible
|
|
2254
|
+
});
|
|
2255
|
+
const stored = detector.getActive().find((m) => m.id === id);
|
|
2256
|
+
return success2({
|
|
2257
|
+
pushed: true,
|
|
2258
|
+
id,
|
|
2259
|
+
title: stored?.title,
|
|
2260
|
+
type: stored?.type,
|
|
2261
|
+
blocking: stored?.blocking,
|
|
2262
|
+
dismissible: stored?.dismissible,
|
|
2263
|
+
pushedAt: stored?.detectedAt ?? Date.now()
|
|
2264
|
+
});
|
|
2265
|
+
},
|
|
2266
|
+
dismissModal: async (ctx) => {
|
|
2267
|
+
const detector = registry.getEnrichers().modalDetector;
|
|
2268
|
+
if (!detector) {
|
|
2269
|
+
return error2(
|
|
2270
|
+
"modal detector not registered",
|
|
2271
|
+
"ENRICHER_UNAVAILABLE"
|
|
2272
|
+
);
|
|
2273
|
+
}
|
|
2274
|
+
const { id } = ctx.params;
|
|
2275
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
2276
|
+
return error2(
|
|
2277
|
+
"dismissModal requires a modal id in the path",
|
|
2278
|
+
"INVALID_REQUEST"
|
|
2279
|
+
);
|
|
2280
|
+
}
|
|
2281
|
+
const wasPresent = detector.getActive().some((m) => m.id === id);
|
|
2282
|
+
if (!wasPresent) {
|
|
2283
|
+
return error2(`modal not found: ${id}`, "MODAL_NOT_FOUND");
|
|
2284
|
+
}
|
|
2285
|
+
detector.dismissModal(id);
|
|
2286
|
+
return success2({ dismissed: id });
|
|
2287
|
+
},
|
|
2051
2288
|
// Design Review
|
|
2052
2289
|
...designHandlers,
|
|
2053
2290
|
// Health
|
|
2054
2291
|
health: async () => {
|
|
2055
2292
|
const stats = registry.getStats();
|
|
2293
|
+
const registration = registry.getRegistrationCoverage();
|
|
2056
2294
|
const response = {
|
|
2057
2295
|
status: "healthy",
|
|
2058
2296
|
timestamp: Date.now(),
|
|
2059
|
-
...stats
|
|
2297
|
+
...stats,
|
|
2298
|
+
registration
|
|
2060
2299
|
};
|
|
2061
2300
|
if (config?.appInfo) {
|
|
2062
2301
|
response.uiBridge = {
|
|
@@ -2157,7 +2396,24 @@ var WS_ROUTES = [
|
|
|
2157
2396
|
{ pattern: "design/evaluate/baseline", handler: "saveBaseline", httpMethods: ["POST"] },
|
|
2158
2397
|
{ pattern: "design/evaluate/diff", handler: "diffBaseline", httpMethods: ["POST"] },
|
|
2159
2398
|
// Meta / Introspection
|
|
2160
|
-
{ pattern: "meta/methods", handler: "getMethods", httpMethods: ["GET"] }
|
|
2399
|
+
{ pattern: "meta/methods", handler: "getMethods", httpMethods: ["GET"] },
|
|
2400
|
+
// AI helpers — mobile parity with the runner's `/ui-bridge/ai/*` family.
|
|
2401
|
+
{ pattern: "ai/fill-form", handler: "fillForm", httpMethods: ["POST"] },
|
|
2402
|
+
// Test hooks — gated behind `features.testHooks`. Let external runners
|
|
2403
|
+
// drive the ModalDetector over HTTP so tests can flip `snapshot.modalStack`
|
|
2404
|
+
// without mounting React components.
|
|
2405
|
+
{
|
|
2406
|
+
pattern: "control/modal/push",
|
|
2407
|
+
handler: "pushModal",
|
|
2408
|
+
httpMethods: ["POST"],
|
|
2409
|
+
requiresTestHooks: true
|
|
2410
|
+
},
|
|
2411
|
+
{
|
|
2412
|
+
pattern: "control/modal/dismiss/:id",
|
|
2413
|
+
handler: "dismissModal",
|
|
2414
|
+
httpMethods: ["POST"],
|
|
2415
|
+
requiresTestHooks: true
|
|
2416
|
+
}
|
|
2161
2417
|
];
|
|
2162
2418
|
var HANDLER_DESCRIPTIONS = {
|
|
2163
2419
|
health: "Liveness probe; returns app metadata + element count",
|
|
@@ -2190,7 +2446,10 @@ var HANDLER_DESCRIPTIONS = {
|
|
|
2190
2446
|
getQualityContexts: "List available quality evaluation contexts",
|
|
2191
2447
|
saveBaseline: "Save a quality baseline snapshot",
|
|
2192
2448
|
diffBaseline: "Diff current UI against a saved baseline",
|
|
2193
|
-
getMethods: "List every handler name (legacy introspection)"
|
|
2449
|
+
getMethods: "List every handler name (legacy introspection)",
|
|
2450
|
+
fillForm: "Fill multiple inputs in one call: body { fields: [{elementId, value}] } \u2014 each field dispatches `type` + `clear:true`",
|
|
2451
|
+
pushModal: "TEST HOOK \u2014 push a modal onto the snapshot.modalStack via the registry ModalDetector",
|
|
2452
|
+
dismissModal: "TEST HOOK \u2014 dismiss a modal by id from the snapshot.modalStack via the registry ModalDetector"
|
|
2194
2453
|
};
|
|
2195
2454
|
var ACTION_REFERENCE = [
|
|
2196
2455
|
{ name: "press", aliases: ["click"], params: "PressActionParams (optional position)" },
|
|
@@ -2207,10 +2466,11 @@ var ACTION_REFERENCE = [
|
|
|
2207
2466
|
{ name: "swipe", params: "SwipeActionParams { direction, distance }" },
|
|
2208
2467
|
{ name: "toggle", params: "none \u2014 flips boolean controls" }
|
|
2209
2468
|
];
|
|
2210
|
-
function buildRoutesPayload() {
|
|
2469
|
+
function buildRoutesPayload(testHooks) {
|
|
2211
2470
|
const entries = [];
|
|
2212
2471
|
for (const r of WS_ROUTES) {
|
|
2213
2472
|
if (!r.httpMethods) continue;
|
|
2473
|
+
if (r.requiresTestHooks && !testHooks) continue;
|
|
2214
2474
|
for (const m of r.httpMethods) {
|
|
2215
2475
|
entries.push({ method: m, path: `/ui-bridge/${r.pattern}` });
|
|
2216
2476
|
}
|
|
@@ -2220,11 +2480,11 @@ function buildRoutesPayload() {
|
|
|
2220
2480
|
entries.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
|
|
2221
2481
|
return { routes: entries, count: entries.length };
|
|
2222
2482
|
}
|
|
2223
|
-
function buildHelpPayload() {
|
|
2483
|
+
function buildHelpPayload(testHooks) {
|
|
2224
2484
|
return {
|
|
2225
2485
|
version: 1,
|
|
2226
2486
|
description: "UI Bridge Native HTTP surface. Prefix every path with /ui-bridge/.",
|
|
2227
|
-
routes: WS_ROUTES.map((r) => ({
|
|
2487
|
+
routes: WS_ROUTES.filter((r) => !r.requiresTestHooks || testHooks).map((r) => ({
|
|
2228
2488
|
pattern: r.pattern,
|
|
2229
2489
|
httpMethods: r.httpMethods ?? [],
|
|
2230
2490
|
wsOnly: !r.httpMethods,
|
|
@@ -2260,7 +2520,8 @@ var NativeUIBridgeServer = class {
|
|
|
2260
2520
|
appInfo: this.config.appInfo
|
|
2261
2521
|
});
|
|
2262
2522
|
this.handlers.getMethods = async () => {
|
|
2263
|
-
const
|
|
2523
|
+
const testHooks = this.config.testHooks === true;
|
|
2524
|
+
const methods = WS_ROUTES.filter((r) => !r.requiresTestHooks || testHooks).map((r) => ({
|
|
2264
2525
|
method: r.pattern,
|
|
2265
2526
|
httpMethods: r.httpMethods ? [...r.httpMethods] : void 0
|
|
2266
2527
|
}));
|
|
@@ -2303,7 +2564,11 @@ var NativeUIBridgeServer = class {
|
|
|
2303
2564
|
timestamp: Date.now()
|
|
2304
2565
|
};
|
|
2305
2566
|
} catch (e) {
|
|
2306
|
-
return {
|
|
2567
|
+
return {
|
|
2568
|
+
success: false,
|
|
2569
|
+
error: `Navigation failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2570
|
+
timestamp: Date.now()
|
|
2571
|
+
};
|
|
2307
2572
|
}
|
|
2308
2573
|
};
|
|
2309
2574
|
this.handlers.pageReplace = async (ctx) => {
|
|
@@ -2329,7 +2594,7 @@ var NativeUIBridgeServer = class {
|
|
|
2329
2594
|
} catch (e) {
|
|
2330
2595
|
return {
|
|
2331
2596
|
success: false,
|
|
2332
|
-
error: `Replace navigation failed: ${e.message}`,
|
|
2597
|
+
error: `Replace navigation failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2333
2598
|
timestamp: Date.now()
|
|
2334
2599
|
};
|
|
2335
2600
|
}
|
|
@@ -2348,7 +2613,7 @@ var NativeUIBridgeServer = class {
|
|
|
2348
2613
|
} catch (e) {
|
|
2349
2614
|
return {
|
|
2350
2615
|
success: false,
|
|
2351
|
-
error: `Back navigation failed: ${e.message}`,
|
|
2616
|
+
error: `Back navigation failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2352
2617
|
timestamp: Date.now()
|
|
2353
2618
|
};
|
|
2354
2619
|
}
|
|
@@ -2369,7 +2634,11 @@ var NativeUIBridgeServer = class {
|
|
|
2369
2634
|
timestamp: Date.now()
|
|
2370
2635
|
};
|
|
2371
2636
|
} catch (e) {
|
|
2372
|
-
return {
|
|
2637
|
+
return {
|
|
2638
|
+
success: false,
|
|
2639
|
+
error: `Refresh failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2640
|
+
timestamp: Date.now()
|
|
2641
|
+
};
|
|
2373
2642
|
}
|
|
2374
2643
|
};
|
|
2375
2644
|
}
|
|
@@ -2427,7 +2696,7 @@ var NativeUIBridgeServer = class {
|
|
|
2427
2696
|
} catch (e) {
|
|
2428
2697
|
return {
|
|
2429
2698
|
success: false,
|
|
2430
|
-
error: `Screenshot capture failed: ${e.message}`,
|
|
2699
|
+
error: `Screenshot capture failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2431
2700
|
timestamp: Date.now()
|
|
2432
2701
|
};
|
|
2433
2702
|
}
|
|
@@ -2982,9 +3251,11 @@ var NativeUIBridgeServer = class {
|
|
|
2982
3251
|
* "control/component/my-comp/action/submit" → handlers.executeComponentAction
|
|
2983
3252
|
*/
|
|
2984
3253
|
async routeMethodToHandler(method, params) {
|
|
3254
|
+
const testHooks = this.config.testHooks === true;
|
|
2985
3255
|
for (const route of WS_ROUTES) {
|
|
2986
3256
|
const pathParams = parsePath(route.pattern, method);
|
|
2987
3257
|
if (!pathParams) continue;
|
|
3258
|
+
if (route.requiresTestHooks && !testHooks) continue;
|
|
2988
3259
|
const ctx = {
|
|
2989
3260
|
params: pathParams,
|
|
2990
3261
|
query: params.query && typeof params.query === "object" ? params.query : {},
|
|
@@ -3062,17 +3333,18 @@ var NativeUIBridgeServer = class {
|
|
|
3062
3333
|
};
|
|
3063
3334
|
}
|
|
3064
3335
|
const stripped = path.slice(PREFIX.length);
|
|
3336
|
+
const testHooks = this.config.testHooks === true;
|
|
3065
3337
|
if (method === "GET" && stripped === "_help") {
|
|
3066
3338
|
return {
|
|
3067
3339
|
success: true,
|
|
3068
|
-
data: buildHelpPayload(),
|
|
3340
|
+
data: buildHelpPayload(testHooks),
|
|
3069
3341
|
timestamp: Date.now()
|
|
3070
3342
|
};
|
|
3071
3343
|
}
|
|
3072
3344
|
if (method === "GET" && stripped === "_routes") {
|
|
3073
3345
|
return {
|
|
3074
3346
|
success: true,
|
|
3075
|
-
data: buildRoutesPayload(),
|
|
3347
|
+
data: buildRoutesPayload(testHooks),
|
|
3076
3348
|
timestamp: Date.now()
|
|
3077
3349
|
};
|
|
3078
3350
|
}
|
|
@@ -3083,6 +3355,9 @@ var NativeUIBridgeServer = class {
|
|
|
3083
3355
|
if (!route.httpMethods) {
|
|
3084
3356
|
continue;
|
|
3085
3357
|
}
|
|
3358
|
+
if (route.requiresTestHooks && !testHooks) {
|
|
3359
|
+
continue;
|
|
3360
|
+
}
|
|
3086
3361
|
if (!route.httpMethods.includes(method)) {
|
|
3087
3362
|
patternMatchedButWrongVerb = true;
|
|
3088
3363
|
continue;
|
|
@@ -3376,6 +3651,27 @@ var WebSocketEventBridge = class {
|
|
|
3376
3651
|
}, this.heartbeatIntervalMs);
|
|
3377
3652
|
}
|
|
3378
3653
|
};
|
|
3654
|
+
|
|
3655
|
+
// src/transport/logger.ts
|
|
3656
|
+
function createTransportLogger() {
|
|
3657
|
+
return {
|
|
3658
|
+
silent: false,
|
|
3659
|
+
log(message, ...args) {
|
|
3660
|
+
if (!this.silent) {
|
|
3661
|
+
console.log(message, ...args);
|
|
3662
|
+
}
|
|
3663
|
+
},
|
|
3664
|
+
warn(message, ...args) {
|
|
3665
|
+
console.warn(message, ...args);
|
|
3666
|
+
},
|
|
3667
|
+
error(message, ...args) {
|
|
3668
|
+
console.error(message, ...args);
|
|
3669
|
+
}
|
|
3670
|
+
};
|
|
3671
|
+
}
|
|
3672
|
+
var transportLogger = createTransportLogger();
|
|
3673
|
+
|
|
3674
|
+
// src/transport/DeviceAnnouncer.ts
|
|
3379
3675
|
var DEFAULT_PORT = 8087;
|
|
3380
3676
|
var RECONNECT_INITIAL_DELAY_MS = 2e3;
|
|
3381
3677
|
var RECONNECT_MAX_DELAY_MS = 6e4;
|
|
@@ -3410,7 +3706,7 @@ var DeviceAnnouncer = class {
|
|
|
3410
3706
|
*/
|
|
3411
3707
|
async startMdnsAdvertise(ZeroconfCtor) {
|
|
3412
3708
|
if (!ZeroconfCtor) {
|
|
3413
|
-
|
|
3709
|
+
transportLogger.log(
|
|
3414
3710
|
"[DeviceAnnouncer] mDNS advertisement skipped (no Zeroconf constructor provided). Pass one to enable mDNS."
|
|
3415
3711
|
);
|
|
3416
3712
|
return;
|
|
@@ -3426,9 +3722,9 @@ var DeviceAnnouncer = class {
|
|
|
3426
3722
|
pairing_token: this.pairingToken
|
|
3427
3723
|
});
|
|
3428
3724
|
this.state = { ...this.state, mdnsActive: true };
|
|
3429
|
-
|
|
3725
|
+
transportLogger.log(`[DeviceAnnouncer] mDNS: advertising "${serviceName}" on port ${port}`);
|
|
3430
3726
|
} catch (err) {
|
|
3431
|
-
|
|
3727
|
+
transportLogger.warn("[DeviceAnnouncer] mDNS start failed:", err);
|
|
3432
3728
|
}
|
|
3433
3729
|
}
|
|
3434
3730
|
// ── Cloud relay ───────────────────────────────────────────────────────────
|
|
@@ -3444,7 +3740,7 @@ var DeviceAnnouncer = class {
|
|
|
3444
3740
|
}
|
|
3445
3741
|
if (this.stopped) return;
|
|
3446
3742
|
const url = `${this.config.cloudRelayUrl}?token=${encodeURIComponent(this.config.cloudToken)}`;
|
|
3447
|
-
|
|
3743
|
+
transportLogger.log("[DeviceAnnouncer] Cloud relay: connecting to", url);
|
|
3448
3744
|
const ws = new WebSocket(url);
|
|
3449
3745
|
this.cloudWs = ws;
|
|
3450
3746
|
ws.onopen = () => {
|
|
@@ -3454,7 +3750,7 @@ var DeviceAnnouncer = class {
|
|
|
3454
3750
|
}
|
|
3455
3751
|
this.reconnectDelay = RECONNECT_INITIAL_DELAY_MS;
|
|
3456
3752
|
this.state = { ...this.state, cloudConnected: true };
|
|
3457
|
-
|
|
3753
|
+
transportLogger.log("[DeviceAnnouncer] Cloud relay: connected");
|
|
3458
3754
|
ws.send(
|
|
3459
3755
|
JSON.stringify({
|
|
3460
3756
|
type: "device_register",
|
|
@@ -3469,7 +3765,7 @@ var DeviceAnnouncer = class {
|
|
|
3469
3765
|
ws.onmessage = (event) => {
|
|
3470
3766
|
try {
|
|
3471
3767
|
const msg = JSON.parse(event.data);
|
|
3472
|
-
|
|
3768
|
+
transportLogger.log("[DeviceAnnouncer] Cloud relay message:", msg.type);
|
|
3473
3769
|
} catch {
|
|
3474
3770
|
}
|
|
3475
3771
|
};
|
|
@@ -3480,7 +3776,7 @@ var DeviceAnnouncer = class {
|
|
|
3480
3776
|
}
|
|
3481
3777
|
};
|
|
3482
3778
|
ws.onerror = (err) => {
|
|
3483
|
-
|
|
3779
|
+
transportLogger.warn("[DeviceAnnouncer] Cloud relay error:", err);
|
|
3484
3780
|
};
|
|
3485
3781
|
}
|
|
3486
3782
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
@@ -3515,7 +3811,7 @@ var DeviceAnnouncer = class {
|
|
|
3515
3811
|
scheduleReconnect() {
|
|
3516
3812
|
if (this.stopped) return;
|
|
3517
3813
|
const delay = this.reconnectDelay;
|
|
3518
|
-
|
|
3814
|
+
transportLogger.log(`[DeviceAnnouncer] Cloud relay: reconnecting in ${delay}ms`);
|
|
3519
3815
|
this.reconnectTimer = setTimeout(() => {
|
|
3520
3816
|
this.reconnectTimer = null;
|
|
3521
3817
|
if (!this.stopped) {
|
|
@@ -3577,7 +3873,7 @@ var CloudRelayClient = class {
|
|
|
3577
3873
|
connect() {
|
|
3578
3874
|
if (this.stopped) return;
|
|
3579
3875
|
const url = `${this.config.relayUrl}?token=${encodeURIComponent(this.config.authToken)}`;
|
|
3580
|
-
|
|
3876
|
+
transportLogger.log("[CloudRelayClient] Connecting to", url);
|
|
3581
3877
|
const ws = new WebSocket(url);
|
|
3582
3878
|
this.ws = ws;
|
|
3583
3879
|
ws.onopen = () => {
|
|
@@ -3587,7 +3883,7 @@ var CloudRelayClient = class {
|
|
|
3587
3883
|
}
|
|
3588
3884
|
this._isConnected = true;
|
|
3589
3885
|
this.reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
|
|
3590
|
-
|
|
3886
|
+
transportLogger.log("[CloudRelayClient] Connected to relay");
|
|
3591
3887
|
ws.send(
|
|
3592
3888
|
JSON.stringify({
|
|
3593
3889
|
type: "device_register",
|
|
@@ -3605,7 +3901,7 @@ var CloudRelayClient = class {
|
|
|
3605
3901
|
}
|
|
3606
3902
|
};
|
|
3607
3903
|
ws.onerror = (err) => {
|
|
3608
|
-
|
|
3904
|
+
transportLogger.warn("[CloudRelayClient] WebSocket error:", err);
|
|
3609
3905
|
};
|
|
3610
3906
|
}
|
|
3611
3907
|
async onMessage(raw) {
|
|
@@ -3613,12 +3909,12 @@ var CloudRelayClient = class {
|
|
|
3613
3909
|
try {
|
|
3614
3910
|
msg = JSON.parse(raw);
|
|
3615
3911
|
} catch {
|
|
3616
|
-
|
|
3912
|
+
transportLogger.warn("[CloudRelayClient] Received non-JSON message");
|
|
3617
3913
|
return;
|
|
3618
3914
|
}
|
|
3619
3915
|
if (msg["type"] !== "tunnel_request") return;
|
|
3620
3916
|
if (typeof msg["id"] !== "string" || typeof msg["method"] !== "string" || typeof msg["path"] !== "string") {
|
|
3621
|
-
|
|
3917
|
+
transportLogger.warn("[CloudRelayClient] Malformed tunnel_request \u2014 missing required fields");
|
|
3622
3918
|
return;
|
|
3623
3919
|
}
|
|
3624
3920
|
const req = {
|
|
@@ -3659,7 +3955,7 @@ var CloudRelayClient = class {
|
|
|
3659
3955
|
body: bodyValue
|
|
3660
3956
|
};
|
|
3661
3957
|
} catch (err) {
|
|
3662
|
-
|
|
3958
|
+
transportLogger.error("[CloudRelayClient] Error handling tunneled request:", err);
|
|
3663
3959
|
return {
|
|
3664
3960
|
id: req.id,
|
|
3665
3961
|
status: 500,
|
|
@@ -3674,7 +3970,7 @@ var CloudRelayClient = class {
|
|
|
3674
3970
|
scheduleReconnect() {
|
|
3675
3971
|
if (this.stopped) return;
|
|
3676
3972
|
const delay = this.reconnectDelay;
|
|
3677
|
-
|
|
3973
|
+
transportLogger.log(`[CloudRelayClient] Reconnecting in ${delay}ms`);
|
|
3678
3974
|
this.reconnectTimer = setTimeout(() => {
|
|
3679
3975
|
this.reconnectTimer = null;
|
|
3680
3976
|
if (!this.stopped) {
|
|
@@ -3717,8 +4013,14 @@ var ModalDetector = class {
|
|
|
3717
4013
|
};
|
|
3718
4014
|
this.modals.set(modal.id, next);
|
|
3719
4015
|
}
|
|
4016
|
+
/**
|
|
4017
|
+
* Remove a modal from the stack. Returns `true` if a modal with that id
|
|
4018
|
+
* was actually deleted, `false` if nothing matched (idempotent so repeated
|
|
4019
|
+
* calls don't throw, but callers — e.g. the HTTP test-hook endpoint — can
|
|
4020
|
+
* surface the miss to clients).
|
|
4021
|
+
*/
|
|
3720
4022
|
dismissModal(id) {
|
|
3721
|
-
this.modals.delete(id);
|
|
4023
|
+
return this.modals.delete(id);
|
|
3722
4024
|
}
|
|
3723
4025
|
getSnapshotModalContext() {
|
|
3724
4026
|
const active = this.getActive();
|
|
@@ -3930,7 +4232,8 @@ function UIBridgeNativeProvider({
|
|
|
3930
4232
|
const server = createNativeServer(registry, executor, {
|
|
3931
4233
|
serverPort: config.serverPort || 8087,
|
|
3932
4234
|
cors: true,
|
|
3933
|
-
appInfo: config.appInfo
|
|
4235
|
+
appInfo: config.appInfo,
|
|
4236
|
+
testHooks: features.testHooks === true
|
|
3934
4237
|
});
|
|
3935
4238
|
server.setAdapter(serverAdapter);
|
|
3936
4239
|
if (navigationProvider) {
|
|
@@ -3984,7 +4287,9 @@ function UIBridgeNativeProvider({
|
|
|
3984
4287
|
}
|
|
3985
4288
|
}, [
|
|
3986
4289
|
features.server,
|
|
4290
|
+
features.testHooks,
|
|
3987
4291
|
config.serverPort,
|
|
4292
|
+
config.appInfo,
|
|
3988
4293
|
registry,
|
|
3989
4294
|
executor,
|
|
3990
4295
|
serverAdapter,
|
|
@@ -4018,7 +4323,8 @@ function UIBridgeNativeProvider({
|
|
|
4018
4323
|
const bareServer = createNativeServer(registry, executor, {
|
|
4019
4324
|
serverPort: config.serverPort || 8087,
|
|
4020
4325
|
cors: true,
|
|
4021
|
-
appInfo: config.appInfo
|
|
4326
|
+
appInfo: config.appInfo,
|
|
4327
|
+
testHooks: features.testHooks === true
|
|
4022
4328
|
});
|
|
4023
4329
|
if (navigationProvider) bareServer.setNavigationProvider(navigationProvider);
|
|
4024
4330
|
if (screenshotProvider) bareServer.setScreenshotProvider(screenshotProvider);
|
|
@@ -4249,7 +4555,7 @@ function useUIElement(options) {
|
|
|
4249
4555
|
registeredRef.current = false;
|
|
4250
4556
|
registeredIdRef.current = null;
|
|
4251
4557
|
setRegistered(false);
|
|
4252
|
-
}, [bridge,
|
|
4558
|
+
}, [bridge, id]);
|
|
4253
4559
|
const onLayout = react.useCallback(
|
|
4254
4560
|
(event) => {
|
|
4255
4561
|
const { x, y, width, height } = event.nativeEvent.layout;
|
|
@@ -4515,7 +4821,7 @@ function useUIComponent(options) {
|
|
|
4515
4821
|
const registeredComponent = react.useMemo(() => {
|
|
4516
4822
|
if (!bridge) return null;
|
|
4517
4823
|
return bridge.registry.getComponent(id) || null;
|
|
4518
|
-
}, [bridge, id
|
|
4824
|
+
}, [bridge, id]);
|
|
4519
4825
|
return {
|
|
4520
4826
|
registered: registeredRef.current,
|
|
4521
4827
|
executeAction,
|
|
@@ -4666,6 +4972,12 @@ function useToastRecorder() {
|
|
|
4666
4972
|
const dismissToast = react.useCallback((id) => toastCapture.dismissToast(id), [toastCapture]);
|
|
4667
4973
|
return { recordToast, dismissToast };
|
|
4668
4974
|
}
|
|
4975
|
+
function State(props) {
|
|
4976
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: props.children });
|
|
4977
|
+
}
|
|
4978
|
+
function TransitionTo(props) {
|
|
4979
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: props.children });
|
|
4980
|
+
}
|
|
4669
4981
|
|
|
4670
4982
|
// src/server/types.ts
|
|
4671
4983
|
var UI_BRIDGE_NATIVE_ROUTES = {
|
|
@@ -4795,6 +5107,25 @@ var UI_BRIDGE_NATIVE_ROUTES = {
|
|
|
4795
5107
|
path: "/ui-bridge/design/style-guide",
|
|
4796
5108
|
description: "Clear loaded style guide"
|
|
4797
5109
|
},
|
|
5110
|
+
// AI helpers
|
|
5111
|
+
AI_FILL_FORM: {
|
|
5112
|
+
method: "POST",
|
|
5113
|
+
path: "/ui-bridge/ai/fill-form",
|
|
5114
|
+
description: "Fill multiple input elements in a single call"
|
|
5115
|
+
},
|
|
5116
|
+
// Test hooks — drive ModalDetector state from outside the React tree
|
|
5117
|
+
// (gated by `features.testHooks`). Mirrors `pushModal` / `dismissModal`
|
|
5118
|
+
// calls that components normally make in-process.
|
|
5119
|
+
PUSH_MODAL: {
|
|
5120
|
+
method: "POST",
|
|
5121
|
+
path: "/ui-bridge/control/modal/push",
|
|
5122
|
+
description: "Push a modal onto the modal stack (testHooks)"
|
|
5123
|
+
},
|
|
5124
|
+
DISMISS_MODAL: {
|
|
5125
|
+
method: "POST",
|
|
5126
|
+
path: "/ui-bridge/control/modal/dismiss/:id",
|
|
5127
|
+
description: "Dismiss a modal by id (testHooks)"
|
|
5128
|
+
},
|
|
4798
5129
|
// Health
|
|
4799
5130
|
HEALTH: {
|
|
4800
5131
|
method: "GET",
|
|
@@ -5304,8 +5635,8 @@ function UIBridgeInspector({
|
|
|
5304
5635
|
const bridge = useUIBridgeNativeOptional();
|
|
5305
5636
|
const [showInspector, setShowInspector] = react.useState(visible);
|
|
5306
5637
|
const [selectedElement, setSelectedElement] = react.useState(null);
|
|
5307
|
-
const elements = react.useMemo(() => bridge ? bridge.getElements() : [], [bridge
|
|
5308
|
-
const components = react.useMemo(() => bridge ? bridge.getComponents() : [], [bridge
|
|
5638
|
+
const elements = react.useMemo(() => bridge ? bridge.getElements() : [], [bridge]);
|
|
5639
|
+
const components = react.useMemo(() => bridge ? bridge.getComponents() : [], [bridge]);
|
|
5309
5640
|
const handleToggle = react.useCallback(() => {
|
|
5310
5641
|
setShowInspector((prev) => !prev);
|
|
5311
5642
|
}, []);
|
|
@@ -5573,7 +5904,9 @@ exports.DeviceAnnouncer = DeviceAnnouncer;
|
|
|
5573
5904
|
exports.ModalDetector = ModalDetector;
|
|
5574
5905
|
exports.NativeUIBridgeRegistry = NativeUIBridgeRegistry;
|
|
5575
5906
|
exports.NativeUIBridgeServer = NativeUIBridgeServer;
|
|
5907
|
+
exports.State = State;
|
|
5576
5908
|
exports.ToastCapture = ToastCapture;
|
|
5909
|
+
exports.TransitionTo = TransitionTo;
|
|
5577
5910
|
exports.UIBridgeInspector = UIBridgeInspector;
|
|
5578
5911
|
exports.UIBridgeNativeProvider = UIBridgeNativeProvider;
|
|
5579
5912
|
exports.UI_BRIDGE_NATIVE_ROUTES = UI_BRIDGE_NATIVE_ROUTES;
|