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/README.md CHANGED
@@ -243,7 +243,22 @@ Windows use Playwright's Chromium headless shell instead: the same build and
243
243
  software renderer without the desktop browser's printing and service discovery
244
244
  libraries (`libcups2`, `libavahi-client3`). Headed launches and every launch on
245
245
  a Mac use full Chromium. When the needed browser is not installed, the CLI runs
246
- the resolved Playwright core's absolute installer entry for just that browser. Later opens reuse that sim and
246
+ the resolved Playwright core's absolute installer entry for just that browser.
247
+ On Linux the CLI checks the browser's shared libraries before launching and,
248
+ when any are missing, exits naming them and the exact `apt-get install` line.
249
+ The headless shell needs these Debian 12 packages (Ubuntu 24.04 and Debian 13
250
+ add a `t64` suffix to `libglib2.0-0`, `libatk1.0-0`, `libatk-bridge2.0-0`,
251
+ `libatspi2.0-0` and `libasound2`):
252
+
253
+ ```sh
254
+ apt-get install --no-install-recommends libglib2.0-0 libnspr4 libnss3 \
255
+ libatk1.0-0 libatk-bridge2.0-0 libatspi2.0-0 libdbus-1-3 libx11-6 libxcb1 \
256
+ libxcomposite1 libxdamage1 libxext6 libxfixes3 libxrandr2 libgbm1 libexpat1 \
257
+ libxkbcommon0 libudev1 libasound2
258
+ ```
259
+
260
+ Headed launches on Linux use full Chromium, which also needs `libcups2`,
261
+ `libcairo2` and `libpango-1.0-0`. Later opens reuse that sim and
247
262
  reload it at the requested target. Use `peach do reload` when the target has not
248
263
  changed.
249
264
  Reserve `--new` for a genuinely concurrent sim; repeated new browser trees
@@ -627,8 +642,12 @@ Per-repo configuration is optional. Every installed-runtime `peach open`
627
642
  serves its selected version from a version-specific localhost origin, so a
628
643
  background activation cannot mix an already loaded shell with chunks from the
629
644
  next runtime. Without config, each new open selects the active stable version.
630
- When `rnx.config.ts` sets `runtimeVersion`, `peach open` installs that
631
- version without changing the machine default. App config, native-linked fonts,
645
+ When `peach.config.ts` sets `runtimeVersion`, `peach open` installs that
646
+ version without changing the machine default. The same file replaces an app's own
647
+ native modules (`nativeModules`, `turboModules`) and native views
648
+ (`nativeViews`, keyed by the `requireNativeComponent` name or an Expo view's
649
+ `Module_View` name) with project-local files; the configuration guide in the
650
+ docs lists every slot. App config, native-linked fonts,
632
651
  and splash assets all resolve from the project root advertised by the selected
633
652
  development server. Bare Metro, which has no Expo manifest project root, uses
634
653
  the listening development process's cwd inside that same resolver. The invoking
package/cli/app-config.ts CHANGED
@@ -4,7 +4,7 @@ import { isValidRuntimeVersion } from '@contrast/runtime-delivery'
4
4
  import { createJiti } from 'jiti'
5
5
  import { rnxPublicBrand } from '../src/public-brand'
6
6
  import { resolveAppProject, type ResolvedAppProject } from './app-project'
7
- import type { RNXConfig } from '../src/config'
7
+ import type { PeachConfig } from '../src/config'
8
8
 
9
9
  const require = createRequire(import.meta.url)
10
10
 
@@ -15,7 +15,7 @@ function isOptionalRecord(value: unknown): boolean {
15
15
  )
16
16
  }
17
17
 
