peachsim 0.1.578 → 0.1.579

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 (178) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +11 -9
  3. package/cli/app-catalog.ts +77 -0
  4. package/cli/app-fonts.ts +4 -20
  5. package/cli/app-splash.ts +2 -18
  6. package/cli/bridge-flow-runner.ts +505 -139
  7. package/cli/cloud-client.ts +1 -4
  8. package/cli/cloud-session.ts +1 -4
  9. package/cli/cloud-test-result-files.ts +3 -0
  10. package/cli/cloud-test-tunnel.ts +2 -5
  11. package/cli/command-registry.ts +2 -0
  12. package/cli/commands/agent.ts +149 -23
  13. package/cli/commands/assert.ts +1 -4
  14. package/cli/commands/box/peach-command.ts +62 -0
  15. package/cli/commands/box/plane-share.ts +0 -70
  16. package/cli/commands/box/rnx-command.ts +1 -60
  17. package/cli/commands/box/search-command.ts +14 -0
  18. package/cli/commands/box.ts +2 -2
  19. package/cli/commands/control.ts +121 -27
  20. package/cli/commands/cpu-profile.ts +23 -10
  21. package/cli/commands/daemon.ts +1 -9
  22. package/cli/commands/detox.ts +26 -18
  23. package/cli/commands/device.ts +129 -1
  24. package/cli/commands/film.ts +25 -16
  25. package/cli/commands/flow.ts +9 -2
  26. package/cli/commands/inspect/actions.ts +75 -1
  27. package/cli/commands/inspect/core.ts +81 -2
  28. package/cli/commands/inspect/describe.ts +2 -6
  29. package/cli/commands/inspect/env.ts +1 -1
  30. package/cli/commands/inspect/settling.ts +20 -3
  31. package/cli/commands/inspect/shared.ts +1 -4
  32. package/cli/commands/inspect/sleep.ts +1 -1
  33. package/cli/commands/inspect.ts +16 -49
  34. package/cli/commands/install-desktop.ts +8 -6
  35. package/cli/commands/login.ts +3 -5
  36. package/cli/commands/maestro-generate.ts +1 -4
  37. package/cli/commands/maestro.ts +38 -21
  38. package/cli/commands/no-bridge-hint.ts +0 -14
  39. package/cli/commands/permissions.ts +2 -2
  40. package/cli/commands/platform.ts +13 -6
  41. package/cli/commands/screenshot.ts +6 -1
  42. package/cli/commands/skills.ts +1 -1
  43. package/cli/commands/test.ts +6 -2
  44. package/cli/commands/three-mode.ts +162 -19
  45. package/cli/commands/upload.ts +3 -18
  46. package/cli/current-sim.ts +0 -1
  47. package/cli/desktop-companion.ts +40 -8
  48. package/cli/drivers/playwright-provisioning.ts +1 -4
  49. package/cli/flow-export.ts +1 -8
  50. package/cli/flow-session.ts +0 -4
  51. package/cli/hints.ts +1 -16
  52. package/cli/inspect-notice-state.ts +0 -4
  53. package/cli/main.ts +2 -4
  54. package/cli/outbound-endpoints.ts +1 -24
  55. package/cli/parse-args.ts +1 -2
  56. package/cli/rnx-in-process.ts +3 -1
  57. package/cli/run-rnx-core.ts +4 -2
  58. package/cli/run-rnx.ts +18 -10
  59. package/cli/send-to-box.ts +1 -4
  60. package/cli/telemetry.ts +1 -1
  61. package/cli/ws-bridge.ts +1 -5
  62. package/detox/gestures.ts +20 -6
  63. package/detox/index.ts +379 -123
  64. package/detox/navigation-lifecycle.ts +22 -13
  65. package/dist-lib/agent-daemon-client.cjs +21 -15
  66. package/dist-lib/agent-events.cjs +1 -1
  67. package/dist-lib/agent-identity.cjs +3 -17
  68. package/dist-lib/agent-sessions.cjs +5 -4
  69. package/dist-lib/attached-projects.cjs +1 -77
  70. package/dist-lib/auth/shared-session.cjs +1 -1
  71. package/dist-lib/backend-origin.cjs +11 -1
  72. package/dist-lib/beta.cjs +1 -1
  73. package/dist-lib/beta.mjs +1 -1
  74. package/dist-lib/bridge-constants.cjs +1 -1
  75. package/dist-lib/bridge-contract-input.cjs +7 -5
  76. package/dist-lib/bridge-contract-input.mjs +7 -5
  77. package/dist-lib/bridge-contract.cjs +1 -1
  78. package/dist-lib/bridge-contract.mjs +1 -1
  79. package/dist-lib/capture-contract.cjs +30 -7
  80. package/dist-lib/capture-contract.mjs +30 -7
  81. package/dist-lib/cli-constants.cjs +1 -1
  82. package/dist-lib/cloud-contract.cjs +1 -1
  83. package/dist-lib/cloud-contract.mjs +1 -1
  84. package/dist-lib/cloud-test-args.cjs +1 -1
  85. package/dist-lib/cloud-test-args.mjs +1 -1
  86. package/dist-lib/cloud.cjs +17 -11
  87. package/dist-lib/cloud.mjs +17 -11
  88. package/dist-lib/config.cjs +1 -1
  89. package/dist-lib/detox/index.cjs +321 -122
  90. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  91. package/dist-lib/home-paths.cjs +1 -1
  92. package/dist-lib/host/bridge-host.cjs +427 -212
  93. package/dist-lib/host/fetch-proxy-handler.cjs +5 -28
  94. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  95. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  96. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  97. package/dist-lib/host/websocket-proxy.cjs +1 -1
  98. package/dist-lib/index.cjs +315 -58
  99. package/dist-lib/jump-to-source-babel.cjs +1 -1
  100. package/dist-lib/jump-to-source-native.cjs +1 -1
  101. package/dist-lib/menu.cjs +67 -31
  102. package/dist-lib/menu.mjs +64 -28
  103. package/dist-lib/metro-fingerprint-registry.cjs +8 -4
  104. package/dist-lib/metro-fingerprint-registry.mjs +6 -4
  105. package/dist-lib/metro-production-bundle.cjs +2 -2
  106. package/dist-lib/metro-production-bundle.mjs +2 -2
  107. package/dist-lib/metro.cjs +8 -4
  108. package/dist-lib/profiles.cjs +1 -1
  109. package/dist-lib/public-brand.cjs +7 -4
  110. package/dist-lib/react-native-host-modules.cjs +1 -1
  111. package/dist-lib/react-native-host-modules.mjs +1 -1
  112. package/dist-lib/render-mode.cjs +3 -13
  113. package/dist-lib/scripts/dev-server-scanner.cjs +5 -17
  114. package/dist-lib/sdk.cjs +129 -10
  115. package/dist-lib/sdk.mjs +127 -10
  116. package/dist-lib/skills.cjs +1635 -1121
  117. package/dist-lib/swift-compile.cjs +277 -70
  118. package/dist-lib/swift-compile.mjs +275 -70
  119. package/dist-lib/swift-package.cjs +17 -5
  120. package/dist-lib/swift-package.mjs +15 -4
  121. package/dist-lib/swift-submit.cjs +32 -8
  122. package/dist-lib/swift-submit.mjs +29 -7
  123. package/dist-lib/swift.cjs +30912 -577
  124. package/dist-lib/vite-kotlin.cjs +356 -0
  125. package/dist-lib/vite-swift.cjs +1743 -0
  126. package/dist-lib/vite.cjs +208 -1039
  127. package/kotlin/README.md +18 -0
  128. package/kotlin/Sources/androidx/annotation/Keep.kt +4 -0
  129. package/kotlin/Sources/dev/onejs/one/OneNativeSourceDispatch.kt +7 -0
  130. package/kotlin/Sources/dev/onejs/one/source/OneModule.kt +4 -0
  131. package/kotlin/Sources/org/json/JSON.kt +244 -0
  132. package/kotlin/toolchain.json +19 -0
  133. package/package.json +14 -2
  134. package/scripts/dev-server-scanner.ts +0 -18
  135. package/scripts/optional-demo-registry.ts +0 -17
  136. package/skills/contrast/SKILL.md +2 -2
  137. package/skills/peach-setup/SKILL.md +16 -0
  138. package/src/agent-daemon-client.ts +36 -22
  139. package/src/agent-identity.ts +0 -15
  140. package/src/agent-prompt.ts +12 -0
  141. package/src/attached-projects.ts +0 -72
  142. package/src/backend-origin.ts +10 -0
  143. package/src/bridge-contract-input.ts +2 -5
  144. package/src/bridge-contract.ts +2 -0
  145. package/src/capture-contract.ts +53 -11
  146. package/src/cloud.ts +4 -9
  147. package/src/dev-server-open.ts +0 -99
  148. package/src/home-paths.ts +2 -3
  149. package/src/host/agent-host.ts +82 -36
  150. package/src/host/bridge-host.ts +5 -11
  151. package/src/host/fetch-proxy-handler.ts +19 -43
  152. package/src/is-record.ts +4 -0
  153. package/src/menu.ts +82 -37
  154. package/src/metro-fingerprint-registry.ts +1 -4
  155. package/src/metro-production-bundle.ts +1 -1
  156. package/src/plist.ts +1 -4
  157. package/src/public-brand.ts +5 -3
  158. package/src/render-mode.ts +0 -12
  159. package/src/runtime-assets.ts +9 -2
  160. package/src/screenshots/compose.ts +7 -1
  161. package/src/screenshots/frame-compose.ts +21 -10
  162. package/src/screenshots/schema.ts +1 -4
  163. package/src/sim-client.ts +2 -4
  164. package/src/skills/builtin/maestro-test.ts +1 -4
  165. package/src/sleep.ts +4 -0
  166. package/src/swift-compile.ts +209 -47
  167. package/src/swift-icu.ts +140 -0
  168. package/src/swift-package.ts +26 -2
  169. package/src/swift-submit.ts +84 -4
  170. package/src/swift.ts +2 -0
  171. package/src/vite-plugin-kotlin.ts +376 -0
  172. package/src/vite-plugin-one.ts +224 -36
  173. package/src/vite-plugin-swift.ts +382 -96
  174. package/src/vite-plugin.ts +121 -134
  175. package/src/vite.d.ts +10 -0
  176. package/cli/resolve-assets.ts +0 -63
  177. package/src/rolldown-plugin-swift.ts +0 -244
  178. package/src/team-machine-prompt.ts +0 -73
