peachsim 0.1.527

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.
Files changed (324) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +640 -0
  3. package/cli/app-config.ts +82 -0
  4. package/cli/app-fonts.ts +388 -0
  5. package/cli/app-project.ts +59 -0
  6. package/cli/app-splash.ts +171 -0
  7. package/cli/app-state-reset.ts +26 -0
  8. package/cli/auth.ts +184 -0
  9. package/cli/bin.ts +59 -0
  10. package/cli/bridge-diagnostics.ts +226 -0
  11. package/cli/bridge-flow-runner.ts +2806 -0
  12. package/cli/browser-backend.ts +44 -0
  13. package/cli/browser-evals.ts +96 -0
  14. package/cli/cli-update.ts +261 -0
  15. package/cli/cloud-client.ts +1530 -0
  16. package/cli/cloud-dispatch.ts +206 -0
  17. package/cli/cloud-session.ts +227 -0
  18. package/cli/command-contract.ts +221 -0
  19. package/cli/command-registry.ts +304 -0
  20. package/cli/commands/agent-wrapper.ts +1048 -0
  21. package/cli/commands/agent.ts +439 -0
  22. package/cli/commands/app-fonts.ts +99 -0
  23. package/cli/commands/assert.ts +542 -0
  24. package/cli/commands/auth.ts +71 -0
  25. package/cli/commands/box/bundle-plane.ts +71 -0
  26. package/cli/commands/box/checkout-plane.ts +256 -0
  27. package/cli/commands/box/plane-share.ts +208 -0
  28. package/cli/commands/box/rnx-command.ts +60 -0
  29. package/cli/commands/box/search-command.ts +157 -0
  30. package/cli/commands/box.ts +925 -0
  31. package/cli/commands/camera.ts +266 -0
  32. package/cli/commands/cleanup.ts +169 -0
  33. package/cli/commands/compat.ts +176 -0
  34. package/cli/commands/config.ts +34 -0
  35. package/cli/commands/control.ts +2154 -0
  36. package/cli/commands/cpu-profile.ts +941 -0
  37. package/cli/commands/daemon-mac-app.ts +165 -0
  38. package/cli/commands/daemon.ts +800 -0
  39. package/cli/commands/debug.ts +751 -0
  40. package/cli/commands/desktop.ts +39 -0
  41. package/cli/commands/detect.ts +201 -0
  42. package/cli/commands/detox.ts +481 -0
  43. package/cli/commands/device.ts +134 -0
  44. package/cli/commands/diagnose.ts +589 -0
  45. package/cli/commands/do-chain.ts +408 -0
  46. package/cli/commands/electron.ts +94 -0
  47. package/cli/commands/film.ts +379 -0
  48. package/cli/commands/flow.ts +1240 -0
  49. package/cli/commands/inspect/actions.ts +516 -0
  50. package/cli/commands/inspect/core.ts +2086 -0
  51. package/cli/commands/inspect/count.ts +17 -0
  52. package/cli/commands/inspect/describe.ts +119 -0
  53. package/cli/commands/inspect/env.ts +25 -0
  54. package/cli/commands/inspect/find.ts +169 -0
  55. package/cli/commands/inspect/get-layout.ts +53 -0
  56. package/cli/commands/inspect/keyboard.ts +54 -0
  57. package/cli/commands/inspect/list.ts +175 -0
  58. package/cli/commands/inspect/memory.ts +215 -0
  59. package/cli/commands/inspect/redaction.ts +39 -0
  60. package/cli/commands/inspect/resolve-target.ts +77 -0
  61. package/cli/commands/inspect/screens.ts +78 -0
  62. package/cli/commands/inspect/settle.ts +32 -0
  63. package/cli/commands/inspect/settling.ts +197 -0
  64. package/cli/commands/inspect/shared.ts +259 -0
  65. package/cli/commands/inspect/sleep.ts +15 -0
  66. package/cli/commands/inspect/tree.ts +23 -0
  67. package/cli/commands/inspect/url.ts +17 -0
  68. package/cli/commands/inspect/wait-event.ts +211 -0
  69. package/cli/commands/inspect/wait-idle.ts +25 -0
  70. package/cli/commands/inspect/wait-ready.ts +87 -0
  71. package/cli/commands/inspect/wait-selector.ts +59 -0
  72. package/cli/commands/inspect/wait.ts +32 -0
  73. package/cli/commands/inspect.ts +4738 -0
  74. package/cli/commands/install-cli.ts +275 -0
  75. package/cli/commands/install-desktop.ts +385 -0
  76. package/cli/commands/login.ts +357 -0
  77. package/cli/commands/logout.ts +24 -0
  78. package/cli/commands/maestro-generate.ts +375 -0
  79. package/cli/commands/maestro.ts +536 -0
  80. package/cli/commands/mode.ts +58 -0
  81. package/cli/commands/no-bridge-hint.ts +80 -0
  82. package/cli/commands/perf.ts +67 -0
  83. package/cli/commands/permissions.ts +203 -0
  84. package/cli/commands/platform.ts +410 -0
  85. package/cli/commands/preview.ts +103 -0
  86. package/cli/commands/profile.ts +106 -0
  87. package/cli/commands/react.ts +353 -0
  88. package/cli/commands/record.ts +1435 -0
  89. package/cli/commands/report-issue.ts +305 -0
  90. package/cli/commands/reset.ts +89 -0
  91. package/cli/commands/runtime.ts +348 -0
  92. package/cli/commands/screenshot-command.ts +106 -0
  93. package/cli/commands/screenshot-layers.ts +144 -0
  94. package/cli/commands/screenshot-mode.ts +38 -0
  95. package/cli/commands/screenshot.ts +492 -0
  96. package/cli/commands/screenshots-capture.ts +608 -0
  97. package/cli/commands/screenshots.ts +127 -0
  98. package/cli/commands/serve.ts +139 -0
  99. package/cli/commands/setup.ts +416 -0
  100. package/cli/commands/shell-boolean-mode.ts +82 -0
  101. package/cli/commands/skills.ts +468 -0
  102. package/cli/commands/slides.ts +357 -0
  103. package/cli/commands/state.ts +94 -0
  104. package/cli/commands/storage.ts +58 -0
  105. package/cli/commands/telemetry.ts +55 -0
  106. package/cli/commands/test.ts +535 -0
  107. package/cli/commands/three-mode.ts +771 -0
  108. package/cli/commands/timeline.ts +121 -0
  109. package/cli/commands/upgrade.ts +167 -0
  110. package/cli/commands/upload.ts +1203 -0
  111. package/cli/commands/version.ts +63 -0
  112. package/cli/commands/welcome.ts +17 -0
  113. package/cli/commands/what-happened.ts +328 -0
  114. package/cli/current-sim.ts +217 -0
  115. package/cli/desktop-companion.ts +315 -0
  116. package/cli/drivers/electron.ts +70 -0
  117. package/cli/drivers/index.ts +20 -0
  118. package/cli/drivers/playwright-provisioning.ts +184 -0
  119. package/cli/drivers/playwright-sim-host.ts +537 -0
  120. package/cli/drivers/playwright.ts +209 -0
  121. package/cli/drivers/registry.ts +65 -0
  122. package/cli/drivers/types.ts +102 -0
  123. package/cli/flow-export.ts +55 -0
  124. package/cli/flow-file.ts +142 -0
  125. package/cli/flow-live-status.ts +126 -0
  126. package/cli/flow-session.ts +140 -0
  127. package/cli/help-core.ts +143 -0
  128. package/cli/help.ts +44 -0
  129. package/cli/hidden-runtime-alias.ts +19 -0
  130. package/cli/hints.ts +214 -0
  131. package/cli/inspect-notice-state.ts +114 -0
  132. package/cli/internal-child.ts +176 -0
  133. package/cli/maestro-js.ts +323 -0
  134. package/cli/main.ts +704 -0
  135. package/cli/open-url.ts +8 -0
  136. package/cli/outbound-endpoints.ts +830 -0
  137. package/cli/parent-pid.ts +204 -0
  138. package/cli/parse-args.ts +189 -0
  139. package/cli/privacy.ts +53 -0
  140. package/cli/prompt.ts +56 -0
  141. package/cli/recording-access.ts +108 -0
  142. package/cli/registry.ts +2 -0
  143. package/cli/resolve-assets.ts +63 -0
  144. package/cli/rnx-in-process.ts +98 -0
  145. package/cli/run-registry.ts +226 -0
  146. package/cli/run-rnx-core.ts +148 -0
  147. package/cli/run-rnx.ts +137 -0
  148. package/cli/runtime-notes.ts +66 -0
  149. package/cli/runtime-summary.ts +25 -0
  150. package/cli/secure-file.ts +31 -0
  151. package/cli/self-invocation.ts +58 -0
  152. package/cli/send-to-box.ts +109 -0
  153. package/cli/setup-repository.ts +191 -0
  154. package/cli/shell-init.ts +98 -0
  155. package/cli/standalone.ts +4 -0
  156. package/cli/telemetry.ts +84 -0
  157. package/cli/ui.ts +68 -0
  158. package/cli/ws-bridge.ts +1020 -0
  159. package/detox/attempt-events.cjs +23 -0
  160. package/detox/colors.ts +54 -0
  161. package/detox/config-loader.ts +135 -0
  162. package/detox/element-types.ts +48 -0
  163. package/detox/expectations.ts +534 -0
  164. package/detox/gestures.ts +501 -0
  165. package/detox/index.ts +2694 -0
  166. package/detox/jest-preset.cjs +29 -0
  167. package/detox/jest-setup-after-env.cjs +22 -0
  168. package/detox/matchers.ts +30 -0
  169. package/detox/motion-change.cjs +109 -0
  170. package/detox/motion-change.d.cts +35 -0
  171. package/detox/navigation-lifecycle.ts +290 -0
  172. package/detox/navigation.ts +43 -0
  173. package/detox/proof-frame.cjs +114 -0
  174. package/detox/record-block-execution.cjs +78 -0
  175. package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
  176. package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
  177. package/detox/screenshots/color-test-rest-norngh.png +0 -0
  178. package/detox/screenshots/theme-blue-toggle.png +0 -0
  179. package/detox/screenshots/theme-blue.png +0 -0
  180. package/detox/screenshots/theme-red-toggle.png +0 -0
  181. package/detox/screenshots/theme-red.png +0 -0
  182. package/dist-lib/agent-daemon-client.cjs +502 -0
  183. package/dist-lib/agent-events.cjs +48 -0
  184. package/dist-lib/agent-identity.cjs +53 -0
  185. package/dist-lib/agent-sessions.cjs +759 -0
  186. package/dist-lib/attached-projects.cjs +526 -0
  187. package/dist-lib/auth/shared-session.cjs +208 -0
  188. package/dist-lib/backend-origin.cjs +88 -0
  189. package/dist-lib/beta.cjs +41 -0
  190. package/dist-lib/beta.mjs +13 -0
  191. package/dist-lib/bridge-constants.cjs +161 -0
  192. package/dist-lib/bridge-contract-input.cjs +451 -0
  193. package/dist-lib/bridge-contract-input.mjs +414 -0
  194. package/dist-lib/bridge-contract.cjs +130 -0
  195. package/dist-lib/bridge-contract.mjs +94 -0
  196. package/dist-lib/capture-contract.cjs +220 -0
  197. package/dist-lib/capture-contract.mjs +190 -0
  198. package/dist-lib/cli-constants.cjs +32 -0
  199. package/dist-lib/cloud-contract.cjs +194 -0
  200. package/dist-lib/cloud-contract.mjs +145 -0
  201. package/dist-lib/cloud.cjs +670 -0
  202. package/dist-lib/cloud.mjs +639 -0
  203. package/dist-lib/config.cjs +96 -0
  204. package/dist-lib/detox/index.cjs +3789 -0
  205. package/dist-lib/detox/jest-preset.cjs +29 -0
  206. package/dist-lib/dev-bundle-resolution.cjs +290 -0
  207. package/dist-lib/home-paths.cjs +638 -0
  208. package/dist-lib/host/bridge-host.cjs +10903 -0
  209. package/dist-lib/host/fetch-proxy-handler.cjs +421 -0
  210. package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
  211. package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
  212. package/dist-lib/host/replacement-module-handler.cjs +469 -0
  213. package/dist-lib/host/websocket-proxy.cjs +298 -0
  214. package/dist-lib/index.cjs +4268 -0
  215. package/dist-lib/jump-to-source-babel.cjs +93 -0
  216. package/dist-lib/jump-to-source-native.cjs +112 -0
  217. package/dist-lib/menu.cjs +646 -0
  218. package/dist-lib/menu.mjs +604 -0
  219. package/dist-lib/metro-fingerprint-registry.cjs +186 -0
  220. package/dist-lib/metro-fingerprint-registry.mjs +156 -0
  221. package/dist-lib/metro-production-bundle.cjs +329 -0
  222. package/dist-lib/metro-production-bundle.mjs +295 -0
  223. package/dist-lib/metro.cjs +904 -0
  224. package/dist-lib/profiles.cjs +246 -0
  225. package/dist-lib/public-brand.cjs +39 -0
  226. package/dist-lib/react-native-host-modules.cjs +51 -0
  227. package/dist-lib/react-native-host-modules.mjs +25 -0
  228. package/dist-lib/render-mode.cjs +54 -0
  229. package/dist-lib/scripts/dev-server-scanner.cjs +1232 -0
  230. package/dist-lib/sdk.cjs +2433 -0
  231. package/dist-lib/sdk.mjs +2336 -0
  232. package/dist-lib/skills.cjs +39132 -0
  233. package/dist-lib/swift-submit.cjs +92 -0
  234. package/dist-lib/swift-submit.mjs +65 -0
  235. package/dist-lib/swift.cjs +1646 -0
  236. package/dist-lib/vite.cjs +1591 -0
  237. package/package.json +290 -0
  238. package/scripts/dev-server-scanner.ts +1291 -0
  239. package/scripts/optional-demo-registry.ts +113 -0
  240. package/skills/contrast/SKILL.md +165 -0
  241. package/skills/rnx-debug/SKILL.md +433 -0
  242. package/skills/rnx-perf/SKILL.md +399 -0
  243. package/skills/rnx-setup/SKILL.md +172 -0
  244. package/skills/rnx-test/SKILL.md +321 -0
  245. package/skills/rnx-visual/SKILL.md +126 -0
  246. package/src/agent-daemon-client.ts +410 -0
  247. package/src/agent-events.ts +71 -0
  248. package/src/agent-identity.ts +23 -0
  249. package/src/agent-prompt.ts +71 -0
  250. package/src/agent-sessions.ts +580 -0
  251. package/src/app-url.ts +39 -0
  252. package/src/attached-projects.ts +539 -0
  253. package/src/auth/shared-session.ts +235 -0
  254. package/src/backend-origin.ts +70 -0
  255. package/src/beta.ts +19 -0
  256. package/src/bridge-constants.ts +175 -0
  257. package/src/bridge-contract-input.ts +502 -0
  258. package/src/bridge-contract.ts +558 -0
  259. package/src/browser-cache.ts +16 -0
  260. package/src/capture-contract.ts +587 -0
  261. package/src/cli-constants.ts +1 -0
  262. package/src/cli-version.ts +40 -0
  263. package/src/cloud-contract.ts +274 -0
  264. package/src/cloud.ts +762 -0
  265. package/src/codex-client.ts +234 -0
  266. package/src/config.ts +132 -0
  267. package/src/connect.ts +101 -0
  268. package/src/dev-bundle-resolution.ts +346 -0
  269. package/src/dev-server-open.ts +198 -0
  270. package/src/disk-cleanup.ts +540 -0
  271. package/src/flow-timeout-scale.ts +15 -0
  272. package/src/home-paths.ts +806 -0
  273. package/src/host/agent-host.ts +666 -0
  274. package/src/host/bridge-host.ts +2788 -0
  275. package/src/host/fetch-proxy-handler.ts +449 -0
  276. package/src/host/fetch-proxy-overrides.ts +51 -0
  277. package/src/host/open-url.ts +89 -0
  278. package/src/host/plane-host.ts +203 -0
  279. package/src/host/replacement-module-handler.ts +158 -0
  280. package/src/host/websocket-proxy.ts +315 -0
  281. package/src/index.ts +71 -0
  282. package/src/jump-to-source-babel.ts +135 -0
  283. package/src/jump-to-source-native.ts +102 -0
  284. package/src/menu.ts +777 -0
  285. package/src/metro-fingerprint-registry.ts +302 -0
  286. package/src/metro-plugin.ts +341 -0
  287. package/src/metro-production-bundle.ts +471 -0
  288. package/src/native-dev-bundle-url.ts +118 -0
  289. package/src/native-seam-manifest.ts +341 -0
  290. package/src/playwright-host-log.ts +16 -0
  291. package/src/plist.ts +27 -0
  292. package/src/poll-until.ts +123 -0
  293. package/src/profiles.ts +179 -0
  294. package/src/public-brand.ts +9 -0
  295. package/src/react-native-host-modules.ts +26 -0
  296. package/src/render-mode.ts +27 -0
  297. package/src/rolldown-plugin-swift.ts +240 -0
  298. package/src/runtime-assets.ts +233 -0
  299. package/src/runtime-delivery.ts +54 -0
  300. package/src/screenshots/compose.ts +422 -0
  301. package/src/screenshots/frame-compose.ts +514 -0
  302. package/src/screenshots/orchestrate.ts +256 -0
  303. package/src/screenshots/org-deck.ts +91 -0
  304. package/src/screenshots/registry.ts +58 -0
  305. package/src/screenshots/schema.ts +364 -0
  306. package/src/sdk.ts +15 -0
  307. package/src/settings-bundle.ts +418 -0
  308. package/src/shell-bridge-identity.ts +15 -0
  309. package/src/sim-client.ts +697 -0
  310. package/src/skills/builtin/a11y-review.ts +132 -0
  311. package/src/skills/builtin/compat-check.ts +71 -0
  312. package/src/skills/builtin/maestro-test.ts +121 -0
  313. package/src/skills/builtin/perf-profile.ts +90 -0
  314. package/src/skills/builtin/screenshot-all.ts +52 -0
  315. package/src/skills/builtin/visual-diff.ts +100 -0
  316. package/src/skills/registry.ts +107 -0
  317. package/src/skills/types.ts +41 -0
  318. package/src/swift-submit.ts +185 -0
  319. package/src/swift.ts +7 -0
  320. package/src/team-machine-prompt.ts +73 -0
  321. package/src/vite-plugin-one.ts +264 -0
  322. package/src/vite-plugin-swift.ts +620 -0
  323. package/src/vite-plugin.ts +1946 -0
  324. package/src/worklets-babel.ts +250 -0
