peachsim 0.1.638 → 0.1.640
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/cli/command-registry.ts +1 -0
- package/cli/commands/control.ts +75 -6
- package/cli/commands/swift.ts +326 -0
- package/cli/main.ts +11 -0
- package/cli/outbound-endpoints.ts +5 -1
- 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 +1 -1
- 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 +13 -4
- 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 +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 +389 -232
- package/dist-lib/swift-compile.cjs +1 -1
- package/dist-lib/swift-compile.mjs +1 -1
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +89 -8
- package/dist-lib/swift-submit.mjs +78 -8
- package/dist-lib/swift.cjs +16 -33308
- package/dist-lib/vite-swift.cjs +422 -247
- package/dist-lib/vite.cjs +13 -4
- package/package.json +1 -1
- package/skills/contrast/SKILL.md +1 -0
- package/src/assetCatalogContract.ts +16 -0
- package/src/dot-env.ts +148 -0
- package/src/metro-runtime-source.ts +15 -0
- package/src/swift-guest.ts +51 -0
- package/src/swift-host.ts +900 -0
- package/src/swift-submit.ts +97 -6
- package/src/swift.ts +36 -8
- package/src/swiftContract.ts +52 -0
- package/src/vite-plugin-one.ts +4 -3
- package/src/vite-plugin-swift.ts +43 -755
package/cli/command-registry.ts
CHANGED
package/cli/commands/control.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
resolveConnectionInput,
|
|
25
25
|
type ResolvedDevBundle,
|
|
26
26
|
} from '../../src/dev-bundle-resolution'
|
|
27
|
+
import { readProjectDotEnv } from '../../src/dot-env'
|
|
27
28
|
import { flowTimeoutScale } from '../../src/flow-timeout-scale'
|
|
28
29
|
import {
|
|
29
30
|
isDaemonLockfileFresh,
|
|
@@ -99,6 +100,14 @@ interface ControlCommandOptions {
|
|
|
99
100
|
runtimeConfig?: RNXConfig
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
export interface OpenCommandResources {
|
|
104
|
+
bridge?: ReturnType<typeof spawn>
|
|
105
|
+
bridgeReady?: boolean
|
|
106
|
+
port?: number
|
|
107
|
+
driver?: DriverLaunchResult & { driverId: DriverId }
|
|
108
|
+
simId?: string
|
|
109
|
+
}
|
|
110
|
+
|
|
102
111
|
interface DriverConnectWaitOptions {
|
|
103
112
|
attempts: number
|
|
104
113
|
intervalMs: number
|
|
@@ -184,6 +193,7 @@ function normalizeKnownTarget(target: string): string {
|
|
|
184
193
|
export async function ensureBridgeForOpen(
|
|
185
194
|
preferredPort: number,
|
|
186
195
|
explicitPort = false,
|
|
196
|
+
resources?: OpenCommandResources,
|
|
187
197
|
): Promise<number> {
|
|
188
198
|
const lock = readDaemonLockfile()
|
|
189
199
|
assertExplicitBridgeLock(preferredPort, explicitPort, lock)
|
|
@@ -218,6 +228,10 @@ export async function ensureBridgeForOpen(
|
|
|
218
228
|
|
|
219
229
|
await ensureRuntimeForOpen()
|
|
220
230
|
const child = spawnSootsim(['serve', '--quiet', '--port', String(preferredPort)])
|
|
231
|
+
if (resources) {
|
|
232
|
+
resources.bridge = child
|
|
233
|
+
resources.port = preferredPort
|
|
234
|
+
}
|
|
221
235
|
let childExit: string | null = null
|
|
222
236
|
child.once('exit', (code, signal) => {
|
|
223
237
|
childExit = String(code ?? signal ?? 'unknown')
|
|
@@ -252,6 +266,10 @@ export async function ensureBridgeForOpen(
|
|
|
252
266
|
console.error(
|
|
253
267
|
` Started a local bridge on port ${selectedPort}. Run \`peach daemon install\` to keep it ready between commands.`,
|
|
254
268
|
)
|
|
269
|
+
if (resources) {
|
|
270
|
+
resources.bridgeReady = true
|
|
271
|
+
resources.port = selectedPort
|
|
272
|
+
}
|
|
255
273
|
return selectedPort
|
|
256
274
|
}
|
|
257
275
|
if (childExit) {
|
|
@@ -869,6 +887,9 @@ export interface BuildShellUrlOptions {
|
|
|
869
887
|
// launch-time environment variables, stamped as repeated ?env=KEY%3DVALUE.
|
|
870
888
|
// see the --env flag above.
|
|
871
889
|
env?: string[]
|
|
890
|
+
// the project's env file as react-native-config's iOS build compiles it,
|
|
891
|
+
// stamped as ?dotEnv=<json>. set by `peach open --load-env` only.
|
|
892
|
+
dotEnv?: Record<string, string>
|
|
872
893
|
}
|
|
873
894
|
|
|
874
895
|
function bundleUrlOf(urlStr: string): string | undefined {
|
|
@@ -904,10 +925,12 @@ export async function buildShellUrl(
|
|
|
904
925
|
!opts.proof &&
|
|
905
926
|
!opts.posture &&
|
|
906
927
|
!opts.osVersion &&
|
|
907
|
-
!opts.env?.length
|
|
928
|
+
!opts.env?.length &&
|
|
929
|
+
!opts.dotEnv
|
|
908
930
|
)
|
|
909
931
|
return stamped
|
|
910
932
|
const url = new URL(stamped)
|
|
933
|
+
if (opts.dotEnv) url.searchParams.set('dotEnv', JSON.stringify(opts.dotEnv))
|
|
911
934
|
if (opts.device) url.searchParams.set('device', opts.device)
|
|
912
935
|
if (opts.posture) url.searchParams.set('posture', opts.posture)
|
|
913
936
|
if (opts.osVersion) url.searchParams.set('osVersion', opts.osVersion)
|
|
@@ -1048,13 +1071,14 @@ export async function waitForSimMatchOrHostExit(
|
|
|
1048
1071
|
commandTimeoutMs: number,
|
|
1049
1072
|
predicate: (sim: BridgeSimInfo) => boolean,
|
|
1050
1073
|
hostPid: number | undefined,
|
|
1051
|
-
opts: { attempts?: number; intervalMs?: number } = {},
|
|
1074
|
+
opts: { attempts?: number; intervalMs?: number; signal?: AbortSignal } = {},
|
|
1052
1075
|
): Promise<WaitForSimMatchOrHostExitResult> {
|
|
1053
1076
|
const attempts = opts.attempts ?? 30
|
|
1054
1077
|
const intervalMs = opts.intervalMs ?? 500
|
|
1055
1078
|
const watchHost = !!hostPid && Number.isInteger(hostPid) && hostPid > 1
|
|
1056
1079
|
const hostExited = () => watchHost && hostPid !== undefined && !isProcessAlive(hostPid)
|
|
1057
1080
|
for (let i = 0; i < attempts; i++) {
|
|
1081
|
+
opts.signal?.throwIfAborted()
|
|
1058
1082
|
if (hostExited()) {
|
|
1059
1083
|
return { kind: 'host-exit' }
|
|
1060
1084
|
}
|
|
@@ -1430,7 +1454,11 @@ export async function runLinkCommand(args: string[], opts: ControlCommandOptions
|
|
|
1430
1454
|
|
|
1431
1455
|
export async function runOpenCommand(
|
|
1432
1456
|
args: string[],
|
|
1433
|
-
opts: ControlCommandOptions
|
|
1457
|
+
opts: ControlCommandOptions & {
|
|
1458
|
+
resources?: OpenCommandResources
|
|
1459
|
+
signal?: AbortSignal
|
|
1460
|
+
ownerPid?: number
|
|
1461
|
+
} = {},
|
|
1434
1462
|
): Promise<{ simId: string; launched: boolean } | undefined> {
|
|
1435
1463
|
const parsed = parseBridgeCliArgs(args, {
|
|
1436
1464
|
port: opts.port,
|
|
@@ -1445,6 +1473,7 @@ export async function runOpenCommand(
|
|
|
1445
1473
|
'--no-describe',
|
|
1446
1474
|
'--quiet',
|
|
1447
1475
|
'--proof',
|
|
1476
|
+
'--load-env',
|
|
1448
1477
|
],
|
|
1449
1478
|
stripValueFlags: [
|
|
1450
1479
|
'--base-url',
|
|
@@ -1581,7 +1610,14 @@ export async function runOpenCommand(
|
|
|
1581
1610
|
}
|
|
1582
1611
|
}
|
|
1583
1612
|
|
|
1584
|
-
|
|
1613
|
+
opts.signal?.throwIfAborted()
|
|
1614
|
+
parsed.wsPort = await ensureBridgeForOpen(
|
|
1615
|
+
parsed.wsPort,
|
|
1616
|
+
parsed.explicitPort,
|
|
1617
|
+
opts.resources,
|
|
1618
|
+
)
|
|
1619
|
+
if (opts.resources) opts.resources.port = parsed.wsPort
|
|
1620
|
+
opts.signal?.throwIfAborted()
|
|
1585
1621
|
|
|
1586
1622
|
// playwright is the browser-sim launcher. it owns an isolated profile and
|
|
1587
1623
|
// can reap exactly its own browser tree. electron remains available for the
|
|
@@ -1698,6 +1734,30 @@ export async function runOpenCommand(
|
|
|
1698
1734
|
? { width: Number(viewportMatch[1]), height: Number(viewportMatch[2]) }
|
|
1699
1735
|
: undefined
|
|
1700
1736
|
|
|
1737
|
+
// --load-env reads the project's env file the way react-native-config's iOS
|
|
1738
|
+
// build phase compiles it into the binary (ENVFILE, then .env), so the app's
|
|
1739
|
+
// `Config` holds those values. without it `Config` is empty, as in a build
|
|
1740
|
+
// that found no env file, and the app's first read warns in the console.
|
|
1741
|
+
let dotEnv: Record<string, string> | undefined
|
|
1742
|
+
if (args.includes('--load-env')) {
|
|
1743
|
+
if (!project) {
|
|
1744
|
+
console.error(
|
|
1745
|
+
' peach open: --load-env needs a local app project to read the env file from',
|
|
1746
|
+
)
|
|
1747
|
+
rnxExit(1)
|
|
1748
|
+
}
|
|
1749
|
+
try {
|
|
1750
|
+
const result = readProjectDotEnv(project.appDir)
|
|
1751
|
+
if (!quiet) for (const notice of result.notices) console.log(` ${notice}`)
|
|
1752
|
+
dotEnv = result.config
|
|
1753
|
+
} catch (error) {
|
|
1754
|
+
console.error(
|
|
1755
|
+
` peach open: ${error instanceof Error ? error.message : String(error)}`,
|
|
1756
|
+
)
|
|
1757
|
+
rnxExit(1)
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1701
1761
|
const buildShellUrlOpts: BuildShellUrlOptions = {
|
|
1702
1762
|
hot: hotMode,
|
|
1703
1763
|
device: deviceArg,
|
|
@@ -1707,6 +1767,7 @@ export async function runOpenCommand(
|
|
|
1707
1767
|
project,
|
|
1708
1768
|
proof: args.includes('--proof'),
|
|
1709
1769
|
env: envArgs.length ? envArgs : undefined,
|
|
1770
|
+
dotEnv,
|
|
1710
1771
|
}
|
|
1711
1772
|
|
|
1712
1773
|
// resolve the shell base URL from the bridge port we settled on, so the page
|
|
@@ -1890,7 +1951,7 @@ export async function runOpenCommand(
|
|
|
1890
1951
|
|
|
1891
1952
|
const configuredBaseUrl = applyRNXConfigToUrl(baseUrl, runtimeConfig)
|
|
1892
1953
|
const openUrl = await buildOpenUrl(target, configuredBaseUrl, token, buildShellUrlOpts)
|
|
1893
|
-
const ownerPid = getStableOwnerPid()
|
|
1954
|
+
const ownerPid = opts.ownerPid ?? getStableOwnerPid()
|
|
1894
1955
|
if (!ownerPid) {
|
|
1895
1956
|
console.error(
|
|
1896
1957
|
' could not resolve a stable owning process; refusing unsupervised sim host',
|
|
@@ -1903,6 +1964,7 @@ export async function runOpenCommand(
|
|
|
1903
1964
|
rnxExit(1)
|
|
1904
1965
|
}
|
|
1905
1966
|
const connectWait = resolveDriverConnectWaitOptions()
|
|
1967
|
+
opts.signal?.throwIfAborted()
|
|
1906
1968
|
const result = await driver.launch({
|
|
1907
1969
|
url: openUrl,
|
|
1908
1970
|
// the shell serves the sim page but its WS bridge may have drifted off
|
|
@@ -1925,6 +1987,8 @@ export async function runOpenCommand(
|
|
|
1925
1987
|
: undefined),
|
|
1926
1988
|
connectTimeoutMs: connectWait.hostTimeoutMs,
|
|
1927
1989
|
})
|
|
1990
|
+
if (opts.resources) opts.resources.driver = { ...result, driverId }
|
|
1991
|
+
opts.signal?.throwIfAborted()
|
|
1928
1992
|
if (!result.launched) {
|
|
1929
1993
|
console.error(` ${driver.name} driver: ${result.message}`)
|
|
1930
1994
|
rnxExit(1)
|
|
@@ -1934,7 +1998,11 @@ export async function runOpenCommand(
|
|
|
1934
1998
|
parsed.commandTimeoutMs,
|
|
1935
1999
|
simMatchesToken,
|
|
1936
2000
|
result.pid,
|
|
1937
|
-
{
|
|
2001
|
+
{
|
|
2002
|
+
attempts: connectWait.attempts,
|
|
2003
|
+
intervalMs: connectWait.intervalMs,
|
|
2004
|
+
signal: opts.signal,
|
|
2005
|
+
},
|
|
1938
2006
|
)
|
|
1939
2007
|
if (outcome.kind === 'host-exit') {
|
|
1940
2008
|
printDriverDiagnosticLogTail(result)
|
|
@@ -1955,6 +2023,7 @@ export async function runOpenCommand(
|
|
|
1955
2023
|
rnxExit(1)
|
|
1956
2024
|
}
|
|
1957
2025
|
const match = outcome.sim
|
|
2026
|
+
if (opts.resources) opts.resources.simId = match.id
|
|
1958
2027
|
saveCurrentSimId(match.id)
|
|
1959
2028
|
signalDriverLaunchConnected(result)
|
|
1960
2029
|
printOpenedSim(openUrl, match, `${driver.name} driver`, quiet)
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import http from 'node:http'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { WebSocket, WebSocketServer } from 'ws'
|
|
5
|
+
import { isLoopbackHost, resolveContrastBackendOrigin } from '../../src/backend-origin'
|
|
6
|
+
import { swiftGuestBundle } from '../../src/swift-guest'
|
|
7
|
+
import { SWIFT_HMR_PATH, SWIFT_PACKAGE_PATH } from '../../src/swift-submit'
|
|
8
|
+
import { authHeaderValue, resolveCliAuth } from '../auth'
|
|
9
|
+
import { PeachExit } from '../run-rnx'
|
|
10
|
+
import { createBridge } from '../ws-bridge'
|
|
11
|
+
import { runCloseCommand, runOpenCommand, type OpenCommandResources } from './control'
|
|
12
|
+
import type { SwiftEntryModuleArgs } from '../../../sootsim-swift/src/entryModule'
|
|
13
|
+
|
|
14
|
+
export async function runSwift(
|
|
15
|
+
args: string[],
|
|
16
|
+
opts: { port?: number; device?: string; driver?: string; headless?: boolean } = {},
|
|
17
|
+
): Promise<number> {
|
|
18
|
+
const input = [...args]
|
|
19
|
+
const openArgs: string[] = []
|
|
20
|
+
let hostPort = 0
|
|
21
|
+
let uploadSource = false
|
|
22
|
+
const valueFlags = new Set([
|
|
23
|
+
'--device',
|
|
24
|
+
'--driver',
|
|
25
|
+
'--profile',
|
|
26
|
+
'--cdp-port',
|
|
27
|
+
'--os-version',
|
|
28
|
+
'--base-url',
|
|
29
|
+
])
|
|
30
|
+
const booleanFlags = new Set([
|
|
31
|
+
'--headless',
|
|
32
|
+
'--headed',
|
|
33
|
+
'--ephemeral',
|
|
34
|
+
'--quiet',
|
|
35
|
+
'--proof',
|
|
36
|
+
])
|
|
37
|
+
let packagePath = ''
|
|
38
|
+
for (let index = 0; index < input.length; index++) {
|
|
39
|
+
const arg = input[index]
|
|
40
|
+
if (arg === '--upload-source') uploadSource = true
|
|
41
|
+
else if (arg === '--host-port' || valueFlags.has(arg)) {
|
|
42
|
+
const value = input[++index]
|
|
43
|
+
if (!value || value.startsWith('-')) throw new Error(`${arg} requires a value`)
|
|
44
|
+
if (arg === '--host-port') {
|
|
45
|
+
hostPort = Number(value)
|
|
46
|
+
if (!Number.isInteger(hostPort) || hostPort < 1 || hostPort > 65535) {
|
|
47
|
+
throw new Error('--host-port must be a port from 1 to 65535')
|
|
48
|
+
}
|
|
49
|
+
} else openArgs.push(arg, value)
|
|
50
|
+
} else if (booleanFlags.has(arg)) openArgs.push(arg)
|
|
51
|
+
else if (arg.startsWith('-')) throw new Error(`unknown peach swift flag: ${arg}`)
|
|
52
|
+
else if (packagePath) throw new Error('peach swift expects one SwiftPM directory')
|
|
53
|
+
else packagePath = path.resolve(arg)
|
|
54
|
+
}
|
|
55
|
+
if (!packagePath)
|
|
56
|
+
throw new Error('usage: peach swift <SwiftPM-directory> --upload-source')
|
|
57
|
+
if (!uploadSource) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
'peach swift uploads the selected package sources and resources to the authenticated Contrast compiler. Pass --upload-source to consent to this directory and its watched edits.',
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
const auth = resolveCliAuth()
|
|
63
|
+
if (!auth || auth.kind === 'github') {
|
|
64
|
+
throw new Error(
|
|
65
|
+
'peach swift needs `peach login` or PEACH_API_KEY before reading project sources',
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
const compileOrigin = resolveContrastBackendOrigin(
|
|
69
|
+
auth.kind === 'session' ? auth.origin : null,
|
|
70
|
+
)
|
|
71
|
+
packagePath = fs.realpathSync(packagePath)
|
|
72
|
+
if (
|
|
73
|
+
!fs.statSync(packagePath).isDirectory() ||
|
|
74
|
+
!fs.existsSync(path.join(packagePath, 'Package.swift'))
|
|
75
|
+
) {
|
|
76
|
+
throw new Error('peach swift requires a directory containing Package.swift')
|
|
77
|
+
}
|
|
78
|
+
const host = await import('../../src/swift-host')
|
|
79
|
+
// collection preflights every input path before any source bytes are read.
|
|
80
|
+
host.swiftPackageWatchFiles(packagePath, undefined)
|
|
81
|
+
const packageName = path.basename(packagePath)
|
|
82
|
+
const guest = swiftGuestBundle(packageName)
|
|
83
|
+
const controller = new AbortController()
|
|
84
|
+
const resources: OpenCommandResources = {}
|
|
85
|
+
let metadata: (SwiftEntryModuleArgs & { hmrPath: string }) | null = null
|
|
86
|
+
let compileError = 'Swift compilation is starting'
|
|
87
|
+
let origin = ''
|
|
88
|
+
let requested = 0
|
|
89
|
+
let completed = 0
|
|
90
|
+
let building: Promise<void> | undefined
|
|
91
|
+
const sockets = new WebSocketServer({ noServer: true })
|
|
92
|
+
const send = (message: object) => {
|
|
93
|
+
const text = JSON.stringify(message)
|
|
94
|
+
for (const client of sockets.clients)
|
|
95
|
+
if (client.readyState === WebSocket.OPEN) client.send(text)
|
|
96
|
+
}
|
|
97
|
+
const refresh = (): Promise<void> => {
|
|
98
|
+
requested++
|
|
99
|
+
if (building) return building
|
|
100
|
+
building = (async () => {
|
|
101
|
+
while (!controller.signal.aborted && completed !== requested) {
|
|
102
|
+
const revision = requested
|
|
103
|
+
try {
|
|
104
|
+
const next = await host.swiftPackageMetadata(
|
|
105
|
+
packagePath,
|
|
106
|
+
packageName,
|
|
107
|
+
undefined,
|
|
108
|
+
origin,
|
|
109
|
+
{
|
|
110
|
+
origin: compileOrigin,
|
|
111
|
+
authorization: authHeaderValue(auth),
|
|
112
|
+
signal: controller.signal,
|
|
113
|
+
},
|
|
114
|
+
)
|
|
115
|
+
if (controller.signal.aborted) return
|
|
116
|
+
if (revision === requested) {
|
|
117
|
+
metadata = { ...next, hmrPath: `${origin}${SWIFT_HMR_PATH}` }
|
|
118
|
+
compileError = ''
|
|
119
|
+
send({ type: 'package', packageName, metadata })
|
|
120
|
+
console.log(` Swift artifact: ${next.hash}`)
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (controller.signal.aborted) return
|
|
124
|
+
if (revision === requested) {
|
|
125
|
+
compileError = error instanceof Error ? error.message : String(error)
|
|
126
|
+
console.error(compileError)
|
|
127
|
+
send({ type: 'error', packageName, message: compileError })
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
completed = revision
|
|
131
|
+
}
|
|
132
|
+
})().finally(() => {
|
|
133
|
+
building = undefined
|
|
134
|
+
})
|
|
135
|
+
return building
|
|
136
|
+
}
|
|
137
|
+
const validRequest = (req: http.IncomingMessage): boolean => {
|
|
138
|
+
if (req.headers.host !== new URL(origin).host) return false
|
|
139
|
+
if (!req.headers.origin) return true
|
|
140
|
+
try {
|
|
141
|
+
const requestOrigin = new URL(req.headers.origin)
|
|
142
|
+
return (
|
|
143
|
+
(requestOrigin.protocol === 'http:' || requestOrigin.protocol === 'https:') &&
|
|
144
|
+
isLoopbackHost(requestOrigin.hostname)
|
|
145
|
+
)
|
|
146
|
+
} catch {
|
|
147
|
+
return false
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const server = http.createServer((req, res) => {
|
|
151
|
+
if (!validRequest(req)) {
|
|
152
|
+
res.statusCode = 403
|
|
153
|
+
res.end('forbidden Swift host origin')
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
res.setHeader('access-control-allow-origin', req.headers.origin ?? '*')
|
|
157
|
+
res.setHeader('vary', 'origin')
|
|
158
|
+
res.setHeader('cache-control', 'no-store')
|
|
159
|
+
if (req.method !== 'GET') {
|
|
160
|
+
res.statusCode = 405
|
|
161
|
+
res.end('GET required')
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
const pathname = new URL(req.url ?? '/', origin).pathname
|
|
165
|
+
if (pathname === '/__peach/swift-guest.js') {
|
|
166
|
+
res.setHeader('content-type', 'application/javascript')
|
|
167
|
+
res.end(guest)
|
|
168
|
+
} else if (pathname === `${SWIFT_PACKAGE_PATH}${encodeURIComponent(packageName)}`) {
|
|
169
|
+
if (!metadata) {
|
|
170
|
+
res.statusCode = 503
|
|
171
|
+
res.end(compileError)
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
res.setHeader('content-type', 'application/json')
|
|
175
|
+
res.end(JSON.stringify(metadata))
|
|
176
|
+
} else if (!host.serveSwiftPackageAsset(pathname, res)) {
|
|
177
|
+
res.statusCode = 404
|
|
178
|
+
res.end('unknown Swift host asset')
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
server.on('upgrade', (req, socket, head) => {
|
|
182
|
+
if (
|
|
183
|
+
!validRequest(req) ||
|
|
184
|
+
new URL(req.url ?? '/', origin).pathname !== SWIFT_HMR_PATH
|
|
185
|
+
) {
|
|
186
|
+
socket.destroy()
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
sockets.handleUpgrade(req, socket, head, (client) => {
|
|
190
|
+
sockets.emit('connection', client, req)
|
|
191
|
+
client.send(
|
|
192
|
+
JSON.stringify(
|
|
193
|
+
metadata
|
|
194
|
+
? { type: 'package', packageName, metadata }
|
|
195
|
+
: { type: 'error', packageName, message: compileError },
|
|
196
|
+
),
|
|
197
|
+
)
|
|
198
|
+
if (metadata && compileError)
|
|
199
|
+
client.send(JSON.stringify({ type: 'error', packageName, message: compileError }))
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
const stop = () => controller.abort()
|
|
203
|
+
process.once('SIGINT', stop)
|
|
204
|
+
process.once('SIGTERM', stop)
|
|
205
|
+
let watcher: fs.FSWatcher | undefined
|
|
206
|
+
let code = 0
|
|
207
|
+
try {
|
|
208
|
+
await new Promise<void>((resolve, reject) => {
|
|
209
|
+
server.once('error', reject)
|
|
210
|
+
server.listen(hostPort, '127.0.0.1', () => {
|
|
211
|
+
server.removeListener('error', reject)
|
|
212
|
+
const address = server.address()
|
|
213
|
+
if (!address || typeof address === 'string') {
|
|
214
|
+
reject(new Error('Swift host has no loopback port'))
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
origin = `http://127.0.0.1:${address.port}`
|
|
218
|
+
resolve()
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
server.on('error', (error) => {
|
|
222
|
+
console.error(error.message)
|
|
223
|
+
code = 1
|
|
224
|
+
stop()
|
|
225
|
+
})
|
|
226
|
+
watcher = fs.watch(packagePath, { recursive: true }, (_event, file) => {
|
|
227
|
+
if (
|
|
228
|
+
file &&
|
|
229
|
+
file
|
|
230
|
+
.split(/[\\/]/)
|
|
231
|
+
.some(
|
|
232
|
+
(segment) =>
|
|
233
|
+
(segment.startsWith('.') &&
|
|
234
|
+
segment !== '.one-native-source.json' &&
|
|
235
|
+
segment !== '.one-native-source.swift.txt') ||
|
|
236
|
+
['node_modules', 'ios', 'android', 'build', 'dist', 'types'].includes(
|
|
237
|
+
segment,
|
|
238
|
+
),
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
return
|
|
242
|
+
void refresh()
|
|
243
|
+
})
|
|
244
|
+
watcher.on('error', (error) => {
|
|
245
|
+
console.error(error.message)
|
|
246
|
+
code = 1
|
|
247
|
+
stop()
|
|
248
|
+
})
|
|
249
|
+
console.log(
|
|
250
|
+
` Swift source upload: ${packagePath} → ${compileOrigin}/api/swift-compile`,
|
|
251
|
+
)
|
|
252
|
+
console.log(` Swift preview host: ${origin}`)
|
|
253
|
+
await refresh()
|
|
254
|
+
controller.signal.throwIfAborted()
|
|
255
|
+
if (opts.device) openArgs.push('--device', opts.device)
|
|
256
|
+
if (opts.driver) openArgs.push('--driver', opts.driver)
|
|
257
|
+
if (opts.headless) openArgs.push('--headless')
|
|
258
|
+
await runOpenCommand(
|
|
259
|
+
[`${origin}/__peach/swift-guest.js`, '--new', '--no-hmr', ...openArgs],
|
|
260
|
+
{
|
|
261
|
+
port: opts.port,
|
|
262
|
+
resources,
|
|
263
|
+
signal: controller.signal,
|
|
264
|
+
ownerPid: process.pid,
|
|
265
|
+
},
|
|
266
|
+
)
|
|
267
|
+
await new Promise<void>((resolve) => {
|
|
268
|
+
if (controller.signal.aborted) resolve()
|
|
269
|
+
else controller.signal.addEventListener('abort', () => resolve(), { once: true })
|
|
270
|
+
})
|
|
271
|
+
} catch (error) {
|
|
272
|
+
if (!controller.signal.aborted) {
|
|
273
|
+
console.error(
|
|
274
|
+
` peach swift failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
275
|
+
)
|
|
276
|
+
code = error instanceof PeachExit ? error.code : 1
|
|
277
|
+
}
|
|
278
|
+
} finally {
|
|
279
|
+
controller.abort()
|
|
280
|
+
process.removeListener('SIGINT', stop)
|
|
281
|
+
process.removeListener('SIGTERM', stop)
|
|
282
|
+
watcher?.close()
|
|
283
|
+
await building
|
|
284
|
+
for (const client of sockets.clients) client.terminate()
|
|
285
|
+
sockets.close()
|
|
286
|
+
server.closeAllConnections()
|
|
287
|
+
await new Promise<void>((resolve) => server.close(() => resolve()))
|
|
288
|
+
if (resources.simId && resources.port) {
|
|
289
|
+
try {
|
|
290
|
+
await runCloseCommand(['--sim', resources.simId], { port: resources.port })
|
|
291
|
+
} catch (error) {
|
|
292
|
+
console.error(` Swift preview cleanup failed: ${String(error)}`)
|
|
293
|
+
code = 1
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (
|
|
297
|
+
resources.driver?.driverId === 'playwright' &&
|
|
298
|
+
resources.driver.launched &&
|
|
299
|
+
resources.driver.pid
|
|
300
|
+
) {
|
|
301
|
+
try {
|
|
302
|
+
process.kill(resources.driver.pid, 'SIGTERM')
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (!(error instanceof Error && 'code' in error && error.code === 'ESRCH')) {
|
|
305
|
+
console.error(String(error))
|
|
306
|
+
code = 1
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (resources.bridge && !resources.bridgeReady) resources.bridge.kill('SIGTERM')
|
|
311
|
+
if (resources.bridge && resources.bridgeReady && resources.port) {
|
|
312
|
+
const bridge = createBridge(resources.port)
|
|
313
|
+
try {
|
|
314
|
+
if ((await bridge.listSims()).length === 0) resources.bridge.kill('SIGTERM')
|
|
315
|
+
else console.log(' preserving the bridge adopted by another simulator')
|
|
316
|
+
} catch (error) {
|
|
317
|
+
console.error(` cannot verify owned bridge cleanup: ${String(error)}`)
|
|
318
|
+
code = 1
|
|
319
|
+
} finally {
|
|
320
|
+
bridge.close()
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
host.disposeSwiftProject(packagePath)
|
|
324
|
+
}
|
|
325
|
+
return code
|
|
326
|
+
}
|
package/cli/main.ts
CHANGED
|
@@ -551,6 +551,17 @@ try {
|
|
|
551
551
|
}
|
|
552
552
|
}
|
|
553
553
|
|
|
554
|
+
case 'swift': {
|
|
555
|
+
if (globalSimTarget) {
|
|
556
|
+
console.error(' peach swift owns a new preview and cannot target --sim')
|
|
557
|
+
await exitWithFlush(1)
|
|
558
|
+
}
|
|
559
|
+
const { runSwift } = await import('./commands/swift')
|
|
560
|
+
await exitWithFlush(
|
|
561
|
+
await runSwift(parsed.commandArgs, { port, device, driver, headless }),
|
|
562
|
+
)
|
|
563
|
+
}
|
|
564
|
+
|
|
554
565
|
case 'preview': {
|
|
555
566
|
const { runPreview } = await import('./commands/preview')
|
|
556
567
|
await exitWithFlush(await runPreview(parsed.commandArgs))
|
|
@@ -124,7 +124,7 @@ export const OUTBOUND_ENDPOINTS: Record<string, OutboundEndpointDeclaration> = {
|
|
|
124
124
|
'the origin already serving the app, or the absolute Contrast origin a node caller passes',
|
|
125
125
|
preConsent: false,
|
|
126
126
|
payload:
|
|
127
|
-
'the project .swift sources, packed bundle resources and images,
|
|
127
|
+
'the selected project .swift sources, packed bundle resources and images, authorized with a preview token, stored session or API key; peach swift requires --upload-source for that root and its watched edits',
|
|
128
128
|
},
|
|
129
129
|
peach_cloud_sim: {
|
|
130
130
|
category: 'explicit-user-action',
|
|
@@ -221,6 +221,10 @@ export const DECLARED_OUTBOUND_CALLS: Record<string, OutboundCallDeclaration> =
|
|
|
221
221
|
// compiling a project's .swift sources needs a swift toolchain, so the
|
|
222
222
|
// bundler posts them to the app's own /api/swift-compile. it only fires with
|
|
223
223
|
// a compile credential and only for a project that has .swift sources.
|
|
224
|
+
'packages/peachsim/src/swift-host.ts :: fetch :: address.url': {
|
|
225
|
+
category: 'swift_compile',
|
|
226
|
+
count: 1,
|
|
227
|
+
},
|
|
224
228
|
'packages/peachsim/src/swift-submit.ts :: fetch :: url': {
|
|
225
229
|
category: 'swift_compile',
|
|
226
230
|
count: 1,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.640 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.640 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.640 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.640 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.640 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.640 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.640 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
package/dist-lib/beta.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.640 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
package/dist-lib/beta.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.640 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.640 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.640 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|