peachsim 0.1.649 → 0.1.650

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 (102) hide show
  1. package/README.md +10 -9
  2. package/cli/command-registry.ts +2 -1
  3. package/cli/commands/guest-host.ts +67 -0
  4. package/cli/commands/kotlin.ts +231 -0
  5. package/cli/commands/project-report.ts +1 -1
  6. package/cli/commands/report-issue.ts +55 -32
  7. package/cli/commands/setup.ts +10 -14
  8. package/cli/commands/support.ts +399 -0
  9. package/cli/commands/swift.ts +6 -52
  10. package/cli/commands/telemetry.ts +10 -13
  11. package/cli/main.ts +22 -10
  12. package/cli/outbound-endpoints.ts +23 -2
  13. package/cli/privacy.ts +23 -25
  14. package/cli/runtime-notes.ts +3 -2
  15. package/cli/telemetry.ts +109 -14
  16. package/dist-lib/agent-daemon-client.cjs +1 -1
  17. package/dist-lib/agent-events.cjs +1 -1
  18. package/dist-lib/agent-identity.cjs +1 -1
  19. package/dist-lib/agent-sessions.cjs +1 -1
  20. package/dist-lib/attached-projects.cjs +1 -1
  21. package/dist-lib/auth/shared-session.cjs +1 -1
  22. package/dist-lib/backend-origin.cjs +1 -1
  23. package/dist-lib/beta.cjs +1 -1
  24. package/dist-lib/beta.mjs +1 -1
  25. package/dist-lib/bridge-constants.cjs +1 -1
  26. package/dist-lib/bridge-contract-input.cjs +1 -1
  27. package/dist-lib/bridge-contract-input.mjs +1 -1
  28. package/dist-lib/bridge-contract.cjs +1 -1
  29. package/dist-lib/bridge-contract.mjs +1 -1
  30. package/dist-lib/capture-contract.cjs +1 -1
  31. package/dist-lib/capture-contract.mjs +1 -1
  32. package/dist-lib/cli-constants.cjs +1 -1
  33. package/dist-lib/cloud-contract.cjs +1 -1
  34. package/dist-lib/cloud-contract.mjs +1 -1
  35. package/dist-lib/cloud-test-args.cjs +1 -1
  36. package/dist-lib/cloud-test-args.mjs +1 -1
  37. package/dist-lib/cloud.cjs +1 -1
  38. package/dist-lib/cloud.mjs +1 -1
  39. package/dist-lib/config.cjs +1 -1
  40. package/dist-lib/detox/index.cjs +1 -1
  41. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  42. package/dist-lib/home-paths.cjs +3 -3
  43. package/dist-lib/host/bridge-host.cjs +1 -1
  44. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  45. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  46. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  47. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  48. package/dist-lib/host/websocket-proxy.cjs +1 -1
  49. package/dist-lib/index.cjs +1 -1
  50. package/dist-lib/jump-to-source-babel.cjs +1 -1
  51. package/dist-lib/jump-to-source-native.cjs +1 -1
  52. package/dist-lib/kotlin-host.cjs +93 -28
  53. package/dist-lib/menu.cjs +1 -1
  54. package/dist-lib/menu.mjs +1 -1
  55. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  56. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  57. package/dist-lib/metro-production-bundle.cjs +1 -1
  58. package/dist-lib/metro-production-bundle.mjs +1 -1
  59. package/dist-lib/metro.cjs +1 -1
  60. package/dist-lib/native-app-identity.cjs +1 -1
  61. package/dist-lib/native-app-identity.mjs +1 -1
  62. package/dist-lib/profiles.cjs +1 -1
  63. package/dist-lib/public-brand.cjs +1 -1
  64. package/dist-lib/react-native-host-modules.cjs +1 -1
  65. package/dist-lib/react-native-host-modules.mjs +1 -1
  66. package/dist-lib/render-mode.cjs +1 -1
  67. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  68. package/dist-lib/sdk.cjs +1 -1
  69. package/dist-lib/sdk.mjs +1 -1
  70. package/dist-lib/skills.cjs +15 -26
  71. package/dist-lib/swift-compile.cjs +6 -3
  72. package/dist-lib/swift-compile.mjs +6 -3
  73. package/dist-lib/swift-package.cjs +1 -1
  74. package/dist-lib/swift-package.mjs +1 -1
  75. package/dist-lib/swift-submit.cjs +1 -1
  76. package/dist-lib/swift-submit.mjs +1 -1
  77. package/dist-lib/swift.cjs +1 -1
  78. package/dist-lib/vite-swift.cjs +6 -3
  79. package/dist-lib/vite.cjs +1 -1
  80. package/kotlin/AppSources/android/app/Activity.kt +85 -0
  81. package/kotlin/AppSources/android/app/Application.kt +7 -0
  82. package/kotlin/AppSources/android/content/Context.kt +5 -0
  83. package/kotlin/AppSources/android/content/Intent.kt +16 -0
  84. package/kotlin/AppSources/android/os/Bundle.kt +5 -0
  85. package/kotlin/AppSources/android/util/SuperNotCalledException.kt +3 -0
  86. package/kotlin/AppSources/android/view/View.kt +72 -0
  87. package/kotlin/AppSources/android/view/Window.kt +44 -0
  88. package/kotlin/AppSources/android/widget/FrameLayout.kt +6 -0
  89. package/kotlin/AppSources/androidx/activity/ComponentActivity.kt +57 -0
  90. package/kotlin/AppSources/androidx/activity/compose/ComponentActivity.kt +29 -0
  91. package/kotlin/AppSources/androidx/compose/ui/platform/ComposeView.kt +67 -0
  92. package/kotlin/AppSources/dev/peach/app/PeachApp.kt +136 -0
  93. package/kotlin/AppSources/dev/peach/app/PeachWindowHost.kt +13 -0
  94. package/kotlin/ComposeSources/PeachComposeHost.kt +58 -37
  95. package/package.json +1 -1
  96. package/skills/contrast/SKILL.md +2 -1
  97. package/skills/peach-setup/SKILL.md +2 -2
  98. package/src/{swift-guest.ts → guest-bundle.ts} +22 -7
  99. package/src/home-paths.ts +5 -3
  100. package/src/kotlin-app.ts +411 -0
  101. package/src/kotlin-host.ts +139 -32
  102. package/src/swift-compile.ts +7 -2
