peachsim 0.1.656 → 0.1.658

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 (111) hide show
  1. package/README.md +22 -3
  2. package/cli/app-config.ts +34 -15
  3. package/cli/app-project.ts +6 -6
  4. package/cli/bridge-flow-runner.ts +10 -23
  5. package/cli/commands/compat.ts +234 -10
  6. package/cli/commands/control.ts +20 -20
  7. package/cli/commands/do-chain.ts +5 -1
  8. package/cli/commands/flow.ts +3 -3
  9. package/cli/commands/inspect/actions.ts +33 -76
  10. package/cli/commands/inspect/resolve-target.ts +67 -2
  11. package/cli/commands/inspect.ts +16 -1
  12. package/cli/drivers/playwright-provisioning.ts +95 -0
  13. package/cli/ws-bridge.ts +7 -7
  14. package/detox/config-loader.ts +1 -1
  15. package/dist-lib/agent-daemon-client.cjs +1 -1
  16. package/dist-lib/agent-events.cjs +1 -1
  17. package/dist-lib/agent-identity.cjs +1 -1
  18. package/dist-lib/agent-sessions.cjs +1 -1
  19. package/dist-lib/attached-projects.cjs +1 -1
  20. package/dist-lib/auth/shared-session.cjs +1 -1
  21. package/dist-lib/backend-origin.cjs +1 -1
  22. package/dist-lib/beta.cjs +1 -1
  23. package/dist-lib/beta.mjs +1 -1
  24. package/dist-lib/bridge-constants.cjs +1 -1
  25. package/dist-lib/bridge-contract-input.cjs +9 -1
  26. package/dist-lib/bridge-contract-input.mjs +9 -1
  27. package/dist-lib/bridge-contract.cjs +1 -1
  28. package/dist-lib/bridge-contract.mjs +1 -1
  29. package/dist-lib/capture-contract.cjs +1 -1
  30. package/dist-lib/capture-contract.mjs +1 -1
  31. package/dist-lib/cli-constants.cjs +1 -1
  32. package/dist-lib/cloud-contract.cjs +1 -1
  33. package/dist-lib/cloud-contract.mjs +1 -1
  34. package/dist-lib/cloud-test-args.cjs +1 -1
  35. package/dist-lib/cloud-test-args.mjs +1 -1
  36. package/dist-lib/cloud.cjs +1 -1
  37. package/dist-lib/cloud.mjs +1 -1
  38. package/dist-lib/config.cjs +27 -26
  39. package/dist-lib/detox/index.cjs +1 -1
  40. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  41. package/dist-lib/home-paths.cjs +1 -1
  42. package/dist-lib/host/bridge-host.cjs +90 -47
  43. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  44. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  45. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  46. package/dist-lib/host/replacement-module-handler.cjs +51 -8
  47. package/dist-lib/host/websocket-proxy.cjs +1 -1
  48. package/dist-lib/index.cjs +317 -70
  49. package/dist-lib/jump-to-source-babel.cjs +1 -1
  50. package/dist-lib/jump-to-source-native.cjs +1 -1
  51. package/dist-lib/kotlin-host.cjs +8 -4
  52. package/dist-lib/menu.cjs +1 -1
  53. package/dist-lib/menu.mjs +1 -1
  54. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  55. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  56. package/dist-lib/metro-production-bundle.cjs +1 -1
  57. package/dist-lib/metro-production-bundle.mjs +1 -1
  58. package/dist-lib/metro.cjs +300 -29
  59. package/dist-lib/native-app-identity.cjs +58 -25
  60. package/dist-lib/native-app-identity.mjs +57 -25
  61. package/dist-lib/plist.cjs +64 -0
  62. package/dist-lib/plist.mjs +29 -0
  63. package/dist-lib/profiles.cjs +1 -1
  64. package/dist-lib/public-brand.cjs +1 -1
  65. package/dist-lib/react-native-host-modules.cjs +1 -1
  66. package/dist-lib/react-native-host-modules.mjs +1 -1
  67. package/dist-lib/render-mode.cjs +1 -1
  68. package/dist-lib/scripts/dev-server-scanner.cjs +14 -16
  69. package/dist-lib/sdk.cjs +36 -49
  70. package/dist-lib/sdk.mjs +36 -49
  71. package/dist-lib/skills.cjs +341 -198
  72. package/dist-lib/swift-compile.cjs +1 -1
  73. package/dist-lib/swift-compile.mjs +1 -1
  74. package/dist-lib/swift-package.cjs +1 -1
  75. package/dist-lib/swift-package.mjs +1 -1
  76. package/dist-lib/swift-submit.cjs +1 -1
  77. package/dist-lib/swift-submit.mjs +1 -1
  78. package/dist-lib/swift.cjs +1 -1
  79. package/dist-lib/vite-swift.cjs +1 -1
  80. package/dist-lib/vite.cjs +51 -8
  81. package/kotlin/AppSources/android/app/UiModeManager.kt +9 -0
  82. package/kotlin/AppSources/android/content/Context.kt +9 -2
  83. package/kotlin/AppSources/android/content/res/Configuration.kt +16 -0
  84. package/kotlin/AppSources/android/content/res/Resources.kt +7 -0
  85. package/kotlin/AppSources/android/graphics/Color.kt +13 -0
  86. package/kotlin/AppSources/android/view/View.kt +2 -0
  87. package/kotlin/AppSources/android/view/Window.kt +99 -8
  88. package/kotlin/AppSources/android/view/WindowInsetsController.kt +12 -0
  89. package/kotlin/AppSources/android/view/WindowManager.kt +16 -0
  90. package/kotlin/AppSources/androidx/activity/EdgeToEdge.kt +71 -0
  91. package/kotlin/AppSources/androidx/core/view/WindowCompat.kt +32 -0
  92. package/kotlin/AppSources/dev/peach/app/PeachApp.kt +16 -11
  93. package/kotlin/AppSources/dev/peach/app/PeachWindowHost.kt +2 -1
  94. package/kotlin/ComposeSources/PeachComposeHost.kt +11 -1
  95. package/package.json +8 -1
  96. package/scripts/dev-server-scanner.ts +3 -3
  97. package/scripts/optional-demo-registry.ts +1 -1
  98. package/skills/peach-setup/SKILL.md +27 -0
  99. package/src/android-theme.ts +167 -0
  100. package/src/android-xml.ts +70 -0
  101. package/src/bridge-contract-input.ts +10 -1
  102. package/src/bridge-contract.ts +7 -1
  103. package/src/config.ts +34 -26
  104. package/src/generated/android-framework-themes.json +6303 -0
  105. package/src/host/replacement-module-handler.ts +33 -0
  106. package/src/index.ts +1 -0
  107. package/src/kotlin-app.ts +75 -61
  108. package/src/kotlin-host.ts +7 -3
  109. package/src/metro-plugin.ts +46 -3
  110. package/src/native-app-identity.ts +75 -26
  111. package/src/sim-client.ts +2 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peachsim",
