rnxsim 0.0.0 → 0.1.313
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/LICENSE +21 -0
- package/README.md +512 -0
- package/detox/colors.ts +54 -0
- package/detox/config-loader.ts +135 -0
- package/detox/element-types.ts +39 -0
- package/detox/expectations.ts +537 -0
- package/detox/gestures.ts +480 -0
- package/detox/index.ts +2118 -0
- package/detox/jest-preset.cjs +51 -0
- package/detox/jest-setup-after-env.cjs +18 -0
- package/detox/matchers.ts +30 -0
- package/detox/motion-change.cjs +109 -0
- package/detox/motion-change.d.cts +35 -0
- package/detox/navigation.ts +43 -0
- package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
- package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
- package/detox/screenshots/color-test-rest-norngh.png +0 -0
- package/detox/screenshots/theme-blue-toggle.png +0 -0
- package/detox/screenshots/theme-blue.png +0 -0
- package/detox/screenshots/theme-red-toggle.png +0 -0
- package/detox/screenshots/theme-red.png +0 -0
- package/dist-cli/bin.js +15 -0
- package/dist-cli/chunks/agent-XZ2KTPCU.js +48 -0
- package/dist-cli/chunks/agent-wrapper-JJYYW2WH.js +17 -0
- package/dist-cli/chunks/app-fonts-IXRNQG6B.js +4 -0
- package/dist-cli/chunks/assert-54T5SK5F.js +49 -0
- package/dist-cli/chunks/auth-FI5UDI45.js +12 -0
- package/dist-cli/chunks/beta-JV6UKADW.js +4 -0
- package/dist-cli/chunks/camera-VL73YIKP.js +22 -0
- package/dist-cli/chunks/chunk-2D2UPBBR.js +4 -0
- package/dist-cli/chunks/chunk-2YR5BGA5.js +5 -0
- package/dist-cli/chunks/chunk-3NV2NCNX.js +4 -0
- package/dist-cli/chunks/chunk-46EUUFJ5.js +4 -0
- package/dist-cli/chunks/chunk-4NPPOV2N.js +5 -0
- package/dist-cli/chunks/chunk-5DHC6KHQ.js +7 -0
- package/dist-cli/chunks/chunk-5TEF3ET3.js +74 -0
- package/dist-cli/chunks/chunk-5TPRP5QT.js +4 -0
- package/dist-cli/chunks/chunk-5YJCOWCH.js +4 -0
- package/dist-cli/chunks/chunk-7GN3LVWB.js +4 -0
- package/dist-cli/chunks/chunk-BBULZ7CG.js +568 -0
- package/dist-cli/chunks/chunk-BTWORNNG.js +71 -0
- package/dist-cli/chunks/chunk-D4FFVGI5.js +5 -0
- package/dist-cli/chunks/chunk-DCEMHR2Y.js +4 -0
- package/dist-cli/chunks/chunk-DZS6WPUI.js +4 -0
- package/dist-cli/chunks/chunk-F5ZRSS3C.js +3 -0
- package/dist-cli/chunks/chunk-FSUYIVJ6.js +9 -0
- package/dist-cli/chunks/chunk-G2WW6L2C.js +23 -0
- package/dist-cli/chunks/chunk-GASE6UBA.js +4 -0
- package/dist-cli/chunks/chunk-GGRX24GF.js +142 -0
- package/dist-cli/chunks/chunk-HI5TFJWN.js +5 -0
- package/dist-cli/chunks/chunk-IJ5CAZZC.js +5 -0
- package/dist-cli/chunks/chunk-IJO63TDP.js +4 -0
- package/dist-cli/chunks/chunk-KTHV3RUS.js +26 -0
- package/dist-cli/chunks/chunk-LF2ZVT7O.js +6 -0
- package/dist-cli/chunks/chunk-MJRLLB4R.js +38 -0
- package/dist-cli/chunks/chunk-NFK7T35W.js +4 -0
- package/dist-cli/chunks/chunk-NMF2ZMZQ.js +15 -0
- package/dist-cli/chunks/chunk-OHAZNXLK.js +4 -0
- package/dist-cli/chunks/chunk-OVFJFXUD.js +101 -0
- package/dist-cli/chunks/chunk-OZSSI4WN.js +28 -0
- package/dist-cli/chunks/chunk-QKDWYITG.js +25 -0
- package/dist-cli/chunks/chunk-RSZWCKNT.js +30 -0
- package/dist-cli/chunks/chunk-RTN5C5RL.js +4 -0
- package/dist-cli/chunks/chunk-TIVZIMMW.js +4 -0
- package/dist-cli/chunks/chunk-TZFFR3SD.js +7 -0
- package/dist-cli/chunks/chunk-UC6U3MML.js +5 -0
- package/dist-cli/chunks/chunk-VFCMSYZK.js +6 -0
- package/dist-cli/chunks/chunk-VNQEB4L7.js +5 -0
- package/dist-cli/chunks/chunk-VZXWHRUZ.js +983 -0
- package/dist-cli/chunks/chunk-W6K4EFPH.js +107 -0
- package/dist-cli/chunks/chunk-WEXDAC74.js +4 -0
- package/dist-cli/chunks/chunk-WF3T4SVI.js +14 -0
- package/dist-cli/chunks/chunk-WINYQ44O.js +4 -0
- package/dist-cli/chunks/chunk-WMIIKMGK.js +10 -0
- package/dist-cli/chunks/chunk-WUSWBCWA.js +507 -0
- package/dist-cli/chunks/chunk-WWZIXIRD.js +4 -0
- package/dist-cli/chunks/chunk-XEVZYVIW.js +766 -0
- package/dist-cli/chunks/chunk-YDGQTMQL.js +4 -0
- package/dist-cli/chunks/chunk-YIFT42WN.js +4 -0
- package/dist-cli/chunks/chunk-ZMJD5GEC.js +125 -0
- package/dist-cli/chunks/cleanup-P27PA6JI.js +25 -0
- package/dist-cli/chunks/cli-version-WWLPBDQ7.js +4 -0
- package/dist-cli/chunks/compat-ZD65FED3.js +20 -0
- package/dist-cli/chunks/config-XMJRNM2A.js +13 -0
- package/dist-cli/chunks/control-KMIQT3QP.js +4 -0
- package/dist-cli/chunks/daemon-G2ME7NLB.js +4 -0
- package/dist-cli/chunks/debug-PT4HOP7N.js +200 -0
- package/dist-cli/chunks/desktop-S3FG72AK.js +31 -0
- package/dist-cli/chunks/detox-B3D4IFCN.js +52 -0
- package/dist-cli/chunks/device-XBNDSB2R.js +18 -0
- package/dist-cli/chunks/diagnose-HMQXJE5N.js +43 -0
- package/dist-cli/chunks/disk-cleanup-BLCZ5BSZ.js +4 -0
- package/dist-cli/chunks/drivers-LDECZGP2.js +4 -0
- package/dist-cli/chunks/film-BJGTBYZB.js +74 -0
- package/dist-cli/chunks/flow-UEQNVTU7.js +4 -0
- package/dist-cli/chunks/help-T5FYSVGB.js +4 -0
- package/dist-cli/chunks/hidden-runtime-alias-ANOYADHM.js +4 -0
- package/dist-cli/chunks/home-paths-GT3LFNOR.js +4 -0
- package/dist-cli/chunks/inspect-ZA6XF5LD.js +4 -0
- package/dist-cli/chunks/install-desktop-TIMUDHPL.js +4 -0
- package/dist-cli/chunks/login-FJ737MWG.js +28 -0
- package/dist-cli/chunks/logout-ZCNMMHMY.js +4 -0
- package/dist-cli/chunks/maestro-SZTNKLDF.js +104 -0
- package/dist-cli/chunks/maestro-generate-DCFAIZ4H.js +36 -0
- package/dist-cli/chunks/mode-GRMQCRXR.js +26 -0
- package/dist-cli/chunks/optional-demo-registry-W36EWFFB.js +4 -0
- package/dist-cli/chunks/perf-QYBAAUZG.js +48 -0
- package/dist-cli/chunks/permissions-3QCQ6VF4.js +13 -0
- package/dist-cli/chunks/record-QPPC2S4E.js +72 -0
- package/dist-cli/chunks/report-issue-7NMFP4HK.js +29 -0
- package/dist-cli/chunks/runtime-XOAXMSTU.js +4 -0
- package/dist-cli/chunks/screenshot-command-67AECJFB.js +159 -0
- package/dist-cli/chunks/screenshot-layers-ASWBYPJL.js +20 -0
- package/dist-cli/chunks/screenshots-capture-PXA3HFQK.js +42 -0
- package/dist-cli/chunks/serve-BI2NBAXG.js +44 -0
- package/dist-cli/chunks/setup-7DWPMRSB.js +32 -0
- package/dist-cli/chunks/skills-S3Y22TUA.js +50 -0
- package/dist-cli/chunks/storage-XUIMJWAJ.js +37 -0
- package/dist-cli/chunks/store-JTHEJLAZ.js +4 -0
- package/dist-cli/chunks/telemetry-ZYJGD2DB.js +4 -0
- package/dist-cli/chunks/timeline-TMPLQPSP.js +24 -0
- package/dist-cli/chunks/upgrade-7HDSIM7K.js +7 -0
- package/dist-cli/chunks/upload-GMSZPWM6.js +4 -0
- package/dist-cli/chunks/version-HOCHZ37L.js +6 -0
- package/dist-cli/chunks/web-DG3WBYD3.js +4 -0
- package/dist-cli/chunks/what-happened-XFVUTZR7.js +25 -0
- package/dist-lib/agent-daemon-client.cjs +468 -0
- package/dist-lib/agent-events.cjs +48 -0
- package/dist-lib/agent-identity.cjs +53 -0
- package/dist-lib/agent-sessions.cjs +725 -0
- package/dist-lib/attached-projects.cjs +526 -0
- package/dist-lib/auth/shared-session.cjs +195 -0
- package/dist-lib/backend-origin.cjs +70 -0
- package/dist-lib/beta.cjs +41 -0
- package/dist-lib/beta.mjs +13 -0
- package/dist-lib/bridge-constants.cjs +161 -0
- package/dist-lib/cli-constants.cjs +32 -0
- package/dist-lib/config.cjs +96 -0
- package/dist-lib/detox/index.cjs +2863 -0
- package/dist-lib/detox/jest-preset.cjs +51 -0
- package/dist-lib/dev-bundle-resolution.cjs +223 -0
- package/dist-lib/home-paths.cjs +421 -0
- package/dist-lib/host/bridge-host.cjs +5600 -0
- package/dist-lib/host/fetch-proxy-handler.cjs +373 -0
- package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
- package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
- package/dist-lib/host/websocket-proxy.cjs +214 -0
- package/dist-lib/index.cjs +513 -0
- package/dist-lib/jump-to-source-babel.cjs +93 -0
- package/dist-lib/menu.cjs +620 -0
- package/dist-lib/menu.mjs +581 -0
- package/dist-lib/metro.cjs +297 -0
- package/dist-lib/profiles.cjs +246 -0
- package/dist-lib/public-brand.cjs +39 -0
- package/dist-lib/render-mode.cjs +54 -0
- package/dist-lib/scripts/dev-server-scanner.cjs +1034 -0
- package/dist-lib/sdk.cjs +2333 -0
- package/dist-lib/sdk.mjs +2240 -0
- package/dist-lib/skills.cjs +23922 -0
- package/dist-lib/vite.cjs +358 -0
- package/package.json +189 -18
- package/scripts/dev-server-scanner.ts +941 -0
- package/scripts/optional-demo-registry.ts +113 -0
- package/scripts/postinstall.cjs +3 -0
- package/scripts/preuninstall.cjs +118 -0
- package/skills/contrast/SKILL.md +171 -0
- package/skills/rnx-debug/SKILL.md +425 -0
- package/skills/rnx-perf/SKILL.md +295 -0
- package/skills/rnx-setup/SKILL.md +160 -0
- package/skills/rnx-test/SKILL.md +292 -0
- package/skills/rnx-visual/SKILL.md +122 -0
- package/src/agent-daemon-client.ts +410 -0
- package/src/agent-events.ts +71 -0
- package/src/agent-identity.ts +23 -0
- package/src/agent-prompt.ts +71 -0
- package/src/agent-sessions.ts +573 -0
- package/src/attached-projects.ts +539 -0
- package/src/auth/shared-session.ts +204 -0
- package/src/backend-origin.ts +49 -0
- package/src/beta.ts +19 -0
- package/src/bridge-constants.ts +171 -0
- package/src/browser-cache.ts +16 -0
- package/src/cli-constants.ts +1 -0
- package/src/cli-version.ts +32 -0
- package/src/codex-client.ts +215 -0
- package/src/config.ts +134 -0
- package/src/dev-bundle-resolution.ts +236 -0
- package/src/disk-cleanup.ts +466 -0
- package/src/home-paths.ts +552 -0
- package/src/host/agent-host.ts +666 -0
- package/src/host/bridge-host.ts +2626 -0
- package/src/host/fetch-proxy-handler.ts +397 -0
- package/src/host/fetch-proxy-overrides.ts +51 -0
- package/src/host/open-url.ts +89 -0
- package/src/host/websocket-proxy.ts +210 -0
- package/src/index.ts +12 -705
- package/src/jump-to-source-babel.ts +135 -0
- package/src/menu.ts +734 -0
- package/src/metro-plugin.ts +139 -0
- package/src/native-dev-bundle-url.ts +96 -0
- package/src/native-seam-manifest.ts +341 -0
- package/src/plist.ts +27 -0
- package/src/poll-until.ts +92 -0
- package/src/profiles.ts +179 -0
- package/src/public-brand.ts +9 -0
- package/src/render-mode.ts +27 -0
- package/src/runtime-assets.ts +228 -0
- package/src/runtime-delivery.ts +54 -0
- package/src/screenshots/compose.ts +422 -0
- package/src/screenshots/frame-compose.ts +454 -0
- package/src/screenshots/orchestrate.ts +256 -0
- package/src/screenshots/org-deck.ts +91 -0
- package/src/screenshots/registry.ts +58 -0
- package/src/screenshots/schema.ts +364 -0
- package/src/sdk.ts +7 -0
- package/src/settings-bundle.ts +418 -0
- package/src/skills/builtin/a11y-review.ts +132 -0
- package/src/skills/builtin/compat-check.ts +71 -0
- package/src/skills/builtin/maestro-test.ts +121 -0
- package/src/skills/builtin/perf-profile.ts +90 -0
- package/src/skills/builtin/screenshot-all.ts +52 -0
- package/src/skills/builtin/visual-diff.ts +100 -0
- package/src/skills/registry.ts +107 -0
- package/src/skills/types.ts +41 -0
- package/src/team-machine-prompt.ts +73 -0
- package/src/vite-plugin-one.ts +190 -0
- package/src/vite-plugin.ts +1750 -0
- package/src/worklets-babel.ts +134 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// minimal client for `codex app-server` — a long-lived child process that
|
|
2
|
+
// speaks line-delimited JSON-RPC 2.0 over stdio.
|
|
3
|
+
//
|
|
4
|
+
// scope: just the subset sootsim needs for the attached-projects agent
|
|
5
|
+
// wrapper. each connection owns exactly one spawned app-server child; caller
|
|
6
|
+
// is responsible for lifecycle. bidirectional: client can issue requests +
|
|
7
|
+
// receive server-initiated notifications. we treat every method we don't
|
|
8
|
+
// recognize as an observable event that we pass straight to the handler map.
|
|
9
|
+
|
|
10
|
+
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
|
|
11
|
+
import readline from 'node:readline'
|
|
12
|
+
|
|
13
|
+
export interface CodexClientOptions {
|
|
14
|
+
/** resolved path to the codex binary (via `which codex` or user override). */
|
|
15
|
+
bin: string
|
|
16
|
+
/** working directory for the spawned process. defaults to process.cwd(). */
|
|
17
|
+
cwd?: string
|
|
18
|
+
/** extra environment overrides forwarded to the child. */
|
|
19
|
+
env?: Record<string, string | undefined>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type CodexNotificationHandler = (params: unknown) => void
|
|
23
|
+
|
|
24
|
+
export interface CodexClient {
|
|
25
|
+
/** promise that resolves when the child exits. use to await shutdown. */
|
|
26
|
+
readonly exited: Promise<{ code: number | null; signal: NodeJS.Signals | null }>
|
|
27
|
+
/** register a handler for a server-sent notification method. returns
|
|
28
|
+
* a dispose fn. multiple handlers per method are allowed. */
|
|
29
|
+
on(method: string, handler: CodexNotificationHandler): () => void
|
|
30
|
+
/** send a JSON-RPC request and await its response. rejects on error
|
|
31
|
+
* response or child death. */
|
|
32
|
+
request<TResult = unknown>(method: string, params?: unknown): Promise<TResult>
|
|
33
|
+
/** send a JSON-RPC notification (no id, no response). */
|
|
34
|
+
notify(method: string, params?: unknown): void
|
|
35
|
+
/** close stdin (graceful shutdown), then SIGTERM after timeout. */
|
|
36
|
+
shutdown(timeoutMs?: number): Promise<void>
|
|
37
|
+
/** force-kill immediately. */
|
|
38
|
+
kill(signal?: NodeJS.Signals): void
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface JsonRpcResponse {
|
|
42
|
+
jsonrpc?: '2.0'
|
|
43
|
+
id?: number | string | null
|
|
44
|
+
result?: unknown
|
|
45
|
+
error?: { code: number; message: string; data?: unknown }
|
|
46
|
+
method?: string
|
|
47
|
+
params?: unknown
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class CodexRpcError extends Error {
|
|
51
|
+
code: number
|
|
52
|
+
data: unknown
|
|
53
|
+
constructor(message: string, code: number, data?: unknown) {
|
|
54
|
+
super(message)
|
|
55
|
+
this.name = 'CodexRpcError'
|
|
56
|
+
this.code = code
|
|
57
|
+
this.data = data
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function spawnCodexClient(options: CodexClientOptions): CodexClient {
|
|
62
|
+
const child: ChildProcessWithoutNullStreams = spawn(options.bin, ['app-server'], {
|
|
63
|
+
cwd: options.cwd,
|
|
64
|
+
env: { ...process.env, ...options.env },
|
|
65
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const pending = new Map<
|
|
69
|
+
number,
|
|
70
|
+
{
|
|
71
|
+
resolve: (value: unknown) => void
|
|
72
|
+
reject: (err: unknown) => void
|
|
73
|
+
method: string
|
|
74
|
+
}
|
|
75
|
+
>()
|
|
76
|
+
const handlers = new Map<string, Set<CodexNotificationHandler>>()
|
|
77
|
+
let nextId = 1
|
|
78
|
+
let closed = false
|
|
79
|
+
|
|
80
|
+
const exited = new Promise<{
|
|
81
|
+
code: number | null
|
|
82
|
+
signal: NodeJS.Signals | null
|
|
83
|
+
}>((resolve) => {
|
|
84
|
+
child.on('exit', (code, signal) => {
|
|
85
|
+
closed = true
|
|
86
|
+
const err = new Error(
|
|
87
|
+
`codex app-server exited (code=${code}, signal=${signal ?? ''})`,
|
|
88
|
+
)
|
|
89
|
+
for (const { reject } of pending.values()) reject(err)
|
|
90
|
+
pending.clear()
|
|
91
|
+
resolve({ code, signal })
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// stdout carries JSON-RPC messages, one per line.
|
|
96
|
+
const rl = readline.createInterface({ input: child.stdout, crlfDelay: Infinity })
|
|
97
|
+
rl.on('line', (line) => {
|
|
98
|
+
const trimmed = line.trim()
|
|
99
|
+
if (!trimmed) return
|
|
100
|
+
let msg: JsonRpcResponse
|
|
101
|
+
try {
|
|
102
|
+
msg = JSON.parse(trimmed) as JsonRpcResponse
|
|
103
|
+
} catch {
|
|
104
|
+
// app-server occasionally emits non-JSON warnings; ignore.
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
if (msg.id != null && (msg.result !== undefined || msg.error !== undefined)) {
|
|
108
|
+
const idNum = typeof msg.id === 'string' ? Number(msg.id) : msg.id
|
|
109
|
+
const entry = idNum != null ? pending.get(idNum) : undefined
|
|
110
|
+
if (!entry) return
|
|
111
|
+
pending.delete(idNum!)
|
|
112
|
+
if (msg.error) {
|
|
113
|
+
entry.reject(new CodexRpcError(msg.error.message, msg.error.code, msg.error.data))
|
|
114
|
+
} else {
|
|
115
|
+
entry.resolve(msg.result)
|
|
116
|
+
}
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
if (msg.method) {
|
|
120
|
+
const set = handlers.get(msg.method)
|
|
121
|
+
if (!set) return
|
|
122
|
+
for (const h of set) {
|
|
123
|
+
try {
|
|
124
|
+
h(msg.params)
|
|
125
|
+
} catch (err) {
|
|
126
|
+
// a buggy handler must not kill the bridge, but it MUST be visible —
|
|
127
|
+
// silent swallowing turned a notification-handler typo into a
|
|
128
|
+
// "nothing happens" bug once already.
|
|
129
|
+
console.error(
|
|
130
|
+
`[codex-client] handler for "${msg.method}" threw:`,
|
|
131
|
+
err instanceof Error ? (err.stack ?? err.message) : err,
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
// stderr is diagnostic only; surfaced via the "stderr" notification channel
|
|
139
|
+
// so callers can forward it into transcripts if they want.
|
|
140
|
+
child.stderr.setEncoding('utf8')
|
|
141
|
+
child.stderr.on('data', (chunk: string) => {
|
|
142
|
+
const set = handlers.get('__stderr__')
|
|
143
|
+
if (!set) return
|
|
144
|
+
for (const h of set) {
|
|
145
|
+
try {
|
|
146
|
+
h({ text: chunk })
|
|
147
|
+
} catch {}
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
function write(msg: unknown): void {
|
|
152
|
+
if (closed) return
|
|
153
|
+
try {
|
|
154
|
+
child.stdin.write(JSON.stringify(msg) + '\n')
|
|
155
|
+
} catch {
|
|
156
|
+
// if stdin has been closed, subsequent writes will throw; ignore.
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
exited,
|
|
162
|
+
on(method, handler) {
|
|
163
|
+
let set = handlers.get(method)
|
|
164
|
+
if (!set) {
|
|
165
|
+
set = new Set()
|
|
166
|
+
handlers.set(method, set)
|
|
167
|
+
}
|
|
168
|
+
set.add(handler)
|
|
169
|
+
return () => {
|
|
170
|
+
set?.delete(handler)
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
request<TResult>(method: string, params?: unknown): Promise<TResult> {
|
|
174
|
+
if (closed) {
|
|
175
|
+
return Promise.reject(new Error(`codex app-server closed; cannot call ${method}`))
|
|
176
|
+
}
|
|
177
|
+
const id = nextId++
|
|
178
|
+
return new Promise<TResult>((resolve, reject) => {
|
|
179
|
+
pending.set(id, {
|
|
180
|
+
resolve: (v) => resolve(v as TResult),
|
|
181
|
+
reject,
|
|
182
|
+
method,
|
|
183
|
+
})
|
|
184
|
+
write({ jsonrpc: '2.0', id, method, params })
|
|
185
|
+
})
|
|
186
|
+
},
|
|
187
|
+
notify(method, params) {
|
|
188
|
+
write({ jsonrpc: '2.0', method, params })
|
|
189
|
+
},
|
|
190
|
+
async shutdown(timeoutMs = 1500) {
|
|
191
|
+
if (closed) return
|
|
192
|
+
try {
|
|
193
|
+
child.stdin.end()
|
|
194
|
+
} catch {}
|
|
195
|
+
const t = setTimeout(() => {
|
|
196
|
+
if (!closed) {
|
|
197
|
+
try {
|
|
198
|
+
child.kill('SIGTERM')
|
|
199
|
+
} catch {}
|
|
200
|
+
}
|
|
201
|
+
}, timeoutMs)
|
|
202
|
+
try {
|
|
203
|
+
await exited
|
|
204
|
+
} finally {
|
|
205
|
+
clearTimeout(t)
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
kill(signal = 'SIGTERM') {
|
|
209
|
+
if (closed) return
|
|
210
|
+
try {
|
|
211
|
+
child.kill(signal)
|
|
212
|
+
} catch {}
|
|
213
|
+
},
|
|
214
|
+
}
|
|
215
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// sootsim.config.ts — per-app module resolution, turbo modules, env, state, and settings.
|
|
2
|
+
//
|
|
3
|
+
// usage:
|
|
4
|
+
// import { defineConfig } from 'rnxsim/config'
|
|
5
|
+
// export default defineConfig({ modules: { ... }, env: { ... } })
|
|
6
|
+
|
|
7
|
+
import type { RevenueCatTestStoreConfig } from '@sootsim/compat/revenuecat-test-store'
|
|
8
|
+
|
|
9
|
+
export type ModuleResolution =
|
|
10
|
+
// an empty module under `modules`; under `nativeModules` / `turboModules` a
|
|
11
|
+
// present-but-inert native module whose every method call is a no-op.
|
|
12
|
+
| 'noop'
|
|
13
|
+
| false // disable builtin stub, use original
|
|
14
|
+
| { use: string } // redirect to existing compat stub
|
|
15
|
+
| { file: string } // resolve to a file relative to the config
|
|
16
|
+
| { inline: Record<string, any> } // inline stub object
|
|
17
|
+
|
|
18
|
+
export type NativeModuleResolution = ModuleResolution | Record<string, any> // direct native module object for non-url configs
|
|
19
|
+
|
|
20
|
+
export interface SootSimConfig<
|
|
21
|
+
Settings extends Record<string, any> = Record<string, any>,
|
|
22
|
+
> {
|
|
23
|
+
// host-only engine selection. omitted means the current stable runtime.
|
|
24
|
+
runtimeVersion?: string
|
|
25
|
+
modules?: Record<string, ModuleResolution>
|
|
26
|
+
turboModules?: Record<string, NativeModuleResolution>
|
|
27
|
+
nativeModules?: Record<string, NativeModuleResolution>
|
|
28
|
+
env?: Record<string, string>
|
|
29
|
+
// generic per-sim network rewrites for the guest app's runtime fetches.
|
|
30
|
+
// `remap` keys/values are `host:port` (or bare `host`) — a tenant fetch whose
|
|
31
|
+
// url matches a key has its host:port swapped for the value before the
|
|
32
|
+
// request is issued. exists for third-party e2e suites that hardcode a port
|
|
33
|
+
// (e.g. bluesky's flows type `http://localhost:3000`) which collides with a
|
|
34
|
+
// host service; map it to the real bound port without editing the app.
|
|
35
|
+
network?: {
|
|
36
|
+
remap?: Record<string, string>
|
|
37
|
+
}
|
|
38
|
+
settings?: Partial<Settings>
|
|
39
|
+
initialState?: {
|
|
40
|
+
authenticated?: boolean
|
|
41
|
+
locale?: string
|
|
42
|
+
colorScheme?: 'light' | 'dark' | 'auto'
|
|
43
|
+
featureFlags?: Record<string, boolean | string | number>
|
|
44
|
+
// opt-in RevenueCat Test Store catalog consumed by the RNPurchases seam.
|
|
45
|
+
// omitting it preserves the empty-project result with purchases unavailable.
|
|
46
|
+
revenueCat?: RevenueCatTestStoreConfig
|
|
47
|
+
[key: string]: any
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const SOOTSIM_CONFIG_QUERY_PARAM = 'sootsimConfig'
|
|
52
|
+
|
|
53
|
+
export function defineConfig<Settings extends Record<string, any> = Record<string, any>>(
|
|
54
|
+
config: SootSimConfig<Settings>,
|
|
55
|
+
): SootSimConfig<Settings> {
|
|
56
|
+
return config
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function hasOwnKeys(value: Record<string, unknown> | undefined): boolean {
|
|
60
|
+
return !!value && Object.keys(value).length > 0
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function hasSootSimConfig<
|
|
64
|
+
Settings extends Record<string, any> = Record<string, any>,
|
|
65
|
+
>(config?: SootSimConfig<Settings> | null): config is SootSimConfig<Settings> {
|
|
66
|
+
if (!config) return false
|
|
67
|
+
return (
|
|
68
|
+
(typeof config.runtimeVersion === 'string' && config.runtimeVersion.length > 0) ||
|
|
69
|
+
hasOwnKeys(config.modules as Record<string, unknown> | undefined) ||
|
|
70
|
+
hasOwnKeys(config.turboModules as Record<string, unknown> | undefined) ||
|
|
71
|
+
hasOwnKeys(config.nativeModules as Record<string, unknown> | undefined) ||
|
|
72
|
+
hasOwnKeys(config.env as Record<string, unknown> | undefined) ||
|
|
73
|
+
hasOwnKeys(config.network?.remap as Record<string, unknown> | undefined) ||
|
|
74
|
+
hasOwnKeys(config.settings as Record<string, unknown> | undefined) ||
|
|
75
|
+
hasOwnKeys(config.initialState as Record<string, unknown> | undefined)
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function mergeSootSimConfig<
|
|
80
|
+
Settings extends Record<string, any> = Record<string, any>,
|
|
81
|
+
>(
|
|
82
|
+
base?: SootSimConfig<Settings> | null,
|
|
83
|
+
override?: SootSimConfig<Settings> | null,
|
|
84
|
+
): SootSimConfig<Settings> | undefined {
|
|
85
|
+
if (!hasSootSimConfig(base) && !hasSootSimConfig(override)) return undefined
|
|
86
|
+
if (!hasSootSimConfig(base)) return override ? { ...override } : undefined
|
|
87
|
+
if (!hasSootSimConfig(override)) return base ? { ...base } : undefined
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
runtimeVersion: override.runtimeVersion ?? base.runtimeVersion,
|
|
91
|
+
modules: { ...(base.modules || {}), ...(override.modules || {}) },
|
|
92
|
+
turboModules: { ...(base.turboModules || {}), ...(override.turboModules || {}) },
|
|
93
|
+
nativeModules: {
|
|
94
|
+
...(base.nativeModules || {}),
|
|
95
|
+
...(override.nativeModules || {}),
|
|
96
|
+
},
|
|
97
|
+
env: { ...(base.env || {}), ...(override.env || {}) },
|
|
98
|
+
network: {
|
|
99
|
+
remap: { ...(base.network?.remap || {}), ...(override.network?.remap || {}) },
|
|
100
|
+
},
|
|
101
|
+
settings: {
|
|
102
|
+
...(base.settings || {}),
|
|
103
|
+
...(override.settings || {}),
|
|
104
|
+
} as Partial<Settings>,
|
|
105
|
+
initialState: { ...(base.initialState || {}), ...(override.initialState || {}) },
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function readSootSimConfigFromSearchParams<
|
|
110
|
+
Settings extends Record<string, any> = Record<string, any>,
|
|
111
|
+
>(searchParams: URLSearchParams): SootSimConfig<Settings> | undefined {
|
|
112
|
+
const raw = searchParams.get(SOOTSIM_CONFIG_QUERY_PARAM)
|
|
113
|
+
if (!raw) return undefined
|
|
114
|
+
try {
|
|
115
|
+
const parsed = JSON.parse(raw) as SootSimConfig<Settings>
|
|
116
|
+
return hasSootSimConfig(parsed) ? parsed : undefined
|
|
117
|
+
} catch (error) {
|
|
118
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
119
|
+
console.warn(`[rnx] invalid ${SOOTSIM_CONFIG_QUERY_PARAM}: ${message}`)
|
|
120
|
+
return undefined
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function applySootSimConfigToUrl<
|
|
125
|
+
Settings extends Record<string, any> = Record<string, any>,
|
|
126
|
+
>(url: string, config?: SootSimConfig<Settings> | null): string {
|
|
127
|
+
const parsed = new URL(url)
|
|
128
|
+
if (hasSootSimConfig(config)) {
|
|
129
|
+
parsed.searchParams.set(SOOTSIM_CONFIG_QUERY_PARAM, JSON.stringify(config))
|
|
130
|
+
} else {
|
|
131
|
+
parsed.searchParams.delete(SOOTSIM_CONFIG_QUERY_PARAM)
|
|
132
|
+
}
|
|
133
|
+
return parsed.toString()
|
|
134
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { isLoopbackHost } from './backend-origin'
|
|
2
|
+
import { METRO_BUNDLE_PATH, normalizeNativeDevBundleUrl } from './native-dev-bundle-url'
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_TEST_DRIVE_PORT = 8081
|
|
5
|
+
|
|
6
|
+
export interface ResolvedDevBundle {
|
|
7
|
+
bundleUrl: string
|
|
8
|
+
port: number
|
|
9
|
+
framework: string
|
|
10
|
+
projectName?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function buildUnresolvedTargetError(targetLabel: string): Error {
|
|
14
|
+
return new Error(
|
|
15
|
+
`could not resolve a native bundle for ${targetLabel}. pass an explicit bundle URL or open Connect and choose the app there.`,
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function inferManifestFramework(
|
|
20
|
+
launchUrl: string | undefined,
|
|
21
|
+
sdkVersion: unknown,
|
|
22
|
+
): string {
|
|
23
|
+
if (launchUrl?.includes('/one/metro-entry.bundle')) return 'one'
|
|
24
|
+
if (typeof sdkVersion === 'string' && sdkVersion) return 'expo'
|
|
25
|
+
return 'unknown'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function buildBaseUrl(protocol: string, host: string, port: number) {
|
|
29
|
+
return `${protocol}//${host}:${port}`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function shouldProxyLoopbackProbe(targetUrl: string) {
|
|
33
|
+
if (typeof window === 'undefined') return false
|
|
34
|
+
try {
|
|
35
|
+
const parsed = new URL(targetUrl)
|
|
36
|
+
return (
|
|
37
|
+
isLoopbackHost(parsed.hostname) &&
|
|
38
|
+
isLoopbackHost(window.location.hostname) &&
|
|
39
|
+
parsed.origin !== window.location.origin
|
|
40
|
+
)
|
|
41
|
+
} catch {
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function fetchDevProbe(targetUrl: string, init?: RequestInit): Promise<Response> {
|
|
47
|
+
const requestInit: RequestInit = {
|
|
48
|
+
...init,
|
|
49
|
+
cache: init?.cache ?? 'no-store',
|
|
50
|
+
}
|
|
51
|
+
if (!shouldProxyLoopbackProbe(targetUrl)) {
|
|
52
|
+
return fetch(targetUrl, requestInit)
|
|
53
|
+
}
|
|
54
|
+
return fetch(`/__fetch-proxy?url=${encodeURIComponent(targetUrl)}`, requestInit)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getDefaultPortForProtocol(protocol: string) {
|
|
58
|
+
return protocol === 'https:' ? 443 : 80
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isBaseServerUrl(url: URL) {
|
|
62
|
+
const path = url.pathname || '/'
|
|
63
|
+
return (path === '/' || path === '') && !url.search && !url.hash
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// resolve a loopback dev-server port through the SAME server-side scanner the
|
|
67
|
+
// dev-server list uses (`/__server-scan`), so an explicit port (`/rn/<port>`, a
|
|
68
|
+
// typed port) gets the EXACT manifest `launchAsset.url` the list resolves —
|
|
69
|
+
// byte-identical, one resolver.
|
|
70
|
+
//
|
|
71
|
+
// why this exists: in a browser/worker the direct manifest probe below is
|
|
72
|
+
// cross-origin (shell on :5173, dev server on :<port>), and a dev server
|
|
73
|
+
// answers a cross-origin `GET /` with its SSR HTML, not the expo manifest. so
|
|
74
|
+
// the manifest branch silently fails (`.json()` throws on HTML) and the probe
|
|
75
|
+
// falls back to a generic `/index.bundle` — a DIFFERENT metro entry point than
|
|
76
|
+
// the manifest's `metro-entry.bundle`, which makes metro build the same app
|
|
77
|
+
// twice. the scanner runs server-side (same-origin node fetch, sends the
|
|
78
|
+
// expo-platform header) and reads `launchAsset.url` correctly.
|
|
79
|
+
//
|
|
80
|
+
// env-agnostic by design: it just attempts `/__server-scan` and lets the result
|
|
81
|
+
// decide. in the tenant worker (which has no `location`) the patched fetch
|
|
82
|
+
// resolves the relative path to the shell origin and returns the scan json. in a
|
|
83
|
+
// node/bun CLI a relative-URL fetch with no base throws → null → the direct probe
|
|
84
|
+
// runs (correct there, since node can read the cross-origin manifest). in prod
|
|
85
|
+
// the path SPA-falls-back to HTML → the json content-type guard returns null →
|
|
86
|
+
// direct probe. so there is exactly one resolver per environment, no env sniff.
|
|
87
|
+
async function resolvePortViaServerScan(port: number): Promise<ResolvedDevBundle | null> {
|
|
88
|
+
try {
|
|
89
|
+
const res = await fetch('/__server-scan', { cache: 'no-store' })
|
|
90
|
+
if (!res.ok) return null
|
|
91
|
+
if (!(res.headers.get('content-type') || '').toLowerCase().includes('json'))
|
|
92
|
+
return null
|
|
93
|
+
const servers: unknown = await res.json()
|
|
94
|
+
if (!Array.isArray(servers)) return null
|
|
95
|
+
const match = servers.find(
|
|
96
|
+
(
|
|
97
|
+
s,
|
|
98
|
+
): s is {
|
|
99
|
+
port: number
|
|
100
|
+
bundleUrl: string
|
|
101
|
+
framework?: string
|
|
102
|
+
projectName?: string
|
|
103
|
+
} =>
|
|
104
|
+
!!s &&
|
|
105
|
+
typeof s === 'object' &&
|
|
106
|
+
(s as { port?: unknown }).port === port &&
|
|
107
|
+
typeof (s as { bundleUrl?: unknown }).bundleUrl === 'string',
|
|
108
|
+
)
|
|
109
|
+
if (!match) return null
|
|
110
|
+
return {
|
|
111
|
+
// normalize exactly like the manifest-probe branch below — the scan
|
|
112
|
+
// returns metro's raw launchAsset url (lazy=true for code-split one/vxrn
|
|
113
|
+
// apps), but sootsim loads ONE self-contained bundle into the tenant
|
|
114
|
+
// worker, so a deferred React.lazy chunk throws "Requiring unknown module
|
|
115
|
+
// <id>". normalizeNativeDevBundleUrl forces lazy=false + strips bytecode.
|
|
116
|
+
bundleUrl: normalizeNativeDevBundleUrl(match.bundleUrl),
|
|
117
|
+
port,
|
|
118
|
+
framework: typeof match.framework === 'string' ? match.framework : 'unknown',
|
|
119
|
+
projectName: typeof match.projectName === 'string' ? match.projectName : undefined,
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
return null
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function probeBaseUrlBundle(
|
|
127
|
+
baseUrl: string,
|
|
128
|
+
port: number,
|
|
129
|
+
): Promise<ResolvedDevBundle | null> {
|
|
130
|
+
const normalizedBaseUrl = baseUrl.replace(/\/+$/, '')
|
|
131
|
+
|
|
132
|
+
// browser/worker loopback targets resolve through the server-side scanner so
|
|
133
|
+
// they match the dev-server list exactly — see resolvePortViaServerScan.
|
|
134
|
+
try {
|
|
135
|
+
if (isLoopbackHost(new URL(normalizedBaseUrl).hostname)) {
|
|
136
|
+
const viaScan = await resolvePortViaServerScan(port)
|
|
137
|
+
if (viaScan) return viaScan
|
|
138
|
+
}
|
|
139
|
+
} catch {}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const manifestRes = await fetchDevProbe(`${normalizedBaseUrl}/`, {
|
|
143
|
+
headers: { 'expo-platform': 'ios' },
|
|
144
|
+
})
|
|
145
|
+
if (manifestRes.ok) {
|
|
146
|
+
const manifest = await manifestRes.json()
|
|
147
|
+
const client = manifest?.extra?.expoClient || manifest?.extra || {}
|
|
148
|
+
const launchUrl =
|
|
149
|
+
typeof manifest?.launchAsset?.url === 'string'
|
|
150
|
+
? manifest.launchAsset.url
|
|
151
|
+
: undefined
|
|
152
|
+
if (launchUrl || client.name) {
|
|
153
|
+
return {
|
|
154
|
+
bundleUrl: normalizeNativeDevBundleUrl(
|
|
155
|
+
launchUrl || `${normalizedBaseUrl}${METRO_BUNDLE_PATH}`,
|
|
156
|
+
),
|
|
157
|
+
port,
|
|
158
|
+
framework: inferManifestFramework(launchUrl, client.sdkVersion),
|
|
159
|
+
projectName: client.name,
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch {}
|
|
164
|
+
|
|
165
|
+
// vanilla Metro (or any non-Expo dev server) — packager-status:running on
|
|
166
|
+
// /status is the canonical signature. we previously also did `HEAD
|
|
167
|
+
// /node_modules/one/metro-entry.bundle` as a framework discriminator, but
|
|
168
|
+
// Metro lazy-resolves modules at request time, so HEAD on *any* `.bundle`
|
|
169
|
+
// path can return 200 even when the underlying file doesn't exist. that
|
|
170
|
+
// caused false-positive One framework detection on non-One apps that had
|
|
171
|
+
// `node_modules/one` anywhere on disk (or that returned 200 HEAD + 404
|
|
172
|
+
// GET for missing bundles), surfacing as
|
|
173
|
+
// "failed to fetch bundle (404|502): .../node_modules/one/metro-entry.bundle…"
|
|
174
|
+
// the Expo manifest probe above already returns the correct launchAsset.url
|
|
175
|
+
// for One apps (which serve the Expo manifest format via vxrn), so we
|
|
176
|
+
// drop the bespoke One probe entirely.
|
|
177
|
+
try {
|
|
178
|
+
const statusRes = await fetchDevProbe(`${normalizedBaseUrl}/status`)
|
|
179
|
+
if (statusRes.ok) {
|
|
180
|
+
const text = await statusRes.text()
|
|
181
|
+
if (text.includes('packager-status:running')) {
|
|
182
|
+
return {
|
|
183
|
+
bundleUrl: `${normalizedBaseUrl}${METRO_BUNDLE_PATH}`,
|
|
184
|
+
port,
|
|
185
|
+
framework: 'metro',
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} catch {}
|
|
190
|
+
|
|
191
|
+
return null
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function probePortBundle(port: number): Promise<ResolvedDevBundle | null> {
|
|
195
|
+
return probeBaseUrlBundle(buildBaseUrl('http:', 'localhost', port), port)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function resolveConnectionInput(input: string): Promise<ResolvedDevBundle> {
|
|
199
|
+
const trimmed = input.trim()
|
|
200
|
+
|
|
201
|
+
if (/^\d+$/.test(trimmed)) {
|
|
202
|
+
const port = parseInt(trimmed, 10)
|
|
203
|
+
const resolved = await probePortBundle(port)
|
|
204
|
+
if (resolved) return resolved
|
|
205
|
+
throw buildUnresolvedTargetError(`localhost:${port}`)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const bundleUrl = trimmed.startsWith('http') ? trimmed : `http://${trimmed}`
|
|
209
|
+
let parsed: URL
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
parsed = new URL(bundleUrl)
|
|
213
|
+
} catch {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`could not parse "${input}". pass a dev-server port, a dev-server base URL, or a full bundle URL.`,
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const protocol = parsed.protocol || 'http:'
|
|
220
|
+
const port = parsed.port
|
|
221
|
+
? parseInt(parsed.port, 10)
|
|
222
|
+
: getDefaultPortForProtocol(protocol)
|
|
223
|
+
const baseUrl = buildBaseUrl(protocol, parsed.hostname, port)
|
|
224
|
+
|
|
225
|
+
// a bare base URL (just an origin, no path) is unambiguously a dev server
|
|
226
|
+
// to probe — regardless of host. probe its live manifest rather than
|
|
227
|
+
// guessing that the origin itself is the bundle URL. loopback vs. remote
|
|
228
|
+
// only affects how the probe fetch is routed (see shouldProxyLoopbackProbe).
|
|
229
|
+
if (isBaseServerUrl(parsed)) {
|
|
230
|
+
const resolved = await probeBaseUrlBundle(baseUrl, port)
|
|
231
|
+
if (resolved) return resolved
|
|
232
|
+
throw buildUnresolvedTargetError(baseUrl)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return { bundleUrl: parsed.toString(), port, framework: 'unknown' }
|
|
236
|
+
}
|