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.
- package/README.md +10 -9
- package/cli/command-registry.ts +2 -1
- package/cli/commands/guest-host.ts +67 -0
- package/cli/commands/kotlin.ts +231 -0
- package/cli/commands/project-report.ts +1 -1
- package/cli/commands/report-issue.ts +55 -32
- package/cli/commands/setup.ts +10 -14
- package/cli/commands/support.ts +399 -0
- package/cli/commands/swift.ts +6 -52
- package/cli/commands/telemetry.ts +10 -13
- package/cli/main.ts +22 -10
- package/cli/outbound-endpoints.ts +23 -2
- package/cli/privacy.ts +23 -25
- package/cli/runtime-notes.ts +3 -2
- package/cli/telemetry.ts +109 -14
- 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 +1 -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 +3 -3
- package/dist-lib/host/bridge-host.cjs +1 -1
- 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 +93 -28
- 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/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 +15 -26
- package/dist-lib/swift-compile.cjs +6 -3
- package/dist-lib/swift-compile.mjs +6 -3
- 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 +6 -3
- package/dist-lib/vite.cjs +1 -1
- package/kotlin/AppSources/android/app/Activity.kt +85 -0
- package/kotlin/AppSources/android/app/Application.kt +7 -0
- package/kotlin/AppSources/android/content/Context.kt +5 -0
- package/kotlin/AppSources/android/content/Intent.kt +16 -0
- package/kotlin/AppSources/android/os/Bundle.kt +5 -0
- package/kotlin/AppSources/android/util/SuperNotCalledException.kt +3 -0
- package/kotlin/AppSources/android/view/View.kt +72 -0
- package/kotlin/AppSources/android/view/Window.kt +44 -0
- package/kotlin/AppSources/android/widget/FrameLayout.kt +6 -0
- package/kotlin/AppSources/androidx/activity/ComponentActivity.kt +57 -0
- package/kotlin/AppSources/androidx/activity/compose/ComponentActivity.kt +29 -0
- package/kotlin/AppSources/androidx/compose/ui/platform/ComposeView.kt +67 -0
- package/kotlin/AppSources/dev/peach/app/PeachApp.kt +136 -0
- package/kotlin/AppSources/dev/peach/app/PeachWindowHost.kt +13 -0
- package/kotlin/ComposeSources/PeachComposeHost.kt +58 -37
- package/package.json +1 -1
- package/skills/contrast/SKILL.md +2 -1
- package/skills/peach-setup/SKILL.md +2 -2
- package/src/{swift-guest.ts → guest-bundle.ts} +22 -7
- package/src/home-paths.ts +5 -3
- package/src/kotlin-app.ts +411 -0
- package/src/kotlin-host.ts +139 -32
- package/src/swift-compile.ts +7 -2
package/src/kotlin-host.ts
CHANGED
|
@@ -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
|
-
|
|
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('.') ||
|
|
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 =
|
|
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
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
//
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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: {
|
|
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 =
|
|
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) {
|
package/src/swift-compile.ts
CHANGED
|
@@ -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)
|