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.
Files changed (105) hide show
  1. package/README.md +21 -2
  2. package/cli/app-config.ts +25 -9
  3. package/cli/bridge-flow-runner.ts +10 -23
  4. package/cli/commands/compat.ts +234 -10
  5. package/cli/commands/do-chain.ts +5 -1
  6. package/cli/commands/inspect/actions.ts +33 -76
  7. package/cli/commands/inspect/resolve-target.ts +67 -2
  8. package/cli/commands/inspect.ts +16 -1
  9. package/cli/drivers/playwright-provisioning.ts +95 -0
  10. package/cli/ws-bridge.ts +7 -7
  11. package/dist-lib/agent-daemon-client.cjs +1 -1
  12. package/dist-lib/agent-events.cjs +1 -1
  13. package/dist-lib/agent-identity.cjs +1 -1
  14. package/dist-lib/agent-sessions.cjs +1 -1
  15. package/dist-lib/attached-projects.cjs +1 -1
  16. package/dist-lib/auth/shared-session.cjs +1 -1
  17. package/dist-lib/backend-origin.cjs +1 -1
  18. package/dist-lib/beta.cjs +1 -1
  19. package/dist-lib/beta.mjs +1 -1
  20. package/dist-lib/bridge-constants.cjs +1 -1
  21. package/dist-lib/bridge-contract-input.cjs +9 -1
  22. package/dist-lib/bridge-contract-input.mjs +9 -1
  23. package/dist-lib/bridge-contract.cjs +1 -1
  24. package/dist-lib/bridge-contract.mjs +1 -1
  25. package/dist-lib/capture-contract.cjs +1 -1
  26. package/dist-lib/capture-contract.mjs +1 -1
  27. package/dist-lib/cli-constants.cjs +1 -1
  28. package/dist-lib/cloud-contract.cjs +1 -1
  29. package/dist-lib/cloud-contract.mjs +1 -1
  30. package/dist-lib/cloud-test-args.cjs +1 -1
  31. package/dist-lib/cloud-test-args.mjs +1 -1
  32. package/dist-lib/cloud.cjs +1 -1
  33. package/dist-lib/cloud.mjs +1 -1
  34. package/dist-lib/config.cjs +3 -2
  35. package/dist-lib/detox/index.cjs +1 -1
  36. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  37. package/dist-lib/home-paths.cjs +1 -1
  38. package/dist-lib/host/bridge-host.cjs +78 -35
  39. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  40. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  41. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  42. package/dist-lib/host/replacement-module-handler.cjs +51 -8
  43. package/dist-lib/host/websocket-proxy.cjs +1 -1
  44. package/dist-lib/index.cjs +317 -70
  45. package/dist-lib/jump-to-source-babel.cjs +1 -1
  46. package/dist-lib/jump-to-source-native.cjs +1 -1
  47. package/dist-lib/kotlin-host.cjs +8 -4
  48. package/dist-lib/menu.cjs +1 -1
  49. package/dist-lib/menu.mjs +1 -1
  50. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  51. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  52. package/dist-lib/metro-production-bundle.cjs +1 -1
  53. package/dist-lib/metro-production-bundle.mjs +1 -1
  54. package/dist-lib/metro.cjs +300 -29
  55. package/dist-lib/native-app-identity.cjs +58 -25
  56. package/dist-lib/native-app-identity.mjs +57 -25
  57. package/dist-lib/plist.cjs +64 -0
  58. package/dist-lib/plist.mjs +29 -0
  59. package/dist-lib/profiles.cjs +1 -1
  60. package/dist-lib/public-brand.cjs +1 -1
  61. package/dist-lib/react-native-host-modules.cjs +1 -1
  62. package/dist-lib/react-native-host-modules.mjs +1 -1
  63. package/dist-lib/render-mode.cjs +1 -1
  64. package/dist-lib/scripts/dev-server-scanner.cjs +2 -2
  65. package/dist-lib/sdk.cjs +36 -49
  66. package/dist-lib/sdk.mjs +36 -49
  67. package/dist-lib/skills.cjs +250 -131
  68. package/dist-lib/swift-compile.cjs +1 -1
  69. package/dist-lib/swift-compile.mjs +1 -1
  70. package/dist-lib/swift-package.cjs +1 -1
  71. package/dist-lib/swift-package.mjs +1 -1
  72. package/dist-lib/swift-submit.cjs +1 -1
  73. package/dist-lib/swift-submit.mjs +1 -1
  74. package/dist-lib/swift.cjs +1 -1
  75. package/dist-lib/vite-swift.cjs +1 -1
  76. package/dist-lib/vite.cjs +51 -8
  77. package/kotlin/AppSources/android/app/UiModeManager.kt +9 -0
  78. package/kotlin/AppSources/android/content/Context.kt +9 -2
  79. package/kotlin/AppSources/android/content/res/Configuration.kt +16 -0
  80. package/kotlin/AppSources/android/content/res/Resources.kt +7 -0
  81. package/kotlin/AppSources/android/graphics/Color.kt +13 -0
  82. package/kotlin/AppSources/android/view/View.kt +2 -0
  83. package/kotlin/AppSources/android/view/Window.kt +99 -8
  84. package/kotlin/AppSources/android/view/WindowInsetsController.kt +12 -0
  85. package/kotlin/AppSources/android/view/WindowManager.kt +16 -0
  86. package/kotlin/AppSources/androidx/activity/EdgeToEdge.kt +71 -0
  87. package/kotlin/AppSources/androidx/core/view/WindowCompat.kt +32 -0
  88. package/kotlin/AppSources/dev/peach/app/PeachApp.kt +16 -11
  89. package/kotlin/AppSources/dev/peach/app/PeachWindowHost.kt +2 -1
  90. package/kotlin/ComposeSources/PeachComposeHost.kt +11 -1
  91. package/package.json +8 -1
  92. package/skills/peach-setup/SKILL.md +27 -0
  93. package/src/android-theme.ts +167 -0
  94. package/src/android-xml.ts +70 -0
  95. package/src/bridge-contract-input.ts +10 -1
  96. package/src/bridge-contract.ts +7 -1
  97. package/src/config.ts +8 -0
  98. package/src/generated/android-framework-themes.json +6303 -0
  99. package/src/host/replacement-module-handler.ts +33 -0
  100. package/src/index.ts +1 -0
  101. package/src/kotlin-app.ts +75 -61
  102. package/src/kotlin-host.ts +7 -3
  103. package/src/metro-plugin.ts +46 -3
  104. package/src/native-app-identity.ts +75 -26
  105. 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.657",
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): 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
@@ -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 || {}) },