@@ -22,6 +22,18 @@
22
22
 
23
23
  export const SWIFT_COMPILE_PATH = '/api/swift-compile'
24
24
 
25
+ /**
26
+ * what an artifact needs from the mount that instantiates it. every request
27
+ * carries the sender's, and the compile host refuses one that is not its own,
28
+ * so a page still running an older mount (an open tab across a deploy, an
29
+ * older peachsim) is told to reload instead of receiving an artifact it cannot
30
+ * run. bump it with any change that makes an artifact require something new of
31
+ * its host. 1: the host fills Sources/PeachICU's reserved icu data and provides
32
+ * `peach.icu_data_v1` (`packages/sootsim-swift/src/icuData.ts`). a request
33
+ * without one comes from before this field, a host that is 0.
34
+ */
35
+ export const SWIFT_HOST_ABI = 1
36
+
25
37
  export interface SwiftImageUpload {
26
38
  /** the raster file picked for the asset (has the extension). */
27
39
  file: string
@@ -43,6 +55,12 @@ export type SwiftCompileOutcome =
43
55
  status: 'published'
44
56
  artifactUrl: string
45
57
  hash: string
58
+ /**
59
+ * the icu common data the artifact reserves, root-relative like the
60
+ * artifact url: one immutable blob per toolchain, so every app and every
61
+ * edit shares the browser's one copy.
62
+ */
63
+ icuUrl: string
46
64
  /**
47
65
  * the published photo rasters, bundle asset name to absolute url.
48
66
  * root-relative like the artifact url; the caller resolves it against
@@ -50,6 +68,8 @@ export type SwiftCompileOutcome =
50
68
  * when the request packs no images.
51
69
  */
52
70
  imageUrls?: Record<string, string>
71
+ /** the callable swift modules the sources export; absent when none. */
72
+ native?: SwiftNativeModules
53
73
  }