@@ -12,6 +12,7 @@ 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
16
  import type { ServerResponse } from 'node:http'
16
17
 
17
18
  const run = promisify(execFile)
@@ -192,11 +193,16 @@ const SKIP_DIRS = new Set([
192
193
  'scripts',
193
194
  ])
194
195
 
195
- export function kotlinSources(root: string): string[] {
196
+ // peach's own platform sources live in package dirs such as `android`, so
197
+ // they walk with no skip set
198
+ export function kotlinSources(
199
+ root: string,
200
+ skip: ReadonlySet<string> = SKIP_DIRS,
201
+ ): string[] {
196
202
  const sources: string[] = []
197
203
  const walk = (dir: string) => {
198
204
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
199
- if (entry.name.startsWith('.') || SKIP_DIRS.has(entry.name)) continue
205
+ if (entry.name.startsWith('.') || skip.has(entry.name)) continue
200
206
  const full = path.join(dir, entry.name)
201
207
  if (entry.isDirectory()) walk(full)
202
208
  else if (entry.isFile() && entry.name.endsWith('.kt')) sources.push(full)
@@ -256,7 +262,7 @@ function entrySource(ids: string[]): string {
256
262
  ].join('\n')
257
263
  }
258
264
 
259
- const BUILD_FORMAT = 5
265
+ const BUILD_FORMAT = 7
260
266
  const builds = new Map<string, Promise<KotlinArtifact>>()
261
267
 
262
268
  function buildsDir(): string {
@@ -291,21 +297,92 @@ export async function kotlinArtifact(
291
297
  generated[`OneNativeSource_${id}.kt`] = renderKotlinSourceGlue(id, contract).source
292
298
  }
293
299
  generated['PeachEntry.kt'] = entrySource(ids)
294
- if (viewIds.length > 0) {
295
- generated['PeachViews.kt'] = [
296
- 'import dev.onejs.one.OneNativeSourceViewDispatch',
297
- '',
298
- 'fun peachViewDispatcher(source: String): OneNativeSourceViewDispatch = when (source) {',
299
- ...viewIds.map((id) => ` ${JSON.stringify(id)} -> OneNativeSourceViews_${id}()`),
300
- ' else -> error("composable source $source is not linked; rebuild the app")',
301
- '}',
302
- ].join('\n')
303
- }
300
+ if (viewIds.length > 0) generated['PeachViews.kt'] = viewsSource(viewIds)
304
301
  const shims = kotlinSources(path.join(moduleRoot, 'Sources')).filter(
305
302
  (file) => usesCompose || !file.endsWith('/OneNativeSourceViewDispatch.kt'),
306
303
  )
307
304
  if (viewIds.length > 0)
308
305
  shims.push(...kotlinSources(path.join(moduleRoot, 'ComposeSources')))
306
+ return compileKotlin(
307
+ { root, toolchain, language, sources, generated, shims, usesCompose },
308
+ origin,
309
+ )
310
+ }
311
+
312
+ function viewsSource(viewIds: string[]): string {
313
+ return [
314
+ 'import dev.onejs.one.OneNativeSourceViewDispatch',
315
+ '',
316
+ 'fun peachViewDispatcher(source: String): OneNativeSourceViewDispatch = when (source) {',
317
+ ...viewIds.map((id) => ` ${JSON.stringify(id)} -> OneNativeSourceViews_${id}()`),
318
+ ' else -> error("composable source $source is not linked; rebuild the app")',
319
+ '}',
320
+ ].join('\n')
321
+ }
322
+
323
+ // a pure Kotlin app: its module's sources, the Android platform actuals and
324
+ // the compose host, with an entry that cold-starts the resolved launcher
325
+ export async function kotlinAppArtifact(
326
+ app: KotlinAppProject,
327
+ moduleRoot: string,
328
+ origin: string,
329
+ ): Promise<KotlinArtifact> {
330
+ const toolchain = readToolchain(moduleRoot)
331
+ 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': [
336
+ '@file:OptIn(kotlin.js.ExperimentalJsExport::class)',
337
+ '',
338
+ '@JsExport',
339
+ 'fun peachAppMount(density: Float, fontScale: Float, locale: String, backend: String, stencilBits: Int,',
340
+ ' regular: String, medium: String, bold: String, statusBar: Int, navigationBar: Int,',
341
+ ' invalidate: (Boolean) -> Unit): String = dev.peach.app.peachStartApp(',
342
+ ' dev.peach.app.PeachAppSpec(',
343
+ ` android.content.ComponentName(${kotlinString(app.packageName)}, ${kotlinString(app.launcher.name)}),`,
344
+ ` android.content.ComponentName(${kotlinString(app.packageName)}, ${kotlinString(app.launcher.targetClass)}),`,
345
+ ` ${app.targetSdk}, ${app.optOutEdgeToEdge},`,
346
+ ` { ${app.applicationClass ?? 'android.app.Application'}() },`,
347
+ ` { ${app.launcher.targetClass}() },`,
348
+ ' ),',
349
+ ' density, fontScale, locale, backend, stencilBits, regular, medium, bold, statusBar, navigationBar, invalidate,',
350
+ ')',
351
+ '',
352
+ ].join('\n'),
353
+ }
354
+ const shims = [
355
+ ...kotlinSources(path.join(moduleRoot, 'Sources')),
356
+ ...kotlinSources(path.join(moduleRoot, 'ComposeSources')),
357
+ ...kotlinSources(path.join(moduleRoot, 'AppSources'), new Set()),
358
+ ]
359
+ return compileKotlin(
360
+ {
361
+ root: app.projectDir,
362
+ toolchain,
363
+ language: app.language,
364
+ sources: app.sources,
365
+ generated,
366
+ shims,
367
+ usesCompose: true,
368
+ },
369
+ origin,
370
+ )
371
+ }
372
+
373
+ async function compileKotlin(
374
+ input: {
375
+ root: string
376
+ toolchain: KotlinToolchain
377
+ language: string
378
+ sources: string[]
379
+ generated: Record<string, string>
380
+ shims: string[]
381
+ usesCompose: boolean
382
+ },
383
+ origin: string,
384
+ ): Promise<KotlinArtifact> {
385
+ const { root, toolchain, language, sources, generated, shims, usesCompose } = input
309
386
  const digest = createHash('sha256')
310
387
  // BUILD_FORMAT moves when this host changes what it does to a build
311
388
  digest.update(JSON.stringify({ format: BUILD_FORMAT, toolchain, language }))
@@ -440,25 +517,46 @@ export async function kotlinArtifact(
440
517
  `kotlin compile failed:\n${output.trim() || (error instanceof Error ? error.message : String(error))}`,
441
518
  )
442
519
  }
