peachsim 0.1.660 → 0.1.662

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 (86) hide show
  1. package/README.md +10 -15
  2. package/cli/commands/inspect/wait-ready.ts +10 -1
  3. package/cli/drivers/linux-browser-libs.lock.json +1088 -0
  4. package/cli/drivers/linux-browser-libs.ts +182 -0
  5. package/cli/drivers/playwright-provisioning.ts +20 -98
  6. package/cli/drivers/playwright-sim-host.ts +9 -0
  7. package/cli/drivers/playwright.ts +2 -1
  8. package/dist-lib/agent-daemon-client.cjs +1 -1
  9. package/dist-lib/agent-events.cjs +1 -1
  10. package/dist-lib/agent-identity.cjs +1 -1
  11. package/dist-lib/agent-sessions.cjs +1 -1
  12. package/dist-lib/attached-projects.cjs +1 -1
  13. package/dist-lib/auth/shared-session.cjs +1 -1
  14. package/dist-lib/backend-origin.cjs +18 -1
  15. package/dist-lib/beta.cjs +1 -1
  16. package/dist-lib/beta.mjs +1 -1
  17. package/dist-lib/bridge-constants.cjs +1 -1
  18. package/dist-lib/bridge-contract-input.cjs +1 -1
  19. package/dist-lib/bridge-contract-input.mjs +1 -1
  20. package/dist-lib/bridge-contract.cjs +1 -1
  21. package/dist-lib/bridge-contract.mjs +1 -1
  22. package/dist-lib/capture-contract.cjs +1 -1
  23. package/dist-lib/capture-contract.mjs +1 -1
  24. package/dist-lib/cli-constants.cjs +1 -1
  25. package/dist-lib/cloud-contract.cjs +1 -1
  26. package/dist-lib/cloud-contract.mjs +1 -1
  27. package/dist-lib/cloud-test-args.cjs +1 -1
  28. package/dist-lib/cloud-test-args.mjs +1 -1
  29. package/dist-lib/cloud.cjs +1 -1
  30. package/dist-lib/cloud.mjs +1 -1
  31. package/dist-lib/config.cjs +1 -1
  32. package/dist-lib/detox/index.cjs +1 -1
  33. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  34. package/dist-lib/home-paths.cjs +1 -1
  35. package/dist-lib/host/bridge-host.cjs +45 -41
  36. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  37. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  38. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  39. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  40. package/dist-lib/host/websocket-proxy.cjs +1 -1
  41. package/dist-lib/index.cjs +1 -1
  42. package/dist-lib/jump-to-source-babel.cjs +1 -1
  43. package/dist-lib/jump-to-source-native.cjs +1 -1
  44. package/dist-lib/kotlin-host.cjs +191 -59
  45. package/dist-lib/menu.cjs +1 -1
  46. package/dist-lib/menu.mjs +1 -1
  47. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  48. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  49. package/dist-lib/metro-production-bundle.cjs +1 -1
  50. package/dist-lib/metro-production-bundle.mjs +1 -1
  51. package/dist-lib/metro.cjs +1 -1
  52. package/dist-lib/native-app-identity.cjs +1 -1
  53. package/dist-lib/native-app-identity.mjs +1 -1
  54. package/dist-lib/plist.cjs +1 -1
  55. package/dist-lib/plist.mjs +1 -1
  56. package/dist-lib/profiles.cjs +1 -1
  57. package/dist-lib/public-brand.cjs +1 -1
  58. package/dist-lib/react-native-host-modules.cjs +1 -1
  59. package/dist-lib/react-native-host-modules.mjs +1 -1
  60. package/dist-lib/render-mode.cjs +1 -1
  61. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  62. package/dist-lib/sdk.cjs +1 -1
  63. package/dist-lib/sdk.mjs +1 -1
  64. package/dist-lib/skills.cjs +1332 -177
  65. package/dist-lib/swift-compile.cjs +1 -1
  66. package/dist-lib/swift-compile.mjs +1 -1
  67. package/dist-lib/swift-package.cjs +1 -1
  68. package/dist-lib/swift-package.mjs +1 -1
  69. package/dist-lib/swift-submit.cjs +1 -1
  70. package/dist-lib/swift-submit.mjs +1 -1
  71. package/dist-lib/swift.cjs +1 -1
  72. package/dist-lib/vite-swift.cjs +1 -1
  73. package/dist-lib/vite.cjs +1 -1
  74. package/kotlin/AppSources/android/view/Window.kt +15 -9
  75. package/kotlin/AppSources/androidx/activity/ActivityViewModelLazy.kt +21 -0
  76. package/kotlin/AppSources/androidx/activity/ComponentActivity.kt +5 -3
  77. package/kotlin/AppSources/androidx/lifecycle/AndroidViewModel.kt +10 -0
  78. package/kotlin/AppSources/dev/peach/app/PeachApp.kt +37 -16
  79. package/kotlin/tools/abi-classes.main.kts +41 -0
  80. package/package.json +2 -1
  81. package/src/android-theme.ts +14 -0
  82. package/src/backend-origin.ts +25 -0
  83. package/src/host/bridge-host.ts +24 -37
  84. package/src/kotlin-app.ts +22 -4
  85. package/src/kotlin-host.ts +94 -12
  86. package/src/kotlin-view-models.ts +133 -0
