@felipe-software/react-native-gpui 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.reanimated-prebuilt/react-native-reanimated.mjs +29373 -0
- package/LICENSE +21 -0
- package/README.md +657 -2
- package/cli/bin.ts +379 -0
- package/cli/bundle.ts +32 -0
- package/cli/commands/do.ts +213 -0
- package/cli/commands/flow.ts +283 -0
- package/cli/commands/get.ts +584 -0
- package/cli/commands/shot.ts +312 -0
- package/cli/commands/trace.ts +293 -0
- package/cli/host.ts +714 -0
- package/cli/hotReload.ts +210 -0
- package/cli/run.ts +66 -0
- package/cli/selectors.ts +205 -0
- package/dist/Animated.d.ts +120 -0
- package/dist/ChatList.d.ts +28 -0
- package/dist/Dimensions.d.ts +31 -0
- package/dist/StyleSheet.d.ts +18 -0
- package/dist/Window.d.ts +33 -0
- package/dist/apis.d.ts +165 -0
- package/dist/app-state.d.ts +27 -0
- package/dist/chat-list-types.d.ts +136 -0
- package/dist/colors.d.ts +32 -0
- package/dist/commands.d.ts +230 -0
- package/dist/components.d.ts +419 -0
- package/dist/hotModules.d.ts +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.development.js +14876 -0
- package/dist/index.js +12 -0
- package/dist/keyboard.d.ts +140 -0
- package/dist/native-menu.d.ts +7 -0
- package/dist/platform-driver.d.ts +32 -0
- package/dist/platform-services.d.ts +35 -0
- package/dist/portal.d.ts +43 -0
- package/dist/raf.d.ts +4 -0
- package/dist/reanimated/colors-processor-shim.d.ts +10 -0
- package/dist/reanimated/reanimated-host.d.ts +10 -0
- package/dist/reanimated/seam.d.ts +34 -0
- package/dist/reanimated/ui-entry.d.ts +2 -0
- package/dist/reanimated/worklet-channel.d.ts +10 -0
- package/dist/reanimated/worklet-runtime.d.ts +466 -0
- package/dist/reanimated/worklets.d.ts +199 -0
- package/dist/reconciler.d.ts +102 -0
- package/dist/refresh.d.ts +1 -0
- package/dist/render.d.ts +37 -0
- package/dist/runtime.d.ts +235 -0
- package/dist/surfaces.d.ts +51 -0
- package/dist/types.d.ts +276 -0
- package/dist/ui-runtime.js +42276 -0
- package/dist/window-context.d.ts +7 -0
- package/dist/window-host.d.ts +41 -0
- package/dist/wire-delta.d.ts +20 -0
- package/package.json +176 -5
- package/scripts/bundle-app.mjs +88 -0
- package/scripts/conformance-utils.mjs +293 -0
- package/scripts/hot-update-alias-plugin.mjs +88 -0
- package/scripts/native-package-exports-plugin.mjs +235 -0
- package/scripts/pixel.mjs +152 -0
- package/scripts/png.mjs +198 -0
- package/scripts/react-refresh-swc.mjs +35 -0
- package/scripts/reanimated-bun-plugin.mjs +188 -0
- package/scripts/service-binary.d.mts +27 -0
- package/scripts/service-binary.mjs +100 -0
- package/src/reanimated/colors-processor-shim.ts +119 -0
- package/src/reanimated/reanimated-host.ts +69 -0
- package/src/reanimated/rngpui-reanimated-prebuilt.d.ts +7 -0
- package/src/reanimated/seam.ts +566 -0
- package/src/reanimated/ui-entry.ts +80 -0
- package/src/reanimated/worklet-channel.ts +64 -0
- package/src/reanimated/worklet-runtime.ts +2569 -0
- package/src/reanimated/worklets.ts +1334 -0
- package/src/refresh.ts +33 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// `rngpui flow` - run a short semantic input flow and write a profile artifact.
|
|
2
|
+
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join, resolve } from "node:path";
|
|
7
|
+
import { isDriveableHost, type AttachedHost, type DriveableHost, type DumpNode, type LaunchedHost } from "../host";
|
|
8
|
+
import { centerOf, parsePoint, resolve as resolveSelector, walk } from "../selectors";
|
|
9
|
+
import { sleep } from "../../scripts/conformance-utils.mjs";
|
|
10
|
+
|
|
11
|
+
type Host = LaunchedHost | AttachedHost;
|
|
12
|
+
type FlowOptions = { json: boolean; profile: boolean; screenshots?: boolean; settleMs?: number; cadenceMs?: number; out?: string };
|
|
13
|
+
type ControlResponse = { ok: boolean; error?: string; targetId?: number; focusedId?: number; activated?: boolean };
|
|
14
|
+
type FlowStep = { kind: "tap"; selector: string };
|
|
15
|
+
|
|
16
|
+
const ACTIONS = new Set(["tap"]);
|
|
17
|
+
|
|
18
|
+
export async function runFlow(host: Host, args: string[], options: FlowOptions): Promise<number> {
|
|
19
|
+
if (!isDriveableHost(host)) {
|
|
20
|
+
console.error(" flow needs a driveable target: use --launch, --bundle, --session, or attach to an app with RNGPUI_CONTROL_SOCKET metadata");
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
23
|
+
const steps = parseSteps(args);
|
|
24
|
+
if (!steps.length) {
|
|
25
|
+
console.error(" usage: rngpui flow [--profile] tap <selector> [tap <selector> ...]");
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const outDir = resolve(options.out || join(tmpdir(), `rngpui-flow-${Date.now().toString(36)}`));
|
|
30
|
+
mkdirSync(outDir, { recursive: true });
|
|
31
|
+
const startedAt = new Date().toISOString();
|
|
32
|
+
const captureScreenshots = options.screenshots ?? options.profile;
|
|
33
|
+
const settleMs = Number.isFinite(options.settleMs) ? Math.max(0, options.settleMs!) : 700;
|
|
34
|
+
const cadenceMs = Number.isFinite(options.cadenceMs) ? Math.max(0, options.cadenceMs!) : 0;
|
|
35
|
+
const profile: Record<string, unknown> = {
|
|
36
|
+
version: 1,
|
|
37
|
+
startedAt,
|
|
38
|
+
target: targetInfo(host),
|
|
39
|
+
options: { profile: options.profile, screenshots: captureScreenshots, settleMs, cadenceMs },
|
|
40
|
+
steps: [],
|
|
41
|
+
};
|
|
42
|
+
const started = performance.now();
|
|
43
|
+
|
|
44
|
+
const initial = await host.dump();
|
|
45
|
+
writeJson(join(outDir, "initial-tree.json"), initial);
|
|
46
|
+
captureIfRequested(host, captureScreenshots, join(outDir, "initial.png"));
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < steps.length; i++) {
|
|
49
|
+
const step = steps[i];
|
|
50
|
+
const index = i + 1;
|
|
51
|
+
const prefix = `step-${String(index).padStart(3, "0")}`;
|
|
52
|
+
const before = await host.dump();
|
|
53
|
+
const beforeHash = hashDump(before);
|
|
54
|
+
writeJson(join(outDir, `${prefix}-before-tree.json`), before);
|
|
55
|
+
captureIfRequested(host, captureScreenshots, join(outDir, `${prefix}-before.png`));
|
|
56
|
+
|
|
57
|
+
const target = resolveTapTarget(before, step.selector);
|
|
58
|
+
if (!target) {
|
|
59
|
+
console.error(` step ${index}: no node matched "${step.selector}"`);
|
|
60
|
+
return 1;
|
|
61
|
+
}
|
|
62
|
+
const targetActiveBefore = isTargetActive(before, target);
|
|
63
|
+
|
|
64
|
+
const tapStart = performance.now();
|
|
65
|
+
const response = await host.request<ControlResponse>({ $cmd: "tap", x: target.x, y: target.y });
|
|
66
|
+
const tapRoundTripMs = performance.now() - tapStart;
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
console.error(` step ${index}: tap failed: ${response.error || "no native target"}`);
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const settled =
|
|
73
|
+
settleMs > 0
|
|
74
|
+
? await waitForTapEffect(host, beforeHash, target, targetActiveBefore, settleMs)
|
|
75
|
+
: await dumpAfterCadence(host, beforeHash, target, targetActiveBefore, cadenceMs);
|
|
76
|
+
writeJson(join(outDir, `${prefix}-after-tree.json`), settled.dump);
|
|
77
|
+
captureIfRequested(host, captureScreenshots, join(outDir, `${prefix}-after.png`));
|
|
78
|
+
|
|
79
|
+
const record = {
|
|
80
|
+
index,
|
|
81
|
+
kind: step.kind,
|
|
82
|
+
selector: step.selector,
|
|
83
|
+
target,
|
|
84
|
+
response,
|
|
85
|
+
tapRoundTripMs: round(tapRoundTripMs),
|
|
86
|
+
treeChanged: settled.firstTreeChangeMs !== null,
|
|
87
|
+
firstTreeChangeMs: settled.firstTreeChangeMs === null ? null : round(settled.firstTreeChangeMs),
|
|
88
|
+
targetActiveBefore,
|
|
89
|
+
targetActive: settled.targetActive,
|
|
90
|
+
targetActiveMs: settled.targetActiveMs === null ? null : round(settled.targetActiveMs),
|
|
91
|
+
samples: settled.samples,
|
|
92
|
+
beforeHash,
|
|
93
|
+
afterHash: hashDump(settled.dump),
|
|
94
|
+
beforeDigest: digestTree(before),
|
|
95
|
+
afterDigest: digestTree(settled.dump),
|
|
96
|
+
};
|
|
97
|
+
(profile.steps as unknown[]).push(record);
|
|
98
|
+
|
|
99
|
+
if (!options.json) {
|
|
100
|
+
const changed = settled.firstTreeChangeMs !== null ? `${round(settled.firstTreeChangeMs)}ms` : "none";
|
|
101
|
+
const active = settled.targetActiveMs !== null ? `${round(settled.targetActiveMs)}ms` : settled.targetActive ? "active" : "not-active";
|
|
102
|
+
console.log(` ${index}. tap ${JSON.stringify(step.selector)} -> ${target.label} at ${target.x.toFixed(0)},${target.y.toFixed(0)} socket=${round(tapRoundTripMs)}ms tree=${changed} active=${active}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
profile.finishedAt = new Date().toISOString();
|
|
107
|
+
profile.durationMs = round(performance.now() - started);
|
|
108
|
+
profile.outDir = outDir;
|
|
109
|
+
writeJson(join(outDir, "profile.json"), profile);
|
|
110
|
+
|
|
111
|
+
if (options.json) {
|
|
112
|
+
console.log(JSON.stringify(profile, null, 2));
|
|
113
|
+
} else {
|
|
114
|
+
console.log(` profile: ${outDir}`);
|
|
115
|
+
}
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function parseSteps(args: string[]): FlowStep[] {
|
|
120
|
+
const steps: FlowStep[] = [];
|
|
121
|
+
let i = 0;
|
|
122
|
+
while (i < args.length) {
|
|
123
|
+
const verb = stripComma(args[i]).toLowerCase();
|
|
124
|
+
if (!ACTIONS.has(verb)) {
|
|
125
|
+
i += 1;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
i += 1;
|
|
129
|
+
const parts: string[] = [];
|
|
130
|
+
while (i < args.length && !ACTIONS.has(stripComma(args[i]).toLowerCase())) {
|
|
131
|
+
parts.push(stripComma(args[i]));
|
|
132
|
+
i += 1;
|
|
133
|
+
}
|
|
134
|
+
const selector = parts.join(" ").trim();
|
|
135
|
+
if (verb === "tap" && selector) steps.push({ kind: "tap", selector });
|
|
136
|
+
}
|
|
137
|
+
return steps;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function stripComma(value: string): string {
|
|
141
|
+
return value.replace(/,+$/, "");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function resolveTapTarget(dump: DumpNode, selector: string): { x: number; y: number; label: string; globalId: number | null; selector: string } | null {
|
|
145
|
+
const point = parsePoint(selector);
|
|
146
|
+
if (point) return { ...point, label: selector, globalId: null, selector };
|
|
147
|
+
const { best } = resolveSelector(dump, selector);
|
|
148
|
+
const center = best ? centerOf(best.node) : null;
|
|
149
|
+
if (!best || !center) return null;
|
|
150
|
+
return {
|
|
151
|
+
...center,
|
|
152
|
+
label: `${shortId(best.node)} #${best.node.globalId}`,
|
|
153
|
+
globalId: best.node.globalId,
|
|
154
|
+
selector,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function waitForTapEffect(
|
|
159
|
+
host: DriveableHost,
|
|
160
|
+
beforeHash: string,
|
|
161
|
+
target: { globalId: number | null; selector: string },
|
|
162
|
+
targetActiveBefore: boolean,
|
|
163
|
+
timeoutMs: number,
|
|
164
|
+
): Promise<{ firstTreeChangeMs: number | null; targetActive: boolean; targetActiveMs: number | null; samples: number; dump: DumpNode }> {
|
|
165
|
+
const started = performance.now();
|
|
166
|
+
let samples = 0;
|
|
167
|
+
let firstTreeChangeMs: number | null = null;
|
|
168
|
+
let targetActiveMs: number | null = targetActiveBefore ? 0 : null;
|
|
169
|
+
let latest = await host.dump();
|
|
170
|
+
while (performance.now() - started < timeoutMs) {
|
|
171
|
+
await sleep(35);
|
|
172
|
+
latest = await host.dump();
|
|
173
|
+
samples += 1;
|
|
174
|
+
const elapsedMs = performance.now() - started;
|
|
175
|
+
if (firstTreeChangeMs === null && hashDump(latest) !== beforeHash) firstTreeChangeMs = elapsedMs;
|
|
176
|
+
if (targetActiveMs === null && isTargetActive(latest, target)) targetActiveMs = elapsedMs;
|
|
177
|
+
if (firstTreeChangeMs !== null && targetActiveMs !== null) break;
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
firstTreeChangeMs,
|
|
181
|
+
targetActive: isTargetActive(latest, target),
|
|
182
|
+
targetActiveMs,
|
|
183
|
+
samples,
|
|
184
|
+
dump: latest,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function dumpAfterCadence(
|
|
189
|
+
host: DriveableHost,
|
|
190
|
+
beforeHash: string,
|
|
191
|
+
target: { globalId: number | null; selector: string },
|
|
192
|
+
targetActiveBefore: boolean,
|
|
193
|
+
cadenceMs: number,
|
|
194
|
+
): Promise<{ firstTreeChangeMs: number | null; targetActive: boolean; targetActiveMs: number | null; samples: number; dump: DumpNode }> {
|
|
195
|
+
if (cadenceMs > 0) await sleep(cadenceMs);
|
|
196
|
+
const dump = await host.dump();
|
|
197
|
+
const targetActive = isTargetActive(dump, target);
|
|
198
|
+
return {
|
|
199
|
+
firstTreeChangeMs: hashDump(dump) !== beforeHash ? cadenceMs : null,
|
|
200
|
+
targetActive,
|
|
201
|
+
targetActiveMs: targetActiveBefore ? 0 : targetActive ? cadenceMs : null,
|
|
202
|
+
samples: 1,
|
|
203
|
+
dump,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function targetInfo(host: DriveableHost) {
|
|
208
|
+
return {
|
|
209
|
+
mode: host.mode,
|
|
210
|
+
appName: host.mode === "launch" ? host.appName : host.appName ?? null,
|
|
211
|
+
servicePid: host.servicePid,
|
|
212
|
+
window: host.window,
|
|
213
|
+
controlSocketPath: host.mode === "attach" ? host.controlSocketPath ?? null : "(session)",
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function digestTree(dump: DumpNode) {
|
|
218
|
+
const interactive = [...walk(dump)]
|
|
219
|
+
.filter((node) => node.bounds && node.events?.length)
|
|
220
|
+
.slice(0, 20)
|
|
221
|
+
.map((node) => ({
|
|
222
|
+
globalId: node.globalId,
|
|
223
|
+
id: shortId(node),
|
|
224
|
+
bounds: node.bounds,
|
|
225
|
+
events: node.events ?? [],
|
|
226
|
+
stateStyle: stateStyleOf(node),
|
|
227
|
+
}));
|
|
228
|
+
const webviews = [...walk(dump)]
|
|
229
|
+
.filter((node) => node.type === "webview")
|
|
230
|
+
.map((node) => ({ globalId: node.globalId, id: shortId(node), bounds: node.bounds ?? null }));
|
|
231
|
+
return { interactive, webviews };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function isTargetActive(dump: DumpNode, target: { globalId: number | null; selector: string }): boolean {
|
|
235
|
+
const node = target.globalId === null ? resolveSelector(dump, target.selector).best?.node : findNodeByGlobalId(dump, target.globalId);
|
|
236
|
+
if (!node) return false;
|
|
237
|
+
const style = stateStyleOf(node);
|
|
238
|
+
return !!(style.backgroundColor || style.boxShadow || style.borderColor);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function findNodeByGlobalId(root: DumpNode, globalId: number): DumpNode | null {
|
|
242
|
+
for (const node of walk(root)) if (node.globalId === globalId) return node;
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function hashDump(dump: DumpNode): string {
|
|
247
|
+
return createHash("sha1").update(JSON.stringify(dump)).digest("hex");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function shortId(node: DumpNode): string {
|
|
251
|
+
const a = node.accessibility ?? {};
|
|
252
|
+
return a.testID ?? a.identifier ?? a.nativeID ?? a.label ?? (node.text ? `"${node.text.slice(0, 40)}"` : `#${node.globalId}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function stateStyleOf(node: DumpNode): Record<string, unknown> {
|
|
256
|
+
const style = node.style ?? {};
|
|
257
|
+
const out: Record<string, unknown> = {};
|
|
258
|
+
for (const key of ["backgroundColor", "borderColor", "boxShadow", "opacity", "display"]) {
|
|
259
|
+
const value = style[key];
|
|
260
|
+
if (value !== undefined && value !== "none" && value !== "#00000000") out[key] = value;
|
|
261
|
+
}
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function writeJson(path: string, value: unknown) {
|
|
266
|
+
writeFileSync(path, JSON.stringify(value, null, 2));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function captureIfRequested(host: Host, enabled: boolean, path: string) {
|
|
270
|
+
if (!enabled) return;
|
|
271
|
+
try {
|
|
272
|
+
host.capture(path);
|
|
273
|
+
if (!existsSync(path)) {
|
|
274
|
+
writeFileSync(`${path}.missing`, "capture did not write a file\n");
|
|
275
|
+
}
|
|
276
|
+
} catch (error) {
|
|
277
|
+
writeFileSync(`${path}.error.txt`, error instanceof Error ? error.message : String(error));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function round(value: number): number {
|
|
282
|
+
return Math.round(value * 10) / 10;
|
|
283
|
+
}
|