peachsim 0.1.661 → 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 (75) hide show
  1. package/cli/commands/inspect/wait-ready.ts +10 -1
  2. package/dist-lib/agent-daemon-client.cjs +1 -1
  3. package/dist-lib/agent-events.cjs +1 -1
  4. package/dist-lib/agent-identity.cjs +1 -1
  5. package/dist-lib/agent-sessions.cjs +1 -1
  6. package/dist-lib/attached-projects.cjs +1 -1
  7. package/dist-lib/auth/shared-session.cjs +1 -1
  8. package/dist-lib/backend-origin.cjs +1 -1
  9. package/dist-lib/beta.cjs +1 -1
  10. package/dist-lib/beta.mjs +1 -1
  11. package/dist-lib/bridge-constants.cjs +1 -1
  12. package/dist-lib/bridge-contract-input.cjs +1 -1
  13. package/dist-lib/bridge-contract-input.mjs +1 -1
  14. package/dist-lib/bridge-contract.cjs +1 -1
  15. package/dist-lib/bridge-contract.mjs +1 -1
  16. package/dist-lib/capture-contract.cjs +1 -1
  17. package/dist-lib/capture-contract.mjs +1 -1
  18. package/dist-lib/cli-constants.cjs +1 -1
  19. package/dist-lib/cloud-contract.cjs +1 -1
  20. package/dist-lib/cloud-contract.mjs +1 -1
  21. package/dist-lib/cloud-test-args.cjs +1 -1
  22. package/dist-lib/cloud-test-args.mjs +1 -1
  23. package/dist-lib/cloud.cjs +1 -1
  24. package/dist-lib/cloud.mjs +1 -1
  25. package/dist-lib/config.cjs +1 -1
  26. package/dist-lib/detox/index.cjs +1 -1
  27. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  28. package/dist-lib/home-paths.cjs +1 -1
  29. package/dist-lib/host/bridge-host.cjs +1 -1
  30. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  31. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  32. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  33. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  34. package/dist-lib/host/websocket-proxy.cjs +1 -1
  35. package/dist-lib/index.cjs +1 -1
  36. package/dist-lib/jump-to-source-babel.cjs +1 -1
  37. package/dist-lib/jump-to-source-native.cjs +1 -1
  38. package/dist-lib/kotlin-host.cjs +191 -60
  39. package/dist-lib/menu.cjs +1 -1
  40. package/dist-lib/menu.mjs +1 -1
  41. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  42. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  43. package/dist-lib/metro-production-bundle.cjs +1 -1
  44. package/dist-lib/metro-production-bundle.mjs +1 -1
  45. package/dist-lib/metro.cjs +1 -1
  46. package/dist-lib/native-app-identity.cjs +1 -1
  47. package/dist-lib/native-app-identity.mjs +1 -1
  48. package/dist-lib/plist.cjs +1 -1
  49. package/dist-lib/plist.mjs +1 -1
  50. package/dist-lib/profiles.cjs +1 -1
  51. package/dist-lib/public-brand.cjs +1 -1
  52. package/dist-lib/react-native-host-modules.cjs +1 -1
  53. package/dist-lib/react-native-host-modules.mjs +1 -1
  54. package/dist-lib/render-mode.cjs +1 -1
  55. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  56. package/dist-lib/sdk.cjs +1 -1
  57. package/dist-lib/sdk.mjs +1 -1
  58. package/dist-lib/skills.cjs +1 -1
  59. package/dist-lib/swift-compile.cjs +1 -1
  60. package/dist-lib/swift-compile.mjs +1 -1
  61. package/dist-lib/swift-package.cjs +1 -1
  62. package/dist-lib/swift-package.mjs +1 -1
  63. package/dist-lib/swift-submit.cjs +1 -1
  64. package/dist-lib/swift-submit.mjs +1 -1
  65. package/dist-lib/swift.cjs +1 -1
  66. package/dist-lib/vite-swift.cjs +1 -1
  67. package/dist-lib/vite.cjs +1 -1
  68. package/kotlin/AppSources/androidx/activity/ActivityViewModelLazy.kt +21 -0
  69. package/kotlin/AppSources/androidx/activity/ComponentActivity.kt +5 -3
  70. package/kotlin/AppSources/androidx/lifecycle/AndroidViewModel.kt +10 -0
  71. package/kotlin/AppSources/dev/peach/app/PeachApp.kt +8 -0
  72. package/kotlin/tools/abi-classes.main.kts +41 -0
  73. package/package.json +1 -1
  74. package/src/kotlin-host.ts +91 -10
  75. package/src/kotlin-view-models.ts +133 -0
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.661 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.662 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.661 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.662 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/native-app-identity.ts
4
4
  var NATIVE_APP_IDENTITY_PREFIX = "globalThis.__sootsimNativeAppIdentity=";
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.661 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.662 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.661 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.662 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/plist.ts
4
4
  import plistModule from "@expo/plist";
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.661 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.662 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.661 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.662 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.661 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.662 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.661 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.662 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/react-native-host-modules.ts
4
4
  var REACT_NATIVE_PASSTHROUGH_SPECIFIERS = [
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.661 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.662 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.661 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.662 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
package/dist-lib/sdk.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.661 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.662 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
package/dist-lib/sdk.mjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.661 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.662 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  var __defProp = Object.defineProperty;
3
3
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
4
4
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.661 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.662 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.661 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.662 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.661 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.662 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/swift-compile.ts
4
4
  import { execFile as execFile2 } from "node:child_process";
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.661 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.662 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.661 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.662 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/swift-package.ts
4
4
  function isSwiftManifestFile(file) {
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.661 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.662 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.661 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.662 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/swift-submit.ts
4
4
  var SWIFT_COMPILE_PATH = "/api/swift-compile";
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.661 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.662 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.661 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.662 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
package/dist-lib/vite.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.661 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.662 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -0,0 +1,21 @@
1
+ package androidx.activity
2
+
3
+ import androidx.lifecycle.ViewModel
4
+ import androidx.lifecycle.ViewModelLazy
5
+ import androidx.lifecycle.ViewModelProvider
6
+ import androidx.lifecycle.viewmodel.CreationExtras
7
+
8
+ // activity-ktx's viewModels(): the activity's store, its default factory and
9
+ // creation extras unless the caller passes its own
10
+ inline fun <reified VM : ViewModel> ComponentActivity.viewModels(
11
+ noinline extrasProducer: (() -> CreationExtras)? = null,
12
+ noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null,
13
+ ): Lazy<VM> {
14
+ val factoryPromise = factoryProducer ?: { defaultViewModelProviderFactory }
15
+ return ViewModelLazy(
16
+ VM::class,
17
+ { viewModelStore },
18
+ factoryPromise,
19
+ { extrasProducer?.invoke() ?: this.defaultViewModelCreationExtras },
20
+ )
21
+ }
@@ -8,7 +8,6 @@ import androidx.lifecycle.LifecycleEventObserver
8
8
  import androidx.lifecycle.LifecycleOwner
9
9
  import androidx.lifecycle.LifecycleRegistry
10
10
  import androidx.lifecycle.SAVED_STATE_REGISTRY_OWNER_KEY
11
- import androidx.lifecycle.SavedStateViewModelFactory
12
11
  import androidx.lifecycle.VIEW_MODEL_STORE_OWNER_KEY
13
12
  import androidx.lifecycle.ViewModelStore
14
13
  import androidx.lifecycle.ViewModelStoreOwner
@@ -23,7 +22,8 @@ import androidx.savedstate.SavedStateRegistryOwner
23
22
  // lifecycle, viewmodel and savedstate libraries: the registry attaches in the
24
23
  // constructor and restores in onCreate before super, the lifecycle moves up on
25
24
  // the post callbacks and down on the pre callbacks, as ReportFragment does from
26
- // API 29, and destroy clears the view model store.
25
+ // API 29, and destroy clears the view model store unless the activity is
26
+ // changing configurations.
27
27
  open class ComponentActivity : Activity(), LifecycleOwner, ViewModelStoreOwner,
28
28
  HasDefaultViewModelProviderFactory, SavedStateRegistryOwner, OnBackPressedDispatcherOwner {
29
29
  private val lifecycleRegistry = LifecycleRegistry(this)
@@ -48,7 +48,9 @@ open class ComponentActivity : Activity(), LifecycleOwner, ViewModelStoreOwner,
48
48
  }
49
49
  }
50
50
  override val savedStateRegistry: SavedStateRegistry get() = savedStateRegistryController.savedStateRegistry
51
- override val defaultViewModelProviderFactory = SavedStateViewModelFactory()
51
+ // SavedStateViewModelFactory's constructor choice, made at compile time
52
+ // for the app's view models since Kotlin/Wasm has no reflection
53
+ override val defaultViewModelProviderFactory get() = dev.peach.app.viewModelFactory()
52
54
  override val defaultViewModelCreationExtras: CreationExtras
53
55
  get() = MutableCreationExtras().also {
54
56
  it[SAVED_STATE_REGISTRY_OWNER_KEY] = this
@@ -0,0 +1,10 @@
1
+ package androidx.lifecycle
2
+
3
+ import android.app.Application
4
+
5
+ // androidx.lifecycle's AndroidViewModel: a view model that holds its
6
+ // application, which the default factory passes to its constructor
7
+ open class AndroidViewModel(private val application: Application) : ViewModel() {
8
+ @Suppress("UNCHECKED_CAST")
9
+ open fun <T : Application> getApplication(): T = application as T
10
+ }
@@ -15,6 +15,7 @@ import android.view.ViewGroup
15
15
  import android.view.Window
16
16
  import android.view.WindowTheme
17
17
  import androidx.activity.ComponentActivity
18
+ import androidx.lifecycle.ViewModelProvider
18
19
  import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
19
20
  import androidx.compose.runtime.Composable
20
21
  import androidx.compose.runtime.CompositionLocalProvider
@@ -51,8 +52,15 @@ internal class PeachAppSpec(
51
52
  // the activity's configChanges includes uiMode: it takes a night mode
52
53
  // change in onConfigurationChanged rather than relaunching
53
54
  val handlesUiMode: Boolean,
55
+ // the app's view models, each built with the constructor the device's
56
+ // default factory finds for it
57
+ val viewModels: ViewModelProvider.Factory,
54
58
  )
55
59
 
60
+ internal fun viewModelFactory(): ViewModelProvider.Factory = runningApp().spec.viewModels
61
+
62
+ internal fun peachApplication(): Application = runningApp().application
63
+
56
64
  // the insets the device delivers to the window's content, in pixels: all of
57
65
  // them on an edge-to-edge window, none once the decor fits system windows.
58
66
  // insets lists the status bars, navigation bars, display cutout, system
@@ -0,0 +1,41 @@
1
+ // prints an app klib's classes as the compiler resolved them, one JSON object
2
+ // per line: qualified name, kind, modality, whether it is inner or generic,
3
+ // its supertypes and its public constructors' parameter types, each marked
4
+ // when every parameter has a default
5
+ @file:OptIn(org.jetbrains.kotlin.library.abi.ExperimentalLibraryAbiReader::class)
6
+
7
+ import org.jetbrains.kotlin.library.abi.*
8
+
9
+ fun name(qualified: AbiQualifiedName): String {
10
+ val pkg = qualified.packageName.value.replace('/', '.')
11
+ val relative = qualified.relativeName.value
12
+ return if (pkg.isEmpty()) relative else "$pkg.$relative"
13
+ }
14
+
15
+ fun type(type: AbiType): String =
16
+ ((type as? AbiType.Simple)?.classifierReference as? AbiClassifierReference.ClassReference)
17
+ ?.let { name(it.className) } ?: "?"
18
+
19
+ fun quote(value: String) = "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\""
20
+
21
+ fun walk(declaration: AbiDeclaration) {
22
+ if (declaration !is AbiClass) return
23
+ val constructors = declaration.declarations.filterIsInstance<AbiFunction>()
24
+ .filter { it.isConstructor }
25
+ .map { constructor ->
26
+ val parameters = constructor.valueParameters.joinToString(",", "[", "]") { quote(type(it.type)) }
27
+ "{\"parameters\":$parameters,\"defaulted\":${constructor.valueParameters.all { it.hasDefaultArg }}}"
28
+ }
29
+ println(
30
+ "{\"name\":${quote(name(declaration.qualifiedName))}" +
31
+ ",\"kind\":${quote(declaration.kind.name)}" +
32
+ ",\"modality\":${quote(declaration.modality.name)}" +
33
+ ",\"inner\":${declaration.isInner}" +
34
+ ",\"generic\":${declaration.typeParameters.isNotEmpty()}" +
35
+ ",\"supers\":${declaration.superTypes.joinToString(",", "[", "]") { quote(type(it)) }}" +
36
+ ",\"constructors\":${constructors.joinToString(",", "[", "]")}}",
37
+ )
38
+ declaration.declarations.forEach(::walk)
39
+ }
40
+
41
+ LibraryAbiReader.readAbiInfo(java.io.File(args[0])).topLevelDeclarations.declarations.forEach(::walk)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peachsim",
3
- "version": "0.1.661",
3
+ "version": "0.1.662",
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",
@@ -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,12 +356,12 @@ 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
367
  ' regular: String, medium: String, bold: String, insets: String, night: Boolean,',
@@ -351,6 +378,9 @@ export async function kotlinAppArtifact(
351
378
  ` { ${app.applicationClass ?? 'android.app.Application'}() },`,
352
379
  ` { ${app.launcher.targetClass}() },`,
353
380
  ` ${app.handlesUiMode},`,
381
+ ' androidx.lifecycle.viewmodel.viewModelFactory {',
382
+ ...viewModels.map(viewModelInitializer),
383
+ ' },',
354
384
  ' ),',
355
385
  ' density, fontScale, locale, backend, stencilBits, regular, medium, bold, insets, night, invalidate,',
356
386
  ')',
@@ -364,7 +394,11 @@ export async function kotlinAppArtifact(
364
394
  '@JsExport',
365
395
  'fun peachAppNight(night: Boolean) = dev.peach.app.peachAppNight(night)',
366
396
  '',
367
- ].join('\n'),
397
+ ].join('\n')
398
+ const generated: Record<string, string> = {
399
+ 'PeachEntry.kt': entrySource([]),
400
+ 'PeachViews.kt': viewsSource([]),
401
+ 'PeachAppEntry.kt': appEntry([]),
368
402
  }
369
403
  const shims = [
370
404
  ...kotlinSources(path.join(moduleRoot, 'Sources')),
@@ -380,6 +414,11 @@ export async function kotlinAppArtifact(
380
414
  generated,
381
415
  shims,
382
416
  usesCompose: true,
417
+ viewModels: {
418
+ file: 'PeachAppEntry.kt',
419
+ render: appEntry,
420
+ script: path.join(moduleRoot, 'tools/abi-classes.main.kts'),
421
+ },
383
422
  },
384
423
  origin,
385
424
  )
@@ -394,10 +433,26 @@ async function compileKotlin(
394
433
  generated: Record<string, string>
395
434
  shims: string[]
396
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
+ }
397
443
  },
398
444
  origin: string,
399
445
  ): Promise<KotlinArtifact> {
400
- 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
401
456
  const digest = createHash('sha256')
402
457
  // BUILD_FORMAT moves when this host changes what it does to a build
403
458
  digest.update(JSON.stringify({ format: BUILD_FORMAT, toolchain, language }))
@@ -444,8 +499,8 @@ async function compileKotlin(
444
499
  '-libraries',
445
500
  libraries.join(path.delimiter),
446
501
  ]
447
- try {
448
- await run(
502
+ const compileKlib = (outputDir: string) =>
503
+ run(
449
504
  kotlinc,
450
505
  [
451
506
  ...common,
@@ -461,7 +516,7 @@ async function compileKotlin(
461
516
  '-nowarn',
462
517
  '-nopack',
463
518
  '-ir-output-dir',
464
- path.join(work, 'klib'),
519
+ outputDir,
465
520
  '-ir-output-name',
466
521
  'app',
467
522
  ...shims,
@@ -470,6 +525,32 @@ async function compileKotlin(
470
525
  ],
471
526
  { maxBuffer: 16 * 1024 * 1024 },
472
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'))
473
554
  await run(
474
555
  kotlinc,
475
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
+ }