@farmslot/adapter-rn 0.15.0 → 0.17.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/CHANGELOG.md +14 -0
- package/README.md +48 -10
- package/bridge-runtime/bridge-core.cjs +1481 -0
- package/bridge-runtime/console-forwarder.cjs +485 -0
- package/bridge-runtime/lib/bridge-errors.cjs +108 -0
- package/bridge-runtime/lib/cdp-eval.cjs +118 -0
- package/bridge-runtime/lib/config.cjs +68 -0
- package/bridge-runtime/lib/console-format.cjs +54 -0
- package/bridge-runtime/lib/devtools-proxy.cjs +169 -0
- package/bridge-runtime/lib/issue-capture.cjs +86 -0
- package/bridge-runtime/lib/match-bridge-target.cjs +89 -0
- package/bridge-runtime/lib/target-discovery.cjs +436 -0
- package/bridge-runtime/lib/ws-client.cjs +119 -0
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/devices.d.ts +16 -0
- package/dist/devices.d.ts.map +1 -0
- package/dist/devices.js +131 -0
- package/dist/devices.js.map +1 -0
- package/dist/doctor.js +1 -1
- package/dist/doctor.js.map +1 -1
- package/dist/fingerprint-baseline.d.ts +8 -0
- package/dist/fingerprint-baseline.d.ts.map +1 -0
- package/dist/fingerprint-baseline.js +68 -0
- package/dist/fingerprint-baseline.js.map +1 -0
- package/dist/frame-metrics.d.ts +26 -0
- package/dist/frame-metrics.d.ts.map +1 -0
- package/dist/frame-metrics.js +134 -0
- package/dist/frame-metrics.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/metro-env.d.ts +11 -0
- package/dist/metro-env.d.ts.map +1 -0
- package/dist/metro-env.js +29 -0
- package/dist/metro-env.js.map +1 -0
- package/dist/source-freshness.d.ts +11 -0
- package/dist/source-freshness.d.ts.map +1 -0
- package/dist/source-freshness.js +98 -0
- package/dist/source-freshness.js.map +1 -0
- package/dist/tool-paths.d.ts +13 -0
- package/dist/tool-paths.d.ts.map +1 -0
- package/dist/tool-paths.js +125 -0
- package/dist/tool-paths.js.map +1 -0
- package/dist/video-recorder.d.ts +7 -0
- package/dist/video-recorder.d.ts.map +1 -0
- package/dist/video-recorder.js +250 -0
- package/dist/video-recorder.js.map +1 -0
- package/metro/coalesce-metro-log.cjs +28 -0
- package/metro/launch-metro.cjs +84 -0
- package/metro/metro-config.cjs +92 -0
- package/metro/metro-log-generation.cjs +116 -0
- package/package.json +27 -7
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Evaluate a JS expression in the app's Hermes runtime via CDP Runtime.evaluate.
|
|
5
|
+
* Returns the evaluated value (primitives and JSON-serialisable objects).
|
|
6
|
+
*/
|
|
7
|
+
async function cdpEval(client, expression) {
|
|
8
|
+
// Hermes doesn't support async in Runtime.evaluate, use a plain IIFE
|
|
9
|
+
const wrapped = `(function() { return (${expression}); })()`;
|
|
10
|
+
const result = await client.send('Runtime.evaluate', {
|
|
11
|
+
expression: wrapped,
|
|
12
|
+
returnByValue: true,
|
|
13
|
+
awaitPromise: false,
|
|
14
|
+
generatePreview: false,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
if (result.exceptionDetails) {
|
|
18
|
+
const desc =
|
|
19
|
+
result.exceptionDetails.exception?.description ||
|
|
20
|
+
result.exceptionDetails.text ||
|
|
21
|
+
JSON.stringify(result.exceptionDetails);
|
|
22
|
+
throw new Error(`Evaluation error: ${desc}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return result.result?.value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Evaluate a JS expression that returns a Promise.
|
|
30
|
+
* Hermes CDP doesn't support awaitPromise, so we store the result on
|
|
31
|
+
* globalThis.__cdp_async__ and poll for it.
|
|
32
|
+
*/
|
|
33
|
+
async function cdpEvalAsync(client, expression, timeoutMs = cdpMessageTimeout()) {
|
|
34
|
+
// Unique key per call to avoid collisions
|
|
35
|
+
const key = `__cdp_async_${Date.now()}_${Math.random().toString(36).slice(2)}__`;
|
|
36
|
+
|
|
37
|
+
// Kick off the promise, store result when done.
|
|
38
|
+
// The try/catch guards against synchronous throws during argument evaluation
|
|
39
|
+
// of Promise.resolve(<expression>) — without it, a sync error escapes the
|
|
40
|
+
// IIFE and globalThis[key] stays 'pending' forever.
|
|
41
|
+
const kickoff = `(function() {
|
|
42
|
+
globalThis['${key}'] = { status: 'pending' };
|
|
43
|
+
try {
|
|
44
|
+
Promise.resolve(${expression})
|
|
45
|
+
.then(function(v) { globalThis['${key}'] = { status: 'resolved', value: v }; })
|
|
46
|
+
.catch(function(e) { globalThis['${key}'] = { status: 'rejected', error: String(e) }; });
|
|
47
|
+
} catch(e) {
|
|
48
|
+
globalThis['${key}'] = { status: 'rejected', error: String(e) };
|
|
49
|
+
}
|
|
50
|
+
return 'started';
|
|
51
|
+
})()`;
|
|
52
|
+
|
|
53
|
+
const kickoffResult = await client.send(
|
|
54
|
+
'Runtime.evaluate',
|
|
55
|
+
{
|
|
56
|
+
expression: kickoff,
|
|
57
|
+
returnByValue: true,
|
|
58
|
+
awaitPromise: false,
|
|
59
|
+
},
|
|
60
|
+
timeoutMs,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// If the IIFE itself failed to evaluate (syntax error, etc.), bail early
|
|
64
|
+
if (kickoffResult.exceptionDetails) {
|
|
65
|
+
const desc =
|
|
66
|
+
kickoffResult.exceptionDetails.exception?.description ||
|
|
67
|
+
kickoffResult.exceptionDetails.text ||
|
|
68
|
+
JSON.stringify(kickoffResult.exceptionDetails);
|
|
69
|
+
throw new Error(`Async evaluation error: ${desc}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Best-effort cleanup — swallow errors so a disconnected WebSocket
|
|
73
|
+
// doesn't obscure the actual result or diagnostic error.
|
|
74
|
+
const cleanup = () =>
|
|
75
|
+
client
|
|
76
|
+
.send('Runtime.evaluate', {
|
|
77
|
+
expression: `delete globalThis['${key}']`,
|
|
78
|
+
returnByValue: true,
|
|
79
|
+
awaitPromise: false,
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
.catch(() => {});
|
|
83
|
+
|
|
84
|
+
// Poll for completion
|
|
85
|
+
const pollInterval = 200;
|
|
86
|
+
const deadline = Date.now() + timeoutMs;
|
|
87
|
+
try {
|
|
88
|
+
while (Date.now() < deadline) {
|
|
89
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
90
|
+
const check = await client.send(
|
|
91
|
+
'Runtime.evaluate',
|
|
92
|
+
{
|
|
93
|
+
expression: `(function() { return globalThis['${key}']; })()`,
|
|
94
|
+
returnByValue: true,
|
|
95
|
+
awaitPromise: false,
|
|
96
|
+
},
|
|
97
|
+
timeoutMs,
|
|
98
|
+
);
|
|
99
|
+
const val = check.result?.value;
|
|
100
|
+
if (val?.status === 'resolved') {
|
|
101
|
+
return val.value;
|
|
102
|
+
}
|
|
103
|
+
if (val?.status === 'rejected') {
|
|
104
|
+
throw new Error(`Async evaluation error: ${val.error}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
throw new Error(`Async evaluation timed out after ${timeoutMs}ms`);
|
|
108
|
+
} finally {
|
|
109
|
+
await cleanup();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function cdpMessageTimeout() {
|
|
114
|
+
const timeoutMs = Number.parseInt(process.env.CDP_TIMEOUT || '30000', 10);
|
|
115
|
+
return Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 30000;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = { cdpEval, cdpEvalAsync };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
/** Read a value from .js.env */
|
|
7
|
+
function loadEnvValue(key, appRoot = process.env.APP_ROOT || process.cwd()) {
|
|
8
|
+
try {
|
|
9
|
+
const envPath = path.resolve(appRoot, '.js.env');
|
|
10
|
+
const content = fs.readFileSync(envPath, 'utf8');
|
|
11
|
+
// .js.env uses `export KEY="value"` (shell-sourceable format),
|
|
12
|
+
// so we handle the optional `export` prefix and strip surrounding quotes.
|
|
13
|
+
const match = content.match(new RegExp(String.raw`^(?:export\s+)?${key}=(.+)$`, 'm'));
|
|
14
|
+
if (match) return match[1].trim().replace(/^["']/, '').replace(/["']$/, '');
|
|
15
|
+
} catch {
|
|
16
|
+
// .js.env may not exist — fall through to undefined
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function resolvePort(env = process.env, appRoot = env.APP_ROOT || process.cwd()) {
|
|
22
|
+
return (
|
|
23
|
+
env.WATCHER_PORT ||
|
|
24
|
+
env.METRO_PORT ||
|
|
25
|
+
loadEnvValue('WATCHER_PORT', appRoot) ||
|
|
26
|
+
loadEnvValue('METRO_PORT', appRoot) ||
|
|
27
|
+
'8081'
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Read WATCHER_PORT from .js.env or env (default: 8081) */
|
|
32
|
+
function loadPort() {
|
|
33
|
+
return Number.parseInt(resolvePort(), 10);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Read IOS_SIMULATOR name from .js.env or env (default: none — accept any device) */
|
|
37
|
+
function loadSimulatorName() {
|
|
38
|
+
// Explicit empty string in env means "no simulator" — don't fall through to .js.env
|
|
39
|
+
if ('IOS_SIMULATOR' in process.env) return process.env.IOS_SIMULATOR;
|
|
40
|
+
return loadEnvValue('IOS_SIMULATOR') || '';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Read ANDROID_DEVICE from .js.env or env (default: none — accept any device) */
|
|
44
|
+
function loadAndroidDevice() {
|
|
45
|
+
if ('ANDROID_DEVICE' in process.env) return process.env.ANDROID_DEVICE;
|
|
46
|
+
return loadEnvValue('ANDROID_DEVICE') || '';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Read ANDROID_TARGET_DEVICE_NAME from env — the Metro-compatible model prefix
|
|
51
|
+
* resolved from an adb serial by bridge.mjs. Used by target-discovery to select
|
|
52
|
+
* the correct Metro CDP target when multiple devices are connected.
|
|
53
|
+
*
|
|
54
|
+
* Set automatically by the bridge when --device <serial> is used. Users never
|
|
55
|
+
* need to set this; pass --device <adb-serial> and the harness maps it internally.
|
|
56
|
+
*/
|
|
57
|
+
function loadAndroidTargetDeviceName() {
|
|
58
|
+
return process.env.ANDROID_TARGET_DEVICE_NAME || '';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = {
|
|
62
|
+
loadEnvValue,
|
|
63
|
+
loadPort,
|
|
64
|
+
resolvePort,
|
|
65
|
+
loadSimulatorName,
|
|
66
|
+
loadAndroidDevice,
|
|
67
|
+
loadAndroidTargetDeviceName,
|
|
68
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Render CDP Runtime.consoleAPICalled args (RemoteObjects) into a log line.
|
|
4
|
+
// Primitives carry `value`; objects/arrays do not — a bare console.log(obj)
|
|
5
|
+
// arrived as the literal "Object". Hermes ships an inline `preview` (the same
|
|
6
|
+
// one React Native DevTools renders in its console), so objects expand from it
|
|
7
|
+
// synchronously — no extra CDP round-trip, so the stream never blocks. Depth is
|
|
8
|
+
// bounded because previews nest one level via `valuePreview`; beyond that the
|
|
9
|
+
// child preview's own description is used.
|
|
10
|
+
|
|
11
|
+
const MAX_PREVIEW_DEPTH = 2;
|
|
12
|
+
|
|
13
|
+
function formatPropertyValue(prop, depth) {
|
|
14
|
+
if (prop.valuePreview && depth < MAX_PREVIEW_DEPTH) {
|
|
15
|
+
return formatPreview(prop.valuePreview, depth + 1);
|
|
16
|
+
}
|
|
17
|
+
if (prop.type === 'string' && prop.value !== undefined) return JSON.stringify(prop.value);
|
|
18
|
+
if (prop.value !== undefined) return String(prop.value);
|
|
19
|
+
return prop.subtype || prop.type || '';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function formatPreview(preview, depth) {
|
|
23
|
+
const properties = Array.isArray(preview.properties) ? preview.properties : [];
|
|
24
|
+
const overflow = preview.overflow ? ', …' : '';
|
|
25
|
+
if (preview.subtype === 'array') {
|
|
26
|
+
return `[${properties.map((p) => formatPropertyValue(p, depth)).join(', ')}${overflow}]`;
|
|
27
|
+
}
|
|
28
|
+
const body = properties.map((p) => `${p.name}: ${formatPropertyValue(p, depth)}`).join(', ');
|
|
29
|
+
// Name a non-plain constructor (Error, Map, custom class) so the class is not lost.
|
|
30
|
+
const ctor =
|
|
31
|
+
preview.description && preview.description !== 'Object' ? `${preview.description} ` : '';
|
|
32
|
+
return properties.length > 0 || overflow ? `${ctor}{ ${body}${overflow} }` : `${ctor}{}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function formatRemoteObject(a) {
|
|
36
|
+
if (!a || typeof a !== 'object') return String(a == null ? '' : a);
|
|
37
|
+
// Primitives (number/boolean/string) carry a directly-usable value.
|
|
38
|
+
if (a.type !== 'object' && a.type !== 'function' && a.value !== undefined) return String(a.value);
|
|
39
|
+
if (a.type === 'undefined') return 'undefined';
|
|
40
|
+
if (a.subtype === 'null') return 'null';
|
|
41
|
+
if (a.preview) return formatPreview(a.preview, 1);
|
|
42
|
+
// Functions, errors, and objects with no preview: the description is the best
|
|
43
|
+
// available text (className / Error stack / etc.); fall back to bare value/type.
|
|
44
|
+
if (a.description !== undefined) return a.description;
|
|
45
|
+
if (a.value !== undefined) return String(a.value);
|
|
46
|
+
return a.type || '';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function formatArgs(args, maxLineChars) {
|
|
50
|
+
const text = (args || []).map(formatRemoteObject).join(' ');
|
|
51
|
+
return maxLineChars && text.length > maxLineChars ? `${text.slice(0, maxLineChars)}…` : text;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { formatArgs, formatRemoteObject, formatPreview };
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const { WebSocket, WebSocketServer } = require('ws');
|
|
5
|
+
|
|
6
|
+
const MAX_FRAME_BYTES = 8 * 1024 * 1024;
|
|
7
|
+
const SESSION_TIMEOUT_MS = 10_000;
|
|
8
|
+
const EVENT_GATED_DOMAINS = new Set([
|
|
9
|
+
'Debugger',
|
|
10
|
+
'Log',
|
|
11
|
+
'Network',
|
|
12
|
+
'Page',
|
|
13
|
+
'ReactNativeApplication',
|
|
14
|
+
'Runtime',
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
function createDevtoolsProxy({
|
|
18
|
+
descriptorPath,
|
|
19
|
+
sessions,
|
|
20
|
+
sendCommand,
|
|
21
|
+
requestDiscovery,
|
|
22
|
+
allowedOrigins,
|
|
23
|
+
}) {
|
|
24
|
+
if (!Array.isArray(allowedOrigins) || allowedOrigins.length === 0) {
|
|
25
|
+
throw new Error('DevTools proxy requires at least one allowed Origin');
|
|
26
|
+
}
|
|
27
|
+
let listeningPort = null;
|
|
28
|
+
const clients = new Set();
|
|
29
|
+
const server = new WebSocketServer({
|
|
30
|
+
host: '127.0.0.1',
|
|
31
|
+
port: 0,
|
|
32
|
+
maxPayload: MAX_FRAME_BYTES,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
async function waitForSession(deviceId) {
|
|
36
|
+
const ready = sessions.get(deviceId);
|
|
37
|
+
if (ready?.brokerReady) return ready;
|
|
38
|
+
requestDiscovery();
|
|
39
|
+
const deadline = Date.now() + SESSION_TIMEOUT_MS;
|
|
40
|
+
while (Date.now() < deadline) {
|
|
41
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
42
|
+
const session = sessions.get(deviceId);
|
|
43
|
+
if (session?.brokerReady) return session;
|
|
44
|
+
}
|
|
45
|
+
throw new Error('Hermes runtime is unavailable or reloading; retry the command');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function writeDescriptor() {
|
|
49
|
+
if (!listeningPort) return;
|
|
50
|
+
fs.writeFileSync(
|
|
51
|
+
descriptorPath,
|
|
52
|
+
`${JSON.stringify({ schemaVersion: 1, pid: process.pid, port: listeningPort })}\n`,
|
|
53
|
+
{ mode: 0o600 },
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function forward(client, message) {
|
|
58
|
+
if (
|
|
59
|
+
!message ||
|
|
60
|
+
typeof message !== 'object' ||
|
|
61
|
+
!Number.isInteger(message.id) ||
|
|
62
|
+
typeof message.method !== 'string'
|
|
63
|
+
) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (message.method.endsWith('.disable')) {
|
|
67
|
+
client.enabled.delete(message.method.replace(/\.disable$/u, '.enable'));
|
|
68
|
+
if (client.socket.readyState === WebSocket.OPEN) {
|
|
69
|
+
client.socket.send(JSON.stringify({ id: message.id, result: {} }));
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const isEnable = message.method.endsWith('.enable');
|
|
74
|
+
if (isEnable) {
|
|
75
|
+
client.enabled.set(message.method, message.params || {});
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const session = await waitForSession(client.deviceId);
|
|
79
|
+
const result = await sendCommand(
|
|
80
|
+
session,
|
|
81
|
+
message.method,
|
|
82
|
+
message.params || {},
|
|
83
|
+
SESSION_TIMEOUT_MS,
|
|
84
|
+
);
|
|
85
|
+
if (client.socket.readyState === WebSocket.OPEN) {
|
|
86
|
+
client.socket.send(JSON.stringify({ id: message.id, result }));
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (isEnable) client.enabled.delete(message.method);
|
|
90
|
+
if (client.socket.readyState === WebSocket.OPEN) {
|
|
91
|
+
client.socket.send(
|
|
92
|
+
JSON.stringify({
|
|
93
|
+
id: message.id,
|
|
94
|
+
error: {
|
|
95
|
+
code: -32000,
|
|
96
|
+
message: String(error?.message || error).slice(0, 256),
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
server.on('connection', (socket, request) => {
|
|
105
|
+
const url = new URL(request.url || '/', 'http://127.0.0.1');
|
|
106
|
+
const deviceId = url.searchParams.get('device');
|
|
107
|
+
if (!deviceId || !allowedOrigins.includes(request.headers.origin)) {
|
|
108
|
+
socket.close(1008, 'invalid DevTools proxy Origin or device');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
for (const client of clients) {
|
|
112
|
+
if (client.deviceId === deviceId) {
|
|
113
|
+
client.socket.close(1000, 'replaced by a newer DevTools window');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const client = { socket, deviceId, enabled: new Map() };
|
|
117
|
+
clients.add(client);
|
|
118
|
+
socket.on('message', (data) => {
|
|
119
|
+
let message;
|
|
120
|
+
try {
|
|
121
|
+
message = JSON.parse(String(data));
|
|
122
|
+
} catch {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
void forward(client, message);
|
|
126
|
+
});
|
|
127
|
+
const drop = () => clients.delete(client);
|
|
128
|
+
socket.on('close', drop);
|
|
129
|
+
socket.on('error', drop);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
server.on('listening', () => {
|
|
133
|
+
const address = server.address();
|
|
134
|
+
if (!address || typeof address === 'string') return;
|
|
135
|
+
listeningPort = address.port;
|
|
136
|
+
writeDescriptor();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
onSessionOpen(deviceId, session) {
|
|
141
|
+
for (const client of clients) {
|
|
142
|
+
if (client.deviceId !== deviceId) continue;
|
|
143
|
+
for (const [method, params] of client.enabled) {
|
|
144
|
+
void sendCommand(session, method, params, SESSION_TIMEOUT_MS).catch(() => undefined);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
onCdpEvent(deviceId, method, params) {
|
|
149
|
+
const payload = JSON.stringify({ method, params });
|
|
150
|
+
const domain = method.split('.', 1)[0];
|
|
151
|
+
for (const client of clients) {
|
|
152
|
+
if (
|
|
153
|
+
client.deviceId === deviceId &&
|
|
154
|
+
client.socket.readyState === WebSocket.OPEN &&
|
|
155
|
+
(!EVENT_GATED_DOMAINS.has(domain) || client.enabled.has(`${domain}.enable`))
|
|
156
|
+
) {
|
|
157
|
+
client.socket.send(payload);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
close() {
|
|
162
|
+
for (const client of clients) client.socket.close();
|
|
163
|
+
server.close();
|
|
164
|
+
fs.rmSync(descriptorPath, { force: true });
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = { createDevtoolsProxy };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// In-app issue buffer for a React Native (Hermes) runtime: the arm snippet hooks
|
|
4
|
+
// console.warn/error, global errors and unhandled rejections into a bounded
|
|
5
|
+
// buffer; the collect snippet drains it. Callers evaluate both over CDP.
|
|
6
|
+
|
|
7
|
+
// Installed into the Hermes runtime via Runtime.evaluate. Returns a string
|
|
8
|
+
// so callers can embed it directly into eval expressions. Keeps the hook
|
|
9
|
+
// idempotent and bounded (500-entry cap).
|
|
10
|
+
function buildArmSnippet() {
|
|
11
|
+
return `(() => {
|
|
12
|
+
var g = globalThis;
|
|
13
|
+
if (g.__AGENTIC_ISSUES_INSTALLED__) { return { installed: true, reason: 'already-installed' }; }
|
|
14
|
+
g.__AGENTIC_ISSUES__ = [];
|
|
15
|
+
g.__AGENTIC_ISSUES_CAP__ = 500;
|
|
16
|
+
var push = function (entry) {
|
|
17
|
+
try {
|
|
18
|
+
if (g.__AGENTIC_ISSUES__.length < g.__AGENTIC_ISSUES_CAP__) {
|
|
19
|
+
g.__AGENTIC_ISSUES__.push(entry);
|
|
20
|
+
}
|
|
21
|
+
} catch (_e) {}
|
|
22
|
+
};
|
|
23
|
+
['warn', 'error'].forEach(function (k) {
|
|
24
|
+
var orig = console[k] && console[k].bind(console);
|
|
25
|
+
if (!orig) return;
|
|
26
|
+
console[k] = function () {
|
|
27
|
+
var args = Array.prototype.slice.call(arguments);
|
|
28
|
+
try {
|
|
29
|
+
var text = args.map(function (a) {
|
|
30
|
+
if (a && a.stack) return String(a.stack);
|
|
31
|
+
if (typeof a === 'string') return a;
|
|
32
|
+
try { return JSON.stringify(a); } catch (_e) { return String(a); }
|
|
33
|
+
}).join(' ');
|
|
34
|
+
push({ t: Date.now(), level: k, text: text });
|
|
35
|
+
} catch (_e) {}
|
|
36
|
+
return orig.apply(console, args);
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
if (typeof g.addEventListener === 'function') {
|
|
40
|
+
try {
|
|
41
|
+
g.addEventListener('error', function (e) {
|
|
42
|
+
var err = e && (e.error || e.message);
|
|
43
|
+
var text = err && err.stack ? err.stack : String(err || e);
|
|
44
|
+
push({ t: Date.now(), level: 'exception', text: text });
|
|
45
|
+
});
|
|
46
|
+
g.addEventListener('unhandledrejection', function (e) {
|
|
47
|
+
var r = e && e.reason;
|
|
48
|
+
var text = r && r.stack ? r.stack : String(r || e);
|
|
49
|
+
push({ t: Date.now(), level: 'exception', text: text });
|
|
50
|
+
});
|
|
51
|
+
} catch (_e) {}
|
|
52
|
+
}
|
|
53
|
+
var ep = g.ErrorUtils;
|
|
54
|
+
if (ep && typeof ep.setGlobalHandler === 'function') {
|
|
55
|
+
try {
|
|
56
|
+
var priorHandler = typeof ep.getGlobalHandler === 'function' ? ep.getGlobalHandler() : null;
|
|
57
|
+
ep.setGlobalHandler(function (err, isFatal) {
|
|
58
|
+
try {
|
|
59
|
+
var text = err && err.stack ? err.stack : String(err);
|
|
60
|
+
push({ t: Date.now(), level: 'exception', text: (isFatal ? '[FATAL] ' : '') + text });
|
|
61
|
+
} catch (_e) {}
|
|
62
|
+
if (typeof priorHandler === 'function') {
|
|
63
|
+
try { priorHandler(err, isFatal); } catch (_e) {}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
} catch (_e) {}
|
|
67
|
+
}
|
|
68
|
+
g.__AGENTIC_ISSUES_INSTALLED__ = true;
|
|
69
|
+
return { installed: true };
|
|
70
|
+
})()`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function buildCollectSnippet() {
|
|
74
|
+
return `(() => {
|
|
75
|
+
var g = globalThis;
|
|
76
|
+
var buf = g.__AGENTIC_ISSUES__ || [];
|
|
77
|
+
var snapshot = buf.slice();
|
|
78
|
+
g.__AGENTIC_ISSUES__ = [];
|
|
79
|
+
return { count: snapshot.length, entries: snapshot };
|
|
80
|
+
})()`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = {
|
|
84
|
+
buildArmSnippet,
|
|
85
|
+
buildCollectSnippet,
|
|
86
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Boundary-safe matcher for CDP bridge targets, shared by wait-for-bridge and the
|
|
4
|
+
// launch ready-path confirm. A launch (or its confirm) may only accept a target
|
|
5
|
+
// belonging to the REQUESTED platform — and the requested device when one is pinned.
|
|
6
|
+
//
|
|
7
|
+
// The requested platform is authoritative and is passed explicitly
|
|
8
|
+
// (WAIT_FOR_BRIDGE_PLATFORM), so it wins over env inference. On a dual-platform slot
|
|
9
|
+
// the other platform's device identity is ambiently injected (a slot-context
|
|
10
|
+
// IOS_SIMULATOR while launching android, or vice versa); without an explicit platform
|
|
11
|
+
// the matcher would fall back to that ambient identity and confirm against the wrong
|
|
12
|
+
// platform's target. Binding the platform up front closes that corner.
|
|
13
|
+
|
|
14
|
+
function matchesBridgeTarget(target, env) {
|
|
15
|
+
const e = env || process.env;
|
|
16
|
+
if (!target || typeof target !== 'object') return false;
|
|
17
|
+
|
|
18
|
+
const requirePlatform = e.WAIT_FOR_BRIDGE_PLATFORM || '';
|
|
19
|
+
// Requested platform is authoritative: never accept the other platform's target,
|
|
20
|
+
// whatever device env is ambiently present.
|
|
21
|
+
if (requirePlatform && target.platform !== requirePlatform) return false;
|
|
22
|
+
|
|
23
|
+
const androidName = e.ANDROID_TARGET_DEVICE_NAME || e.ANDROID_DEVICE || '';
|
|
24
|
+
const adbSerial = e.ADB_SERIAL || e.ANDROID_SERIAL || '';
|
|
25
|
+
const iosSimulator = e.IOS_SIMULATOR || '';
|
|
26
|
+
|
|
27
|
+
// Android device pin applies only when android is requested (or nothing constrains
|
|
28
|
+
// the platform); it must never redirect a platform-bound iOS confirm — a slot-context
|
|
29
|
+
// ADB_SERIAL while launching iOS must not reject every iOS target. Symmetric with the
|
|
30
|
+
// iOS-simulator branch below.
|
|
31
|
+
if ((adbSerial || androidName) && (!requirePlatform || requirePlatform === 'android')) {
|
|
32
|
+
// Require android + boundary-safe device-name match.
|
|
33
|
+
if (target.platform !== 'android') return false;
|
|
34
|
+
if (!androidName) return true;
|
|
35
|
+
const deviceName = String(target.deviceName || '');
|
|
36
|
+
return deviceName === androidName || deviceName.startsWith(`${androidName} -`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// iOS simulator pin applies only when iOS is requested (or nothing constrains the
|
|
40
|
+
// platform); it must never redirect a platform-bound android confirm.
|
|
41
|
+
if (iosSimulator && (!requirePlatform || requirePlatform === 'ios')) {
|
|
42
|
+
return target.platform !== 'android' && target.deviceName === iosSimulator;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// No device pin: the platform gate above is the only constraint.
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// True when at least one answering target matches the request AND carries a route
|
|
50
|
+
// (an in-app agentic bridge is live), not merely a registered debug target.
|
|
51
|
+
function hasMatchingRoute(value, env) {
|
|
52
|
+
const targets = Array.isArray(value) ? value : [value];
|
|
53
|
+
return targets.some(
|
|
54
|
+
(t) => matchesBridgeTarget(t, env) && t && t.agenticPresent === true && Boolean(t.route),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Human summary of the targets that answered (platform / deviceName), for teaching on
|
|
59
|
+
// a timeout — e.g. an iOS target answering an android launch.
|
|
60
|
+
function describeTargets(value) {
|
|
61
|
+
const targets = (Array.isArray(value) ? value : [value]).filter(
|
|
62
|
+
(t) => t && typeof t === 'object',
|
|
63
|
+
);
|
|
64
|
+
if (!targets.length) return 'none';
|
|
65
|
+
return targets
|
|
66
|
+
.map(
|
|
67
|
+
(t) =>
|
|
68
|
+
`${t.platform || '?'}${t.deviceName ? `/'${t.deviceName}'` : ''}${t.route ? '' : ' (no route)'}`,
|
|
69
|
+
)
|
|
70
|
+
.join(', ');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Human summary of what the run is pinned to, for the same teaching line.
|
|
74
|
+
function describeRequested(env) {
|
|
75
|
+
const e = env || process.env;
|
|
76
|
+
const platform = e.WAIT_FOR_BRIDGE_PLATFORM || '';
|
|
77
|
+
const androidName = e.ANDROID_TARGET_DEVICE_NAME || e.ANDROID_DEVICE || '';
|
|
78
|
+
const adbSerial = e.ADB_SERIAL || e.ANDROID_SERIAL || '';
|
|
79
|
+
const iosSimulator = e.IOS_SIMULATOR || '';
|
|
80
|
+
// Explicit requested platform wins over device-env inference: an iOS-bound confirm
|
|
81
|
+
// with an ambient ADB_SERIAL is still iOS.
|
|
82
|
+
if (platform === 'android') return `android${androidName ? ` / ${androidName}` : ''}`;
|
|
83
|
+
if (platform === 'ios') return `ios${iosSimulator ? ` / ${iosSimulator}` : ''}`;
|
|
84
|
+
if (adbSerial || androidName) return `android${androidName ? ` / ${androidName}` : ''}`;
|
|
85
|
+
if (iosSimulator) return `ios${iosSimulator ? ` / ${iosSimulator}` : ''}`;
|
|
86
|
+
return 'any platform';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = { matchesBridgeTarget, hasMatchingRoute, describeTargets, describeRequested };
|