peachsim 0.1.661 → 0.1.663

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 (82) hide show
  1. package/cli/commands/inspect/core.ts +9 -1
  2. package/cli/commands/inspect/settling.ts +44 -1
  3. package/cli/commands/inspect/wait-ready.ts +40 -61
  4. package/dist-lib/agent-daemon-client.cjs +1 -1
  5. package/dist-lib/agent-events.cjs +1 -1
  6. package/dist-lib/agent-identity.cjs +1 -1
  7. package/dist-lib/agent-sessions.cjs +1 -1
  8. package/dist-lib/attached-projects.cjs +1 -1
  9. package/dist-lib/auth/shared-session.cjs +1 -1
  10. package/dist-lib/backend-origin.cjs +1 -1
  11. package/dist-lib/beta.cjs +1 -1
  12. package/dist-lib/beta.mjs +1 -1
  13. package/dist-lib/bridge-constants.cjs +1 -1
  14. package/dist-lib/bridge-contract-input.cjs +6 -2
  15. package/dist-lib/bridge-contract-input.mjs +6 -2
  16. package/dist-lib/bridge-contract.cjs +1 -1
  17. package/dist-lib/bridge-contract.mjs +1 -1
  18. package/dist-lib/capture-contract.cjs +1 -1
  19. package/dist-lib/capture-contract.mjs +1 -1
  20. package/dist-lib/cli-constants.cjs +1 -1
  21. package/dist-lib/cloud-contract.cjs +1 -1
  22. package/dist-lib/cloud-contract.mjs +1 -1
  23. package/dist-lib/cloud-test-args.cjs +1 -1
  24. package/dist-lib/cloud-test-args.mjs +1 -1
  25. package/dist-lib/cloud.cjs +1 -1
  26. package/dist-lib/cloud.mjs +1 -1
  27. package/dist-lib/config.cjs +1 -1
  28. package/dist-lib/detox/index.cjs +1 -1
  29. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  30. package/dist-lib/home-paths.cjs +1 -1
  31. package/dist-lib/host/bridge-host.cjs +1 -1
  32. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  33. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  34. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  35. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  36. package/dist-lib/host/websocket-proxy.cjs +1 -1
  37. package/dist-lib/index.cjs +8 -2
  38. package/dist-lib/jump-to-source-babel.cjs +1 -1
  39. package/dist-lib/jump-to-source-native.cjs +1 -1
  40. package/dist-lib/kotlin-host.cjs +197 -60
  41. package/dist-lib/menu.cjs +1 -1
  42. package/dist-lib/menu.mjs +1 -1
  43. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  44. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  45. package/dist-lib/metro-production-bundle.cjs +1 -1
  46. package/dist-lib/metro-production-bundle.mjs +1 -1
  47. package/dist-lib/metro.cjs +1 -1
  48. package/dist-lib/native-app-identity.cjs +1 -1
  49. package/dist-lib/native-app-identity.mjs +1 -1
  50. package/dist-lib/plist.cjs +1 -1
  51. package/dist-lib/plist.mjs +1 -1
  52. package/dist-lib/profiles.cjs +1 -1
  53. package/dist-lib/public-brand.cjs +1 -1
  54. package/dist-lib/react-native-host-modules.cjs +1 -1
  55. package/dist-lib/react-native-host-modules.mjs +1 -1
  56. package/dist-lib/render-mode.cjs +1 -1
  57. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  58. package/dist-lib/sdk.cjs +29 -2
  59. package/dist-lib/sdk.mjs +28 -2
  60. package/dist-lib/skills.cjs +9 -2
  61. package/dist-lib/swift-compile.cjs +1 -1
  62. package/dist-lib/swift-compile.mjs +1 -1
  63. package/dist-lib/swift-package.cjs +1 -1
  64. package/dist-lib/swift-package.mjs +1 -1
  65. package/dist-lib/swift-submit.cjs +1 -1
  66. package/dist-lib/swift-submit.mjs +1 -1
  67. package/dist-lib/swift.cjs +1 -1
  68. package/dist-lib/vite-swift.cjs +1 -1
  69. package/dist-lib/vite.cjs +1 -1
  70. package/kotlin/AppSources/androidx/activity/ActivityViewModelLazy.kt +21 -0
  71. package/kotlin/AppSources/androidx/activity/ComponentActivity.kt +5 -3
  72. package/kotlin/AppSources/androidx/lifecycle/AndroidViewModel.kt +10 -0
  73. package/kotlin/AppSources/dev/peach/app/PeachApp.kt +16 -2
  74. package/kotlin/tools/abi-classes.main.kts +41 -0
  75. package/package.json +1 -1
  76. package/src/bridge-contract-input.ts +5 -1
  77. package/src/bridge-contract.ts +4 -1
  78. package/src/generated/android-framework-themes.json +341 -0
  79. package/src/kotlin-app.ts +47 -26
  80. package/src/kotlin-host.ts +105 -17
  81. package/src/kotlin-view-models.ts +133 -0
  82. package/src/sdk.ts +2 -1