443
- // Kotlin's loader picks node when `process.release.name` says so; the
444
- // Peach tenant defines a react native `process` with no release, so that
445
- // read throws before the browser branch runs. the loader keeps its
446
- // browser path when the read is optional.
447
- let patched = 0
448
- for (const file of fs.readdirSync(path.join(work, 'js'))) {
449
- if (!file.endsWith('.mjs')) continue
450
- const target = path.join(work, 'js', file)
451
- const text = fs.readFileSync(target, 'utf8')
452
- const next = text.replaceAll('process.release.name', 'process.release?.name')
453
- if (next !== text) {
454
- patched++
455
- fs.writeFileSync(target, next)
520
+ // the Kotlin module shares the tenant's react native globals, which a
521
+ // Kotlin process never sees. Kotlin's loader picks node when
522
+ // `process.release.name` says so, and react native's `process` has no
523
+ // release, so the read becomes optional. kotlinx-coroutines dispatches
524
+ // through `window.postMessage(msg, '*')` when it finds a window with
525
+ // `addEventListener`, which the tenant's window has and a worker cannot
526
+ // honor, so the module takes the setTimeout dispatcher a worker gets.
527
+ const seams: Array<[string, string]> = [
528
+ ['process.release.name', 'process.release?.name'],
529
+ ]
530
+ const js = fs
531
+ .readdirSync(path.join(work, 'js'))
532
+ .filter((file) => file.endsWith('.mjs'))
533
+ if (
534
+ js.some((file) =>
535
+ fs
536
+ .readFileSync(path.join(work, 'js', file), 'utf8')
537
+ .includes("'kotlinx.coroutines."),
538
+ )
539
+ )
540
+ seams.push([
541
+ "(typeof(window) !== 'undefined' && window != null && typeof(window.addEventListener) === 'function') ? window : null",
542
+ 'null',
543
+ ])
544
+ for (const [from, to] of seams) {
545
+ let patched = 0
546
+ for (const file of js) {
547
+ const target = path.join(work, 'js', file)
548
+ const text = fs.readFileSync(target, 'utf8')
549
+ const next = text.replaceAll(from, to)
550
+ if (next !== text) {
551
+ patched++
552
+ fs.writeFileSync(target, next)
553
+ }
456
554
  }
555
+ if (patched === 0)
556
+ throw new Error(
557
+ `kotlin output no longer contains ${from}; recheck how it detects its host in Peach`,
558
+ )
457
559
  }
458
- if (patched === 0)
459
- throw new Error(
460
- 'kotlin loader no longer reads process.release.name; recheck its node detection in Peach',
461
- )
462
560
  fs.renameSync(work, out)
463
561
  return artifact
464
562
  })().finally(() => builds.delete(hash))
