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.
- package/README.md +10 -15
- package/cli/commands/inspect/wait-ready.ts +10 -1
- package/cli/drivers/linux-browser-libs.lock.json +1088 -0
- package/cli/drivers/linux-browser-libs.ts +182 -0
- package/cli/drivers/playwright-provisioning.ts +20 -98
- package/cli/drivers/playwright-sim-host.ts +9 -0
- package/cli/drivers/playwright.ts +2 -1
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +18 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.mjs +1 -1
- package/dist-lib/cloud-test-args.cjs +1 -1
- package/dist-lib/cloud-test-args.mjs +1 -1
- package/dist-lib/cloud.cjs +1 -1
- package/dist-lib/cloud.mjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +45 -41
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +1 -1
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/kotlin-host.cjs +191 -59
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/native-app-identity.cjs +1 -1
- package/dist-lib/native-app-identity.mjs +1 -1
- package/dist-lib/plist.cjs +1 -1
- package/dist-lib/plist.mjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +1 -1
- package/dist-lib/sdk.mjs +1 -1
- package/dist-lib/skills.cjs +1332 -177
- package/dist-lib/swift-compile.cjs +1 -1
- package/dist-lib/swift-compile.mjs +1 -1
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +1 -1
- package/dist-lib/swift-submit.mjs +1 -1
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +1 -1
- package/dist-lib/vite.cjs +1 -1
- package/kotlin/AppSources/android/view/Window.kt +15 -9
- package/kotlin/AppSources/androidx/activity/ActivityViewModelLazy.kt +21 -0
- package/kotlin/AppSources/androidx/activity/ComponentActivity.kt +5 -3
- package/kotlin/AppSources/androidx/lifecycle/AndroidViewModel.kt +10 -0
- package/kotlin/AppSources/dev/peach/app/PeachApp.kt +37 -16
- package/kotlin/tools/abi-classes.main.kts +41 -0
- package/package.json +2 -1
- package/src/android-theme.ts +14 -0
- package/src/backend-origin.ts +25 -0
- package/src/host/bridge-host.ts +24 -37
- package/src/kotlin-app.ts +22 -4
- package/src/kotlin-host.ts +94 -12
- package/src/kotlin-view-models.ts +133 -0
package/src/kotlin-host.ts
CHANGED
|
@@ -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 =
|
|
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
|
|
333
|
-
|
|
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,
|
|
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,
|
|
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 {
|
|
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
|
-
|
|
447
|
-
|
|
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
|
-
|
|
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
|
+
}
|