@@ -12,7 +12,12 @@ import path from 'node:path'
12
12
  import { promisify } from 'node:util'
13
13
  import { rnxHomeDir } from './home-paths'
14
14
  import { isRecord } from './is-record'
15
- import type { KotlinAppProject } from './kotlin-app'
15
+ import {
16
+ readAbiClasses,
17
+ viewModelClasses,
18
+ type ViewModelClass,
19
+ } from './kotlin-view-models'
20
+ import type { KotlinAppProject, KotlinWindowTheme } from './kotlin-app'
16
21
  import type { ServerResponse } from 'node:http'
17
22
 
18
23
  const run = promisify(execFile)
@@ -262,7 +267,7 @@ function entrySource(ids: string[]): string {
262
267
  ].join('\n')
263
268
  }
264
269
 
265
- const BUILD_FORMAT = 7
270
+ const BUILD_FORMAT = 8
266
271
  const builds = new Map<string, Promise<KotlinArtifact>>()
267
272
 
268
273
  function buildsDir(): string {
@@ -320,8 +325,41 @@ function viewsSource(viewIds: string[]): string {
320
325
  ].join('\n')
321
326
  }
322
327
 
328
+ // the call SavedStateViewModelFactory makes for each constructor it finds, or
329
+ // its error for one that takes the application and handle in another order
330
+ function viewModelInitializer({ name, signature }: ViewModelClass): string {
331
+ const call = (args: string) =>
332
+ ` addInitializer(${name}::class) { ${name}(${args}) }`
333
+ switch (signature) {
334
+ case 'none':
335
+ return call('')
336
+ case 'handle':
337
+ return call('createSavedStateHandle()')
338
+ case 'application':
339
+ return call('dev.peach.app.peachApplication()')
340
+ case 'application-handle':
341
+ return call('dev.peach.app.peachApplication(), createSavedStateHandle()')
342
+ case 'misordered':
343
+ return ` addInitializer(${name}::class) { throw UnsupportedOperationException(${JSON.stringify(
344
+ `Class ${name.split('.').pop()} must have parameters in the proper order: [class android.app.Application, class androidx.lifecycle.SavedStateHandle]`,
345
+ )}) }`
346
+ }
347
+ }
348
+
349
+ function windowTheme(window: KotlinWindowTheme): string[] {
350
+ const argb = (color: number) => `0x${color.toString(16).padStart(8, '0')}.toInt()`
351
+ return [
352
+ ' android.view.WindowTheme(',
353
+ ` ${window.edgeToEdgeEnforced}, ${argb(window.statusBarColor)}, ${argb(window.navigationBarColor)},`,
354
+ ` ${window.statusBarContrastEnforced}, ${window.navigationBarContrastEnforced},`,
355
+ ` ${window.lightStatusBar}, ${window.lightNavigationBar},`,
356
+ ' ),',
357
+ ]
358
+ }
359
+
323
360
  // a pure Kotlin app: its module's sources, the Android platform actuals and
324
361
  // the compose host, with an entry that cold-starts the resolved launcher