@@ -12,6 +12,11 @@ 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 {
16
+ readAbiClasses,
17
+ viewModelClasses,
18
+ type ViewModelClass,
19
+ } from './kotlin-view-models'
15
20
  import type { KotlinAppProject } from './kotlin-app'
16
21
  import type { ServerResponse } from 'node:http'
17
22
 
@@ -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,30 @@ 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
+
323
349
  // a pure Kotlin app: its module's sources, the Android platform actuals and
324
350
  // the compose host, with an entry that cold-starts the resolved launcher
351
+
325
352
  export async function kotlinAppArtifact(
326
353
  app: KotlinAppProject,
327
354
  moduleRoot: string,
@@ -329,29 +356,33 @@ export async function kotlinAppArtifact(
329
356
  ): Promise<KotlinArtifact> {
330
357
  const toolchain = readToolchain(moduleRoot)
331
358
  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': [
359
+ const appEntry = (viewModels: ViewModelClass[]) =>
360
+ [
336
361
  '@file:OptIn(kotlin.js.ExperimentalJsExport::class)',
337
362
  '',
363
+ 'import androidx.lifecycle.createSavedStateHandle',
364
+ '',
338
365
  '@JsExport',
339
366
  'fun peachAppMount(density: Float, fontScale: Float, locale: String, backend: String, stencilBits: Int,',
340
- ' regular: String, medium: String, bold: String, statusBar: Int, navigationBar: Int, night: Boolean,',
367
+ ' regular: String, medium: String, bold: String, insets: String, night: Boolean,',
341
368
  ' invalidate: (Boolean) -> Unit): String = dev.peach.app.peachStartApp(',
342
369
  ' dev.peach.app.PeachAppSpec(',
343
370
  ` android.content.ComponentName(${kotlinString(app.packageName)}, ${kotlinString(app.launcher.name)}),`,
344
371
  ` android.content.ComponentName(${kotlinString(app.packageName)}, ${kotlinString(app.launcher.targetClass)}),`,
345
372
  ' android.view.WindowTheme(',
346
373
  ` ${app.window.edgeToEdgeEnforced}, 0x${app.window.statusBarColor.toString(16).padStart(8, '0')}.toInt(),`,
374
+ ` 0x${app.window.navigationBarColor.toString(16).padStart(8, '0')}.toInt(),`,
347
375
  ` ${app.window.statusBarContrastEnforced}, ${app.window.navigationBarContrastEnforced},`,
348
376
  ` ${app.window.lightStatusBar}, ${app.window.lightNavigationBar},`,
349
377
  ' ),',
350
378
  ` { ${app.applicationClass ?? 'android.app.Application'}() },`,
351
379
  ` { ${app.launcher.targetClass}() },`,
352
380
  ` ${app.handlesUiMode},`,
381
+ ' androidx.lifecycle.viewmodel.viewModelFactory {',
382
+ ...viewModels.map(viewModelInitializer),
383
+ ' },',
353
384
  ' ),',
354
- ' density, fontScale, locale, backend, stencilBits, regular, medium, bold, statusBar, navigationBar, night, invalidate,',
385
+ ' density, fontScale, locale, backend, stencilBits, regular, medium, bold, insets, night, invalidate,',
355
386
  ')',
356
387
  '',
357
388
  '@JsExport',
@@ -363,7 +394,11 @@ export async function kotlinAppArtifact(
363
394
  '@JsExport',
364
395
  'fun peachAppNight(night: Boolean) = dev.peach.app.peachAppNight(night)',
365
396
  '',
366
- ].join('\n'),
397
+ ].join('\n')
398
+ const generated: Record<string, string> = {
399
+ 'PeachEntry.kt': entrySource([]),
400
+ 'PeachViews.kt': viewsSource([]),
401
+ 'PeachAppEntry.kt': appEntry([]),
367
402
  }
368
403
  const shims = [
369
404
  ...kotlinSources(path.join(moduleRoot, 'Sources')),
@@ -379,6 +414,11 @@ export async function kotlinAppArtifact(
379
414
  generated,
380
415
  shims,
381
416
  usesCompose: true,
417
+ viewModels: {
418
+ file: 'PeachAppEntry.kt',
419
+ render: appEntry,
420
+ script: path.join(moduleRoot, 'tools/abi-classes.main.kts'),
421
+ },
382
422
  },
383
423
  origin,
384
424
  )
@@ -393,10 +433,26 @@ async function compileKotlin(
393
433
  generated: Record<string, string>
394
434
  shims: string[]
395
435
  usesCompose: boolean
436
+ // a generated file that names the app's view models: rendered empty for a
437
+ // first compile, then from that klib's ABI
438
+ viewModels?: {
439
+ file: string
440
+ render: (classes: ViewModelClass[]) => string
441
+ script: string
442
+ }
396
443
  },
397
444
  origin: string,
398
445
  ): Promise<KotlinArtifact> {
399
- const { root, toolchain, language, sources, generated, shims, usesCompose } = input
446
+ const {
447
+ root,
448
+ toolchain,
449
+ language,
450
+ sources,
451
+ generated,
452
+ shims,
453
+ usesCompose,
454
+ viewModels,
455
+ } = input
400
456
  const digest = createHash('sha256')
401
457
  // BUILD_FORMAT moves when this host changes what it does to a build
402
458
  digest.update(JSON.stringify({ format: BUILD_FORMAT, toolchain, language }))
@@ -443,8 +499,8 @@ async function compileKotlin(
443
499
  '-libraries',
444
500
  libraries.join(path.delimiter),
445
501
  ]
446
- try {
447
- await run(
502
+ const compileKlib = (outputDir: string) =>
503
+ run(
448
504
  kotlinc,
449
505
  [
450
506
  ...common,
@@ -460,7 +516,7 @@ async function compileKotlin(
460
516
  '-nowarn',
461
517
  '-nopack',
462
518
  '-ir-output-dir',
463
- path.join(work, 'klib'),
519
+ outputDir,
464
520
  '-ir-output-name',
465
521
  'app',
466
522
  ...shims,
@@ -469,6 +525,32 @@ async function compileKotlin(
469
525
  ],
470
526
  { maxBuffer: 16 * 1024 * 1024 },
471
527
  )
528
+ try {
529
+ // the compiler resolves the app's classes and constructors: a first
530
+ // klib's ABI names the view models the generated factory builds. an app
531
+ // whose sources never name ViewModel declares none and skips it
532
+ if (
533
+ viewModels &&
534
+ sources.some((file) => fs.readFileSync(file, 'utf8').includes('ViewModel'))
535
+ ) {
536
+ await compileKlib(path.join(work, 'abi'))
537
+ const { stdout } = await run(
538
+ path.join(path.dirname(kotlinc), 'kotlinc'),
539
+ [
540
+ '-cp',
541
+ path.resolve(path.dirname(kotlinc), '../lib/kotlin-compiler.jar'),
542
+ '-script',
543
+ viewModels.script,
544
+ path.join(work, 'abi'),
545
+ ],
546
+ { maxBuffer: 64 * 1024 * 1024 },
547
+ )
548
+ fs.writeFileSync(
549
+ path.join(generatedDir, viewModels.file),
550
+ viewModels.render(viewModelClasses(readAbiClasses(stdout))),
551
+ )
552
+ }
553
+ await compileKlib(path.join(work, 'klib'))
472
554
  await run(
473
555
  kotlinc,
474
556
  [
@@ -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
+ }