54
74
  /**
55
75
  * the build is running; poll the same source set again. One build is tens of
@@ -64,6 +84,44 @@ export type SwiftCompileOutcome =
64
84
 
65
85
  const POLL_INTERVAL_MS = 1500
66
86
 
87
+ /**
88
+ * the callable surface of a swift package whose sources declare `RNXModule`
89
+ * classes, as vxrn's native-source contract describes it: `one typegen`
90
+ * writes it beside a One package, and the compile host derives it for a
91
+ * project. the dispatch glue compiled into the artifact checks `hash` on every
92
+ * call, so a module keyed by this descriptor can never call an older build.
93
+ */
94
+ export interface SwiftNativeModules {
95
+ packageId: string
96
+ hash: string
97
+ /** source file to the module names it declares. */
98
+ files: Record<string, string[]>
99
+ /** module name to its method names. */
100
+ modules: Record<string, string[]>
101
+ /** the files that also declare the package's `@main` view. */
102
+ defaultViewFiles: string[]
103
+ }
104
+
105
+ /** what one imported source file exports from a package's native modules. */
106
+ export function swiftNativeEntry(
107
+ native: SwiftNativeModules,
108
+ file: string,
109
+ ): {
110
+ packageId: string
111
+ contractHash: string
112
+ modules: Record<string, string[]>
113
+ defaultView: boolean
114
+ } {
115
+ return {
116
+ packageId: native.packageId,
117
+ contractHash: native.hash,
118
+ modules: Object.fromEntries(
119
+ (native.files[file] ?? []).map((name) => [name, native.modules[name] ?? []]),
120
+ ),
121
+ defaultView: native.defaultViewFiles.includes(file),
122
+ }
123
+ }
124
+
67
125
  export interface SwiftCompileContext {
68
126
  /**
69
127
  * the compile credential: the signed preview token `BundlerSessionRuntime`
@@ -72,6 +130,12 @@ export interface SwiftCompileContext {
72
130
  * project with `.swift` sources cannot be compiled.
73
131
  */
74
132
  assetReadToken?: string
133
+ /**
134
+ * an `Authorization` header value in place of `assetReadToken`, for a
135
+ * caller outside Contrast: peachsim's dev server sends the `peach login`
136
+ * session or a `sk_rnx_` key.
137
+ */
138
+ authorization?: string
75
139
  /**
76
140
  * absolute origin of the app serving `/api/swift-compile`. The browser
77
141
  * worker omits it and posts its own origin; a node caller that is not that
@@ -87,14 +151,16 @@ export type SwiftCompileSubmit = (work: {
87
151
  /** the photo rasters to publish; absent means the project packs none. */
88
152
  images?: Record<string, SwiftImageUpload>
89
153
  entry: string
154
+ /** the id a One package's native modules register under; absent means `App`. */
155
+ packageId?: string
90
156
  }) => Promise<SwiftCompileOutcome>
