peachsim 0.1.1625 → 0.1.1640
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 +15 -2
- package/cli/app-config.ts +18 -6
- package/cli/app-fonts.ts +37 -17
- package/cli/app-stubs.ts +284 -20
- package/cli/bridge-flow-runner.ts +66 -45
- package/cli/cloud-client.ts +60 -40
- package/cli/command-registry.ts +1 -0
- package/cli/commands/control.ts +283 -49
- package/cli/commands/doctor.ts +30 -189
- package/cli/commands/flow.ts +79 -11
- package/cli/commands/inspect/core.ts +10 -5
- package/cli/commands/inspect/settling.ts +14 -2
- package/cli/commands/inspect/shared.ts +9 -4
- package/cli/commands/inspect/wait-ready.ts +12 -0
- package/cli/commands/inspect.ts +91 -50
- package/cli/commands/install-desktop.ts +1 -0
- package/cli/commands/maestro-compare.ts +97 -38
- package/cli/commands/maestro.ts +9 -1
- package/cli/commands/preview.ts +1 -1
- package/cli/commands/record.ts +118 -48
- package/cli/commands/screenshot.ts +23 -1
- package/cli/commands/serve.ts +7 -2
- package/cli/commands/setup.ts +19 -42
- package/cli/commands/swift.ts +14 -3
- package/cli/commands/upload.ts +93 -48
- package/cli/desktop-companion.ts +102 -17
- package/cli/drivers/desktop.ts +14 -1
- package/cli/drivers/playwright-sim-host.ts +103 -45
- package/cli/drivers/playwright.ts +9 -8
- package/cli/drivers/types.ts +5 -0
- package/cli/maestro-accessibility.ts +1 -1
- package/cli/maestro-compare.ts +84 -12
- package/cli/main.ts +3 -2
- package/cli/outbound-endpoints.ts +20 -6
- package/cli/pause-aware-timeout.ts +106 -0
- package/cli/scene-video.ts +89 -0
- package/cli/ws-bridge.ts +37 -19
- package/detox/attempt-events.cjs +66 -5
- package/detox/attempt-events.d.cts +7 -0
- package/detox/expectations.ts +7 -3
- package/detox/index.ts +269 -170
- package/detox/motion-change.cjs +206 -21
- package/detox/motion-change.d.cts +39 -2
- package/detox/navigation-lifecycle.ts +35 -3
- 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 +8 -7
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +13 -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 +14 -8
- package/dist-lib/bridge-contract.mjs +13 -7
- package/dist-lib/bundle-channel.cjs +11887 -7900
- 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 +6 -3
- package/dist-lib/config.mjs +6 -3
- package/dist-lib/detox/index.cjs +1379 -725
- package/dist-lib/dev-bundle-resolution.cjs +14 -2
- package/dist-lib/expo-swift-bundle.cjs +9341 -5462
- package/dist-lib/expo-swift-metadata.cjs +11 -7
- package/dist-lib/expo-swift-metadata.mjs +11 -7
- package/dist-lib/home-paths.cjs +32 -3
- package/dist-lib/host/bridge-host.cjs +50204 -7258
- 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 +3541 -448
- 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 +2931 -104
- package/dist-lib/native-app-identity-project.cjs +289 -49
- package/dist-lib/native-app-identity-project.mjs +289 -49
- package/dist-lib/native-app-identity.cjs +77 -26
- package/dist-lib/native-app-identity.mjs +76 -26
- 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 +6 -2
- package/dist-lib/react-native-host-modules.mjs +6 -2
- package/dist-lib/scripts/dev-server-scanner.cjs +22 -8
- package/dist-lib/sdk.cjs +27 -11
- package/dist-lib/sdk.mjs +27 -10
- package/dist-lib/skills.cjs +72201 -26208
- package/dist-lib/swift-compile.cjs +1007 -140
- package/dist-lib/swift-compile.mjs +1049 -174
- package/dist-lib/swift-package.cjs +12 -3
- package/dist-lib/swift-package.mjs +12 -3
- package/dist-lib/swift-submit.cjs +13 -5
- package/dist-lib/swift-submit.mjs +11 -5
- package/dist-lib/swift-toolchain.cjs +423 -33
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +8628 -5268
- package/dist-lib/vite.cjs +2859 -12
- package/dist-lib/worklet-executable.cjs +14 -2
- package/dist-lib/worklet-executable.mjs +14 -2
- package/kotlin/AppSources/dev/peach/app/PeachDrawable.kt +21 -14
- package/kotlin/Sources/androidx/annotation/DrawableRes.kt +14 -0
- package/package.json +1 -1
- package/scripts/dev-server-scanner.ts +2 -1
- package/skills/peach-perf/SKILL.md +11 -5
- package/skills/peach-setup/SKILL.md +16 -19
- package/src/backend-origin.ts +13 -6
- package/src/bridge-constants.ts +11 -0
- package/src/bridge-contract.ts +22 -34
- package/src/config.ts +15 -0
- package/src/desktop-host-delivery.ts +5 -1
- package/src/dev-bundle-resolution.ts +2 -1
- package/src/expo-manifest-body.ts +13 -0
- package/src/expo-swift-host.ts +196 -49
- package/src/expo-swift-ready.ts +78 -0
- package/src/expoKotlinContract.ts +4 -7
- package/src/expoSwiftContract.ts +17 -6
- package/src/home-paths.ts +48 -2
- package/src/host/bridge-host.ts +5 -2
- package/src/host/staged-host-files.ts +9 -0
- package/src/native-app-identity-project.ts +27 -6
- package/src/native-app-identity.ts +91 -4
- package/src/native-firebase-config.ts +209 -13
- package/src/nativeAppIconContract.ts +1 -4
- package/src/output-lock-stdio.ts +36 -0
- package/src/process-group.ts +25 -2
- package/src/react-native-bridge-modules.ts +431 -0
- package/src/react-native-host-modules.ts +8 -1
- package/src/settings-bundle.ts +1 -4
- package/src/swift-binary-modules.ts +2 -2
- package/src/swift-compile.ts +918 -109
- package/src/swift-compiler-digest.ts +14 -0
- package/src/swift-distribution.lock.json +24 -12
- package/src/swift-distribution.ts +103 -4
- package/src/swift-host.ts +180 -55
- package/src/swift-icu.ts +27 -13
- package/src/swift-objc-compile.ts +355 -0
- package/src/swift-objc-sdk.ts +821 -27
- package/src/swift-objc-standin.ts +148 -14
- package/src/swift-package-manifest.ts +60 -7
- package/src/swift-package.ts +15 -1
- package/src/swift-packages.ts +154 -81
- package/src/swift-standin-emit.ts +96 -15
- package/src/swift-standins.ts +530 -116
- package/src/swift-submit.ts +13 -5
- package/src/swift-toolchain.ts +500 -42
- package/src/vite-plugin.ts +12 -1
- package/src/worker-system-chunks.ts +11 -12
- package/src/worklet-executable.ts +19 -1
- package/src/xcode-project.ts +270 -30
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { fstatSync } from 'node:fs'
|
|
2
|
+
import type { IOType, StdioOptions } from 'node:child_process'
|
|
3
|
+
|
|
4
|
+
// lock identities name actual inherited descriptors, never paths to reopen.
|
|
5
|
+
export function outputLockStdio(
|
|
6
|
+
standard: [IOType, IOType, IOType],
|
|
7
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
8
|
+
): StdioOptions {
|
|
9
|
+
const stdio: StdioOptions = [...standard]
|
|
10
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
11
|
+
if (key.startsWith('REMOTE_OUTPUT_LOCK_') && env[key] !== value)
|
|
12
|
+
throw new Error(`inherited output lock identity changed for ${key}`)
|
|
13
|
+
}
|
|
14
|
+
for (const [key, value] of Object.entries(env)) {
|
|
15
|
+
if (!key.startsWith('REMOTE_OUTPUT_LOCK_')) continue
|
|
16
|
+
const fd = Number(value)
|
|
17
|
+
if (
|
|
18
|
+
!/^REMOTE_OUTPUT_LOCK_[a-f0-9]{64}$/.test(key) ||
|
|
19
|
+
!value ||
|
|
20
|
+
!/^\d+$/.test(value) ||
|
|
21
|
+
!Number.isSafeInteger(fd) ||
|
|
22
|
+
fd < 3
|
|
23
|
+
)
|
|
24
|
+
throw new Error(`invalid inherited output lock descriptor for ${key}`)
|
|
25
|
+
try {
|
|
26
|
+
fstatSync(fd)
|
|
27
|
+
} catch (cause) {
|
|
28
|
+
throw new Error(`inherited output lock descriptor ${fd} unavailable for ${key}`, {
|
|
29
|
+
cause,
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
while (stdio.length <= fd) stdio.push('ignore')
|
|
33
|
+
stdio[fd] = fd
|
|
34
|
+
}
|
|
35
|
+
return stdio
|
|
36
|
+
}
|
package/src/process-group.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { setTimeout as sleep } from 'node:timers/promises'
|
|
|
13
13
|
|
|
14
14
|
const GRACEFUL_TIMEOUT_MS = 3_000
|
|
15
15
|
const FORCED_EXIT_TIMEOUT_MS = 1_000
|
|
16
|
+
const FREEZE_GUARD_NAME = 'process-freeze-guard'
|
|
16
17
|
|
|
17
18
|
// keep this self-contained: the detached browser host embeds its source.
|
|
18
19
|
export function listProcessChildren(parents: number[], timeoutMs = 3_000): number[] {
|
|
@@ -107,7 +108,7 @@ async function protectProcessFreeze(rootPid: number) {
|
|
|
107
108
|
printf 'ready\n'
|
|
108
109
|
if ! IFS= read -r command || [ "$command" != disarm ]; then cleanup; fi
|
|
109
110
|
`,
|
|
110
|
-
|
|
111
|
+
FREEZE_GUARD_NAME,
|
|
111
112
|
journal,
|
|
112
113
|
],
|
|
113
114
|
{
|
|
@@ -151,6 +152,28 @@ async function protectProcessFreeze(rootPid: number) {
|
|
|
151
152
|
}
|
|
152
153
|
}
|
|
153
154
|
|
|
155
|
+
// a nested capture's guard is a descendant of this tree but must keep running
|
|
156
|
+
// to resume its own targets, so a freeze never reaches it. ps exits 1 when
|
|
157
|
+
// some pids already vanished, so only a spawn failure or timeout is an error.
|
|
158
|
+
function withoutFreezeGuards(pids: number[]): number[] {
|
|
159
|
+
if (pids.length === 0) return pids
|
|
160
|
+
const result = spawnSync('ps', ['-o', 'pid=,command=', '-p', pids.join(',')], {
|
|
161
|
+
encoding: 'utf8',
|
|
162
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
163
|
+
timeout: FORCED_EXIT_TIMEOUT_MS,
|
|
164
|
+
killSignal: 'SIGKILL',
|
|
165
|
+
})
|
|
166
|
+
if (result.error || (result.status !== 0 && result.status !== 1))
|
|
167
|
+
throw new Error(result.error?.message || `ps exited ${result.status}`)
|
|
168
|
+
const guards = new Set(
|
|
169
|
+
result.stdout
|
|
170
|
+
.split('\n')
|
|
171
|
+
.filter((line) => line.includes(FREEZE_GUARD_NAME))
|
|
172
|
+
.map((line) => Number(line.trim().split(/\s+/)[0])),
|
|
173
|
+
)
|
|
174
|
+
return pids.filter((pid) => !guards.has(pid))
|
|
175
|
+
}
|
|
176
|
+
|
|
154
177
|
// reparented members remain in the group; detached descendants need their
|
|
155
178
|
// recorded pids. both can leave zombies, which signal zero cannot distinguish
|
|
156
179
|
// from running processes. inspect their states together before confirming exit.
|
|
@@ -243,7 +266,7 @@ export async function terminateProcessGroup(
|
|
|
243
266
|
}
|
|
244
267
|
let children: number[]
|
|
245
268
|
try {
|
|
246
|
-
children = listProcessChildren(parents, remaining)
|
|
269
|
+
children = withoutFreezeGuards(listProcessChildren(parents, remaining))
|
|
247
270
|
} catch (error) {
|
|
248
271
|
inspectionError = new Error(`could not inspect descendants: ${String(error)}`)
|
|
249
272
|
break
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
// an app's own legacy React Native modules: the classes its Objective-C
|
|
2
|
+
// sources register with RCT_EXPORT_MODULE or RCT_EXTERN_MODULE, read from the
|
|
3
|
+
// app target Xcode builds. the macros' arguments are what clang records for
|
|
4
|
+
// RCTBridge (RCTMethodInfo's js and objc names); the Swift host parses each
|
|
5
|
+
// signature at runtime as RCTModuleMethod does (sootsim-swift React target).
|
|
6
|
+
import fs from 'node:fs'
|
|
7
|
+
import path from 'node:path'
|
|
8
|
+
import { swiftCodeMask, swiftSourceClosure } from './swift-compile'
|
|
9
|
+
import { listXcodeAppTargets, readXcodeAppTarget } from './xcode-project'
|
|
10
|
+
import type { ExpoSwiftSourceUnit } from './expoSwiftContract'
|
|
11
|
+
|
|
12
|
+
interface MethodInfo {
|
|
13
|
+
jsName: string
|
|
14
|
+
objcName: string
|
|
15
|
+
isSync: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ModuleDeclaration {
|
|
19
|
+
className: string
|
|
20
|
+
jsName: string
|
|
21
|
+
// RCT_EXTERN_MODULE declares a class implemented elsewhere, usually Swift
|
|
22
|
+
extern: boolean
|
|
23
|
+
file: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ObjectiveCDeclarations {
|
|
27
|
+
modules: ModuleDeclaration[]
|
|
28
|
+
methods: Map<string, MethodInfo[]>
|
|
29
|
+
superclasses: Map<string, string>
|
|
30
|
+
// the instance selectors each class's @implementation blocks define
|
|
31
|
+
implementations: Map<string, Set<string>>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Objective-C with its comments as spaces, offsets kept
|
|
35
|
+
function stripComments(source: string): string {
|
|
36
|
+
let out = ''
|
|
37
|
+
let i = 0
|
|
38
|
+
while (i < source.length) {
|
|
39
|
+
const char = source[i]!
|
|
40
|
+
const next = source[i + 1]
|
|
41
|
+
if (char === '/' && next === '/') {
|
|
42
|
+
const end = source.indexOf('\n', i)
|
|
43
|
+
const stop = end === -1 ? source.length : end
|
|
44
|
+
out += ' '.repeat(stop - i)
|
|
45
|
+
i = stop
|
|
46
|
+
} else if (char === '/' && next === '*') {
|
|
47
|
+
const end = source.indexOf('*/', i + 2)
|
|
48
|
+
const stop = end === -1 ? source.length : end + 2
|
|
49
|
+
out += source.slice(i, stop).replace(/[^\n]/g, ' ')
|
|
50
|
+
i = stop
|
|
51
|
+
} else if (char === '"' || char === "'") {
|
|
52
|
+
let j = i + 1
|
|
53
|
+
while (j < source.length && source[j] !== char && source[j] !== '\n')
|
|
54
|
+
j += source[j] === '\\' ? 2 : 1
|
|
55
|
+
out += source.slice(i, j + 1)
|
|
56
|
+
i = j + 1
|
|
57
|
+
} else {
|
|
58
|
+
out += char
|
|
59
|
+
i++
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return out
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// a macro's arguments split at top-level commas, as the preprocessor does
|
|
66
|
+
function macroArguments(
|
|
67
|
+
source: string,
|
|
68
|
+
open: number,
|
|
69
|
+
): { args: string[]; end: number } | null {
|
|
70
|
+
const args: string[] = []
|
|
71
|
+
let depth = 0
|
|
72
|
+
let start = open + 1
|
|
73
|
+
for (let i = open; i < source.length; i++) {
|
|
74
|
+
const char = source[i]
|
|
75
|
+
if (char === '"' || char === "'") {
|
|
76
|
+
let j = i + 1
|
|
77
|
+
while (j < source.length && source[j] !== char) j += source[j] === '\\' ? 2 : 1
|
|
78
|
+
i = j
|
|
79
|
+
} else if (char === '(') depth++
|
|
80
|
+
else if (char === ')') {
|
|
81
|
+
depth--
|
|
82
|
+
if (depth === 0) {
|
|
83
|
+
args.push(source.slice(start, i))
|
|
84
|
+
return { args: args.map((arg) => arg.replace(/\s+/g, ' ').trim()), end: i + 1 }
|
|
85
|
+
}
|
|
86
|
+
} else if (char === ',' && depth === 1) {
|
|
87
|
+
args.push(source.slice(start, i))
|
|
88
|
+
start = i + 1
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// the selector a method definition's signature names, skipping its types
|
|
95
|
+
function definitionSelector(signature: string): string {
|
|
96
|
+
let selector = ''
|
|
97
|
+
let rest = signature.trim()
|
|
98
|
+
for (;;) {
|
|
99
|
+
const part = /^([A-Za-z_]\w*)?\s*(:)?\s*/.exec(rest)
|
|
100
|
+
if (!part || (!part[1] && !part[2])) break
|
|
101
|
+
selector += (part[1] ?? '') + (part[2] ?? '')
|
|
102
|
+
rest = rest.slice(part[0].length)
|
|
103
|
+
if (!part[2]) break
|
|
104
|
+
if (rest.startsWith('(')) {
|
|
105
|
+
const type = macroArguments(rest, 0)
|
|
106
|
+
if (!type) break
|
|
107
|
+
rest = rest.slice(type.end).trimStart()
|
|
108
|
+
}
|
|
109
|
+
rest = rest.replace(/^[A-Za-z_]\w*\s*/, '')
|
|
110
|
+
}
|
|
111
|
+
return selector
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const DIRECTIVE =
|
|
115
|
+
/@interface\b|@implementation\b|@end\b|\b(_RCT_EXTERN_REMAP_METHOD|RCT_EXTERN_REMAP_MODULE|RCT_EXTERN_MODULE|RCT_EXPORT_MODULE_NO_LOAD|RCT_EXPORT_MODULE|RCT_EXTERN_METHOD|RCT_EXTERN__BLOCKING_SYNCHRONOUS_METHOD|RCT_EXPORT_METHOD|RCT_REMAP_METHOD|RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD|RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD|RCT_REMAP_BLOCKING_SYNCHRONOUS_METHOD)\s*\(/g
|
|
116
|
+
|
|
117
|
+
export function parseObjectiveCModules(
|
|
118
|
+
sources: { file: string; text: string }[],
|
|
119
|
+
): ObjectiveCDeclarations {
|
|
120
|
+
const declarations: ObjectiveCDeclarations = {
|
|
121
|
+
modules: [],
|
|
122
|
+
methods: new Map(),
|
|
123
|
+
superclasses: new Map(),
|
|
124
|
+
implementations: new Map(),
|
|
125
|
+
}
|
|
126
|
+
const addMethod = (className: string, method: MethodInfo) => {
|
|
127
|
+
const methods = declarations.methods.get(className) ?? []
|
|
128
|
+
methods.push(method)
|
|
129
|
+
declarations.methods.set(className, methods)
|
|
130
|
+
}
|
|
131
|
+
const addSelector = (className: string, selector: string) => {
|
|
132
|
+
declarations.implementations.get(className)?.add(selector)
|
|
133
|
+
}
|
|
134
|
+
for (const { file, text } of sources) {
|
|
135
|
+
const source = stripComments(text)
|
|
136
|
+
// the class whose @interface or @implementation encloses a macro
|
|
137
|
+
let owner: { className: string; implementation: boolean; start: number } | null = null
|
|
138
|
+
const closeImplementation = (end: number) => {
|
|
139
|
+
if (!owner?.implementation) return
|
|
140
|
+
// the instance methods the implementation defines: `- (type)selector {`
|
|
141
|
+
const body = source.slice(owner.start, end)
|
|
142
|
+
for (const match of body.matchAll(/^[ \t]*-\s*\(/gm)) {
|
|
143
|
+
const open = match.index + match[0].length - 1
|
|
144
|
+
const type = macroArguments(body, open)
|
|
145
|
+
if (!type) continue
|
|
146
|
+
const close = body.slice(type.end).search(/[{;]/)
|
|
147
|
+
if (close === -1) continue
|
|
148
|
+
const selector = definitionSelector(body.slice(type.end, type.end + close))
|
|
149
|
+
if (selector) addSelector(owner.className, selector)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
for (const match of source.matchAll(DIRECTIVE)) {
|
|
153
|
+
const at = match.index
|
|
154
|
+
const directive = match[0]
|
|
155
|
+
if (directive === '@end') {
|
|
156
|
+
closeImplementation(at)
|
|
157
|
+
owner = null
|
|
158
|
+
continue
|
|
159
|
+
}
|
|
160
|
+
if (directive === '@interface' || directive === '@implementation') {
|
|
161
|
+
const rest = source.slice(at + directive.length)
|
|
162
|
+
const extern = /^\s*(RCT_EXTERN_REMAP_MODULE|RCT_EXTERN_MODULE)\s*\(/.exec(rest)
|
|
163
|
+
if (extern) {
|
|
164
|
+
const parsed = macroArguments(rest, extern[0].length - 1)
|
|
165
|
+
if (!parsed) continue
|
|
166
|
+
const [jsName, className, superclass] =
|
|
167
|
+
extern[1] === 'RCT_EXTERN_MODULE' ? ['', ...parsed.args] : parsed.args
|
|
168
|
+
if (!className) continue
|
|
169
|
+
declarations.modules.push({
|
|
170
|
+
className,
|
|
171
|
+
jsName: jsName ?? '',
|
|
172
|
+
extern: true,
|
|
173
|
+
file,
|
|
174
|
+
})
|
|
175
|
+
if (superclass) declarations.superclasses.set(className, superclass)
|
|
176
|
+
owner = { className, implementation: false, start: at }
|
|
177
|
+
continue
|
|
178
|
+
}
|
|
179
|
+
const named = /^\s*([A-Za-z_]\w*)\s*(?::\s*([A-Za-z_]\w*))?/.exec(rest)
|
|
180
|
+
if (!named) continue
|
|
181
|
+
if (named[2]) declarations.superclasses.set(named[1]!, named[2])
|
|
182
|
+
if (
|
|
183
|
+
directive === '@implementation' &&
|
|
184
|
+
!declarations.implementations.has(named[1]!)
|
|
185
|
+
)
|
|
186
|
+
declarations.implementations.set(named[1]!, new Set())
|
|
187
|
+
owner = {
|
|
188
|
+
className: named[1]!,
|
|
189
|
+
implementation: directive === '@implementation',
|
|
190
|
+
start: at + directive.length + named[0].length,
|
|
191
|
+
}
|
|
192
|
+
continue
|
|
193
|
+
}
|
|
194
|
+
if (!owner) continue
|
|
195
|
+
const parsed = macroArguments(source, at + directive.length - 1)
|
|
196
|
+
if (!parsed) continue
|
|
197
|
+
const args = parsed.args
|
|
198
|
+
switch (match[1]) {
|
|
199
|
+
case 'RCT_EXPORT_MODULE':
|
|
200
|
+
if (owner.implementation)
|
|
201
|
+
declarations.modules.push({
|
|
202
|
+
className: owner.className,
|
|
203
|
+
jsName: args[0] ?? '',
|
|
204
|
+
extern: false,
|
|
205
|
+
file,
|
|
206
|
+
})
|
|
207
|
+
break
|
|
208
|
+
case 'RCT_EXPORT_MODULE_NO_LOAD':
|
|
209
|
+
declarations.modules.push({
|
|
210
|
+
className: args[1] || owner.className,
|
|
211
|
+
jsName: args[0] ?? '',
|
|
212
|
+
extern: false,
|
|
213
|
+
file,
|
|
214
|
+
})
|
|
215
|
+
break
|
|
216
|
+
case 'RCT_EXTERN_METHOD':
|
|
217
|
+
case 'RCT_EXPORT_METHOD':
|
|
218
|
+
addMethod(owner.className, {
|
|
219
|
+
jsName: '',
|
|
220
|
+
objcName: args[0] ?? '',
|
|
221
|
+
isSync: false,
|
|
222
|
+
})
|
|
223
|
+
break
|
|
224
|
+
case 'RCT_EXTERN__BLOCKING_SYNCHRONOUS_METHOD':
|
|
225
|
+
case 'RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD':
|
|
226
|
+
addMethod(owner.className, {
|
|
227
|
+
jsName: '',
|
|
228
|
+
objcName: args[0] ?? '',
|
|
229
|
+
isSync: true,
|
|
230
|
+
})
|
|
231
|
+
break
|
|
232
|
+
case 'RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD':
|
|
233
|
+
addMethod(owner.className, {
|
|
234
|
+
jsName: '',
|
|
235
|
+
objcName: args[1] ?? '',
|
|
236
|
+
isSync: true,
|
|
237
|
+
})
|
|
238
|
+
break
|
|
239
|
+
case 'RCT_REMAP_METHOD':
|
|
240
|
+
addMethod(owner.className, {
|
|
241
|
+
jsName: args[0] ?? '',
|
|
242
|
+
objcName: args[1] ?? '',
|
|
243
|
+
isSync: false,
|
|
244
|
+
})
|
|
245
|
+
break
|
|
246
|
+
case 'RCT_REMAP_BLOCKING_SYNCHRONOUS_METHOD':
|
|
247
|
+
addMethod(owner.className, {
|
|
248
|
+
jsName: args[0] ?? '',
|
|
249
|
+
objcName: args[2] ?? '',
|
|
250
|
+
isSync: true,
|
|
251
|
+
})
|
|
252
|
+
break
|
|
253
|
+
case '_RCT_EXTERN_REMAP_METHOD':
|
|
254
|
+
addMethod(owner.className, {
|
|
255
|
+
jsName: args[0] ?? '',
|
|
256
|
+
objcName: args[1] ?? '',
|
|
257
|
+
isSync: args[2] === 'YES',
|
|
258
|
+
})
|
|
259
|
+
break
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
closeImplementation(source.length)
|
|
263
|
+
}
|
|
264
|
+
return declarations
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// the superclass Peach compiles that a class's chain reaches
|
|
268
|
+
function compiledSuperclass(
|
|
269
|
+
className: string,
|
|
270
|
+
superclasses: Map<string, string>,
|
|
271
|
+
): 'RCTEventEmitter' | 'NSObject' | null {
|
|
272
|
+
const seen = new Set<string>()
|
|
273
|
+
let current = superclasses.get(className)
|
|
274
|
+
while (current && !seen.has(current)) {
|
|
275
|
+
if (current === 'RCTEventEmitter' || current === 'NSObject') return current
|
|
276
|
+
seen.add(current)
|
|
277
|
+
current = superclasses.get(current)
|
|
278
|
+
}
|
|
279
|
+
return null
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const swiftString = (value: string) => JSON.stringify(value)
|
|
283
|
+
|
|
284
|
+
function methodsLiteral(methods: MethodInfo[]): string {
|
|
285
|
+
return `[${methods
|
|
286
|
+
.map(
|
|
287
|
+
(method) =>
|
|
288
|
+
`RCTMethodInfo(jsName: ${swiftString(method.jsName)}, objcName: ${swiftString(method.objcName)}, isSync: ${method.isSync})`,
|
|
289
|
+
)
|
|
290
|
+
.join(', ')}]`
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// a Swift class exposed to Objective-C as `objcName`: `@objc(objcName)` on it,
|
|
294
|
+
// else a class of that name
|
|
295
|
+
function swiftClassFor(objcName: string, files: Record<string, string>): string | null {
|
|
296
|
+
const modifiers =
|
|
297
|
+
'(?:\\s*(?:@\\w+(?:\\([^)]*\\))?|public|open|final|internal|fileprivate|private))*'
|
|
298
|
+
const exposed = new RegExp(
|
|
299
|
+
`@objc\\s*\\(\\s*${objcName}\\s*\\)${modifiers}\\s*class\\s+([A-Za-z_]\\w*)`,
|
|
300
|
+
)
|
|
301
|
+
const named = new RegExp(`\\bclass\\s+${objcName}\\b`)
|
|
302
|
+
let fallback: string | null = null
|
|
303
|
+
for (const source of Object.values(files)) {
|
|
304
|
+
const code = swiftCodeMask(source)
|
|
305
|
+
const match = exposed.exec(code)
|
|
306
|
+
if (match) return match[1]!
|
|
307
|
+
if (named.test(code)) fallback = objcName
|
|
308
|
+
}
|
|
309
|
+
return fallback
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export const BRIDGE_MODULE_FILE_PREFIX = '__peach_react_module_'
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* the app target's legacy bridge modules, one compile unit each, or null
|
|
316
|
+
* when the app has no iOS project or registers none. a module the host
|
|
317
|
+
* cannot provide is a unit with a diagnostic instead of files.
|
|
318
|
+
*/
|
|
319
|
+
export function collectBridgeModuleSources(
|
|
320
|
+
appRoot: string,
|
|
321
|
+
signal?: AbortSignal,
|
|
322
|
+
): {
|
|
323
|
+
units: ExpoSwiftSourceUnit[]
|
|
324
|
+
failures: { namespace: string; diagnostic: string }[]
|
|
325
|
+
} | null {
|
|
326
|
+
signal?.throwIfAborted()
|
|
327
|
+
const iosRoot = path.join(appRoot, 'ios')
|
|
328
|
+
if (!fs.existsSync(iosRoot)) return null
|
|
329
|
+
const projectRoot = fs.realpathSync(appRoot)
|
|
330
|
+
const projects = fs
|
|
331
|
+
.readdirSync(iosRoot, { withFileTypes: true })
|
|
332
|
+
.filter((entry) => entry.isDirectory() && entry.name.endsWith('.xcodeproj'))
|
|
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
|
+
)
|
|
340
|
+
// RCTRegisterModule runs in the one app binary; several targets are
|
|
341
|
+
// several apps, and nothing here says which one this bundle runs in
|
|
342
|
+
if (targets.length !== 1) return null
|
|
343
|
+
const app = readXcodeAppTarget(targets[0]!.project, targets[0]!.name, 'Release', {
|
|
344
|
+
requireSources: false,
|
|
345
|
+
})
|
|
346
|
+
const own = (file: string) => {
|
|
347
|
+
const resolved = fs.realpathSync(file)
|
|
348
|
+
if (
|
|
349
|
+
[file, resolved].some((name) =>
|
|
350
|
+
name.split(path.sep).some((part) => part === 'Pods' || part === 'node_modules'),
|
|
351
|
+
)
|
|
352
|
+
)
|
|
353
|
+
return false
|
|
354
|
+
const relative = path.relative(projectRoot, resolved)
|
|
355
|
+
if (
|
|
356
|
+
relative === '..' ||
|
|
357
|
+
relative.startsWith(`..${path.sep}`) ||
|
|
358
|
+
path.isAbsolute(relative)
|
|
359
|
+
)
|
|
360
|
+
throw new Error(`${file}: app native source outside ${projectRoot}`)
|
|
361
|
+
return true
|
|
362
|
+
}
|
|
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
|
+
const units: ExpoSwiftSourceUnit[] = []
|
|
386
|
+
const failures: { namespace: string; diagnostic: string }[] = []
|
|
387
|
+
const seen = new Set<string>()
|
|
388
|
+
for (const module of declarations.modules) {
|
|
389
|
+
signal?.throwIfAborted()
|
|
390
|
+
if (seen.has(module.className)) continue
|
|
391
|
+
seen.add(module.className)
|
|
392
|
+
const moduleId = `${namespace}.${module.className}`
|
|
393
|
+
const methods = declarations.methods.get(module.className) ?? []
|
|
394
|
+
const swiftClass = module.extern ? swiftClassFor(module.className, swiftFiles) : null
|
|
395
|
+
const unit: ExpoSwiftSourceUnit = {
|
|
396
|
+
namespace,
|
|
397
|
+
moduleId,
|
|
398
|
+
classes: [],
|
|
399
|
+
bridgeModules: [],
|
|
400
|
+
files: {},
|
|
401
|
+
frameworks: [],
|
|
402
|
+
swiftLanguageMode: app.languageMode,
|
|
403
|
+
}
|
|
404
|
+
if (swiftClass) {
|
|
405
|
+
const register = `__peachReactModule_${module.className}`
|
|
406
|
+
unit.files = swiftSourceClosure(swiftFiles, [swiftClass])
|
|
407
|
+
unit.files[
|
|
408
|
+
`Sources/${namespace}/${BRIDGE_MODULE_FILE_PREFIX}${module.className}.swift`
|
|
409
|
+
] =
|
|
410
|
+
`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`
|
|
411
|
+
unit.bridgeModules = [`${namespace}.${register}()`]
|
|
412
|
+
} else {
|
|
413
|
+
const superclass = compiledSuperclass(module.className, declarations.superclasses)
|
|
414
|
+
const selectors = declarations.implementations.get(module.className)
|
|
415
|
+
if (!selectors || !superclass) {
|
|
416
|
+
failures.push({
|
|
417
|
+
namespace: moduleId,
|
|
418
|
+
diagnostic: !selectors
|
|
419
|
+
? `${module.file}: no Swift class or Objective-C @implementation provides ${module.className}`
|
|
420
|
+
: `${module.file}: ${module.className}'s superclass ${declarations.superclasses.get(module.className) ?? '(none)'} is Objective-C, which Peach does not compile`,
|
|
421
|
+
})
|
|
422
|
+
continue
|
|
423
|
+
}
|
|
424
|
+
unit.bridgeModules = [
|
|
425
|
+
`RCTModuleHost.Registration(className: ${swiftString(module.className)}, moduleName: ${swiftString(module.jsName)}, methods: ${methodsLiteral(methods)}, objectiveCSelectors: ${JSON.stringify([...selectors].sort())}, superclass: { ${superclass}() })`,
|
|
426
|
+
]
|
|
427
|
+
}
|
|
428
|
+
units.push(unit)
|
|
429
|
+
}
|
|
430
|
+
return { units, failures }
|
|
431
|
+
}
|
|
@@ -18,6 +18,10 @@ export const REACT_NATIVE_PASSTHROUGH_SPECIFIERS: readonly string[] = [
|
|
|
18
18
|
// expo's metro-runtime dials metro's message socket with this WebSocket,
|
|
19
19
|
// which runs against the engine's WebSocketModule and BlobModule.
|
|
20
20
|
'react-native/Libraries/WebSocket',
|
|
21
|
+
// that WebSocket extends upstream's EventTarget and builds its MessageEvent
|
|
22
|
+
// and CloseEvent from the same dom event classes; a stubbed class there is
|
|
23
|
+
// not a constructor to extend.
|
|
24
|
+
'react-native/src/private/webapis',
|
|
21
25
|
'react-native/Libraries/Blob/Blob',
|
|
22
26
|
'react-native/Libraries/Blob/BlobManager',
|
|
23
27
|
'react-native/Libraries/Blob/BlobRegistry',
|
|
@@ -39,7 +43,10 @@ export const REACT_NATIVE_PASSTHROUGH_SPECIFIERS: readonly string[] = [
|
|
|
39
43
|
]
|
|
40
44
|
|
|
41
45
|
export function isReactNativePassthroughSpecifier(specifier: string): boolean {
|
|
46
|
+
// checked in place: every module of a bundle asks at boot
|
|
42
47
|
return REACT_NATIVE_PASSTHROUGH_SPECIFIERS.some(
|
|
43
|
-
(passthrough) =>
|
|
48
|
+
(passthrough) =>
|
|
49
|
+
specifier.startsWith(passthrough) &&
|
|
50
|
+
(specifier.length === passthrough.length || specifier[passthrough.length] === '/'),
|
|
44
51
|
)
|
|
45
52
|
}
|
package/src/settings-bundle.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile, readdir, stat } from 'node:fs/promises'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
+
import { isRecord } from './is-record'
|
|
3
4
|
import { parsePlistSource } from './plist.ts'
|
|
4
5
|
import type { AppPreferenceValue, AppSettingsSpecifier } from '@peach/globals'
|
|
5
6
|
|
|
@@ -22,10 +23,6 @@ interface CachedSettingsBundle {
|
|
|
22
23
|
|
|
23
24
|
const settingsBundleCache = new Map<string, CachedSettingsBundle>()
|
|
24
25
|
|
|
25
|
-
function isRecord(value: unknown): value is PlistRecord {
|
|
26
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
26
|
function optionalString(value: unknown): string | undefined {
|
|
30
27
|
return typeof value === 'string' ? value : undefined
|
|
31
28
|
}
|
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
import { createHash } from 'node:crypto'
|
|
8
8
|
import fs from 'node:fs'
|
|
9
|
-
import os from 'node:os'
|
|
10
9
|
import path from 'node:path'
|
|
10
|
+
import { cacheDir as peachCacheDir } from './home-paths'
|
|
11
11
|
import type { SwiftPackageBinary } from './swift-packages'
|
|
12
12
|
|
|
13
|
-
const cacheDir = path.join(
|
|
13
|
+
const cacheDir = path.join(peachCacheDir(), 'swift-binary-modules')
|
|
14
14
|
|
|
15
15
|
// the first `<name>.framework` segment of each path: a framework's own
|
|
16
16
|
// nested frameworks and macOS Versions/ trees are not modules of the target
|