362
+
325
363
  export async function kotlinAppArtifact(
326
364
  app: KotlinAppProject,
327
365
  moduleRoot: string,
@@ -329,12 +367,12 @@ export async function kotlinAppArtifact(
329
367
  ): Promise<KotlinArtifact> {
330
368
  const toolchain = readToolchain(moduleRoot)
331
369
  const kotlinString = (value: string) => JSON.stringify(value).replaceAll('$', '\\$')
332
- const generated: Record<string, string> = {
333
- 'PeachEntry.kt': entrySource([]),
334
- 'PeachViews.kt': viewsSource([]),
335
- 'PeachAppEntry.kt': [
370
+ const appEntry = (viewModels: ViewModelClass[]) =>
371
+ [
336
372
  '@file:OptIn(kotlin.js.ExperimentalJsExport::class)',
337
373
  '',
374
+ 'import androidx.lifecycle.createSavedStateHandle',
375
+ '',
338
376
  '@JsExport',
339
377
  'fun peachAppMount(density: Float, fontScale: Float, locale: String, backend: String, stencilBits: Int,',
340
378
  ' regular: String, medium: String, bold: String, insets: String, night: Boolean,',
@@ -342,15 +380,14 @@ export async function kotlinAppArtifact(
342
380
  ' dev.peach.app.PeachAppSpec(',
343
381
  ` android.content.ComponentName(${kotlinString(app.packageName)}, ${kotlinString(app.launcher.name)}),`,
344
382
  ` android.content.ComponentName(${kotlinString(app.packageName)}, ${kotlinString(app.launcher.targetClass)}),`,
345
- ' android.view.WindowTheme(',
346
- ` ${app.window.edgeToEdgeEnforced}, 0x${app.window.statusBarColor.toString(16).padStart(8, '0')}.toInt(),`,
347
- ` 0x${app.window.navigationBarColor.toString(16).padStart(8, '0')}.toInt(),`,
348
- ` ${app.window.statusBarContrastEnforced}, ${app.window.navigationBarContrastEnforced},`,
349
- ` ${app.window.lightStatusBar}, ${app.window.lightNavigationBar},`,
350
- ' ),',
383
+ ...windowTheme(app.window.day),
384
+ ...windowTheme(app.window.night),
351
385
  ` { ${app.applicationClass ?? 'android.app.Application'}() },`,
352
386
  ` { ${app.launcher.targetClass}() },`,
353
387
  ` ${app.handlesUiMode},`,
388
+ ' androidx.lifecycle.viewmodel.viewModelFactory {',
389
+ ...viewModels.map(viewModelInitializer),
390
+ ' },',
354
391
  ' ),',
355
392
  ' density, fontScale, locale, backend, stencilBits, regular, medium, bold, insets, night, invalidate,',
356
393
  ')',
@@ -364,7 +401,11 @@ export async function kotlinAppArtifact(
364
401
  '@JsExport',
365
402
  'fun peachAppNight(night: Boolean) = dev.peach.app.peachAppNight(night)',
366
403
  '',
367
- ].join('\n'),
404
+ ].join('\n')
405
+ const generated: Record<string, string> = {
406
+ 'PeachEntry.kt': entrySource([]),
407
+ 'PeachViews.kt': viewsSource([]),
408
+ 'PeachAppEntry.kt': appEntry([]),
368
409
  }
369
410
  const shims = [
370
411
  ...kotlinSources(path.join(moduleRoot, 'Sources')),
@@ -380,6 +421,11 @@ export async function kotlinAppArtifact(
380
421
  generated,
381
422
  shims,
382
423
  usesCompose: true,
424
+ viewModels: {
425
+ file: 'PeachAppEntry.kt',
426
+ render: appEntry,
427
+ script: path.join(moduleRoot, 'tools/abi-classes.main.kts'),
428
+ },
383
429
  },
384
430
  origin,
385
431
  )
@@ -394,10 +440,26 @@ async function compileKotlin(
394
440
  generated: Record<string, string>
395
441
  shims: string[]
396
442
  usesCompose: boolean
443
+ // a generated file that names the app's view models: rendered empty for a
444
+ // first compile, then from that klib's ABI
445
+ viewModels?: {
446
+ file: string
447
+ render: (classes: ViewModelClass[]) => string
448
+ script: string
449
+ }
397
450
  },
398
451
  origin: string,
399
452
  ): Promise<KotlinArtifact> {
400
- const { root, toolchain, language, sources, generated, shims, usesCompose } = input
453
+ const {
454
+ root,
455
+ toolchain,
456
+ language,
457
+ sources,
458
+ generated,
459
+ shims,
460
+ usesCompose,
461
+ viewModels,
462
+ } = input
401
463
  const digest = createHash('sha256')
402
464
  // BUILD_FORMAT moves when this host changes what it does to a build
403
465
  digest.update(JSON.stringify({ format: BUILD_FORMAT, toolchain, language }))
@@ -444,8 +506,8 @@ async function compileKotlin(
444
506
  '-libraries',
445
507
  libraries.join(path.delimiter),
446
508
  ]
447
- try {
448
- await run(
509
+ const compileKlib = (outputDir: string) =>
510
+ run(
449
511
  kotlinc,
450
512
  [
451
513
  ...common,
@@ -461,7 +523,7 @@ async function compileKotlin(
461
523
  '-nowarn',
462
524
  '-nopack',
463
525
  '-ir-output-dir',
464
- path.join(work, 'klib'),
526
+ outputDir,
465
527
  '-ir-output-name',
466
528
  'app',
467
529
  ...shims,
@@ -470,6 +532,32 @@ async function compileKotlin(
470
532
  ],
471
533
  { maxBuffer: 16 * 1024 * 1024 },
472
534
  )
535
+ try {
536
+ // the compiler resolves the app's classes and constructors: a first
537
+ // klib's ABI names the view models the generated factory builds. an app
538
+ // whose sources never name ViewModel declares none and skips it
539
+ if (
540
+ viewModels &&
541
+ sources.some((file) => fs.readFileSync(file, 'utf8').includes('ViewModel'))
542
+ ) {
543
+ await compileKlib(path.join(work, 'abi'))
544
+ const { stdout } = await run(
545
+ path.join(path.dirname(kotlinc), 'kotlinc'),
546
+ [
547
+ '-cp',
548
+ path.resolve(path.dirname(kotlinc), '../lib/kotlin-compiler.jar'),
549
+ '-script',
550
+ viewModels.script,
551
+ path.join(work, 'abi'),
552
+ ],
553
+ { maxBuffer: 64 * 1024 * 1024 },
554
+ )
555
+ fs.writeFileSync(
556
+ path.join(generatedDir, viewModels.file),
557
+ viewModels.render(viewModelClasses(readAbiClasses(stdout))),
558
+ )
559
+ }
560
+ await compileKlib(path.join(work, 'klib'))
473
561
  await run(
474
562
  kotlinc,
475
563
  [
@@ -0,0 +1,133 @@
1
+ // the ViewModel classes an app declares, read from its compiled klib's ABI
2
+ // (`kotlin/tools/abi-classes.main.kts`), each with the constructor androidx's
3
+ // SavedStateViewModelFactory finds by reflection: an AndroidViewModel takes a
4
+ // public (Application, SavedStateHandle) constructor, else AndroidViewModel
5
+ // Factory's public (Application) one; any other view model a public
6
+ // (SavedStateHandle) one, else NewInstanceFactory's no-argument one, which
7
+ // kotlin adds when every parameter has a default. Kotlin/Wasm has no
8
+ // reflection, so the host generates these calls. a class the factory cannot
9
+ // build is left out and fails when created, as it does on the device; the
10
+ // ABI lists public declarations only, so an internal class is left out too.
11
+ import { isRecord } from './is-record'
12
+
13
+ export type ViewModelSignature =
14
+ | 'none'
15
+ | 'handle'
16
+ | 'application'
17
+ | 'application-handle'
18
+ | 'misordered'
19
+
20
+ export interface ViewModelClass {
21
+ name: string
22
+ signature: ViewModelSignature
23
+ }
24
+
25
+ interface AbiClass {
26
+ name: string
27
+ kind: string
28
+ modality: string
29
+ inner: boolean
30
+ generic: boolean
31
+ supers: string[]
32
+ constructors: Array<{ parameters: string[]; defaulted: boolean }>
33
+ }
34
+
35
+ const VIEW_MODEL = 'androidx.lifecycle.ViewModel'
36
+ const ANDROID_VIEW_MODEL = 'androidx.lifecycle.AndroidViewModel'
37
+ const APPLICATION = 'android.app.Application'
38
+ const HANDLE = 'androidx.lifecycle.SavedStateHandle'
39
+
40
+ function strings(value: unknown): string[] {
41
+ if (!Array.isArray(value) || !value.every((item) => typeof item === 'string'))
42
+ throw new Error('abi-classes printed an invalid list')
43
+ return value
44
+ }
45
+
46
+ export function readAbiClasses(output: string): AbiClass[] {
47
+ return output
48
+ .split('\n')
49
+ .filter((line) => line.startsWith('{'))
50
+ .map((line) => {
51
+ const row: unknown = JSON.parse(line)
52
+ if (
53
+ !isRecord(row) ||
54
+ typeof row.name !== 'string' ||
55
+ typeof row.kind !== 'string' ||
56
+ typeof row.modality !== 'string' ||
57
+ typeof row.inner !== 'boolean' ||
58
+ typeof row.generic !== 'boolean' ||
59
+ !Array.isArray(row.constructors)
60
+ )
61
+ throw new Error(`abi-classes printed an invalid class: ${line}`)
62
+ return {
63
+ name: row.name,
64
+ kind: row.kind,
65
+ modality: row.modality,
66
+ inner: row.inner,
67
+ generic: row.generic,
68
+ supers: strings(row.supers),
69
+ constructors: row.constructors.map((constructor: unknown) => {
70
+ if (!isRecord(constructor) || typeof constructor.defaulted !== 'boolean')
71
+ throw new Error(`abi-classes printed an invalid constructor: ${line}`)
72
+ return {
73
+ parameters: strings(constructor.parameters),
74
+ defaulted: constructor.defaulted,
75
+ }
76
+ }),
77
+ }
78
+ })
79
+ }
80
+
81
+ // SavedStateViewModelFactory's findMatchingConstructor: an exact match, or an
82
+ // error when a constructor takes the same types in another order
83
+ function matching(row: AbiClass, signature: string[]): 'match' | 'misordered' | null {
84
+ for (const { parameters } of row.constructors) {
85
+ if (parameters.join() === signature.join()) return 'match'
86
+ if (
87
+ parameters.length === signature.length &&
88
+ signature.every((type) => parameters.includes(type))
89
+ )
90
+ return 'misordered'
91
+ }
92
+ return null
93
+ }
94
+
95
+ function signatureOf(row: AbiClass, android: boolean): ViewModelSignature | null {
96
+ const handled = matching(row, android ? [APPLICATION, HANDLE] : [HANDLE])
97
+ if (handled === 'misordered') return 'misordered'
98
+ if (handled) return android ? 'application-handle' : 'handle'
99
+ if (android)
100
+ return row.constructors.some(({ parameters }) => parameters.join() === APPLICATION)
101
+ ? 'application'
102
+ : null
103
+ return row.constructors.some(
104
+ ({ parameters, defaulted }) => parameters.length === 0 || defaulted,
105
+ )
106
+ ? 'none'
107
+ : null
108
+ }
109
+
110
+ export function viewModelClasses(rows: AbiClass[]): ViewModelClass[] {
111
+ const byName = new Map(rows.map((row) => [row.name, row]))
112
+ // the framework base a class reaches through the app's own classes
113
+ const base = (row: AbiClass, seen: Set<string>): string | null => {
114
+ for (const name of row.supers) {
115
+ if (name === VIEW_MODEL || name === ANDROID_VIEW_MODEL) return name
116
+ const parent = byName.get(name)
117
+ if (parent && !seen.has(name)) {
118
+ seen.add(name)
119
+ const found = base(parent, seen)
120
+ if (found) return found
121
+ }
122
+ }
123
+ return null
124
+ }
125
+ return rows.flatMap((row) => {
126
+ if (row.kind !== 'CLASS' || row.inner || row.generic) return []
127
+ if (row.modality === 'ABSTRACT' || row.modality === 'SEALED') return []
128
+ const root = base(row, new Set())
129
+ if (!root) return []
130
+ const signature = signatureOf(row, root === ANDROID_VIEW_MODEL)
131
+ return signature ? [{ name: row.name, signature }] : []
132
+ })
133
+ }
package/src/sdk.ts CHANGED
@@ -8,8 +8,9 @@
8
8
  export * from './sim-client'
9
9
  export * from '../cli/commands/inspect/core'
10
10
  export * from '../cli/commands/inspect/actions'
11
- export { waitForSootsimIdle } from '../cli/commands/inspect/settling'
11
+ export { waitForSootsimIdle, waitReadyAndSettle } from '../cli/commands/inspect/settling'
12
12
  export type {
13
+ SettledWaitReady,
13
14
  WaitForSootsimIdleOptions,
14
15
  WaitForSootsimIdleResult,
15
16
  } from '../cli/commands/inspect/settling'