91
157
 
92
158
  /** the HTTP submitter, shared by the browser worker and the node build. */
93
159
  export function httpSwiftCompileSubmit(
94
160
  context: SwiftCompileContext,
95
161
  ): SwiftCompileSubmit | undefined {
96
- const { assetReadToken, endpoint } = context
97
- if (!assetReadToken) return undefined
162
+ const { assetReadToken, authorization, endpoint } = context
163
+ if (!assetReadToken && !authorization) return undefined
98
164
  const url = endpoint
99
165
  ? `${endpoint.replace(/\/+$/, '')}${SWIFT_COMPILE_PATH}`
100
166
  : SWIFT_COMPILE_PATH
@@ -104,8 +170,15 @@ export function httpSwiftCompileSubmit(
104
170
  try {
105
171
  response = await fetch(url, {
106
172
  method: 'POST',
107
- headers: { 'content-type': 'application/json' },
108
- body: JSON.stringify({ ...work, token: assetReadToken }),
173
+ headers: {
174
+ 'content-type': 'application/json',
175
+ ...(authorization ? { authorization } : {}),
176
+ },
177
+ body: JSON.stringify({
178
+ ...work,
179
+ hostAbi: SWIFT_HOST_ABI,
180
+ ...(assetReadToken ? { token: assetReadToken } : {}),
181
+ }),
109
182
  })
110
183
  } catch (error) {
111
184
  throw new Error(
@@ -127,8 +200,12 @@ export interface SwiftModuleAddress {
127
200
  /** absolute url the tenant fetches the wasm from. */
128
201
  url: string
129
202
  hash: string
203
+ /** absolute url of the icu common data the artifact reserves. */
204
+ icuUrl: string
130
205
  /** bundle asset name to absolute photo url, when the pack publishes any. */
131
206
  imageUrls?: Record<string, string>
207
+ /** the callable swift modules the artifact links, when the sources declare any. */
208
+ native?: SwiftNativeModules
132
209
  }
133
210
 
134
211
  /** the service answers root-relative; the caller that knows the origin resolves. */
@@ -159,6 +236,7 @@ export async function compileSwiftProject(
159
236
  resources?: Record<string, string>
160
237
  images?: Record<string, SwiftImageUpload>
161
238
  entry: string
239
+ packageId?: string
162
240
  },
163
241
  origin: string,
164
242
  ): Promise<SwiftModuleAddress> {
@@ -169,9 +247,11 @@ export async function compileSwiftProject(
169
247
  return {
170
248
  url: new URL(outcome.artifactUrl, origin).href,
171
249
  hash: outcome.hash,
250
+ icuUrl: new URL(outcome.icuUrl, origin).href,
172
251
  ...(outcome.imageUrls === undefined
173
252
  ? {}
174
253
  : { imageUrls: resolveImageUrls(outcome.imageUrls, origin) }),
254
+ ...(outcome.native === undefined ? {} : { native: outcome.native }),
175
255
  }
176
256
  case 'compiling':
177
257
  await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS))
package/src/swift.ts CHANGED
@@ -2,6 +2,8 @@
2
2
  // implementation lives beside the toolchain it drives, in sootsim-swift.
3
3
  export {
4
4
  createSwiftEntry,
5
+ callSwiftModule,
6
+ useSwiftHost,
5
7
  type SwiftEntryOptions,
6
8
  type SwiftModuleOptions,
7
9
  } from '../../sootsim-swift/src/mount.tsx'
@@ -0,0 +1,376 @@
1
+ // Peach's Kotlin host for One apps on an Android device: every Kotlin source
2
+ // the app holds compiles, with One's generated dispatch glue, into one
3
+ // Kotlin/Wasm module, as prebuild compiles them all into the one app process.
4
+ // the vite plugin loads this on the first /__peach/kotlin request, so an app
5
+ // with no Kotlin never evaluates it.
6
+
7
+ import { execFile } from 'node:child_process'
8
+ import { createHash } from 'node:crypto'
9
+ import fs from 'node:fs'
10
+ import { createRequire } from 'node:module'
11
+ import path from 'node:path'
12
+ import { promisify } from 'node:util'
13
+ import { rnxHomeDir } from './home-paths'
14
+ import { isRecord } from './is-record'
15
+ import type { ServerResponse } from 'node:http'
16
+
17
+ const run = promisify(execFile)
18
+
19
+ export const KOTLIN_ARTIFACT_PREFIX = '/__peach/kotlin-artifact/'
20
+
21
+ interface Download {
22
+ url: string
23
+ sha256: string
24
+ }
25
+
26
+ interface KotlinToolchain {
27
+ compiler: Download & { version: string }
28
+ libraries: (Download & { name: string })[]
29
+ }
30
+
31
+ export interface KotlinArtifact {
32
+ // the module the seam imports; it loads its wasm beside it
33
+ url: string
34
+ hash: string
35
+ }
36
+
37
+ function readToolchain(moduleRoot: string): KotlinToolchain {
38
+ const parsed: unknown = JSON.parse(
39
+ fs.readFileSync(path.join(moduleRoot, 'toolchain.json'), 'utf8'),
40
+ )
41
+ const download = (value: unknown): Download => {
42
+ if (
43
+ !isRecord(value) ||
44
+ typeof value.url !== 'string' ||
45
+ typeof value.sha256 !== 'string'
46
+ )
47
+ throw new Error(
48
+ 'peachsim kotlin/toolchain.json names a download without url and sha256',
49
+ )
50
+ return { url: value.url, sha256: value.sha256 }
51
+ }
52
+ if (!isRecord(parsed) || !isRecord(parsed.compiler) || !Array.isArray(parsed.libraries))
53
+ throw new Error('peachsim kotlin/toolchain.json has no compiler and libraries')
54
+ const version = parsed.compiler.version
55
+ if (typeof version !== 'string')
56
+ throw new Error('peachsim kotlin/toolchain.json has no compiler version')
57
+ return {
58
+ compiler: { ...download(parsed.compiler), version },
59
+ libraries: parsed.libraries.map((library) => {
60
+ if (!isRecord(library) || typeof library.name !== 'string')
61
+ throw new Error('peachsim kotlin/toolchain.json names a library without a name')
62
+ return { ...download(library), name: library.name }
63
+ }),
64
+ }
65
+ }
66
+
67
+ function kotlinHome(): string {
68
+ return path.join(rnxHomeDir(), 'kotlin')
69
+ }
70
+
71
+ async function fetchVerified(download: Download, target: string): Promise<void> {
72
+ if (fs.existsSync(target)) return
73
+ const response = await fetch(download.url)
74
+ if (!response.ok) throw new Error(`${download.url} answered ${response.status}`)
75
+ const bytes = Buffer.from(await response.arrayBuffer())
76
+ const digest = createHash('sha256').update(bytes).digest('hex')
77
+ if (digest !== download.sha256)
78
+ throw new Error(`${download.url} has sha256 ${digest}, expected ${download.sha256}`)
79
+ fs.mkdirSync(path.dirname(target), { recursive: true })
80
+ const partial = `${target}.${process.pid}.partial`
81
+ fs.writeFileSync(partial, bytes)
82
+ fs.renameSync(partial, target)
83
+ }
84
+
85
+ let toolchainReady: Promise<{ kotlinc: string; libraries: string[] }> | undefined
86
+
87
+ // the pinned compiler and the coroutines klibs, fetched once and checked
88
+ // against toolchain.json
89
+ function ensureToolchain(
90
+ toolchain: KotlinToolchain,
91
+ ): Promise<{ kotlinc: string; libraries: string[] }> {
92
+ toolchainReady ??= (async () => {
93
+ const dir = path.join(kotlinHome(), toolchain.compiler.version)
94
+ const kotlinc = path.join(dir, 'kotlinc', 'bin', 'kotlinc-wasm')
95
+ if (!fs.existsSync(kotlinc)) {
96
+ const zip = path.join(dir, 'kotlin-compiler.zip')
97
+ await fetchVerified(toolchain.compiler, zip)
98
+ await run('unzip', ['-q', '-o', zip, '-d', dir])
99
+ fs.rmSync(zip)
100
+ }
101
+ // kotlinc runs on the JVM an android build already needs; it finds it
102
+ // through JAVA_HOME or PATH, as gradle does
103
+ const java = process.env.JAVA_HOME
104
+ ? path.join(process.env.JAVA_HOME, 'bin', 'java')
105
+ : 'java'
106
+ await run(java, ['-version']).catch(() => {
107
+ throw new Error(
108
+ `Kotlin in Peach compiles with kotlinc, which needs a Java runtime (17 or newer, as android builds use); ${process.env.JAVA_HOME ? `JAVA_HOME ${process.env.JAVA_HOME} has none` : 'set JAVA_HOME or put java on PATH'}`,
109
+ )
110
+ })
111
+ const libraries = [path.join(dir, 'kotlinc', 'lib', 'kotlin-stdlib-wasm-js.klib')]
112
+ for (const library of toolchain.libraries) {
113
+ const target = path.join(dir, library.name)
114
+ await fetchVerified(library, target)
115
+ libraries.push(target)
116
+ }
117
+ return { kotlinc, libraries }
118
+ })().catch((error: unknown) => {
119
+ toolchainReady = undefined
120
+ throw error
121
+ })
122
+ return toolchainReady
123
+ }
124
+
125
+ // the kotlin language the device compiles the app with: react native's
126
+ // android gradle build pins it, and the wasm compile holds the app to it
127
+ function appKotlinLanguage(root: string): string {
128
+ const reactNative = path.dirname(
129
+ createRequire(path.join(root, 'package.json')).resolve('react-native/package.json'),
130
+ )
131
+ const versions = fs.readFileSync(
132
+ path.join(reactNative, 'gradle', 'libs.versions.toml'),
133
+ 'utf8',
134
+ )
135
+ const match = /^kotlin\s*=\s*"(\d+)\.(\d+)/m.exec(versions)
136
+ if (!match)
137
+ throw new Error(`${reactNative}/gradle/libs.versions.toml names no kotlin version`)
138
+ return `${match[1]}.${match[2]}`
139
+ }
140
+
141
+ // the app's Kotlin sources, walked as One's android prebuild walks them into
142
+ // the app (its swift walk covers package dirs with a narrower skip set)
143
+ const SKIP_DIRS = new Set([
144
+ 'node_modules',
145
+ 'ios',
146
+ 'android',
147
+ 'dist',
148
+ 'types',
149
+ 'build',
150
+ 'tests',
151
+ '__tests__',
152
+ 'scripts',
153
+ ])
154
+
155
+ export function kotlinSources(root: string): string[] {
156
+ const sources: string[] = []
157
+ const walk = (dir: string) => {
158
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
159
+ if (entry.name.startsWith('.') || SKIP_DIRS.has(entry.name)) continue
160
+ const full = path.join(dir, entry.name)
161
+ if (entry.isDirectory()) walk(full)
162
+ else if (entry.isFile() && entry.name.endsWith('.kt')) sources.push(full)
163
+ }
164
+ }
165
+ walk(root)
166
+ return sources.sort()
167
+ }
168
+
169
+ // the process's entry: One's android host (HybridOneNativeModules) finds a
170
+ // package's dispatch by its class name, checks the contract hash, and runs
171
+ // the call as a coroutine on the main dispatcher; a thrown error rejects as
172
+ // "<module>.<method>: <message>", and a missing class names itself, as
173
+ // ClassNotFoundException does. wasm has no reflection, so the class names
174
+ // are a table here.
175
+ function entrySource(ids: string[]): string {
176
+ return [
177
+ 'import kotlin.js.JsAny',
178
+ 'import kotlin.js.Promise',
179
+ 'import kotlinx.coroutines.CoroutineStart',
180
+ 'import kotlinx.coroutines.Dispatchers',
181
+ 'import kotlinx.coroutines.GlobalScope',
182
+ 'import kotlinx.coroutines.promise',
183
+ 'import dev.onejs.one.OneNativeSourceDispatch',
184
+ 'import org.json.JSONObject',
185
+ '',
186
+ 'private val instances = mutableMapOf<String, OneNativeSourceDispatch>()',
187
+ '',
188
+ 'private fun dispatcher(packageName: String): OneNativeSourceDispatch? = instances[packageName] ?: when (packageName) {',
189
+ ...ids.map((id) => ` ${JSON.stringify(id)} -> OneNativeSource_${id}()`),
190
+ ' else -> null',
191
+ '}?.also { instances[packageName] = it }',
192
+ '',
193
+ '@OptIn(kotlin.js.ExperimentalJsExport::class, kotlin.js.ExperimentalWasmJsInterop::class, kotlinx.coroutines.DelicateCoroutinesApi::class)',
194
+ '@JsExport',
195
+ 'fun peachNativeCall(module: String, method: String, argsJson: String, contractHash: String): Promise<JsAny?> =',
196
+ ' GlobalScope.promise(Dispatchers.Main, CoroutineStart.UNDISPATCHED) {',
197
+ ' val reply = JSONObject()',
198
+ ' try {',
199
+ ' val packageName = module.substringBefore(\'.\', "")',
200
+ ' val dispatch = dispatcher(packageName)',
201
+ " if (packageName.isEmpty() || !module.contains('.')) {",
202
+ ' reply.put("ok", false).put("code", "E_NATIVE_SOURCE").put("error", "invalid module $module")',
203
+ ' } else if (dispatch == null) {',
204
+ ' reply.put("ok", false).put("code", "E_NATIVE_SOURCE").put("error", "$module.$method: OneNativeSource_$packageName")',
205
+ ' } else if (dispatch.contractHash != contractHash) {',
206
+ ' reply.put("ok", false).put("code", "E_NATIVE_SOURCE_REBUILD").put("error", "native module $module changed; rebuild the app")',
207
+ ' } else {',
208
+ ' reply.put("ok", true).put("value", dispatch.call(module, method, argsJson))',
209
+ ' }',
210
+ ' } catch (error: Exception) {',
211
+ ' reply.put("ok", false).put("code", "E_NATIVE_SOURCE").put("error", "$module.$method: ${error.message}")',
212
+ ' }',
213
+ ' reply.toString().toJsString()',
214
+ ' }',
215
+ '',
216
+ ].join('\n')
217
+ }
218
+
219
+ const BUILD_FORMAT = 1
220
+ const builds = new Map<string, Promise<KotlinArtifact>>()
221
+
222
+ function buildsDir(): string {
223
+ return path.join(kotlinHome(), 'builds')
224
+ }
225
+
226
+ // compiles the app's Kotlin sources, keyed by everything that goes into the
227
+ // module, and answers where the seam imports it from
228
+ export async function kotlinArtifact(
229
+ root: string,
230
+ moduleRoot: string,
231
+ origin: string,
232
+ ): Promise<KotlinArtifact> {
233
+ const { kotlinSourceId, nativeSourceContract, renderKotlinSourceGlue } =
234
+ await import('vxrn/native-source-contract')
235
+ const toolchain = readToolchain(moduleRoot)
236
+ const language = appKotlinLanguage(root)
237
+ const sources = kotlinSources(root)
238
+ if (sources.length === 0) throw new Error(`${root} holds no Kotlin sources`)
239
+ const generated: Record<string, string> = {}
240
+ const ids: string[] = []
241
+ for (const file of sources) {
242
+ const contract = nativeSourceContract(file, fs.readFileSync(file, 'utf8'))
243
+ if (contract.modules.length === 0) continue
244
+ const id = kotlinSourceId(root, file)
245
+ ids.push(id)
246
+ generated[`OneNativeSource_${id}.kt`] = renderKotlinSourceGlue(id, contract).source
247
+ }
248
+ generated['PeachEntry.kt'] = entrySource(ids)
249
+ const shims = kotlinSources(path.join(moduleRoot, 'Sources'))
250
+ const digest = createHash('sha256')
251
+ // BUILD_FORMAT moves when this host changes what it does to a build
252
+ digest.update(JSON.stringify({ format: BUILD_FORMAT, toolchain, language }))
253
+ for (const file of [...shims, ...sources])
254
+ digest.update(`${path.relative(root, file)}\0${fs.readFileSync(file, 'utf8')}\0`)
255
+ for (const [name, text] of Object.entries(generated))
256
+ digest.update(`${name}\0${text}\0`)
257
+ const hash = digest.digest('hex').slice(0, 24)
258
+ const artifact = { url: `${origin}${KOTLIN_ARTIFACT_PREFIX}${hash}/app.mjs`, hash }
259
+ const out = path.join(buildsDir(), hash)
260
+ if (fs.existsSync(path.join(out, 'js', 'app.wasm'))) return artifact
261
+ const pending = builds.get(hash)
262
+ if (pending) return pending
263
+ const build = (async () => {
264
+ const { kotlinc, libraries } = await ensureToolchain(toolchain)
265
+ fs.mkdirSync(buildsDir(), { recursive: true })
266
+ const work = fs.mkdtempSync(path.join(buildsDir(), `${hash}-`))
267
+ const generatedDir = path.join(work, 'generated')
268
+ fs.mkdirSync(generatedDir)
269
+ for (const [name, text] of Object.entries(generated))
270
+ fs.writeFileSync(path.join(generatedDir, name), text)
271
+ const common = ['-Xwasm-target=js', '-libraries', libraries.join(path.delimiter)]
272
+ try {
273
+ await run(
274
+ kotlinc,
275
+ [
276
+ ...common,
277
+ '-language-version',
278
+ language,
279
+ '-api-version',
280
+ language,
281
+ '-nowarn',
282
+ '-nopack',
283
+ '-ir-output-dir',
284
+ path.join(work, 'klib'),
285
+ '-ir-output-name',
286
+ 'app',
287
+ ...shims,
288
+ ...sources,
289
+ ...Object.keys(generated).map((name) => path.join(generatedDir, name)),
290
+ ],
291
+ { maxBuffer: 16 * 1024 * 1024 },
292
+ )
293
+ await run(
294
+ kotlinc,
295
+ [
296
+ ...common,
297
+ '-nowarn',
298
+ `-Xinclude=${path.join(work, 'klib')}`,
299
+ '-Xir-produce-js',
300
+ '-ir-output-dir',
301
+ path.join(work, 'js'),
302
+ '-ir-output-name',
303
+ 'app',
304
+ ],
305
+ { maxBuffer: 16 * 1024 * 1024 },
306
+ )
307
+ } catch (error) {
308
+ fs.rmSync(work, { recursive: true, force: true })
309
+ const output = isRecord(error)
310
+ ? `${String(error.stderr ?? '')}${String(error.stdout ?? '')}`
311
+ : ''
312
+ throw new Error(
313
+ `kotlin compile failed:\n${output.trim() || (error instanceof Error ? error.message : String(error))}`,
314
+ )
315
+ }
316
+ // Kotlin's loader picks node when `process.release.name` says so; the
317
+ // Peach tenant defines a react native `process` with no release, so that
318
+ // read throws before the browser branch runs. the loader keeps its
319
+ // browser path when the read is optional.
320
+ let patched = 0
321
+ for (const file of fs.readdirSync(path.join(work, 'js'))) {
322
+ if (!file.endsWith('.mjs')) continue
323
+ const target = path.join(work, 'js', file)
324
+ const text = fs.readFileSync(target, 'utf8')
325
+ const next = text.replaceAll('process.release.name', 'process.release?.name')
326
+ if (next !== text) {
327
+ patched++
328
+ fs.writeFileSync(target, next)
329
+ }
330
+ }
331
+ if (patched === 0)
332
+ throw new Error(
333
+ 'kotlin loader no longer reads process.release.name; recheck its node detection in Peach',
334
+ )
335
+ fs.renameSync(work, out)
336
+ return artifact
337
+ })().finally(() => builds.delete(hash))
338
+ builds.set(hash, build)
339
+ return build
340
+ }
341
+
342
+ const CONTENT_TYPES: Record<string, string> = {
343
+ '.mjs': 'text/javascript',
344
+ '.wasm': 'application/wasm',
345
+ }
346
+
347
+ // serves a compiled module's loader, import object and wasm, cross-origin
348
+ // to the simulator that imports them
349
+ export function serveKotlinArtifact(pathname: string, res: ServerResponse): boolean {
350
+ if (!pathname.startsWith(KOTLIN_ARTIFACT_PREFIX)) return false
351
+ const [hash, file, ...rest] = pathname.slice(KOTLIN_ARTIFACT_PREFIX.length).split('/')
352
+ const type = file ? CONTENT_TYPES[path.extname(file)] : undefined
353
+ if (
354
+ !hash ||
355
+ !/^[0-9a-f]+$/.test(hash) ||
356
+ !file ||
357
+ rest.length > 0 ||
358
+ !type ||
359
+ file.includes('..')
360
+ ) {
361
+ res.statusCode = 404
362
+ res.end('not a kotlin artifact')
363
+ return true
364
+ }
365
+ const target = path.join(buildsDir(), hash, 'js', file)
366
+ if (!fs.existsSync(target)) {
367
+ res.statusCode = 404
368
+ res.end(`kotlin artifact ${hash}/${file} was not built`)
369
+ return true
370
+ }
371
+ res.setHeader('access-control-allow-origin', '*')
372
+ res.setHeader('content-type', type)
373
+ res.setHeader('cache-control', 'public, max-age=31536000, immutable')
374
+ res.end(fs.readFileSync(target))
375
+ return true
376
+ }