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.
- package/LICENSE +1 -1
- package/README.md +11 -9
- package/cli/app-catalog.ts +77 -0
- package/cli/app-fonts.ts +4 -20
- package/cli/app-splash.ts +2 -18
- package/cli/bridge-flow-runner.ts +505 -139
- package/cli/cloud-client.ts +1 -4
- package/cli/cloud-session.ts +1 -4
- package/cli/cloud-test-result-files.ts +3 -0
- package/cli/cloud-test-tunnel.ts +2 -5
- package/cli/command-registry.ts +2 -0
- package/cli/commands/agent.ts +149 -23
- package/cli/commands/assert.ts +1 -4
- package/cli/commands/box/peach-command.ts +62 -0
- package/cli/commands/box/plane-share.ts +0 -70
- package/cli/commands/box/rnx-command.ts +1 -60
- package/cli/commands/box/search-command.ts +14 -0
- package/cli/commands/box.ts +2 -2
- package/cli/commands/control.ts +121 -27
- package/cli/commands/cpu-profile.ts +23 -10
- package/cli/commands/daemon.ts +1 -9
- package/cli/commands/detox.ts +26 -18
- package/cli/commands/device.ts +129 -1
- package/cli/commands/film.ts +25 -16
- package/cli/commands/flow.ts +9 -2
- package/cli/commands/inspect/actions.ts +75 -1
- package/cli/commands/inspect/core.ts +81 -2
- package/cli/commands/inspect/describe.ts +2 -6
- package/cli/commands/inspect/env.ts +1 -1
- package/cli/commands/inspect/settling.ts +20 -3
- package/cli/commands/inspect/shared.ts +1 -4
- package/cli/commands/inspect/sleep.ts +1 -1
- package/cli/commands/inspect.ts +16 -49
- package/cli/commands/install-desktop.ts +8 -6
- package/cli/commands/login.ts +3 -5
- package/cli/commands/maestro-generate.ts +1 -4
- package/cli/commands/maestro.ts +38 -21
- package/cli/commands/no-bridge-hint.ts +0 -14
- package/cli/commands/permissions.ts +2 -2
- package/cli/commands/platform.ts +13 -6
- package/cli/commands/screenshot.ts +6 -1
- package/cli/commands/skills.ts +1 -1
- package/cli/commands/test.ts +6 -2
- package/cli/commands/three-mode.ts +162 -19
- package/cli/commands/upload.ts +3 -18
- package/cli/current-sim.ts +0 -1
- package/cli/desktop-companion.ts +40 -8
- package/cli/drivers/playwright-provisioning.ts +1 -4
- package/cli/flow-export.ts +1 -8
- package/cli/flow-session.ts +0 -4
- package/cli/hints.ts +1 -16
- package/cli/inspect-notice-state.ts +0 -4
- package/cli/main.ts +2 -4
- package/cli/outbound-endpoints.ts +1 -24
- package/cli/parse-args.ts +1 -2
- package/cli/rnx-in-process.ts +3 -1
- package/cli/run-rnx-core.ts +4 -2
- package/cli/run-rnx.ts +18 -10
- package/cli/send-to-box.ts +1 -4
- package/cli/telemetry.ts +1 -1
- package/cli/ws-bridge.ts +1 -5
- package/detox/gestures.ts +20 -6
- package/detox/index.ts +379 -123
- package/detox/navigation-lifecycle.ts +22 -13
- package/dist-lib/agent-daemon-client.cjs +21 -15
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +3 -17
- package/dist-lib/agent-sessions.cjs +5 -4
- package/dist-lib/attached-projects.cjs +1 -77
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +11 -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 +7 -5
- package/dist-lib/bridge-contract-input.mjs +7 -5
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +30 -7
- package/dist-lib/capture-contract.mjs +30 -7
- 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 +17 -11
- package/dist-lib/cloud.mjs +17 -11
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +321 -122
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +427 -212
- package/dist-lib/host/fetch-proxy-handler.cjs +5 -28
- 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 +315 -58
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/menu.cjs +67 -31
- package/dist-lib/menu.mjs +64 -28
- package/dist-lib/metro-fingerprint-registry.cjs +8 -4
- package/dist-lib/metro-fingerprint-registry.mjs +6 -4
- package/dist-lib/metro-production-bundle.cjs +2 -2
- package/dist-lib/metro-production-bundle.mjs +2 -2
- package/dist-lib/metro.cjs +8 -4
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +7 -4
- 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 +3 -13
- package/dist-lib/scripts/dev-server-scanner.cjs +5 -17
- package/dist-lib/sdk.cjs +129 -10
- package/dist-lib/sdk.mjs +127 -10
- package/dist-lib/skills.cjs +1635 -1121
- package/dist-lib/swift-compile.cjs +277 -70
- package/dist-lib/swift-compile.mjs +275 -70
- package/dist-lib/swift-package.cjs +17 -5
- package/dist-lib/swift-package.mjs +15 -4
- package/dist-lib/swift-submit.cjs +32 -8
- package/dist-lib/swift-submit.mjs +29 -7
- package/dist-lib/swift.cjs +30912 -577
- package/dist-lib/vite-kotlin.cjs +356 -0
- package/dist-lib/vite-swift.cjs +1743 -0
- package/dist-lib/vite.cjs +208 -1039
- package/kotlin/README.md +18 -0
- package/kotlin/Sources/androidx/annotation/Keep.kt +4 -0
- package/kotlin/Sources/dev/onejs/one/OneNativeSourceDispatch.kt +7 -0
- package/kotlin/Sources/dev/onejs/one/source/OneModule.kt +4 -0
- package/kotlin/Sources/org/json/JSON.kt +244 -0
- package/kotlin/toolchain.json +19 -0
- package/package.json +14 -2
- package/scripts/dev-server-scanner.ts +0 -18
- package/scripts/optional-demo-registry.ts +0 -17
- package/skills/contrast/SKILL.md +2 -2
- package/skills/peach-setup/SKILL.md +16 -0
- package/src/agent-daemon-client.ts +36 -22
- package/src/agent-identity.ts +0 -15
- package/src/agent-prompt.ts +12 -0
- package/src/attached-projects.ts +0 -72
- package/src/backend-origin.ts +10 -0
- package/src/bridge-contract-input.ts +2 -5
- package/src/bridge-contract.ts +2 -0
- package/src/capture-contract.ts +53 -11
- package/src/cloud.ts +4 -9
- package/src/dev-server-open.ts +0 -99
- package/src/home-paths.ts +2 -3
- package/src/host/agent-host.ts +82 -36
- package/src/host/bridge-host.ts +5 -11
- package/src/host/fetch-proxy-handler.ts +19 -43
- package/src/is-record.ts +4 -0
- package/src/menu.ts +82 -37
- package/src/metro-fingerprint-registry.ts +1 -4
- package/src/metro-production-bundle.ts +1 -1
- package/src/plist.ts +1 -4
- package/src/public-brand.ts +5 -3
- package/src/render-mode.ts +0 -12
- package/src/runtime-assets.ts +9 -2
- package/src/screenshots/compose.ts +7 -1
- package/src/screenshots/frame-compose.ts +21 -10
- package/src/screenshots/schema.ts +1 -4
- package/src/sim-client.ts +2 -4
- package/src/skills/builtin/maestro-test.ts +1 -4
- package/src/sleep.ts +4 -0
- package/src/swift-compile.ts +209 -47
- package/src/swift-icu.ts +140 -0
- package/src/swift-package.ts +26 -2
- package/src/swift-submit.ts +84 -4
- package/src/swift.ts +2 -0
- package/src/vite-plugin-kotlin.ts +376 -0
- package/src/vite-plugin-one.ts +224 -36
- package/src/vite-plugin-swift.ts +382 -96
- package/src/vite-plugin.ts +121 -134
- package/src/vite.d.ts +10 -0
- package/cli/resolve-assets.ts +0 -63
- package/src/rolldown-plugin-swift.ts +0 -244
- package/src/team-machine-prompt.ts +0 -73
package/src/swift-submit.ts
CHANGED
|
@@ -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: {
|
|
108
|
-
|
|
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
|
@@ -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
|
+
}
|