3
- "version": "0.1.656",
3
+ "version": "0.1.658",
4
4
  "description": "Vite and Metro plugins, testing drivers, and SDK exports for peach.",
5
5
  "author": "Tamagui LLC",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -84,6 +84,13 @@
84
84
  "import": "./dist-lib/metro-production-bundle.mjs",
85
85
  "default": "./dist-lib/metro-production-bundle.cjs"
86
86
  },
87
+ "./plist": {
88
+ "types": "./src/plist.ts",
89
+ "source": "./src/plist.ts",
90
+ "bun": "./src/plist.ts",
91
+ "import": "./dist-lib/plist.mjs",
92
+ "default": "./dist-lib/plist.cjs"
93
+ },
87
94
  "./native-app-identity": {
88
95
  "types": "./src/native-app-identity.ts",
89
96
  "source": "./src/native-app-identity.ts",
@@ -14,19 +14,19 @@
14
14
  // pid changes. before any HTTP, a TCP connect gate skips unreachable ports.
15
15
 
16
16
  import { spawn } from 'child_process'
17
- import { terminateProcessGroup } from '../../contrast/src/runtime/process-group'
18
17
  import { readlink } from 'fs/promises'
19
18
  import http from 'http'
20
19
  import net from 'net'
21
20
  import { realpathSync } from 'node:fs'
22
21
  import { resolve } from 'node:path'
22
+ import { terminateProcessGroup } from '../../contrast/src/runtime/process-group'
23
23
  import {
24
24
  buildLoopbackProbeBaseUrl,
25
25
  isLoopbackHost,
26
26
  LOOPBACK_PROBE_HOSTNAMES,
27
27
  normalizeHostname,
28
28
  } from '../src/backend-origin.ts'
29
- import { applyRNXConfigToUrl } from '../src/config.ts'
29
+ import { applyPeachConfigToUrl } from '../src/config.ts'
30
30
  import {
31
31
  alignLoopbackBundleUrlWithProbe,
32
32
  normalizeNativeDevBundleUrl,
@@ -611,7 +611,7 @@ function withRuntimeConfig(port: number, bundleUrl: string, projectCwd?: string)
611
611
  ? knownApps[0]
612
612
  : undefined
613
613
  const configured = knownApp?.runtimeConfig
614
- ? applyRNXConfigToUrl(bundleUrl, knownApp.runtimeConfig)
614
+ ? applyPeachConfigToUrl(bundleUrl, knownApp.runtimeConfig)
615
615
  : bundleUrl
616
616
  return normalizeNativeDevBundleUrl(configured)
617
617
  }
@@ -63,7 +63,7 @@ export interface OptionalDemoApp {
63
63
  dir: string
64
64
  preferredPort: number
65
65
  framework: 'expo' | 'one' | 'rock'
66
- runtimeConfig?: import('../src/config.ts').RNXConfig
66
+ runtimeConfig?: import('../src/config.ts').PeachConfig
67
67
  }
68
68
 
69
69
  let cached: OptionalDemoApp[] | null = null
@@ -113,6 +113,33 @@ peach open https://my-app.local # load a hosted bundle
113
113
  - the compatibility scan's partial, unsupported, unknown, and version-mismatched
114
114
  packages have been summarized without treating its aggregate score as a pass/fail gate
115
115
 
116
+ ## the app's own native code
117
+
118
+ an app's own native modules and views (a local Expo module, a
119
+ `requireNativeComponent` view written in Swift or Kotlin) have no Peach
120
+ implementation until the app supplies one. when the scan or the running app
121
+ shows one missing (an empty view where content belongs, a "native module not
122
+ found" error), add it to `peach.config.ts` at the app root:
123
+
124
+ ```ts
125
+ import { defineConfig } from 'peachsim/config'
126
+
127
+ export default defineConfig({
128
+ nativeModules: { MyAppModule: { file: './peach/my-app-module.ts' } },
129
+ nativeViews: {
130
+ RNBalanceLabel: { file: './peach/BalanceLabel.tsx' }, // requireNativeComponent name
131
+ MyModule_StatusView: { file: './peach/StatusView.tsx' }, // requireNativeView('MyModule', 'StatusView')
132
+ },
133
+ })
134
+ ```
135
+
136
+ a `nativeViews` file default-exports a component that receives the native
137
+ view's props and renders what it shows on device, using `react-native` and
138
+ `contrast-kit` primitives. a `nativeModules` file exports the module's
139
+ methods, served to `NativeModules` and to Expo's `requireNativeModule`. a
140
+ file may import other project files by relative path, so a large view splits
141
+ into components and helpers. reopen the app to load the change.
142
+
116
143
  ## compatibility feedback requires approval
117
144
 
118
145
  run `peach compat --json` from the app project root during setup. if the scan
@@ -0,0 +1,167 @@
1
+ // android theme resolution for a pure Kotlin app's window: app styles from the
2
+ // module's res/values over the device's framework styles, resolved the way
3
+ // Resources.Theme does (explicit or dotted parents, `?attr` references,
4
+ // `@color` references). the framework half is generated from the platform's
5
+ // own res/values (scripts/generate/android-framework-themes.ts).
6
+
7
+ import { parseXml } from './android-xml'
8
+
9
+ export interface StyleValues {
10
+ parent: string | null
11
+ items: Record<string, string>
12
+ }
13
+
14
+ // a values namespace: the framework's (`android:`) or the app's
15
+ export interface ResourceValues {
16
+ styles: Record<string, StyleValues>
17
+ colors: Record<string, string>
18
+ }
19
+
20
+ export function readResourceValues(
21
+ files: Array<{ file: string; text: string }>,
22
+ ): ResourceValues {
23
+ const values: ResourceValues = { styles: {}, colors: {} }
24
+ for (const { file, text } of files) {
25
+ for (const resources of parseXml(text, file).children) {
26
+ if (resources.name !== 'resources') continue
27
+ for (const entry of resources.children) {
28
+ const name = entry.attributes.name
29
+ if (!name) continue
30
+ if (entry.name === 'color') values.colors[name] = entry.text.trim()
31
+ if (entry.name !== 'style') continue
32
+ const items: Record<string, string> = {}
33
+ for (const item of entry.children)
34
+ if (item.name === 'item' && item.attributes.name)
35
+ items[item.attributes.name] = item.text.trim()
36
+ values.styles[name] = { parent: entry.attributes.parent ?? null, items }
37
+ }
38
+ }
39
+ }
40
+ return values
41
+ }
42
+
43
+ type Namespace = 'android' | 'app'
44
+ interface StyleRef {
45
+ namespace: Namespace
46
+ name: string
47
+ }
48
+
49
+ export interface ThemeSources {
50
+ android: ResourceValues
51
+ app: ResourceValues
52
+ }
53
+
54
+ // a reference written in `namespace`'s files: `@style/X`, `@android:style/X`
55
+ // or a bare `android:X` parent
56
+ function styleRef(value: string, namespace: Namespace): StyleRef {
57
+ const match = /^@?(?:(android):)?(?:style\/)?(.+)$/.exec(value)
58
+ if (!match?.[2]) throw new Error(`unreadable style reference ${value}`)
59
+ return { namespace: match[1] ? 'android' : namespace, name: match[2] }
60
+ }
61
+
62
+ // a theme's styles from itself to its root, as Resources.Theme applies them
63
+ function styleChain(
64
+ theme: StyleRef,
65
+ sources: ThemeSources,
66
+ ): Array<StyleRef & StyleValues> {
67
+ const chain: Array<StyleRef & StyleValues> = []
68
+ let current: StyleRef | null = theme
69
+ while (current) {
70
+ const style: StyleValues | undefined = sources[current.namespace].styles[current.name]
71
+ if (!style)
72
+ throw new Error(
73
+ `${current.namespace === 'android' ? 'android:' : ''}style/${current.name} is not defined`,
74
+ )
75
+ chain.push({ ...current, ...style })
76
+ if (style.parent !== null)
77
+ current = style.parent ? styleRef(style.parent, current.namespace) : null
78
+ else {
79
+ // a dotted name inherits its prefix in the same namespace
80
+ const dot = current.name.lastIndexOf('.')
81
+ current =
82
+ dot > 0
83
+ ? { namespace: current.namespace, name: current.name.slice(0, dot) }
84
+ : null
85
+ }
86
+ }
87
+ return chain
88
+ }
89
+
90
+ // an attribute as items name it in `namespace`'s files: framework files write
91
+ // bare names, app files write `android:` for framework attributes
92
+ function attributeKey(attribute: string, namespace: Namespace): string {
93
+ if (namespace === 'android')
94
+ return attribute.startsWith('android:') ? attribute.slice(8) : attribute
95
+ return attribute
96
+ }
97
+
98
+ function lookup(
99
+ chain: Array<StyleRef & StyleValues>,
100
+ attribute: { namespace: Namespace; name: string },
101
+ ): { value: string; namespace: Namespace } | null {
102
+ for (const style of chain) {
103
+ for (const [key, value] of Object.entries(style.items)) {
104
+ const keyNamespace: Namespace =
105
+ style.namespace === 'android' || key.startsWith('android:') ? 'android' : 'app'
106
+ if (
107
+ keyNamespace === attribute.namespace &&
108
+ attributeKey(key, keyNamespace) === attribute.name
109
+ )
110
+ return { value, namespace: style.namespace }
111
+ }
112
+ }
113
+ return null
114
+ }
115
+
116
+ function color(value: string): number {
117
+ const hex = /^#([0-9a-fA-F]{3,8})$/.exec(value)?.[1]
118
+ if (!hex || ![3, 4, 6, 8].includes(hex.length))
119
+ throw new Error(`unreadable color ${value}`)
120
+ const full = hex.length <= 4 ? hex.replace(/./g, '$&$&') : hex
121
+ const argb = full.length === 6 ? `ff${full}` : full
122
+ return Number.parseInt(argb, 16) >>> 0
123
+ }
124
+
125
+ // resolves a framework attribute (`statusBarColor`) of a theme to a value:
126
+ // an ARGB color, a boolean, null for `@null`, or undefined when no style in the
127
+ // chain sets it
128
+ export function resolveThemeAttribute(
129
+ theme: string,
130
+ attribute: string,
131
+ sources: ThemeSources,
132
+ ): number | boolean | null | undefined {
133
+ const chain = styleChain(styleRef(theme, 'app'), sources)
134
+ const seen = new Set<string>()
135
+ let found = lookup(chain, { namespace: 'android', name: attribute })
136
+ while (found) {
137
+ const { value, namespace } = found
138
+ if (seen.has(`${namespace}:${value}`))
139
+ throw new Error(`${theme}: ${attribute} refers to itself`)
140
+ seen.add(`${namespace}:${value}`)
141
+ if (value === 'true' || value === 'false') return value === 'true'
142
+ if (value === '@null') return null
143
+ if (value.startsWith('#')) return color(value)
144
+ const attr = /^\?(?:(android):)?(?:attr\/)?(.+)$/.exec(value)
145
+ if (attr?.[2]) {
146
+ found = lookup(chain, { namespace: attr[1] ? 'android' : namespace, name: attr[2] })
147
+ if (!found) throw new Error(`${theme}: ${value} is not set`)
148
+ continue
149
+ }
150
+ const ref = /^@(?:(android):)?color\/(.+)$/.exec(value)
151
+ if (ref?.[2]) {
152
+ const colorNamespace: Namespace = ref[1] ? 'android' : namespace
153
+ // system_ colors are the device's dynamic palette, set at runtime
154
+ if (colorNamespace === 'android' && ref[2].startsWith('system_'))
155
+ throw new Error(
156
+ `${theme}: ${attribute} uses the dynamic color ${ref[2]}, which peach kotlin does not model`,
157
+ )
158
+ const next = sources[colorNamespace].colors[ref[2]]
159
+ if (next === undefined) throw new Error(`${theme}: ${value} is not defined`)
160
+ found = { value: next, namespace: colorNamespace }
161
+ continue
162
+ }
163
+ throw new Error(`${theme}: ${attribute} has the unsupported value ${value}`)
164
+ }
165
+ // unset: the reader's own default applies, as PhoneWindow's getBoolean/getColor do
166
+ return undefined
167
+ }
@@ -0,0 +1,70 @@
1
+ // the XML an Android module and the platform's resources are written in:
2
+ // elements, single- or double-quoted attributes and text, with comments and
3
+ // declarations skipped. markup outside that (CDATA, a doctype, a malformed
4
+ // tag or an unknown entity) is rejected with its file, never skipped.
5
+
6
+ export interface XmlElement {
7
+ name: string
8
+ attributes: Record<string, string>
9
+ children: XmlElement[]
10
+ text: string
11
+ }
12
+
13
+ const ENTITIES: Record<string, string> = {
14
+ quot: '"',
15
+ apos: "'",
16
+ amp: '&',
17
+ lt: '<',
18
+ gt: '>',
19
+ }
20
+
21
+ const TAG = /<(\/?)([\w:.-]+)((?:\s+[\w:.-]+\s*=\s*(?:"[^"]*"|'[^']*'))*)\s*(\/?)>/y
22
+ const ATTRIBUTE = /([\w:.-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g
23
+
24
+ function decode(value: string, file: string): string {
25
+ return value.replace(/&(#x[0-9a-fA-F]+|#\d+|\w+);/g, (_match, entity: string) => {
26
+ if (entity.startsWith('#x'))
27
+ return String.fromCodePoint(Number.parseInt(entity.slice(2), 16))
28
+ if (entity.startsWith('#'))
29
+ return String.fromCodePoint(Number.parseInt(entity.slice(1), 10))
30
+ const character = ENTITIES[entity]
31
+ if (character === undefined) throw new Error(`${file}: unknown entity &${entity};`)
32
+ return character
33
+ })
34
+ }
35
+
36
+ export function parseXml(text: string, file: string): XmlElement {
37
+ const root: XmlElement = { name: '#document', attributes: {}, children: [], text: '' }
38
+ const stack = [root]
39
+ const body = text.replace(/<!--[\s\S]*?-->/g, '').replace(/<\?[\s\S]*?\?>/g, '')
40
+ let index = 0
41
+ while (index < body.length) {
42
+ const open = body.indexOf('<', index)
43
+ const content = body.slice(index, open === -1 ? body.length : open)
44
+ const parent = stack.at(-1)
45
+ if (parent && content.trim()) parent.text += decode(content, file)
46
+ if (open === -1) break
47
+ TAG.lastIndex = open
48
+ const match = TAG.exec(body)
49
+ if (!match)
50
+ throw new Error(`${file}: unreadable markup at ${body.slice(open, open + 40)}`)
51
+ index = TAG.lastIndex
52
+ const [, closing, name, rawAttributes, selfClosing] = match
53
+ if (!name) continue
54
+ if (closing) {
55
+ const element = stack.pop()
56
+ if (element?.name !== name)
57
+ throw new Error(`${file}: </${name}> closes <${element?.name}>`)
58
+ continue
59
+ }
60
+ const attributes: Record<string, string> = {}
61
+ for (const [, key, double, single] of (rawAttributes ?? '').matchAll(ATTRIBUTE))
62
+ if (key) attributes[key] = decode(double ?? single ?? '', file)
63
+ const element: XmlElement = { name, attributes, children: [], text: '' }
64
+ stack.at(-1)?.children.push(element)
65
+ if (!selfClosing) stack.push(element)
66
+ }
67
+ if (stack.length !== 1)
68
+ throw new Error(`${file}: <${stack.at(-1)?.name}> is never closed`)
69
+ return root
70
+ }
@@ -9,6 +9,7 @@ import type {
9
9
  SemanticWaitOptions,
10
10
  SimSemanticQuery,
11
11
  SimSemanticResolveSelector,
12
+ SimSemanticTargetSelector,
12
13
  SimSemanticSelector,
13
14
  SimStateOptions,
14
15
  } from './bridge-contract'
@@ -396,7 +397,15 @@ function semanticResolveSelector(
396
397
  return validationFailure(path, 'must select testID or text')
397
398
  }
398
399
 
399
- export function parseSemanticResolveSelector(value: unknown): SimSemanticResolveSelector {
400
+ export function parseSemanticResolveSelector(value: unknown): SimSemanticTargetSelector {
401
+ const selector = recordValue(value, 'selector')
402
+ if (selector.nodeId !== undefined) {
403
+ const nodeId = numberValue(selector.nodeId, 'selector.nodeId')
404
+ if (!Number.isSafeInteger(nodeId)) {
405
+ return validationFailure('selector.nodeId', 'must be an integer')
406
+ }
407
+ return { nodeId }
408
+ }
400
409
  return semanticResolveSelector(value, 'selector')
401
410
  }
402
411
 
@@ -104,7 +104,7 @@ export interface WsCommand {
104
104
  stateOptions?: SimStateOptions
105
105
  // query / resolve — structured visible-tree reads and selector geometry
106
106
  query?: SimSemanticQuery
107
- selector?: SimSemanticResolveSelector
107
+ selector?: SimSemanticTargetSelector
108
108
  // reset — two-tier app state wipe
109
109
  resetOptions?: ResetOptions
110
110
  // camera — fixture injection into the host camera pipeline
@@ -505,10 +505,16 @@ export interface SimSemanticQueryResult {
505
505
 
506
506
  export type SimSemanticResolveSelector = { testID: string } | { text: string }
507
507
 
508
+ // what `resolve` may name: a selector, or a node a tree read already returned,
509
+ // so a caller that picked one node taps that node and never re-searches.
510
+ export type SimSemanticTargetSelector = SimSemanticResolveSelector | { nodeId: number }
511
+
508
512
  export interface SimSemanticResolveResult {
509
513
  match: SimSemanticNode
510
514
  target: SimSemanticNode
511
515
  point: { x: number; y: number }
516
+ // the screen a finger can reach, for refusing an offscreen point.
517
+ viewport: { width: number; height: number }
512
518
  }
513
519
 
514
520
  // ---------------------------------------------------------------------------
package/src/config.ts CHANGED
@@ -1,4 +1,4 @@
1
- // rnx.config.ts — per-app module resolution, turbo modules, env, state, and settings.
1
+ // peach.config.ts — per-app module resolution, turbo modules, env, state, and settings.
2
2
  //
3
3
  // usage:
4
4
  // import { defineConfig } from 'peachsim/config'
@@ -17,12 +17,18 @@ export type ModuleResolution =
17
17
 
18
18
  export type NativeModuleResolution = ModuleResolution | Record<string, any> // direct native module object for non-url configs
19
19
 
20
- export interface RNXConfig<Settings extends Record<string, any> = Record<string, any>> {
20
+ // an app's own native view, keyed by its `requireNativeComponent` name or, for
21
+ // an expo module view, `Module_View` (`Module` for a module's only view). the
22
+ // file default-exports the component that renders in its place with its props.
23
+ export type NativeViewResolution = { file: string }
24
+
25
+ export interface PeachConfig<Settings extends Record<string, any> = Record<string, any>> {
21
26
  // host-only engine selection. omitted means the current stable runtime.
22
27
  runtimeVersion?: string
23
28
  modules?: Record<string, ModuleResolution>
24
29
  turboModules?: Record<string, NativeModuleResolution>
25
30
  nativeModules?: Record<string, NativeModuleResolution>
31
+ nativeViews?: Record<string, NativeViewResolution>
26
32
  env?: Record<string, string>
27
33
  // generic per-sim network rewrites for the guest app's runtime fetches.
28
34
  // `remap` keys/values are `host:port` (or bare `host`) — a tenant fetch whose
@@ -46,11 +52,11 @@ export interface RNXConfig<Settings extends Record<string, any> = Record<string,
46
52
  }
47
53
  }
48
54
 
49
- export const RNX_CONFIG_QUERY_PARAM = 'rnxConfig'
55
+ export const PEACH_CONFIG_QUERY_PARAM = 'peachConfig'
50
56
 
51
57
  export function defineConfig<Settings extends Record<string, any> = Record<string, any>>(
52
- config: RNXConfig<Settings>,
53
- ): RNXConfig<Settings> {
58
+ config: PeachConfig<Settings>,
59
+ ): PeachConfig<Settings> {
54
60
  return config
55
61
  }
56
62
 
@@ -58,15 +64,16 @@ function hasOwnKeys(value: Record<string, unknown> | undefined): boolean {
58
64
  return !!value && Object.keys(value).length > 0
59
65
  }
60
66
 
61
- export function hasRNXConfig<Settings extends Record<string, any> = Record<string, any>>(
62
- config?: RNXConfig<Settings> | null,
63
- ): config is RNXConfig<Settings> {
67
+ export function hasPeachConfig<
68
+ Settings extends Record<string, any> = Record<string, any>,
69
+ >(config?: PeachConfig<Settings> | null): config is PeachConfig<Settings> {
64
70
  if (!config) return false
65
71
  return (
66
72
  (typeof config.runtimeVersion === 'string' && config.runtimeVersion.length > 0) ||
67
73
  hasOwnKeys(config.modules as Record<string, unknown> | undefined) ||
68
74
  hasOwnKeys(config.turboModules as Record<string, unknown> | undefined) ||
69
75
  hasOwnKeys(config.nativeModules as Record<string, unknown> | undefined) ||
76
+ hasOwnKeys(config.nativeViews) ||
70
77
  hasOwnKeys(config.env as Record<string, unknown> | undefined) ||
71
78
  hasOwnKeys(config.network?.remap as Record<string, unknown> | undefined) ||
72
79
  hasOwnKeys(config.settings as Record<string, unknown> | undefined) ||
@@ -74,15 +81,15 @@ export function hasRNXConfig<Settings extends Record<string, any> = Record<strin
74
81
  )
75
82
  }
76
83
 
77
- export function mergeRNXConfig<
84
+ export function mergePeachConfig<
78
85
  Settings extends Record<string, any> = Record<string, any>,
79
86
  >(
80
- base?: RNXConfig<Settings> | null,
81
- override?: RNXConfig<Settings> | null,
82
- ): RNXConfig<Settings> | undefined {
83
- if (!hasRNXConfig(base) && !hasRNXConfig(override)) return undefined
84
- if (!hasRNXConfig(base)) return override ? { ...override } : undefined
85
- if (!hasRNXConfig(override)) return base ? { ...base } : undefined
87
+ base?: PeachConfig<Settings> | null,
88
+ override?: PeachConfig<Settings> | null,
89
+ ): PeachConfig<Settings> | undefined {
90
+ if (!hasPeachConfig(base) && !hasPeachConfig(override)) return undefined
91
+ if (!hasPeachConfig(base)) return override ? { ...override } : undefined
92
+ if (!hasPeachConfig(override)) return base ? { ...base } : undefined
86
93
 
87
94
  return {
88
95
  runtimeVersion: override.runtimeVersion ?? base.runtimeVersion,
@@ -92,6 +99,7 @@ export function mergeRNXConfig<
92
99
  ...(base.nativeModules || {}),
93
100
  ...(override.nativeModules || {}),
94
101
  },
102
+ nativeViews: { ...(base.nativeViews || {}), ...(override.nativeViews || {}) },
95
103
  env: { ...(base.env || {}), ...(override.env || {}) },
96
104
  network: {
97
105
  remap: { ...(base.network?.remap || {}), ...(override.network?.remap || {}) },
@@ -104,29 +112,29 @@ export function mergeRNXConfig<
104
112
  }
105
113
  }
106
114
 
107
- export function readRNXConfigFromSearchParams<
115
+ export function readPeachConfigFromSearchParams<
108
116
  Settings extends Record<string, any> = Record<string, any>,
109
- >(searchParams: URLSearchParams): RNXConfig<Settings> | undefined {
110
- const raw = searchParams.get(RNX_CONFIG_QUERY_PARAM)
117
+ >(searchParams: URLSearchParams): PeachConfig<Settings> | undefined {
118
+ const raw = searchParams.get(PEACH_CONFIG_QUERY_PARAM)
111
119
  if (!raw) return undefined
112
120
  try {
113
- const parsed = JSON.parse(raw) as RNXConfig<Settings>
114
- return hasRNXConfig(parsed) ? parsed : undefined
121
+ const parsed = JSON.parse(raw) as PeachConfig<Settings>
122
+ return hasPeachConfig(parsed) ? parsed : undefined
115
123
  } catch (error) {
116
124
  const message = error instanceof Error ? error.message : String(error)
117
- console.warn(`[peach] invalid ${RNX_CONFIG_QUERY_PARAM}: ${message}`)
125
+ console.warn(`[peach] invalid ${PEACH_CONFIG_QUERY_PARAM}: ${message}`)
118
126
  return undefined
119
127
  }
120
128
  }
121
129
 
122
- export function applyRNXConfigToUrl<
130
+ export function applyPeachConfigToUrl<
123
131
  Settings extends Record<string, any> = Record<string, any>,
124
- >(url: string, config?: RNXConfig<Settings> | null): string {
132
+ >(url: string, config?: PeachConfig<Settings> | null): string {
125
133
  const parsed = new URL(url)
126
- if (hasRNXConfig(config)) {
127
- parsed.searchParams.set(RNX_CONFIG_QUERY_PARAM, JSON.stringify(config))
134
+ if (hasPeachConfig(config)) {
135
+ parsed.searchParams.set(PEACH_CONFIG_QUERY_PARAM, JSON.stringify(config))
128
136
  } else {
129
- parsed.searchParams.delete(RNX_CONFIG_QUERY_PARAM)
137
+ parsed.searchParams.delete(PEACH_CONFIG_QUERY_PARAM)
130
138
  }
131
139
  return parsed.toString()
132
140
  }