peachsim 0.1.1640 → 0.1.1644
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 +5 -0
- package/cli/app-bundle-resources.ts +57 -0
- package/cli/app-stubs.ts +38 -13
- package/cli/cloud-session.ts +16 -0
- package/cli/commands/assert.ts +17 -4
- package/cli/commands/box.ts +33 -19
- package/cli/commands/control.ts +12 -0
- package/cli/commands/doctor.ts +24 -15
- package/cli/commands/inspect/actions.ts +3 -2
- package/cli/commands/inspect/core.ts +0 -8
- package/cli/commands/inspect.ts +0 -14
- package/cli/commands/platform.ts +2 -1
- package/cli/commands/record.ts +64 -28
- package/cli/commands/swift.ts +10 -2
- package/cli/commands/three-mode.ts +4 -38
- package/cli/commands/upload.ts +8 -8
- package/cli/drivers/playwright.ts +39 -0
- package/cli/flow-live-status.ts +0 -2
- package/cli/main.ts +1 -1
- package/cli/outbound-endpoints.ts +4 -2
- package/cli/ws-bridge.ts +0 -6
- package/detox/attempt-events.cjs +41 -5
- package/detox/index.ts +155 -109
- package/detox/jest-environment.cjs +5 -1
- package/detox/motion-change.cjs +7 -5
- package/detox/motion-change.d.cts +1 -0
- package/detox/motion-leg-starts.cjs +20 -0
- package/detox/motion-leg-starts.d.cts +8 -0
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.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 +12 -3
- 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/bundle-channel.cjs +2671 -1833
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +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/config.mjs +1 -1
- package/dist-lib/detox/index.cjs +578 -130
- package/dist-lib/detox/jest-environment.cjs +5 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/expo-swift-bundle.cjs +2374 -1554
- package/dist-lib/expo-swift-metadata.cjs +1 -1
- package/dist-lib/expo-swift-metadata.mjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +2968 -2141
- 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 +132 -57
- 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 +1 -1
- package/dist-lib/menu.cjs +14 -6
- package/dist-lib/menu.mjs +14 -6
- 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 +120 -53
- package/dist-lib/native-app-identity-project.cjs +96 -44
- package/dist-lib/native-app-identity-project.mjs +95 -44
- package/dist-lib/native-app-identity.cjs +1 -1
- package/dist-lib/native-app-identity.mjs +1 -1
- package/dist-lib/native-firebase-metadata.cjs +1 -1
- package/dist-lib/native-firebase-metadata.mjs +1 -1
- package/dist-lib/plist.cjs +1 -1
- package/dist-lib/plist.mjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +5 -3
- package/dist-lib/sdk.mjs +5 -3
- package/dist-lib/skills.cjs +3932 -2868
- package/dist-lib/swift-compile.cjs +587 -374
- package/dist-lib/swift-compile.mjs +595 -369
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +8 -3
- package/dist-lib/swift-submit.mjs +8 -3
- package/dist-lib/swift-toolchain.cjs +1 -1
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +2263 -1467
- package/dist-lib/vite.cjs +1 -1
- package/dist-lib/worklet-executable.cjs +1 -1
- package/dist-lib/worklet-executable.mjs +1 -1
- package/package.json +1 -1
- package/src/appBundleResourcesContract.ts +51 -0
- package/src/backend-origin.ts +11 -1
- package/src/host/bridge-host.ts +17 -11
- package/src/host/staged-host-files.ts +28 -4
- package/src/launchMetadataContract.ts +1 -0
- package/src/menu.ts +12 -4
- package/src/native-app-identity-project.ts +47 -38
- package/src/react-native-bridge-modules.ts +89 -76
- package/src/runtime-delivery.ts +6 -7
- package/src/swift-compile.ts +150 -112
- package/src/swift-compiler-digest.ts +5 -3
- package/src/swift-distribution.lock.json +9 -8
- package/src/swift-host.ts +6 -4
- package/src/swift-objc-compile.ts +416 -194
- package/src/swift-objc-metadata.ts +204 -0
- package/src/swift-objc-sdk.ts +195 -2
- package/src/swift-package-manifest.ts +6 -3
- package/src/swift-root-lock.ts +302 -0
- package/src/swift-submit.ts +15 -2
- package/src/swiftContract.ts +12 -0
- package/src/xcode-project.ts +77 -7
package/dist-lib/vite.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! peach v0.1.
|
|
1
|
+
/*! peach v0.1.1644 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! peach v0.1.
|
|
1
|
+
/*! peach v0.1.1644 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
package/package.json
CHANGED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { PeachBundleAssetReference } from './bundle-asset-resource'
|
|
2
|
+
|
|
3
|
+
// copied native resources keep their bundle-relative namespace, independently
|
|
4
|
+
// of the host URLs used to deliver their immutable bytes.
|
|
5
|
+
export type AppBundleResources = Record<string, PeachBundleAssetReference>
|
|
6
|
+
|
|
7
|
+
export function isAppBundleResourcePath(path: string): boolean {
|
|
8
|
+
return (
|
|
9
|
+
path.length > 0 &&
|
|
10
|
+
!path.includes('\0') &&
|
|
11
|
+
!/[\\?#]/.test(path) &&
|
|
12
|
+
path.split('/').every((part) => part !== '' && part !== '.' && part !== '..')
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function readAppBundleResources(wire: string | null): AppBundleResources {
|
|
17
|
+
if (wire === null) return {}
|
|
18
|
+
const value: unknown = JSON.parse(wire)
|
|
19
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
20
|
+
throw new Error('invalid native app bundle resources')
|
|
21
|
+
const resources: AppBundleResources = Object.create(null)
|
|
22
|
+
for (const [path, resource] of Object.entries(value)) {
|
|
23
|
+
if (
|
|
24
|
+
!isAppBundleResourcePath(path) ||
|
|
25
|
+
!resource ||
|
|
26
|
+
typeof resource !== 'object' ||
|
|
27
|
+
!('uri' in resource) ||
|
|
28
|
+
typeof resource.uri !== 'string' ||
|
|
29
|
+
!('sha256' in resource) ||
|
|
30
|
+
typeof resource.sha256 !== 'string' ||
|
|
31
|
+
!/^[a-f0-9]{64}$/.test(resource.sha256) ||
|
|
32
|
+
!('contentType' in resource) ||
|
|
33
|
+
typeof resource.contentType !== 'string' ||
|
|
34
|
+
!('byteLength' in resource) ||
|
|
35
|
+
typeof resource.byteLength !== 'number' ||
|
|
36
|
+
!Number.isSafeInteger(resource.byteLength) ||
|
|
37
|
+
resource.byteLength < 0
|
|
38
|
+
)
|
|
39
|
+
throw new Error('invalid native app bundle resource')
|
|
40
|
+
const url = new URL(resource.uri)
|
|
41
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:')
|
|
42
|
+
throw new Error('invalid native app bundle resource URL')
|
|
43
|
+
resources[path] = {
|
|
44
|
+
uri: url.href,
|
|
45
|
+
sha256: resource.sha256,
|
|
46
|
+
contentType: resource.contentType,
|
|
47
|
+
byteLength: resource.byteLength,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return resources
|
|
51
|
+
}
|
package/src/backend-origin.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const DEFAULT_CONTRAST_BACKEND_ORIGIN = 'https://
|
|
1
|
+
const DEFAULT_CONTRAST_BACKEND_ORIGIN = 'https://peachsim.com'
|
|
2
2
|
|
|
3
3
|
export const LOOPBACK_PROBE_HOSTNAMES = ['127.0.0.1', '::1'] as const
|
|
4
4
|
|
|
@@ -29,6 +29,14 @@ export function isLoopbackHost(hostname: string): boolean {
|
|
|
29
29
|
)
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
export function isLoopbackOrigin(origin: string): boolean {
|
|
33
|
+
try {
|
|
34
|
+
return isLoopbackHost(new URL(origin).hostname)
|
|
35
|
+
} catch {
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
32
40
|
// a browser page may use a local peach bridge (its proxies, websocket,
|
|
33
41
|
// settings and account routes) only when this machine served the page: a
|
|
34
42
|
// loopback origin, or the bridge's own host when a tunnel or reverse proxy
|
|
@@ -107,6 +115,8 @@ export function isContrastBackendHost(hostname: string): boolean {
|
|
|
107
115
|
isLoopbackHost(normalized) ||
|
|
108
116
|
normalized === 'contrast.dev' ||
|
|
109
117
|
normalized.endsWith('.contrast.dev') ||
|
|
118
|
+
normalized === 'peachsim.com' ||
|
|
119
|
+
normalized === 'www.peachsim.com' ||
|
|
110
120
|
normalized === PEACHSIM_NATIVE_HOST
|
|
111
121
|
)
|
|
112
122
|
}
|
package/src/host/bridge-host.ts
CHANGED
|
@@ -1530,7 +1530,21 @@ export class SootSimBridgeHost {
|
|
|
1530
1530
|
* non-upgrade routes that don't match serve index.html (SPA behavior) so
|
|
1531
1531
|
* electron's webContents can navigate freely inside the runtime. */
|
|
1532
1532
|
private handleHttpRequest(req: IncomingMessage, res: ServerResponse) {
|
|
1533
|
-
|
|
1533
|
+
const method = (req.method || 'GET').toUpperCase()
|
|
1534
|
+
let url: URL
|
|
1535
|
+
try {
|
|
1536
|
+
url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`)
|
|
1537
|
+
} catch {
|
|
1538
|
+
res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' })
|
|
1539
|
+
res.end('invalid request URL or host')
|
|
1540
|
+
return
|
|
1541
|
+
}
|
|
1542
|
+
const publicEngineFont =
|
|
1543
|
+
(method === 'GET' || method === 'HEAD') &&
|
|
1544
|
+
/^\/(?:runtime\/|sootsim\/)?engine\/fonts\/[^\\]*\.(ttf|otf|woff2?)$/.test(
|
|
1545
|
+
url.pathname,
|
|
1546
|
+
)
|
|
1547
|
+
if (!publicEngineFont && rejectUntrustedBridgeOrigin(req, res)) return
|
|
1534
1548
|
if (req.url === '/__peach/fetch-transport') {
|
|
1535
1549
|
res.writeHead(200, {
|
|
1536
1550
|
'Content-Type': 'application/json',
|
|
@@ -1619,16 +1633,6 @@ export class SootSimBridgeHost {
|
|
|
1619
1633
|
return
|
|
1620
1634
|
}
|
|
1621
1635
|
|
|
1622
|
-
const method = (req.method || 'GET').toUpperCase()
|
|
1623
|
-
let url: URL
|
|
1624
|
-
try {
|
|
1625
|
-
url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`)
|
|
1626
|
-
} catch {
|
|
1627
|
-
res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' })
|
|
1628
|
-
res.end('invalid request URL or host')
|
|
1629
|
-
return
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
1636
|
// /__sootsim/shared-config is the one awaited HTTP write. ordinary
|
|
1633
1637
|
// settings fan out over WS, but destructive reloads need confirmation
|
|
1634
1638
|
// that the disk source of truth accepted their final reset first.
|
|
@@ -1972,6 +1976,8 @@ export class SootSimBridgeHost {
|
|
|
1972
1976
|
res.writeHead(200, {
|
|
1973
1977
|
'Content-Type': contentType,
|
|
1974
1978
|
'Cache-Control': 'no-store',
|
|
1979
|
+
// native file documents have an opaque origin when reading public fonts.
|
|
1980
|
+
...(publicEngineFont ? { 'Access-Control-Allow-Origin': '*' } : {}),
|
|
1975
1981
|
})
|
|
1976
1982
|
if (method === 'HEAD') {
|
|
1977
1983
|
res.end()
|
|
@@ -51,11 +51,21 @@ export function stageHostBytes(
|
|
|
51
51
|
origin: string,
|
|
52
52
|
extension = '',
|
|
53
53
|
): string {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
const directory = prepareStagedDirectory()
|
|
55
|
+
return writeStagedBytes(bytes, origin, extension, directory)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// a resource pack prunes once, rather than scanning the cache for every file.
|
|
59
|
+
export function stageHostByteFiles(
|
|
60
|
+
files: readonly Uint8Array[],
|
|
61
|
+
origin: string,
|
|
62
|
+
): string[] {
|
|
63
|
+
const directory = prepareStagedDirectory()
|
|
64
|
+
return files.map((bytes) => writeStagedBytes(bytes, origin, '', directory))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function prepareStagedDirectory(): string {
|
|
57
68
|
const directory = stagedDir()
|
|
58
|
-
const destination = path.join(directory, name)
|
|
59
69
|
fs.mkdirSync(directory, { recursive: true })
|
|
60
70
|
const now = Date.now()
|
|
61
71
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
@@ -63,6 +73,20 @@ export function stageHostBytes(
|
|
|
63
73
|
if (entry.isFile() && now - fs.statSync(stale).mtimeMs > STAGED_RETENTION_MS)
|
|
64
74
|
fs.rmSync(stale, { force: true })
|
|
65
75
|
}
|
|
76
|
+
return directory
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function writeStagedBytes(
|
|
80
|
+
bytes: Uint8Array,
|
|
81
|
+
origin: string,
|
|
82
|
+
extension: string,
|
|
83
|
+
directory: string,
|
|
84
|
+
): string {
|
|
85
|
+
const digest = createHash('sha256').update(bytes).digest('hex')
|
|
86
|
+
extension = extension.toLowerCase()
|
|
87
|
+
const name = STAGED_NAME.test(digest + extension) ? digest + extension : digest
|
|
88
|
+
const destination = path.join(directory, name)
|
|
89
|
+
const now = Date.now()
|
|
66
90
|
if (fs.existsSync(destination)) {
|
|
67
91
|
fs.utimesSync(destination, new Date(now), new Date(now))
|
|
68
92
|
} else {
|
package/src/menu.ts
CHANGED
|
@@ -71,7 +71,13 @@ export const SOOTSIM_RENDER_RESOLUTION_LABELS: Record<SootSimRenderResolution, s
|
|
|
71
71
|
// engine's wireframe store draws. 'wireframe' is the clean structural sketch;
|
|
72
72
|
// the others are its screen-space styles that ship as named effects.
|
|
73
73
|
export type SootSimScreenEffect = 'none' | 'wireframe' | 'blueprint' | 'panel' | 'green'
|
|
74
|
-
export const SOOTSIM_SCREEN_EFFECTS = [
|
|
74
|
+
export const SOOTSIM_SCREEN_EFFECTS = [
|
|
75
|
+
'none',
|
|
76
|
+
'wireframe',
|
|
77
|
+
'blueprint',
|
|
78
|
+
'panel',
|
|
79
|
+
'green',
|
|
80
|
+
] as const
|
|
75
81
|
|
|
76
82
|
export function isSootSimScreenEffect(value: unknown): value is SootSimScreenEffect {
|
|
77
83
|
return (
|
|
@@ -373,9 +379,9 @@ function deviceMenu(
|
|
|
373
379
|
},
|
|
374
380
|
{ type: 'separator' },
|
|
375
381
|
{ label: 'Trigger Screenshot', action: 'screenshot' },
|
|
376
|
-
// pose controls for the current device: open/close on
|
|
377
|
-
// rotate left/right wherever a turn changes geometry, the way
|
|
378
|
-
// simulator holds them (rotate left is cmd+left, rotate right
|
|
382
|
+
// pose controls for the current device: open/close, book and laptop on
|
|
383
|
+
// foldables, rotate left/right wherever a turn changes geometry, the way
|
|
384
|
+
// ios simulator holds them (rotate left is cmd+left, rotate right
|
|
379
385
|
// cmd+right). truthiness, not !== null: a partial state must hide
|
|
380
386
|
// these, never show duo items for the wrong device.
|
|
381
387
|
...(state.devicePosture || state.canRotate
|
|
@@ -390,6 +396,8 @@ function deviceMenu(
|
|
|
390
396
|
: 'Open iPhone Duo',
|
|
391
397
|
action: 'toggle-duo-posture',
|
|
392
398
|
},
|
|
399
|
+
{ label: 'Book Pose', action: 'fold-duo-book' },
|
|
400
|
+
{ label: 'Laptop Pose', action: 'fold-duo-laptop' },
|
|
393
401
|
]
|
|
394
402
|
: []),
|
|
395
403
|
...(state.canRotate
|
|
@@ -326,44 +326,8 @@ function readIosNativeAppIdentity(
|
|
|
326
326
|
parseInfoPlist: (source: string) => unknown,
|
|
327
327
|
iosTarget: string | undefined,
|
|
328
328
|
): NativeAppIdentity | null {
|
|
329
|
-
const
|
|
330
|
-
if (!
|
|
331
|
-
const projects = fs
|
|
332
|
-
.readdirSync(iosRoot, { withFileTypes: true })
|
|
333
|
-
.filter((entry) => entry.isDirectory() && entry.name.endsWith('.xcodeproj'))
|
|
334
|
-
if (projects.length === 0) return null
|
|
335
|
-
if (projects.length > 1 && !iosTarget) {
|
|
336
|
-
throw new NativeAppTargetSelectionError(
|
|
337
|
-
`native app identity requires a selected Xcode project; set withPeach's iosTarget to an app target in one of: ${projects.map((entry) => entry.name).join(', ')}`,
|
|
338
|
-
)
|
|
339
|
-
}
|
|
340
|
-
let project = projects[0]!
|
|
341
|
-
if (iosTarget && projects.length > 1) {
|
|
342
|
-
const matches = projects.filter((entry) =>
|
|
343
|
-
listXcodeAppTargets(path.join(iosRoot, entry.name), {
|
|
344
|
-
requireSources: false,
|
|
345
|
-
}).includes(iosTarget),
|
|
346
|
-
)
|
|
347
|
-
if (matches.length === 0) {
|
|
348
|
-
throw new Error(`native app identity has no application target ${iosTarget}`)
|
|
349
|
-
}
|
|
350
|
-
if (matches.length > 1) {
|
|
351
|
-
throw new Error(
|
|
352
|
-
`native app identity target ${iosTarget} is ambiguous across ${matches.map((entry) => entry.name).join(', ')}`,
|
|
353
|
-
)
|
|
354
|
-
}
|
|
355
|
-
project = matches[0]!
|
|
356
|
-
}
|
|
357
|
-
const xcodeproj = path.join(iosRoot, project.name)
|
|
358
|
-
const appTargets = listXcodeAppTargets(xcodeproj, { requireSources: false })
|
|
359
|
-
if (!iosTarget && appTargets.length > 1) {
|
|
360
|
-
throw new NativeAppTargetSelectionError(
|
|
361
|
-
`native app identity requires a selected app target; set withPeach's iosTarget to one of: ${appTargets.join(', ')}`,
|
|
362
|
-
)
|
|
363
|
-
}
|
|
364
|
-
const app = readXcodeAppTarget(xcodeproj, iosTarget ?? appTargets[0], 'Release', {
|
|
365
|
-
requireSources: false,
|
|
366
|
-
})
|
|
329
|
+
const app = readIosAppTarget(projectRoot, iosTarget)
|
|
330
|
+
if (!app) return null
|
|
367
331
|
const identity = readNativeAppIdentityFromXcodeTarget(app, parseInfoPlist)
|
|
368
332
|
const projectManifestPath = path.join(projectRoot, 'package.json')
|
|
369
333
|
const projectRequire = createRequire(projectManifestPath)
|
|
@@ -422,3 +386,48 @@ function readIosNativeAppIdentity(
|
|
|
422
386
|
}
|
|
423
387
|
return { ...identity, reanimatedStaticFeatureFlags: flags }
|
|
424
388
|
}
|
|
389
|
+
|
|
390
|
+
// native identity and copied resources must select the same application target.
|
|
391
|
+
export function readIosAppTarget(
|
|
392
|
+
projectRoot: string,
|
|
393
|
+
iosTarget?: string,
|
|
394
|
+
): XcodeAppTarget | null {
|
|
395
|
+
const iosRoot = path.join(projectRoot, 'ios')
|
|
396
|
+
if (!fs.existsSync(iosRoot)) return null
|
|
397
|
+
const projects = fs
|
|
398
|
+
.readdirSync(iosRoot, { withFileTypes: true })
|
|
399
|
+
.filter((entry) => entry.isDirectory() && entry.name.endsWith('.xcodeproj'))
|
|
400
|
+
if (projects.length === 0) return null
|
|
401
|
+
if (projects.length > 1 && !iosTarget) {
|
|
402
|
+
throw new NativeAppTargetSelectionError(
|
|
403
|
+
`native app identity requires a selected Xcode project; set withPeach's iosTarget to an app target in one of: ${projects.map((entry) => entry.name).join(', ')}`,
|
|
404
|
+
)
|
|
405
|
+
}
|
|
406
|
+
let project = projects[0]!
|
|
407
|
+
if (iosTarget && projects.length > 1) {
|
|
408
|
+
const matches = projects.filter((entry) =>
|
|
409
|
+
listXcodeAppTargets(path.join(iosRoot, entry.name), {
|
|
410
|
+
requireSources: false,
|
|
411
|
+
}).includes(iosTarget),
|
|
412
|
+
)
|
|
413
|
+
if (matches.length === 0) {
|
|
414
|
+
throw new Error(`native app identity has no application target ${iosTarget}`)
|
|
415
|
+
}
|
|
416
|
+
if (matches.length > 1) {
|
|
417
|
+
throw new Error(
|
|
418
|
+
`native app identity target ${iosTarget} is ambiguous across ${matches.map((entry) => entry.name).join(', ')}`,
|
|
419
|
+
)
|
|
420
|
+
}
|
|
421
|
+
project = matches[0]!
|
|
422
|
+
}
|
|
423
|
+
const xcodeproj = path.join(iosRoot, project.name)
|
|
424
|
+
const appTargets = listXcodeAppTargets(xcodeproj, { requireSources: false })
|
|
425
|
+
if (!iosTarget && appTargets.length > 1) {
|
|
426
|
+
throw new NativeAppTargetSelectionError(
|
|
427
|
+
`native app identity requires a selected app target; set withPeach's iosTarget to one of: ${appTargets.join(', ')}`,
|
|
428
|
+
)
|
|
429
|
+
}
|
|
430
|
+
return readXcodeAppTarget(xcodeproj, iosTarget ?? appTargets[0], 'Release', {
|
|
431
|
+
requireSources: false,
|
|
432
|
+
})
|
|
433
|
+
}
|
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
import fs from 'node:fs'
|
|
7
7
|
import path from 'node:path'
|
|
8
8
|
import { swiftCodeMask, swiftSourceClosure } from './swift-compile'
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
findXcodeProject,
|
|
11
|
+
listXcodeAppTargets,
|
|
12
|
+
readXcodeAppTarget,
|
|
13
|
+
} from './xcode-project'
|
|
10
14
|
import type { ExpoSwiftSourceUnit } from './expoSwiftContract'
|
|
11
15
|
|
|
12
16
|
interface MethodInfo {
|
|
@@ -312,9 +316,9 @@ function swiftClassFor(objcName: string, files: Record<string, string>): string
|
|
|
312
316
|
export const BRIDGE_MODULE_FILE_PREFIX = '__peach_react_module_'
|
|
313
317
|
|
|
314
318
|
/**
|
|
315
|
-
* the app
|
|
316
|
-
* when the app has no iOS project or registers none. a module
|
|
317
|
-
* cannot provide is a unit with a diagnostic instead of files.
|
|
319
|
+
* the app and its framework targets' legacy bridge modules, one compile unit
|
|
320
|
+
* each, or null when the app has no iOS project or registers none. a module
|
|
321
|
+
* the host cannot provide is a unit with a diagnostic instead of files.
|
|
318
322
|
*/
|
|
319
323
|
export function collectBridgeModuleSources(
|
|
320
324
|
appRoot: string,
|
|
@@ -327,20 +331,13 @@ export function collectBridgeModuleSources(
|
|
|
327
331
|
const iosRoot = path.join(appRoot, 'ios')
|
|
328
332
|
if (!fs.existsSync(iosRoot)) return null
|
|
329
333
|
const projectRoot = fs.realpathSync(appRoot)
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
.map((entry) => path.join(iosRoot, entry.name))
|
|
334
|
-
const targets = projects.flatMap((project) =>
|
|
335
|
-
listXcodeAppTargets(project, { requireSources: false }).map((name) => ({
|
|
336
|
-
project,
|
|
337
|
-
name,
|
|
338
|
-
})),
|
|
339
|
-
)
|
|
334
|
+
const project = findXcodeProject(iosRoot)
|
|
335
|
+
if (!project) return null
|
|
336
|
+
const targets = listXcodeAppTargets(project, { requireSources: false })
|
|
340
337
|
// RCTRegisterModule runs in the one app binary; several targets are
|
|
341
338
|
// several apps, and nothing here says which one this bundle runs in
|
|
342
339
|
if (targets.length !== 1) return null
|
|
343
|
-
const app = readXcodeAppTarget(
|
|
340
|
+
const app = readXcodeAppTarget(project, targets[0]!, 'Release', {
|
|
344
341
|
requireSources: false,
|
|
345
342
|
})
|
|
346
343
|
const own = (file: string) => {
|
|
@@ -360,72 +357,88 @@ export function collectBridgeModuleSources(
|
|
|
360
357
|
throw new Error(`${file}: app native source outside ${projectRoot}`)
|
|
361
358
|
return true
|
|
362
359
|
}
|
|
363
|
-
const objc = app.allSources.filter((file) => /\.mm?$/.test(file) && own(file))
|
|
364
|
-
if (objc.length === 0) return null
|
|
365
|
-
// headers declare the superclasses; they sit beside the sources that import them
|
|
366
|
-
const headers = [...new Set(objc.map((file) => path.dirname(file)))].flatMap((dir) =>
|
|
367
|
-
fs
|
|
368
|
-
.readdirSync(dir)
|
|
369
|
-
.filter((name) => name.endsWith('.h'))
|
|
370
|
-
.sort()
|
|
371
|
-
.map((name) => path.join(dir, name))
|
|
372
|
-
.filter(own),
|
|
373
|
-
)
|
|
374
|
-
const declarations = parseObjectiveCModules(
|
|
375
|
-
[...objc, ...headers].map((file) => ({ file, text: fs.readFileSync(file, 'utf8') })),
|
|
376
|
-
)
|
|
377
|
-
if (declarations.modules.length === 0) return null
|
|
378
|
-
const namespace = app.moduleName.replace(/[^A-Za-z0-9_]/g, '_')
|
|
379
|
-
const swiftFiles: Record<string, string> = {}
|
|
380
|
-
for (const file of app.sources.filter(own)) {
|
|
381
|
-
const relative = path.relative(projectRoot, fs.realpathSync(file))
|
|
382
|
-
swiftFiles[`Sources/${namespace}/${relative.split(path.sep).join('/')}`] =
|
|
383
|
-
`#sourceLocation(file: ${JSON.stringify(file)}, line: 1)\n${fs.readFileSync(file, 'utf8')}`
|
|
384
|
-
}
|
|
385
360
|
const units: ExpoSwiftSourceUnit[] = []
|
|
386
361
|
const failures: { namespace: string; diagnostic: string }[] = []
|
|
387
|
-
|
|
388
|
-
|
|
362
|
+
// the reader owns dependency order and visits a shared framework only once.
|
|
363
|
+
// each target keeps its own Swift module identity and compilation settings.
|
|
364
|
+
for (const target of [...app.frameworks, app]) {
|
|
389
365
|
signal?.throwIfAborted()
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
366
|
+
const objc = [
|
|
367
|
+
...new Set(
|
|
368
|
+
target.allSources
|
|
369
|
+
.filter((file) => /\.mm?$/.test(file) && own(file))
|
|
370
|
+
.map((file) => fs.realpathSync(file)),
|
|
371
|
+
),
|
|
372
|
+
]
|
|
373
|
+
if (objc.length === 0) continue
|
|
374
|
+
// headers declare the superclasses; they sit beside the sources that import them
|
|
375
|
+
const headers = [...new Set(objc.map((file) => path.dirname(file)))].flatMap((dir) =>
|
|
376
|
+
fs
|
|
377
|
+
.readdirSync(dir)
|
|
378
|
+
.filter((name) => name.endsWith('.h'))
|
|
379
|
+
.sort()
|
|
380
|
+
.map((name) => path.join(dir, name))
|
|
381
|
+
.filter(own),
|
|
382
|
+
)
|
|
383
|
+
const declarations = parseObjectiveCModules(
|
|
384
|
+
[...objc, ...headers].map((file) => ({
|
|
385
|
+
file,
|
|
386
|
+
text: fs.readFileSync(file, 'utf8'),
|
|
387
|
+
})),
|
|
388
|
+
)
|
|
389
|
+
if (declarations.modules.length === 0) continue
|
|
390
|
+
const namespace = target.moduleName.replace(/[^A-Za-z0-9_]/g, '_')
|
|
391
|
+
const swiftFiles: Record<string, string> = {}
|
|
392
|
+
for (const file of target.sources.filter(own)) {
|
|
393
|
+
const relative = path.relative(projectRoot, fs.realpathSync(file))
|
|
394
|
+
swiftFiles[`Sources/${namespace}/${relative.split(path.sep).join('/')}`] =
|
|
395
|
+
`#sourceLocation(file: ${JSON.stringify(file)}, line: 1)\n${fs.readFileSync(file, 'utf8')}`
|
|
403
396
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
397
|
+
const seen = new Set<string>()
|
|
398
|
+
for (const module of declarations.modules) {
|
|
399
|
+
signal?.throwIfAborted()
|
|
400
|
+
if (seen.has(module.className)) continue
|
|
401
|
+
seen.add(module.className)
|
|
402
|
+
const moduleId = `${namespace}.${module.className}`
|
|
403
|
+
const methods = declarations.methods.get(module.className) ?? []
|
|
404
|
+
const swiftClass = module.extern
|
|
405
|
+
? swiftClassFor(module.className, swiftFiles)
|
|
406
|
+
: null
|
|
407
|
+
const unit: ExpoSwiftSourceUnit = {
|
|
408
|
+
namespace,
|
|
409
|
+
moduleId,
|
|
410
|
+
classes: [],
|
|
411
|
+
bridgeModules: [],
|
|
412
|
+
files: {},
|
|
413
|
+
frameworks: [],
|
|
414
|
+
swiftLanguageMode: target.languageMode,
|
|
415
|
+
}
|
|
416
|
+
if (swiftClass) {
|
|
417
|
+
const register = `__peachReactModule_${module.className}`
|
|
418
|
+
unit.files = swiftSourceClosure(swiftFiles, [swiftClass])
|
|
419
|
+
unit.files[
|
|
420
|
+
`Sources/${namespace}/${BRIDGE_MODULE_FILE_PREFIX}${module.className}.swift`
|
|
421
|
+
] =
|
|
422
|
+
`import React\n\n// ${module.file}: RCT_EXTERN_MODULE(${module.className})\npublic func ${register}() -> RCTModuleHost.Registration {\n RCTModuleHost.Registration(className: ${swiftString(module.className)}, moduleName: ${swiftString(module.jsName)}, methods: ${methodsLiteral(methods)}, make: { ${swiftClass}() })\n}\n`
|
|
423
|
+
unit.bridgeModules = [`${namespace}.${register}()`]
|
|
424
|
+
} else {
|
|
425
|
+
const superclass = compiledSuperclass(module.className, declarations.superclasses)
|
|
426
|
+
const selectors = declarations.implementations.get(module.className)
|
|
427
|
+
if (!selectors || !superclass) {
|
|
428
|
+
failures.push({
|
|
429
|
+
namespace: moduleId,
|
|
430
|
+
diagnostic: !selectors
|
|
431
|
+
? `${module.file}: no Swift class or Objective-C @implementation provides ${module.className}`
|
|
432
|
+
: `${module.file}: ${module.className}'s superclass ${declarations.superclasses.get(module.className) ?? '(none)'} is Objective-C, which Peach does not compile`,
|
|
433
|
+
})
|
|
434
|
+
continue
|
|
435
|
+
}
|
|
436
|
+
unit.bridgeModules = [
|
|
437
|
+
`RCTModuleHost.Registration(className: ${swiftString(module.className)}, moduleName: ${swiftString(module.jsName)}, methods: ${methodsLiteral(methods)}, objectiveCSelectors: ${JSON.stringify([...selectors].sort())}, superclass: { ${superclass}() })`,
|
|
438
|
+
]
|
|
423
439
|
}
|
|
424
|
-
unit
|
|
425
|
-
`RCTModuleHost.Registration(className: ${swiftString(module.className)}, moduleName: ${swiftString(module.jsName)}, methods: ${methodsLiteral(methods)}, objectiveCSelectors: ${JSON.stringify([...selectors].sort())}, superclass: { ${superclass}() })`,
|
|
426
|
-
]
|
|
440
|
+
units.push(unit)
|
|
427
441
|
}
|
|
428
|
-
units.push(unit)
|
|
429
442
|
}
|
|
430
|
-
return { units, failures }
|
|
443
|
+
return units.length === 0 && failures.length === 0 ? null : { units, failures }
|
|
431
444
|
}
|
package/src/runtime-delivery.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// the machinery itself (hosted manifest, sha256-verified tarballs, channels,
|
|
4
4
|
// channel resolution, and auto-update) lives in @contrast/runtime-delivery and
|
|
5
5
|
// knows nothing about peach. this file supplies peach's identity, public env
|
|
6
|
-
// overrides, and the
|
|
6
|
+
// overrides, and the peachsim.com/runtimes hosting layout.
|
|
7
7
|
//
|
|
8
8
|
// @contrast/runtime-delivery is an internal workspace package that is never
|
|
9
9
|
// published, so both the CLI bundle and dist-lib inline it (see the `alias`
|
|
@@ -20,14 +20,13 @@ import {
|
|
|
20
20
|
runtimesDir,
|
|
21
21
|
writeActiveRuntime,
|
|
22
22
|
} from './home-paths.ts'
|
|
23
|
-
import {
|
|
23
|
+
import { peachPublicBrand } from './public-brand.ts'
|
|
24
24
|
|
|
25
|
-
export const DEFAULT_RUNTIME_CDN_ORIGIN =
|
|
25
|
+
export const DEFAULT_RUNTIME_CDN_ORIGIN = peachPublicBrand.origin
|
|
26
26
|
|
|
27
|
-
/** the official curated changelog and the machine-readable release feed.
|
|
28
|
-
|
|
29
|
-
export const
|
|
30
|
-
export const RNX_CHANGELOG_API_URL = 'https://contrast.dev/api/changelog/sootsim'
|
|
27
|
+
/** the official curated changelog and the machine-readable release feed. */
|
|
28
|
+
export const RNX_CHANGELOG_URL = `${peachPublicBrand.origin}/changelog`
|
|
29
|
+
export const RNX_CHANGELOG_API_URL = `${peachPublicBrand.origin}/api/changelog/sootsim`
|
|
31
30
|
|
|
32
31
|
export const RNX_RUNTIME_PRODUCT: RuntimeProduct = {
|
|
33
32
|
id: 'peach',
|