@@ -518,13 +616,22 @@ export function peachKotlinModuleRoot(root: string): string {
518
616
  export async function serveKotlinRequest(
519
617
  pathname: string,
520
618
  res: ServerResponse,
521
- app: { root: string; moduleRoot: string; origin: string; hmrPath: string },
619
+ app: {
620
+ root: string
621
+ moduleRoot: string
622
+ origin: string
623
+ hmrPath: string
624
+ // a pure Kotlin app (peach kotlin); otherwise the root's imported sources
625
+ project?: KotlinAppProject
626
+ },
522
627
  ): Promise<boolean> {
523
628
  if (serveKotlinArtifact(pathname, res)) return true
524
629
  if (pathname !== KOTLIN_PACKAGE_PATH) return false
525
630
  res.setHeader('access-control-allow-origin', '*')
526
631
  try {
527
- const artifact = await kotlinArtifact(app.root, app.moduleRoot, app.origin)
632
+ const artifact = app.project
633
+ ? await kotlinAppArtifact(app.project, app.moduleRoot, app.origin)
634
+ : await kotlinArtifact(app.root, app.moduleRoot, app.origin)
528
635
  res.setHeader('content-type', 'application/json')
529
636
  res.end(JSON.stringify({ ...artifact, hmrPath: app.hmrPath }))
530
637
  } catch (error) {
@@ -529,6 +529,11 @@ export function writeSwiftProject(
529
529
  ): void {
530
530
  const targets = compileTargets(input)
531
531
  const files = new Map<string, string>()
532
+ // swiftpm plans a build root's build once and reuses the plan until a
533
+ // manifest changes, so a file added to the module would never compile
534
+ // there (RAN: a broken new UIKitCore file built "complete" in 0.1s). the
535
+ // module's file list in the manifest replans exactly when it changes.
536
+ const moduleFiles = `\n// sootsim-swift files ${createHash('sha256').update(swiftModuleInputFiles(moduleRoot).join('\n')).digest('hex').slice(0, 16)}\n`
532
537
  if (targets.length === 0) {
533
538
  for (const [file, contents] of Object.entries(input.files)) {
534
539
  if (file === 'Package.swift') continue
@@ -552,7 +557,7 @@ export function writeSwiftProject(
552
557
  .replace(product, `${product}\n ${ONE_PRODUCT},`)
553
558
  .replace(flags, `${flags}${ONE_IMPLICIT_FLAGS}`)
554
559
  }
555
- files.set(path.join(layout.root, 'Package.swift'), manifest)
560
+ files.set(path.join(layout.root, 'Package.swift'), manifest + moduleFiles)
556
561
  } else {
557
562
  for (const [file, contents] of Object.entries(input.files)) {
558
563
  if (file === 'Package.swift') continue
@@ -582,7 +587,7 @@ export function writeSwiftProject(
582
587
  siblings: swiftSiblingDependencies(input.files, targets),
583
588
  moduleRoot,
584
589
  oneModule: input.oneModule,
585
- }),
590
+ }) + moduleFiles,
586
591
  )
587
592
  }
588
593
  syncSwiftTree(layout.root, files)