18
- function isRNXConfig(value: unknown): value is RNXConfig {
18
+ function isPeachConfig(value: unknown): value is PeachConfig {
19
19
  if (!value || typeof value !== 'object' || Array.isArray(value)) return false
20
20
  const runtimeVersion = Reflect.get(value, 'runtimeVersion')
21
21
  const network = Reflect.get(value, 'network')
@@ -27,6 +27,7 @@ function isRNXConfig(value: unknown): value is RNXConfig {
27
27
  isOptionalRecord(Reflect.get(value, 'modules')) &&
28
28
  isOptionalRecord(Reflect.get(value, 'turboModules')) &&
29
29
  isOptionalRecord(Reflect.get(value, 'nativeModules')) &&
30
+ isOptionalRecord(Reflect.get(value, 'nativeViews')) &&
30
31
  isOptionalRecord(Reflect.get(value, 'env')) &&
31
32
  isOptionalRecord(network) &&
32
33
  isOptionalRecord(Reflect.get(value, 'settings')) &&
@@ -34,25 +35,43 @@ function isRNXConfig(value: unknown): value is RNXConfig {
34
35
  )
35
36
  }
36
37
 
37
- function resolveConfigReplacementFiles(config: RNXConfig, configPath: string): RNXConfig {
38
- if (!config.modules) return config
38
+ // `{ file }` entries are relative to the config file; the engine loads them by
39
+ // absolute path from the runtime's replacement-module endpoint.
40
+ function resolveFileEntries<T>(
41
+ entries: Record<string, T> | undefined,
42
+ configDir: string,
43
+ ): Record<string, T> | undefined {
44
+ if (!entries) return entries
45
+ return Object.fromEntries(
46
+ Object.entries(entries).map(([name, resolution]) => [
47
+ name,
48
+ resolution &&
49
+ typeof resolution === 'object' &&
50
+ 'file' in resolution &&
51
+ typeof resolution.file === 'string'
52
+ ? { ...resolution, file: path.resolve(configDir, resolution.file) }
53
+ : resolution,
54
+ ]),
55
+ )
56
+ }
57
+
58
+ function resolveConfigReplacementFiles(
59
+ config: PeachConfig,
60
+ configPath: string,
61
+ ): PeachConfig {
39
62
  const configDir = path.dirname(configPath)
40
63
  return {
41
64
  ...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
- ),
65
+ modules: resolveFileEntries(config.modules, configDir),
66
+ turboModules: resolveFileEntries(config.turboModules, configDir),
67
+ nativeModules: resolveFileEntries(config.nativeModules, configDir),
68
+ nativeViews: resolveFileEntries(config.nativeViews, configDir),
50
69
  }
51
70
  }
52
71
 
53
72
  export async function loadRNXAppConfig(
54
73
  project: ResolvedAppProject | null,
55
- ): Promise<RNXConfig | undefined> {
74
+ ): Promise<PeachConfig | undefined> {
56
75
  if (!project?.configPath) return undefined
57
76
  try {
58
77
  const jiti = createJiti(import.meta.url, {
@@ -64,7 +83,7 @@ export async function loadRNXAppConfig(
64
83
  },
65
84
  })
66
85
  const loaded: unknown = await jiti.import(project.configPath, { default: true })
67
- if (!isRNXConfig(loaded)) {
86
+ if (!isPeachConfig(loaded)) {
68
87
  throw new Error('the default export must be a peach config object')
69
88
  }
70
89
  return resolveConfigReplacementFiles(loaded, project.configPath)
@@ -76,7 +95,7 @@ export async function loadRNXAppConfig(
76
95
 
77
96
  export async function resolveRNXAppConfig(
78
97
  startDir: string = process.cwd(),
79
- ): Promise<{ project: ResolvedAppProject | null; config: RNXConfig | undefined }> {
98
+ ): Promise<{ project: ResolvedAppProject | null; config: PeachConfig | undefined }> {
80
99
  const project = resolveAppProject(startDir)
81
100
  return { project, config: await loadRNXAppConfig(project) }
82
101
  }
@@ -9,11 +9,11 @@ export const EXPO_APP_CONFIG_NAMES = [
9
9
  'app.json',
10
10
  ] as const
11
11
 
12
- export const RNX_APP_CONFIG_NAMES = [
13
- 'rnx.config.ts',
14
- 'rnx.config.js',
15
- 'rnx.config.mjs',
16
- 'rnx.config.cjs',
12
+ export const PEACH_APP_CONFIG_NAMES = [
13
+ 'peach.config.ts',
14
+ 'peach.config.js',
15
+ 'peach.config.mjs',
16
+ 'peach.config.cjs',
17
17
  ] as const
18
18
 
19
19
  export interface ResolvedAppProject {
@@ -36,7 +36,7 @@ export function findExpoAppConfig(dir: string): string | null {
36
36
  }
37
37
 
38
38
  export function findRNXAppConfig(dir: string): string | null {
39
- return firstNamedFile(dir, RNX_APP_CONFIG_NAMES)
39
+ return firstNamedFile(dir, PEACH_APP_CONFIG_NAMES)
40
40
  }
41
41
 
42
42
  export function resolveAppProject(projectRoot: string): ResolvedAppProject | null {
@@ -920,7 +920,6 @@ export class SootSimBridgeFlowRunner {
920
920
 
921
921
  private firstLaunchDone = false
922
922
  private appStopped = false
923
- private launchArgumentsActive = false
924
923
  private profilingEnabled = false
925
924
  private recordingEnabled = false
926
925
  private recordingAccessChecked = false
@@ -2604,22 +2603,6 @@ export class SootSimBridgeFlowRunner {
2604
2603
  throw new Error('extendedWaitUntil timed out')
2605
2604
  }
2606
2605
 
2607
- private async reloadGuestApp() {
2608
- // try the gentle reload first — in-place React re-mount preserves
2609
- // recording state and avoids the full worker lifecycle tear-down.
2610
- const reloaded = await this.bridge.send({
2611
- type: 'call',
2612
- simId: this.opts.simId,
2613
- path: 'SootSim.bridges.hotRemount.reloadExternalApp',
2614
- args: [],
2615
- })
2616
- if (reloaded) return
2617
-
2618
- // fallback for surfaces that don't export the hot-remount bridge: a
2619
- // hard page reload resets the tenant worker.
2620
- await this.evaluate('window.location.reload()').catch(() => {})
2621
- }
2622
-
2623
2606
  private async resetGuestAppData(
2624
2607
  launchOptions?: Pick<
2625
2608
  SootSimExternalAppLifecycleOptions,
@@ -2690,6 +2673,7 @@ export class SootSimBridgeFlowRunner {
2690
2673
  }
2691
2674
  const wantsClear = !!(opts && typeof opts === 'object' && opts.clearState)
2692
2675
  const wantsResetRuntime = readBooleanKey(opts, 'resetRuntime')
2676
+ const wantsForeground = isObjectRecord(opts) && opts.stopApp === false
2693
2677
  await this.setLaunchPermissions(isObjectRecord(opts) ? opts.permissions : undefined)
2694
2678
 
2695
2679
  if (!this.firstLaunchDone) {
@@ -2727,7 +2711,14 @@ export class SootSimBridgeFlowRunner {
2727
2711
  await this.waitForTree(120000)
2728
2712
  await this.waitForTree(30000)
2729
2713
  } else {
2730
- await this.launchShellAppFromHomeIfNeeded(opts)
2714
+ const launched = await this.launchShellAppFromHomeIfNeeded(opts)
2715
+ if (!launched && !wantsForeground) {
2716
+ await this.waitForTree(120000)
2717
+ await this.reloadGuestAppRuntime({
2718
+ launchArguments: null,
2719
+ initialUrl: null,
2720
+ })
2721
+ }
2731
2722
  await this.waitForTree(120000)
2732
2723
  await this.waitForTree(30000)
2733
2724
  }
@@ -2737,13 +2728,11 @@ export class SootSimBridgeFlowRunner {
2737
2728
  launchArguments: hasLaunchArguments ? launchArguments : null,
2738
2729
  initialUrl: null,
2739
2730
  })
2740
- } else if (hasLaunchArguments || this.launchArgumentsActive || this.appStopped) {
2731
+ } else if (hasLaunchArguments || this.appStopped || !wantsForeground) {
2741
2732
  await this.reloadGuestAppRuntime({
2742
2733
  launchArguments: hasLaunchArguments ? launchArguments : null,
2743
2734
  initialUrl: null,
2744
2735
  })
2745
- } else {
2746
- await this.reloadGuestApp()
2747
2736
  }
2748
2737
  await this.waitForTree(120000)
2749
2738
  if (this.opts.onAfterLaunch) {
@@ -2760,7 +2749,6 @@ export class SootSimBridgeFlowRunner {
2760
2749
  }
2761
2750
 
2762
2751
  this.appStopped = false
2763
- this.launchArgumentsActive = hasLaunchArguments
2764
2752
 
2765
2753
  if (this.profilingEnabled) {
2766
2754
  await this.startProfile()
@@ -3014,7 +3002,6 @@ export class SootSimBridgeFlowRunner {
3014
3002
  throw new Error(`openLink failed: ${result?.error || 'no test bridge'}`)
3015
3003
  }
3016
3004
  this.appStopped = false
3017
- this.launchArgumentsActive = false
3018
3005
  this.firstLaunchDone = true
3019
3006
  }
3020
3007
 
@@ -1,6 +1,15 @@
1
- // peach compat — scan dependencies with the shared compatibility engine.
1
+ // peach compat — scan dependencies with the shared compatibility engine, and
2
+ // the app's own native modules and views.
2
3
 
3
- import { relative, resolve } from 'node:path'
4
+ import {
5
+ existsSync,
6
+ lstatSync,
7
+ readFileSync,
8
+ readdirSync,
9
+ realpathSync,
10
+ statSync,
11
+ } from 'node:fs'
12
+ import { dirname, join, relative, resolve } from 'node:path'
4
13
  import {
5
14
  COMPAT_RESULT_CAVEAT,
6
15
  formatDetailedScanLines,
@@ -8,8 +17,11 @@ import {
8
17
  scanDeps,
9
18
  type ScanResult,
10
19
  } from '@peach/compat/web'
20
+ import { rnxPublicBrand } from '../../src/public-brand'
21
+ import { resolveRNXAppConfig } from '../app-config'
11
22
  import {
12
23
  findProjectRoot,
24
+ scanRepository,
13
25
  scanWorkspace,
14
26
  installedManifest,
15
27
  readProjectManifest,
@@ -19,7 +31,7 @@ import type { ProjectManifest } from './projectReportContract'
19
31
  export async function runCompat(args: string[]): Promise<void> {
20
32
  if (args.includes('--help') || args.includes('-h')) {
21
33
  console.log(`
22
- peach compat — check package compatibility
34
+ peach compat — check package compatibility and the app's own native code
23
35
 
24
36
  usage:
25
37
  peach compat [package-name]
@@ -48,13 +60,13 @@ flags:
48
60
  }
49
61
 
50
62
  if (packageName) {
51
- report(args, scanOnePackage(appDir, pkg, packageName))
63
+ report(args, scanOnePackage(appDir, pkg, packageName), null)
52
64
  return
53
65
  }
54
66
 
55
67
  const here = scanDeps(installedManifest(appDir, pkg))
56
68
  if (here.packages.length > 0 || appIndex >= 0) {
57
- report(args, here)
69
+ report(args, here, await scanLocalNative(appDir, pkg))
58
70
  return
59
71
  }
60
72
 
@@ -67,13 +79,13 @@ flags:
67
79
  const manifest = readProjectManifest(app.dir)
68
80
  if (!manifest) return []
69
81
  const result = scanDeps(installedManifest(app.dir, manifest))
70
- return result.packages.length > 0 ? [{ dir: app.dir, result }] : []
82
+ return result.packages.length > 0 ? [{ dir: app.dir, manifest, result }] : []
71
83
  })
72
84
 
73
85
  if (elsewhere.length === 1) {
74
86
  const only = elsewhere[0]
75
87
  console.log(`\n scanning ${relative(appDir, only.dir) || '.'}`)
76
- report(args, only.result)
88
+ report(args, only.result, await scanLocalNative(resolve(only.dir), only.manifest))
77
89
  return
78
90
  }
79
91
  if (elsewhere.length > 1) {
@@ -84,16 +96,20 @@ flags:
84
96
  console.log()
85
97
  return
86
98
  }
87
- report(args, here)
99
+ report(args, here, await scanLocalNative(appDir, pkg))
88
100
  }
89
101
 
90
- function report(args: string[], result: ScanResult): void {
102
+ function report(args: string[], result: ScanResult, local: LocalNative[] | null): void {
91
103
  if (args.includes('--json')) {
92
- console.log(JSON.stringify(result, null, 2))
104
+ console.log(
105
+ JSON.stringify(local ? { ...result, localNative: local } : result, null, 2),
106
+ )
93
107
  return
94
108
  }
95
109
  if (args.includes('--brief')) {
96
110
  console.log(` ${formatScanCounts(result)}`)
111
+ if (local?.length)
112
+ console.log(` the app's own native modules and views: ${local.length}`)
97
113
  console.log(` ${COMPAT_RESULT_CAVEAT}`)
98
114
  return
99
115
  }
@@ -110,9 +126,217 @@ function report(args: string[], result: ScanResult): void {
110
126
  if (result.packages.length === 0) {
111
127
  console.log(` no React Native ecosystem packages detected`)
112
128
  }
129
+ if (local?.length) reportLocalNative(local)
113
130
  console.log(`\n ${COMPAT_RESULT_CAVEAT}\n`)
114
131
  }
115
132
 
133
+ function reportLocalNative(local: LocalNative[]): void {
134
+ console.log(`\n the app's own native modules and views`)
135
+ console.log(` Peach does not run an app's native code. until peach.config.ts answers`)
136
+ console.log(` for one, a module is an inert stand-in and a view renders empty.\n`)
137
+ const open = local.filter((item) => !item.answered)
138
+ const width = Math.max(...local.map((item) => item.name.length))
139
+ for (const item of local) {
140
+ const where = item.expoModule
141
+ ? `${item.path} (Expo module ${item.expoModule}${item.swift ? ', Swift' : ''})`
142
+ : item.path
143
+ console.log(
144
+ ` ${item.kind === 'view' ? 'view ' : 'module'} ${item.name.padEnd(width)} ${where}${item.answered ? ' answered in peach.config.ts' : ''}`,
145
+ )
146
+ }
147
+ const modules = open.filter((item) => item.kind === 'module')
148
+ const views = open.filter((item) => item.kind === 'view')
149
+ if (open.length) {
150
+ console.log(
151
+ `\n answer ${open.length === local.length ? 'them' : 'the rest'} from peach.config.ts:\n`,
152
+ )
153
+ console.log(` export default defineConfig({`)
154
+ if (modules.length) {
155
+ console.log(` nativeModules: {`)
156
+ for (const item of modules) {
157
+ console.log(` ${item.name}: { file: './peach/${item.name}.ts' },`)
158
+ }
159
+ console.log(` },`)
160
+ }
161
+ if (views.length) {
162
+ console.log(` nativeViews: {`)
163
+ for (const item of views) {
164
+ console.log(` ${item.name}: { file: './peach/${item.name}.tsx' },`)
165
+ }
166
+ console.log(` },`)
167
+ }
168
+ console.log(` })`)
169
+ }
170
+ console.log(`\n guide: ${rnxPublicBrand.origin}/docs/rnx/compat/custom-stubs`)
171
+ }
172
+
173
+ // an app's own native code, found from the names its JavaScript asks the native
174
+ // side for and from local Expo modules (a directory with expo-module.config.json).
175
+ // each name is the key the matching peach.config slot takes.
176
+ export interface LocalNative {
177
+ kind: 'module' | 'view'
178
+ name: string
179
+ // first place the name appears, relative to the app, as file:line
180
+ path: string
181
+ // the local Expo module directory the name belongs to, relative to the app
182
+ expoModule: string | null
183
+ swift: boolean
184
+ // the app's peach.config.ts already names it
185
+ answered: boolean
186
+ }
187
+
188
+ const SOURCE_FILE = /\.[cm]?[jt]sx?$/
189
+ const NATIVE_SOURCE_FILE = /\.(?:swift|kt)$/
190
+ // a requireNativeView(module, view) name is `module_view`, the key the engine
191
+ // registers expo views under.
192
+ const LOOKUPS: { kind: LocalNative['kind']; pattern: RegExp }[] = [
193
+ {
194
+ kind: 'view',
195
+ pattern: /\brequireNativeComponent\s*(?:<[^;]{0,200}?>)?\(\s*['"]([\w$.-]+)['"]/g,
196
+ },
197
+ {
198
+ kind: 'view',
199
+ pattern:
200
+ /\brequireNativeView(?:Manager)?\s*(?:<[^;]{0,200}?>)?\(\s*['"](\w+)['"]\s*(?:,\s*['"](\w+)['"])?/g,
201
+ },
202
+ {
203
+ kind: 'module',
204
+ pattern:
205
+ /\brequire(?:Optional)?NativeModule\s*(?:<[^;]{0,200}?>)?\(\s*['"](\w+)['"]/g,
206
+ },
207
+ {
208
+ kind: 'module',
209
+ pattern:
210
+ /\bTurboModuleRegistry\.get(?:Enforcing)?\s*(?:<[^;]{0,200}?>)?\(\s*['"](\w+)['"]/g,
211
+ },
212
+ ]
213
+
214
+ async function scanLocalNative(
215
+ appDir: string,
216
+ pkg: ProjectManifest,
217
+ ): Promise<LocalNative[]> {
218
+ const { config } = await resolveRNXAppConfig(appDir)
219
+ const answered = {
220
+ module: new Set(Object.keys({ ...config?.turboModules, ...config?.nativeModules })),
221
+ view: new Set(Object.keys(config?.nativeViews ?? {})),
222
+ }
223
+ const expoModuleDirs: string[] = []
224
+ const sourceFiles: string[] = []
225
+ for (const root of [appDir, ...workspacePackageDirs(appDir, pkg)]) {
226
+ for (const dir of scanRepository(root).directories) {
227
+ if (existsSync(join(dir, 'expo-module.config.json'))) expoModuleDirs.push(dir)
228
+ for (const name of listDir(dir)) {
229
+ if (SOURCE_FILE.test(name) && !name.endsWith('.d.ts'))
230
+ sourceFiles.push(join(dir, name))
231
+ }
232
+ }
233
+ }
234
+ const nativeFilesByModule = new Map(
235
+ expoModuleDirs.map((dir) => [
236
+ dir,
237
+ scanRepository(dir).directories.flatMap((sub) =>
238
+ listDir(sub)
239
+ .filter((name) => NATIVE_SOURCE_FILE.test(name))
240
+ .map((name) => join(sub, name)),
241
+ ),
242
+ ]),
243
+ )
244
+
245
+ const found = new Map<string, LocalNative>()
246
+ const add = (
247
+ kind: LocalNative['kind'],
248
+ name: string,
249
+ file: string,
250
+ text: string,
251
+ at = 0,
252
+ ) => {
253
+ const key = `${kind}:${name}`
254
+ if (found.has(key)) return
255
+ const moduleDir = expoModuleDirs.find((dir) => file.startsWith(`${dir}/`)) ?? null
256
+ found.set(key, {
257
+ kind,
258
+ name,
259
+ path: `${relative(appDir, file)}:${text.slice(0, at).split('\n').length}`,
260
+ expoModule: moduleDir ? relative(appDir, moduleDir) || '.' : null,
261
+ swift: moduleDir
262
+ ? (nativeFilesByModule.get(moduleDir) ?? []).some((path) =>
263
+ path.endsWith('.swift'),
264
+ )
265
+ : false,
266
+ answered: answered[kind].has(name),
267
+ })
268
+ }
269
+ for (const file of sourceFiles) {
270
+ const text = readSmallFile(file)
271
+ if (!text || !/requireNative|TurboModuleRegistry/.test(text)) continue
272
+ for (const { kind, pattern } of LOOKUPS) {
273
+ for (const match of text.matchAll(pattern)) {
274
+ const name = match[2] ? `${match[1]}_${match[2]}` : match[1]
275
+ if (name) add(kind, name, file, text, match.index)
276
+ }
277
+ }
278
+ }
279
+ // a local expo module declares its name in its native definition, Name("X"),
280
+ // whether or not the app's javascript spells it out.
281
+ for (const [, files] of nativeFilesByModule) {
282
+ for (const file of files) {
283
+ const text = readSmallFile(file)
284
+ if (!text) continue
285
+ for (const match of text.matchAll(/\bName\(\s*"(\w+)"\s*\)/g)) {
286
+ if (match[1]) add('module', match[1], file, text, match.index)
287
+ }
288
+ }
289
+ }
290
+ return [...found.values()].sort(
291
+ (a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name),
292
+ )
293
+ }
294
+
295
+ function listDir(dir: string): string[] {
296
+ try {
297
+ return readdirSync(dir)
298
+ } catch {
299
+ return []
300
+ }
301
+ }
302
+
303
+ function readSmallFile(file: string): string | null {
304
+ try {
305
+ return statSync(file).size <= 1024 * 1024 ? readFileSync(file, 'utf8') : null
306
+ } catch {
307
+ return null
308
+ }
309
+ }
310
+
311
+ // dependencies installed as links to source outside any node_modules are the
312
+ // app's own workspace packages, which hold its native code as often as the app
313
+ // directory does.
314
+ function workspacePackageDirs(appDir: string, pkg: ProjectManifest): string[] {
315
+ const dirs = new Set<string>()
316
+ for (const name of Object.keys({ ...pkg.dependencies, ...pkg.devDependencies })) {
317
+ let current = appDir
318
+ while (true) {
319
+ const link = join(current, 'node_modules', name)
320
+ if (existsSync(link)) {
321
+ if (lstatSync(link).isSymbolicLink()) {
322
+ const target = realpathSync(link)
323
+ if (
324
+ !target.split('/').includes('node_modules') &&
325
+ !target.startsWith(`${appDir}/`)
326
+ ) {
327
+ dirs.add(target)
328
+ }
329
+ }
330
+ break
331
+ }
332
+ const parent = dirname(current)
333
+ if (parent === current) break
334
+ current = parent
335
+ }
336
+ }
337
+ return [...dirs]
338
+ }
339
+
116
340
  function scanOnePackage(
117
341
  appDir: string,
118
342
  pkg: ProjectManifest,
@@ -15,10 +15,10 @@ import { isAppDeepLinkTarget, openAppUrl } from '../../src/app-url'
15
15
  import { isLoopbackHost } from '../../src/backend-origin'
16
16
  import { resolveSootsimShellBaseUrlForBridgePort } from '../../src/bridge-constants'
17
17
  import {
18
- applyRNXConfigToUrl,
19
- mergeRNXConfig,
20
- RNX_CONFIG_QUERY_PARAM,
21
- type RNXConfig,
18
+ applyPeachConfigToUrl,
19
+ mergePeachConfig,
20
+ PEACH_CONFIG_QUERY_PARAM,
21
+ type PeachConfig,
22
22
  } from '../../src/config'
23
23
  import {
24
24
  resolveConnectionInput,
@@ -97,7 +97,7 @@ import type { DriverId, DriverLaunchResult } from '../drivers/types'
97
97
  interface ControlCommandOptions {
98
98
  port?: number
99
99
  timeoutMs?: number
100
- runtimeConfig?: RNXConfig
100
+ runtimeConfig?: PeachConfig
101
101
  }
102
102
 
103
103
  export interface OpenCommandResources {
@@ -555,8 +555,8 @@ function normalizeReplacementPath(filePath: string): string {
555
555
  return path.resolve(process.cwd(), filePath)
556
556
  }
557
557
 
558
- function parseOpenRuntimeConfig(args: string[]): RNXConfig | undefined {
559
- const modules: NonNullable<RNXConfig['modules']> = {}
558
+ function parseOpenRuntimeConfig(args: string[]): PeachConfig | undefined {
559
+ const modules: NonNullable<PeachConfig['modules']> = {}
560
560
  const remap: Record<string, string> = {}
561
561
 
562
562
  for (let i = 0; i < args.length; i++) {
@@ -597,7 +597,7 @@ function parseOpenRuntimeConfig(args: string[]): RNXConfig | undefined {
597
597
  }
598
598
  }
599
599
 
600
- const config: RNXConfig = {}
600
+ const config: PeachConfig = {}
601
601
  if (Object.keys(modules).length > 0) config.modules = modules
602
602
  if (Object.keys(remap).length > 0) config.network = { remap }
603
603
  return config.modules || config.network ? config : undefined
@@ -737,9 +737,9 @@ async function resolveShellUrl(
737
737
  if (looksLikeSootsimUrl(target)) {
738
738
  const url = new URL(target)
739
739
  const base = new URL(baseUrl)
740
- const baseConfig = base.searchParams.get(RNX_CONFIG_QUERY_PARAM)
741
- if (baseConfig && !url.searchParams.has(RNX_CONFIG_QUERY_PARAM)) {
742
- url.searchParams.set(RNX_CONFIG_QUERY_PARAM, baseConfig)
740
+ const baseConfig = base.searchParams.get(PEACH_CONFIG_QUERY_PARAM)
741
+ if (baseConfig && !url.searchParams.has(PEACH_CONFIG_QUERY_PARAM)) {
742
+ url.searchParams.set(PEACH_CONFIG_QUERY_PARAM, baseConfig)
743
743
  }
744
744
  return url.toString()
745
745
  }
@@ -1281,16 +1281,16 @@ export function printOpenedSim(
1281
1281
  try {
1282
1282
  const parsed = new URL(value)
1283
1283
  let changed = false
1284
- if (parsed.searchParams.has(RNX_CONFIG_QUERY_PARAM)) {
1285
- parsed.searchParams.set(RNX_CONFIG_QUERY_PARAM, '[redacted]')
1284
+ if (parsed.searchParams.has(PEACH_CONFIG_QUERY_PARAM)) {
1285
+ parsed.searchParams.set(PEACH_CONFIG_QUERY_PARAM, '[redacted]')
1286
1286
  changed = true
1287
1287
  }
1288
1288
  const bundle = parsed.searchParams.get('bundle')
1289
1289
  if (bundle) {
1290
1290
  try {
1291
1291
  const parsedBundle = new URL(bundle)
1292
- if (parsedBundle.searchParams.has(RNX_CONFIG_QUERY_PARAM)) {
1293
- parsedBundle.searchParams.set(RNX_CONFIG_QUERY_PARAM, '[redacted]')
1292
+ if (parsedBundle.searchParams.has(PEACH_CONFIG_QUERY_PARAM)) {
1293
+ parsedBundle.searchParams.set(PEACH_CONFIG_QUERY_PARAM, '[redacted]')
1294
1294
  parsed.searchParams.set('bundle', parsedBundle.toString())
1295
1295
  changed = true
1296
1296
  }
@@ -1585,8 +1585,8 @@ export async function runOpenCommand(
1585
1585
  const project = await resolveAppProjectForBundleUrl(resolvedBundle?.bundleUrl)
1586
1586
  const appConfig = await loadRNXAppConfig(project)
1587
1587
  const configuredRuntimeVersion = appConfig?.runtimeVersion
1588
- const runtimeConfig = mergeRNXConfig(
1589
- mergeRNXConfig(
1588
+ const runtimeConfig = mergePeachConfig(
1589
+ mergePeachConfig(
1590
1590
  appConfig ? { ...appConfig, runtimeVersion: undefined } : undefined,
1591
1591
  opts.runtimeConfig,
1592
1592
  ),
@@ -1842,7 +1842,7 @@ export async function runOpenCommand(
1842
1842
  if (!targetSim && parsed.simIdSource !== 'flag') {
1843
1843
  targetShellUrl = await buildShellUrl(
1844
1844
  target,
1845
- applyRNXConfigToUrl(baseUrl, runtimeConfig),
1845
+ applyPeachConfigToUrl(baseUrl, runtimeConfig),
1846
1846
  buildShellUrlOpts,
1847
1847
  )
1848
1848
  const normalizedTarget = new URL(targetShellUrl)
@@ -1881,7 +1881,7 @@ export async function runOpenCommand(
1881
1881
  targetShellUrl ??
1882
1882
  (await buildShellUrl(
1883
1883
  target,
1884
- applyRNXConfigToUrl(resolvedBaseUrl, runtimeConfig),
1884
+ applyPeachConfigToUrl(resolvedBaseUrl, runtimeConfig),
1885
1885
  buildShellUrlOpts,
1886
1886
  ))
1887
1887
  const navigationUrl = new URL(navigatedShellUrl)
@@ -1968,7 +1968,7 @@ export async function runOpenCommand(
1968
1968
  }
1969
1969
  }
1970
1970
 
1971
- const configuredBaseUrl = applyRNXConfigToUrl(baseUrl, runtimeConfig)
1971
+ const configuredBaseUrl = applyPeachConfigToUrl(baseUrl, runtimeConfig)
1972
1972
  const openUrl = await buildOpenUrl(target, configuredBaseUrl, token, buildShellUrlOpts)
1973
1973
  const ownerPid = opts.ownerPid ?? getStableOwnerPid()
1974
1974
  if (!ownerPid) {