peachsim 0.1.657 → 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.
- package/README.md +21 -2
- package/cli/app-config.ts +25 -9
- package/cli/bridge-flow-runner.ts +10 -23
- package/cli/commands/compat.ts +234 -10
- package/cli/commands/do-chain.ts +5 -1
- package/cli/commands/inspect/actions.ts +33 -76
- package/cli/commands/inspect/resolve-target.ts +67 -2
- package/cli/commands/inspect.ts +16 -1
- package/cli/drivers/playwright-provisioning.ts +95 -0
- package/cli/ws-bridge.ts +7 -7
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +9 -1
- package/dist-lib/bridge-contract-input.mjs +9 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.mjs +1 -1
- package/dist-lib/cloud-test-args.cjs +1 -1
- package/dist-lib/cloud-test-args.mjs +1 -1
- package/dist-lib/cloud.cjs +1 -1
- package/dist-lib/cloud.mjs +1 -1
- package/dist-lib/config.cjs +3 -2
- package/dist-lib/detox/index.cjs +1 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +78 -35
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +51 -8
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +317 -70
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/kotlin-host.cjs +8 -4
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +300 -29
- package/dist-lib/native-app-identity.cjs +58 -25
- package/dist-lib/native-app-identity.mjs +57 -25
- package/dist-lib/plist.cjs +64 -0
- package/dist-lib/plist.mjs +29 -0
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +2 -2
- package/dist-lib/sdk.cjs +36 -49
- package/dist-lib/sdk.mjs +36 -49
- package/dist-lib/skills.cjs +250 -131
- package/dist-lib/swift-compile.cjs +1 -1
- package/dist-lib/swift-compile.mjs +1 -1
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +1 -1
- package/dist-lib/swift-submit.mjs +1 -1
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +1 -1
- package/dist-lib/vite.cjs +51 -8
- package/kotlin/AppSources/android/app/UiModeManager.kt +9 -0
- package/kotlin/AppSources/android/content/Context.kt +9 -2
- package/kotlin/AppSources/android/content/res/Configuration.kt +16 -0
- package/kotlin/AppSources/android/content/res/Resources.kt +7 -0
- package/kotlin/AppSources/android/graphics/Color.kt +13 -0
- package/kotlin/AppSources/android/view/View.kt +2 -0
- package/kotlin/AppSources/android/view/Window.kt +99 -8
- package/kotlin/AppSources/android/view/WindowInsetsController.kt +12 -0
- package/kotlin/AppSources/android/view/WindowManager.kt +16 -0
- package/kotlin/AppSources/androidx/activity/EdgeToEdge.kt +71 -0
- package/kotlin/AppSources/androidx/core/view/WindowCompat.kt +32 -0
- package/kotlin/AppSources/dev/peach/app/PeachApp.kt +16 -11
- package/kotlin/AppSources/dev/peach/app/PeachWindowHost.kt +2 -1
- package/kotlin/ComposeSources/PeachComposeHost.kt +11 -1
- package/package.json +8 -1
- package/skills/peach-setup/SKILL.md +27 -0
- package/src/android-theme.ts +167 -0
- package/src/android-xml.ts +70 -0
- package/src/bridge-contract-input.ts +10 -1
- package/src/bridge-contract.ts +7 -1
- package/src/config.ts +8 -0
- package/src/generated/android-framework-themes.json +6303 -0
- package/src/host/replacement-module-handler.ts +33 -0
- package/src/index.ts +1 -0
- package/src/kotlin-app.ts +75 -61
- package/src/kotlin-host.ts +7 -3
- package/src/metro-plugin.ts +46 -3
- package/src/native-app-identity.ts +75 -26
- 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.
|
|
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",
|
|
@@ -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):
|
|
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
|
|
package/src/bridge-contract.ts
CHANGED
|
@@ -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?:
|
|
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
|
@@ -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
|
+
// 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
|
+
|
|
20
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
|
|
@@ -67,6 +73,7 @@ export function hasPeachConfig<
|
|
|
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) ||
|
|
@@ -92,6 +99,7 @@ export function mergePeachConfig<
|
|
|
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 || {}) },
|