@@ -0,0 +1,82 @@
1
+ import { createRequire } from 'node:module'
2
+ import path from 'node:path'
3
+ import { isValidRuntimeVersion } from '@contrast/runtime-delivery'
4
+ import { createJiti } from 'jiti'
5
+ import { rnxPublicBrand } from '../src/public-brand'
6
+ import { resolveAppProject, type ResolvedAppProject } from './app-project'
7
+ import type { RNXConfig } from '../src/config'
8
+
9
+ const require = createRequire(import.meta.url)
10
+
11
+ function isOptionalRecord(value: unknown): boolean {
12
+ return (
13
+ value === undefined ||
14
+ (value !== null && typeof value === 'object' && !Array.isArray(value))
15
+ )
16
+ }
17
+
18
+ function isRNXConfig(value: unknown): value is RNXConfig {
19
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false
20
+ const runtimeVersion = Reflect.get(value, 'runtimeVersion')
21
+ const network = Reflect.get(value, 'network')
22
+ return (
23
+ (runtimeVersion === undefined ||
24
+ (typeof runtimeVersion === 'string' &&
25
+ runtimeVersion === runtimeVersion.trim() &&
26
+ isValidRuntimeVersion(runtimeVersion))) &&
27
+ isOptionalRecord(Reflect.get(value, 'modules')) &&
28
+ isOptionalRecord(Reflect.get(value, 'turboModules')) &&
29
+ isOptionalRecord(Reflect.get(value, 'nativeModules')) &&
30
+ isOptionalRecord(Reflect.get(value, 'env')) &&
31
+ isOptionalRecord(network) &&
32
+ isOptionalRecord(Reflect.get(value, 'settings')) &&
33
+ isOptionalRecord(Reflect.get(value, 'initialState'))
34
+ )
35
+ }
36
+
37
+ function resolveConfigReplacementFiles(config: RNXConfig, configPath: string): RNXConfig {
38
+ if (!config.modules) return config
39
+ const configDir = path.dirname(configPath)
40
+ return {
41
+ ...config,
42
+ modules: Object.fromEntries(
43
+ Object.entries(config.modules).map(([name, resolution]) => [
44
+ name,
45
+ resolution && typeof resolution === 'object' && 'file' in resolution
46
+ ? { ...resolution, file: path.resolve(configDir, resolution.file) }
47
+ : resolution,
48
+ ]),
49
+ ),
50
+ }
51
+ }
52
+
53
+ export async function loadRNXAppConfig(
54
+ project: ResolvedAppProject | null,
55
+ ): Promise<RNXConfig | undefined> {
56
+ if (!project?.configPath) return undefined
57
+ try {
58
+ const jiti = createJiti(import.meta.url, {
59
+ interopDefault: true,
60
+ alias: {
61
+ [`${rnxPublicBrand.packageName}/config`]: require.resolve(
62
+ `${rnxPublicBrand.packageName}/config`,
63
+ ),
64
+ },
65
+ })
66
+ const loaded: unknown = await jiti.import(project.configPath, { default: true })
67
+ if (!isRNXConfig(loaded)) {
68
+ throw new Error('the default export must be an rnx config object')
69
+ }
70
+ return resolveConfigReplacementFiles(loaded, project.configPath)
71
+ } catch (error) {
72
+ const message = error instanceof Error ? error.message : String(error)
73
+ throw new Error(`could not load ${project.configPath}: ${message}`)
74
+ }
75
+ }
76
+
77
+ export async function resolveRNXAppConfig(
78
+ startDir: string = process.cwd(),
79
+ ): Promise<{ project: ResolvedAppProject | null; config: RNXConfig | undefined }> {
80
+ const project = resolveAppProject(startDir)
81
+ return { project, config: await loadRNXAppConfig(project) }
82
+ }
@@ -0,0 +1,388 @@
1
+ import { createHash } from 'node:crypto'
2
+ import fs from 'node:fs'
3
+ import { createRequire } from 'node:module'
4
+ import path from 'node:path'
5
+ import {
6
+ expoFontPathsFromPlugins,
7
+ metroFontSpecsFromPaths,
8
+ type ConfigPluginFontSpec,
9
+ } from 'sootsim-engine/engine/config-plugin-fonts'
10
+ import { resolveListeningPid, resolveProcessCwd } from '../scripts/dev-server-scanner'
11
+ import { parsePlistSource } from '../src/plist.ts'
12
+ import { resolveAppProject } from './app-project'
13
+
14
+ const require = createRequire(import.meta.url)
15
+
16
+ // the filesystem scan and the engine's metro-manifest scan share the same spec
17
+ // shape and the same path → spec logic (config-plugin-fonts.ts).
18
+ export type DiscoveredAppFontSpec = ConfigPluginFontSpec
19
+
20
+ type ExpoConfigGetter = (dir: string, opts: Record<string, unknown>) => { exp?: unknown }
21
+
22
+ function localOriginOf(value: string): string | null {
23
+ try {
24
+ const url = new URL(value)
25
+ const host = url.hostname.replace(/^\[|\]$/g, '').toLowerCase()
26
+ if (
27
+ host === 'localhost' ||
28
+ host.endsWith('.localhost') ||
29
+ host === '0.0.0.0' ||
30
+ host === '::1' ||
31
+ /^127(?:\.\d{1,3}){3}$/.test(host)
32
+ ) {
33
+ return url.origin
34
+ }
35
+ } catch {}
36
+ return null
37
+ }
38
+
39
+ export function discoverNativeLinkedAppFonts(opts: {
40
+ bundleUrl: string
41
+ projectDir: string
42
+ platform?: string
43
+ }): DiscoveredAppFontSpec[] {
44
+ const metroOrigin = localOriginOf(opts.bundleUrl)
45
+ if (!metroOrigin) return []
46
+
47
+ const project = resolveAppProject(opts.projectDir)
48
+ const selectedAppDir = project?.appDir ?? null
49
+ if (!selectedAppDir) return []
50
+
51
+ const platform = (opts.platform || process.env.CONTRAST_PLATFORM || 'ios').toLowerCase()
52
+
53
+ let getConfig: ExpoConfigGetter | null = null
54
+ // resolve from the exact app root advertised by the development server.
55
+ if (project) {
56
+ try {
57
+ const modulePath = require.resolve('@expo/config', {
58
+ paths: [selectedAppDir],
59
+ })
60
+ const mod = require(modulePath) as { getConfig?: unknown }
61
+ if (typeof mod.getConfig === 'function') {
62
+ getConfig = mod.getConfig as ExpoConfigGetter
63
+ }
64
+ } catch {}
65
+ }
66
+ let plugins: unknown = []
67
+ if (getConfig) {
68
+ try {
69
+ const result = getConfig(selectedAppDir, { skipSDKVersionRequirement: true })
70
+ if (result?.exp && typeof result.exp === 'object') {
71
+ const resolvedPlugins = (result.exp as Record<string, unknown>).plugins
72
+ if (Array.isArray(resolvedPlugins)) {
73
+ plugins = resolvedPlugins
74
+ }
75
+ }
76
+ } catch {}
77
+ }
78
+
79
+ const fontPaths = expoFontPathsFromPlugins(
80
+ plugins,
81
+ platform === 'android' ? 'android' : 'ios',
82
+ ).slice(0, 256)
83
+ // the walk is bounded by WORK, not by a clock: at most 64 readdirs, at most 64
84
+ // queued dirs, 4 levels deep, 256 paths. those ceilings are what keep a stray
85
+ // node_modules from being crawled, and no machine can blow through them. a
86
+ // wall-clock budget on top of that only makes the RESULT depend on how busy
87
+ // the box is, and silently — a user on a loaded machine would get fewer fonts
88
+ // and no error at all.
89
+ let scannedFontDirectories = 0
90
+
91
+ const collectFontFiles = (root: string, metroPathPrefix: string): void => {
92
+ const pending: Array<{ dir: string; depth: number }> = [{ dir: root, depth: 0 }]
93
+ let nextIndex = 0
94
+ while (
95
+ nextIndex < pending.length &&
96
+ scannedFontDirectories < 64 &&
97
+ fontPaths.length < 256
98
+ ) {
99
+ const current = pending[nextIndex++]
100
+ if (!current) continue
101
+ scannedFontDirectories++
102
+ let entries: fs.Dirent[]
103
+ try {
104
+ entries = fs.readdirSync(current.dir, { withFileTypes: true })
105
+ } catch {
106
+ continue
107
+ }
108
+ for (const entry of entries) {
109
+ if (entry.isSymbolicLink()) continue
110
+ const fullPath = path.join(current.dir, entry.name)
111
+ if (entry.isDirectory()) {
112
+ if (current.depth < 4 && pending.length < 64 && !entry.name.startsWith('.')) {
113
+ pending.push({ dir: fullPath, depth: current.depth + 1 })
114
+ }
115
+ continue
116
+ }
117
+ if (!entry.isFile() || !/\.(ttf|otf|woff|woff2)$/i.test(entry.name)) continue
118
+ const relative = path.relative(root, fullPath).split(path.sep).join('/')
119
+ fontPaths.push(path.posix.join(metroPathPrefix, relative))
120
+ if (fontPaths.length >= 256) break
121
+ }
122
+ }
123
+ }
124
+
125
+ // @react-native-vector-icons links fonts from its configured local font
126
+ // directory and each installed icon package into the native app. those files
127
+ // never enter the metro module graph, so rnx must stage them through the
128
+ // same native-linked font wire as expo-font config-plugin assets. Font Awesome
129
+ // Pro installs its licensed faces under the local directory rather than
130
+ // publishing them in the npm package.
131
+ try {
132
+ const packageJson: object = JSON.parse(
133
+ fs.readFileSync(path.join(selectedAppDir, 'package.json'), 'utf8'),
134
+ )
135
+ const dependencyNames = new Set<string>()
136
+ for (const field of ['dependencies', 'devDependencies', 'optionalDependencies']) {
137
+ const dependencies = Reflect.get(packageJson, field)
138
+ if (!dependencies || typeof dependencies !== 'object') continue
139
+ for (const name of Object.keys(dependencies)) dependencyNames.add(name)
140
+ }
141
+ const vectorIconDependencies = [...dependencyNames].filter(
142
+ (name) =>
143
+ name === 'react-native-vector-icons' ||
144
+ name.startsWith('@react-native-vector-icons/'),
145
+ )
146
+ if (vectorIconDependencies.length > 0) {
147
+ const config = Reflect.get(packageJson, 'reactNativeVectorIcons')
148
+ const configuredFontDir =
149
+ config && typeof config === 'object' ? Reflect.get(config, 'fontDir') : null
150
+ const configuredDir =
151
+ typeof configuredFontDir === 'string' ? configuredFontDir : 'rnvi-fonts'
152
+ const root = path.resolve(selectedAppDir, configuredDir)
153
+ const relativeRoot = path.relative(selectedAppDir, root)
154
+ if (!relativeRoot.startsWith('..') && !path.isAbsolute(relativeRoot)) {
155
+ collectFontFiles(root, relativeRoot.split(path.sep).join('/'))
156
+ }
157
+
158
+ const vectorIconRequire = createRequire(path.join(selectedAppDir, 'package.json'))
159
+ for (const dependencyName of vectorIconDependencies) {
160
+ try {
161
+ const entryPath = vectorIconRequire.resolve(dependencyName)
162
+ const dependencyPath = dependencyName.split('/').join(path.sep)
163
+ const marker = `${path.sep}node_modules${path.sep}${dependencyPath}${path.sep}`
164
+ const markerIndex = entryPath.lastIndexOf(marker)
165
+ if (markerIndex === -1) continue
166
+ const packageRoot = entryPath.slice(0, markerIndex + marker.length - 1)
167
+ const fontsDir =
168
+ dependencyName === 'react-native-vector-icons' ? 'Fonts' : 'fonts'
169
+ collectFontFiles(
170
+ path.join(packageRoot, fontsDir),
171
+ path.posix.join('node_modules', dependencyName, fontsDir),
172
+ )
173
+ } catch {}
174
+ }
175
+ }
176
+ } catch {}
177
+
178
+ // react-native.config.js `assets` is the RN CLI's native asset-linking
179
+ // source: `npx react-native-asset` copies every font in those directories
180
+ // into the app bundle and Info.plist UIAppFonts, and the JS never requires
181
+ // the files, so they enter no module graph. apps like mattermost register
182
+ // icon sets (createIconSetFromFontello) purely by family name against a
183
+ // font linked this way.
184
+ try {
185
+ const configPath = path.join(selectedAppDir, 'react-native.config.js')
186
+ if (fs.statSync(configPath).isFile()) {
187
+ const appConfigRequire = createRequire(path.join(selectedAppDir, 'package.json'))
188
+ const rnConfig: unknown = appConfigRequire(configPath)
189
+ const assets =
190
+ rnConfig && typeof rnConfig === 'object' ? Reflect.get(rnConfig, 'assets') : null
191
+ if (Array.isArray(assets)) {
192
+ for (const assetDir of assets) {
193
+ if (typeof assetDir !== 'string') continue
194
+ const root = path.resolve(selectedAppDir, assetDir)
195
+ const relativeRoot = path.relative(selectedAppDir, root)
196
+ if (relativeRoot.startsWith('..') || path.isAbsolute(relativeRoot)) continue
197
+ collectFontFiles(root, relativeRoot.split(path.sep).join('/'))
198
+ }
199
+ }
200
+ }
201
+ } catch {}
202
+
203
+ // bare iOS projects can embed fonts directly in an Xcode target without a
204
+ // react-native.config.js assets declaration. UIAppFonts is the native
205
+ // contract, so stage those referenced files through Metro's asset endpoint.
206
+ if (platform === 'ios') {
207
+ const iosDir = path.join(selectedAppDir, 'ios')
208
+ try {
209
+ const targetDirs = fs
210
+ .readdirSync(iosDir, { withFileTypes: true })
211
+ .filter(
212
+ (entry) =>
213
+ entry.isDirectory() &&
214
+ !entry.isSymbolicLink() &&
215
+ entry.name !== 'Pods' &&
216
+ entry.name !== 'build' &&
217
+ !entry.name.startsWith('.'),
218
+ )
219
+ .slice(0, 64)
220
+ const plistPaths = [path.join(iosDir, 'Info.plist')]
221
+ for (const entry of targetDirs) {
222
+ plistPaths.push(path.join(iosDir, entry.name, 'Info.plist'))
223
+ }
224
+ for (const plistPath of plistPaths) {
225
+ let parsed: unknown
226
+ try {
227
+ parsed = parsePlistSource(fs.readFileSync(plistPath, 'utf8'))
228
+ } catch {
229
+ continue
230
+ }
231
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) continue
232
+ const declaredFonts = Reflect.get(parsed, 'UIAppFonts')
233
+ if (!Array.isArray(declaredFonts)) continue
234
+ for (const declaredFont of declaredFonts) {
235
+ if (typeof declaredFont !== 'string') continue
236
+ const candidates = [
237
+ path.resolve(path.dirname(plistPath), declaredFont),
238
+ path.resolve(iosDir, declaredFont),
239
+ ]
240
+ const fontPath = candidates.find((candidate) => {
241
+ const relative = path.relative(selectedAppDir, candidate)
242
+ if (relative.startsWith('..') || path.isAbsolute(relative)) return false
243
+ try {
244
+ return fs.statSync(candidate).isFile()
245
+ } catch {
246
+ return false
247
+ }
248
+ })
249
+ if (!fontPath) continue
250
+ fontPaths.push(
251
+ path.relative(selectedAppDir, fontPath).split(path.sep).join('/'),
252
+ )
253
+ }
254
+ }
255
+ } catch {}
256
+ }
257
+
258
+ return metroFontSpecsFromPaths(fontPaths, metroOrigin)
259
+ }
260
+
261
+ // resolve a metro dev server's project root. expo serves a manifest at the
262
+ // server origin carrying `extra.expoClient._internal.projectRoot` — the absolute
263
+ // dir of the app being served. the local `rnx open <port>` path runs from the
264
+ // CLI's cwd, which almost never owns the bundle being opened (e.g.
265
+ // `rnx open 8081` run from ~/soot against a 3pc metro), so scanning cwd for
266
+ // font declarations finds nothing.
267
+ //
268
+ // a bare react-native packager serves no such manifest, so the same resolver
269
+ // asks the OS for the listening process's cwd. that is still the development
270
+ // server's project identity; the invoking CLI's cwd is never consulted.
271
+ export async function resolveMetroProjectRoot(
272
+ bundleUrl: string,
273
+ opts?: { timeoutMs?: number },
274
+ ): Promise<string | null> {
275
+ const origin = localOriginOf(bundleUrl)
276
+ if (!origin) return null
277
+ const controller = new AbortController()
278
+ const timer = setTimeout(() => controller.abort(), opts?.timeoutMs ?? 1500)
279
+ let servedManifest = false
280
+ try {
281
+ const res = await fetch(`${origin}/`, {
282
+ headers: { 'expo-platform': 'ios', Accept: 'application/json' },
283
+ signal: controller.signal,
284
+ })
285
+ if (res.ok) {
286
+ const manifest = (await res.json()) as {
287
+ extra?: { expoClient?: { _internal?: { projectRoot?: unknown } } }
288
+ }
289
+ servedManifest = true
290
+ const root = manifest?.extra?.expoClient?._internal?.projectRoot
291
+ if (typeof root === 'string' && root.length > 0) return root
292
+ }
293
+ } catch {
294
+ } finally {
295
+ clearTimeout(timer)
296
+ }
297
+
298
+ // a server that answered with a manifest has already told us everything it
299
+ // knows. if it did not name a project root there is none to learn, and asking
300
+ // the OS about the listening process would answer with whatever directory
301
+ // that process happens to sit in — which is the CLI's own cwd trap this
302
+ // function exists to avoid. only a server that served no manifest at all gets
303
+ // the process lookup.
304
+ if (servedManifest) return null
305
+
306
+ const port = Number(new URL(origin).port)
307
+ if (!Number.isFinite(port) || port <= 0) return null
308
+ const pid = await resolveListeningPid(port)
309
+ return pid === null ? null : resolveProcessCwd(pid)
310
+ }
311
+
312
+ export function encodeAppFontsWire(specs: DiscoveredAppFontSpec[]): string {
313
+ return specs
314
+ .map((spec) => `${encodeURIComponent(spec.url)}::${encodeURIComponent(spec.family)}`)
315
+ .join(',')
316
+ }
317
+
318
+ export function parseAppFontUrls(wire: string): string[] {
319
+ const urls: string[] = []
320
+ for (const entry of wire.split(',')) {
321
+ const trimmed = entry.trim()
322
+ if (!trimmed) continue
323
+ const sep = trimmed.indexOf('::')
324
+ const urlPart = sep === -1 ? trimmed : trimmed.slice(0, sep)
325
+ try {
326
+ urls.push(decodeURIComponent(urlPart))
327
+ } catch {
328
+ urls.push(urlPart)
329
+ }
330
+ }
331
+ return urls.filter(Boolean)
332
+ }
333
+
334
+ // content type for a config-plugin font url, by extension. config fonts only
335
+ // ever ship as ttf/otf/woff(2); fall back to octet-stream for anything else
336
+ // so R2 still serves bytes the engine can register.
337
+ export function fontContentType(url: string): string {
338
+ const ext = (url.split('?')[0].split('.').pop() || '').toLowerCase()
339
+ if (ext === 'ttf') return 'font/ttf'
340
+ if (ext === 'otf') return 'font/otf'
341
+ if (ext === 'woff') return 'font/woff'
342
+ if (ext === 'woff2') return 'font/woff2'
343
+ return 'application/octet-stream'
344
+ }
345
+
346
+ export type FetchedAppFont = {
347
+ url: string
348
+ // sha256(url) — the share-store key both the build and preview upload paths
349
+ // use, and what /api/preview/share/fetch?url=<fontUrl> resolves against.
350
+ urlhash: string
351
+ contentType: string
352
+ bytes: Uint8Array
353
+ }
354
+
355
+ // fetch every config-plugin font declared on an engine `?appFonts=` wire.
356
+ // these typefaces live in the native binary, never in the metro bundle's
357
+ // __packager_asset graph, so neither asset capture nor recorded fetches see
358
+ // them — both the preview (`rnx record upload`) and build (`rnx app-fonts
359
+ // stage`) paths backfill by re-fetching the declared urls off the still-live
360
+ // metro. one implementation so both pipelines store identical bytes + keys.
361
+ export async function fetchAppFontFiles(
362
+ wire: string,
363
+ hooks?: {
364
+ onStaged?: (info: { url: string; byteLength: number }) => void
365
+ onError?: (url: string, err: unknown) => void
366
+ },
367
+ ): Promise<FetchedAppFont[]> {
368
+ const out: FetchedAppFont[] = []
369
+ const seen = new Set<string>()
370
+ for (const url of parseAppFontUrls(wire)) {
371
+ const urlhash = createHash('sha256').update(url).digest('hex')
372
+ if (seen.has(urlhash)) continue
373
+ seen.add(urlhash)
374
+ try {
375
+ const res = await fetch(url)
376
+ if (!res.ok) {
377
+ hooks?.onError?.(url, new Error(`${res.status} ${res.statusText}`))
378
+ continue
379
+ }
380
+ const bytes = new Uint8Array(await res.arrayBuffer())
381
+ out.push({ url, urlhash, contentType: fontContentType(url), bytes })
382
+ hooks?.onStaged?.({ url, byteLength: bytes.byteLength })
383
+ } catch (err) {
384
+ hooks?.onError?.(url, err)
385
+ }
386
+ }
387
+ return out
388
+ }
@@ -0,0 +1,59 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ export const EXPO_APP_CONFIG_NAMES = [
5
+ 'app.config.js',
6
+ 'app.config.ts',
7
+ 'app.config.cjs',
8
+ 'app.config.mjs',
9
+ 'app.json',
10
+ ] as const
11
+
12
+ export const RNX_APP_CONFIG_NAMES = [
13
+ 'rnx.config.ts',
14
+ 'rnx.config.js',
15
+ 'rnx.config.mjs',
16
+ 'rnx.config.cjs',
17
+ ] as const
18
+
19
+ export interface ResolvedAppProject {
20
+ appDir: string
21
+ configPath: string | null
22
+ }
23
+
24
+ function firstNamedFile(dir: string, names: readonly string[]): string | null {
25
+ for (const name of names) {
26
+ const candidate = path.join(dir, name)
27
+ try {
28
+ if (fs.statSync(candidate).isFile()) return candidate
29
+ } catch {}
30
+ }
31
+ return null
32
+ }
33
+
34
+ export function findExpoAppConfig(dir: string): string | null {
35
+ return firstNamedFile(dir, EXPO_APP_CONFIG_NAMES)
36
+ }
37
+
38
+ export function findRNXAppConfig(dir: string): string | null {
39
+ return firstNamedFile(dir, RNX_APP_CONFIG_NAMES)
40
+ }
41
+
42
+ export function resolveAppProject(projectRoot: string): ResolvedAppProject | null {
43
+ const appDir = path.resolve(projectRoot)
44
+ try {
45
+ if (!fs.statSync(appDir).isDirectory()) return null
46
+ } catch {
47
+ return null
48
+ }
49
+
50
+ const configPath = findRNXAppConfig(appDir)
51
+ if (
52
+ !configPath &&
53
+ !findExpoAppConfig(appDir) &&
54
+ !fs.existsSync(path.join(appDir, 'package.json'))
55
+ ) {
56
+ return null
57
+ }
58
+ return { appDir, configPath }
59
+ }