peachsim 0.1.1635 → 0.1.1641
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -1
- package/cli/app-config.ts +14 -5
- package/cli/app-stubs.ts +96 -22
- package/cli/bridge-flow-runner.ts +2 -2
- package/cli/cloud-client.ts +60 -40
- package/cli/cloud-session.ts +16 -0
- package/cli/commands/assert.ts +17 -4
- package/cli/commands/box.ts +33 -19
- package/cli/commands/doctor.ts +37 -20
- package/cli/commands/flow.ts +1 -1
- package/cli/commands/inspect/actions.ts +3 -2
- package/cli/commands/inspect/core.ts +0 -8
- package/cli/commands/inspect/shared.ts +9 -4
- package/cli/commands/inspect.ts +65 -61
- package/cli/commands/platform.ts +2 -1
- package/cli/commands/preview.ts +1 -1
- package/cli/commands/record.ts +154 -68
- package/cli/commands/swift.ts +18 -4
- package/cli/commands/three-mode.ts +4 -38
- package/cli/commands/upload.ts +55 -49
- package/cli/drivers/playwright-sim-host.ts +65 -42
- package/cli/flow-live-status.ts +0 -2
- package/cli/main.ts +2 -2
- package/cli/outbound-endpoints.ts +7 -5
- package/cli/pause-aware-timeout.ts +4 -0
- package/cli/ws-bridge.ts +19 -16
- package/detox/attempt-events.cjs +99 -6
- package/detox/attempt-events.d.cts +7 -0
- package/detox/expectations.ts +7 -3
- package/detox/index.ts +259 -139
- package/detox/jest-environment.cjs +5 -1
- package/detox/motion-change.cjs +170 -21
- package/detox/motion-change.d.cts +32 -0
- 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 +17 -7
- 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 +5014 -2268
- 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 +1036 -166
- 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 +4613 -1963
- package/dist-lib/expo-swift-metadata.cjs +9 -7
- package/dist-lib/expo-swift-metadata.mjs +9 -7
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +58072 -16400
- 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 +2790 -143
- 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 +2579 -95
- package/dist-lib/native-app-identity-project.cjs +243 -53
- package/dist-lib/native-app-identity-project.mjs +243 -53
- package/dist-lib/native-app-identity.cjs +29 -30
- package/dist-lib/native-app-identity.mjs +29 -30
- 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 +2 -2
- package/dist-lib/react-native-host-modules.mjs +2 -2
- package/dist-lib/scripts/dev-server-scanner.cjs +8 -6
- package/dist-lib/sdk.cjs +4 -3
- package/dist-lib/sdk.mjs +4 -3
- package/dist-lib/skills.cjs +66475 -23097
- package/dist-lib/swift-compile.cjs +452 -260
- package/dist-lib/swift-compile.mjs +451 -252
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +14 -7
- package/dist-lib/swift-submit.mjs +14 -7
- package/dist-lib/swift-toolchain.cjs +316 -41
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +8188 -5636
- package/dist-lib/vite.cjs +2857 -12
- package/dist-lib/worklet-executable.cjs +14 -2
- package/dist-lib/worklet-executable.mjs +14 -2
- package/package.json +1 -1
- package/src/backend-origin.ts +21 -6
- package/src/expo-swift-host.ts +5 -2
- package/src/expoKotlinContract.ts +4 -7
- package/src/expoSwiftContract.ts +4 -6
- package/src/host/staged-host-files.ts +9 -0
- package/src/native-app-identity-project.ts +1 -4
- package/src/native-app-identity.ts +1 -4
- package/src/nativeAppIconContract.ts +1 -4
- package/src/output-lock-stdio.ts +36 -0
- package/src/react-native-bridge-modules.ts +106 -78
- package/src/react-native-host-modules.ts +4 -1
- package/src/settings-bundle.ts +1 -4
- package/src/swift-compile.ts +224 -139
- package/src/swift-distribution.lock.json +23 -12
- package/src/swift-distribution.ts +103 -4
- package/src/swift-host.ts +95 -30
- package/src/swift-icu.ts +16 -10
- package/src/swift-objc-compile.ts +547 -0
- package/src/swift-objc-metadata.ts +204 -0
- package/src/swift-objc-sdk.ts +883 -19
- package/src/swift-objc-standin.ts +126 -14
- package/src/swift-package-manifest.ts +42 -1
- package/src/swift-packages.ts +35 -21
- package/src/swift-root-lock.ts +153 -0
- package/src/swift-standin-emit.ts +40 -7
- package/src/swift-standins.ts +404 -138
- package/src/swift-submit.ts +18 -7
- package/src/swift-toolchain.ts +378 -46
- package/src/swiftContract.ts +12 -0
- package/src/worker-system-chunks.ts +11 -12
- package/src/worklet-executable.ts +19 -1
- package/src/xcode-project.ts +280 -33
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! peach v0.1.
|
|
1
|
+
/*! peach v0.1.1641 | (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;
|
|
@@ -185,6 +185,8 @@ function enter(node, scope) {
|
|
|
185
185
|
const name = identifierName(id);
|
|
186
186
|
if (name !== void 0 && isNode(init))
|
|
187
187
|
declare(target, name, { kind: "value", init, scope });
|
|
188
|
+
else if (name !== void 0 && init === null)
|
|
189
|
+
declare(target, name, { kind: "uninitialized" });
|
|
188
190
|
else declareOpaque(target, patternNames(id));
|
|
189
191
|
}
|
|
190
192
|
return scope;
|
|
@@ -254,12 +256,18 @@ function collectInitData(root) {
|
|
|
254
256
|
immediate: /* @__PURE__ */ new Map(),
|
|
255
257
|
writes: [],
|
|
256
258
|
reassigned: /* @__PURE__ */ new Set(),
|
|
259
|
+
assigned: /* @__PURE__ */ new Map(),
|
|
257
260
|
free: /* @__PURE__ */ new Map()
|
|
258
261
|
};
|
|
259
262
|
visit(root, newScope(void 0, true), collection);
|
|
260
263
|
for (const { name, scope, value } of collection.writes) {
|
|
261
264
|
const binding = lookup(scope, name);
|
|
262
|
-
if (binding
|
|
265
|
+
if (binding?.kind === "uninitialized")
|
|
266
|
+
collection.assigned.set(
|
|
267
|
+
binding,
|
|
268
|
+
collection.assigned.has(binding) || !value ? null : { value, scope }
|
|
269
|
+
);
|
|
270
|
+
else if (binding) collection.reassigned.add(binding);
|
|
263
271
|
else
|
|
264
272
|
collection.free.set(
|
|
265
273
|
name,
|
|
@@ -320,6 +328,10 @@ function resolveObject(expression, scope, collection, depth) {
|
|
|
320
328
|
return free ? resolveObject(free.value, free.scope, collection, depth + 1) : void 0;
|
|
321
329
|
}
|
|
322
330
|
if (collection.reassigned.has(binding)) return void 0;
|
|
331
|
+
if (binding.kind === "uninitialized") {
|
|
332
|
+
const assigned = collection.assigned.get(binding);
|
|
333
|
+
return assigned ? resolveObject(assigned.value, assigned.scope, collection, depth + 1) : void 0;
|
|
334
|
+
}
|
|
323
335
|
if (binding.kind === "value")
|
|
324
336
|
return resolveObject(binding.init, binding.scope, collection, depth + 1);
|
|
325
337
|
if (binding.kind === "param") {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! peach v0.1.
|
|
1
|
+
/*! peach v0.1.1641 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
|
|
2
2
|
|
|
3
3
|
// src/worklet-executable.ts
|
|
4
4
|
import { parse } from "acorn";
|
|
@@ -160,6 +160,8 @@ function enter(node, scope) {
|
|
|
160
160
|
const name = identifierName(id);
|
|
161
161
|
if (name !== void 0 && isNode(init))
|
|
162
162
|
declare(target, name, { kind: "value", init, scope });
|
|
163
|
+
else if (name !== void 0 && init === null)
|
|
164
|
+
declare(target, name, { kind: "uninitialized" });
|
|
163
165
|
else declareOpaque(target, patternNames(id));
|
|
164
166
|
}
|
|
165
167
|
return scope;
|
|
@@ -229,12 +231,18 @@ function collectInitData(root) {
|
|
|
229
231
|
immediate: /* @__PURE__ */ new Map(),
|
|
230
232
|
writes: [],
|
|
231
233
|
reassigned: /* @__PURE__ */ new Set(),
|
|
234
|
+
assigned: /* @__PURE__ */ new Map(),
|
|
232
235
|
free: /* @__PURE__ */ new Map()
|
|
233
236
|
};
|
|
234
237
|
visit(root, newScope(void 0, true), collection);
|
|
235
238
|
for (const { name, scope, value } of collection.writes) {
|
|
236
239
|
const binding = lookup(scope, name);
|
|
237
|
-
if (binding
|
|
240
|
+
if (binding?.kind === "uninitialized")
|
|
241
|
+
collection.assigned.set(
|
|
242
|
+
binding,
|
|
243
|
+
collection.assigned.has(binding) || !value ? null : { value, scope }
|
|
244
|
+
);
|
|
245
|
+
else if (binding) collection.reassigned.add(binding);
|
|
238
246
|
else
|
|
239
247
|
collection.free.set(
|
|
240
248
|
name,
|
|
@@ -295,6 +303,10 @@ function resolveObject(expression, scope, collection, depth) {
|
|
|
295
303
|
return free ? resolveObject(free.value, free.scope, collection, depth + 1) : void 0;
|
|
296
304
|
}
|
|
297
305
|
if (collection.reassigned.has(binding)) return void 0;
|
|
306
|
+
if (binding.kind === "uninitialized") {
|
|
307
|
+
const assigned = collection.assigned.get(binding);
|
|
308
|
+
return assigned ? resolveObject(assigned.value, assigned.scope, collection, depth + 1) : void 0;
|
|
309
|
+
}
|
|
298
310
|
if (binding.kind === "value")
|
|
299
311
|
return resolveObject(binding.init, binding.scope, collection, depth + 1);
|
|
300
312
|
if (binding.kind === "param") {
|
package/package.json
CHANGED
package/src/backend-origin.ts
CHANGED
|
@@ -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
|
|
@@ -98,12 +106,16 @@ export function isBrowserAssetProxyUrl(value: string): boolean {
|
|
|
98
106
|
}
|
|
99
107
|
}
|
|
100
108
|
|
|
109
|
+
// the PeachSim app's host; the backend serves every path on it
|
|
110
|
+
export const PEACHSIM_NATIVE_HOST = 'native.peachsim.com'
|
|
111
|
+
|
|
101
112
|
export function isContrastBackendHost(hostname: string): boolean {
|
|
102
113
|
const normalized = normalizeHostname(hostname)
|
|
103
114
|
return (
|
|
104
115
|
isLoopbackHost(normalized) ||
|
|
105
116
|
normalized === 'contrast.dev' ||
|
|
106
|
-
normalized.endsWith('.contrast.dev')
|
|
117
|
+
normalized.endsWith('.contrast.dev') ||
|
|
118
|
+
normalized === PEACHSIM_NATIVE_HOST
|
|
107
119
|
)
|
|
108
120
|
}
|
|
109
121
|
|
|
@@ -115,15 +127,18 @@ export function isContrastBackendOrigin(origin: string): boolean {
|
|
|
115
127
|
}
|
|
116
128
|
}
|
|
117
129
|
|
|
118
|
-
|
|
119
|
-
|
|
130
|
+
// a page's `?backend=` names another backend origin, judged by the same rule
|
|
131
|
+
export function resolveContrastBackendOrigin(
|
|
132
|
+
currentOrigin?: string | null,
|
|
133
|
+
search?: string | null,
|
|
134
|
+
): string {
|
|
135
|
+
const origin =
|
|
136
|
+
new URLSearchParams(search ?? '').get('backend')?.trim() || currentOrigin?.trim()
|
|
120
137
|
if (!origin) return DEFAULT_CONTRAST_BACKEND_ORIGIN
|
|
121
138
|
|
|
122
139
|
try {
|
|
123
140
|
const parsed = new URL(origin)
|
|
124
|
-
if (
|
|
125
|
-
return parsed.origin
|
|
126
|
-
}
|
|
141
|
+
if (isContrastBackendHost(parsed.hostname)) return parsed.origin
|
|
127
142
|
} catch {}
|
|
128
143
|
|
|
129
144
|
return DEFAULT_CONTRAST_BACKEND_ORIGIN
|
package/src/expo-swift-host.ts
CHANGED
|
@@ -136,7 +136,10 @@ export async function collectExpoSwiftSources(
|
|
|
136
136
|
let bytes = 0
|
|
137
137
|
let fileCount = 0
|
|
138
138
|
// native Expo resolves packages by name before generating its Apple provider.
|
|
139
|
-
|
|
139
|
+
// the search result is keyed by that name; older autolinkers add no name field.
|
|
140
|
+
for (const [packageName, entry] of Object.entries(search).sort(([a], [b]) =>
|
|
141
|
+
a.localeCompare(b),
|
|
142
|
+
)) {
|
|
140
143
|
if (!entry || typeof entry !== 'object')
|
|
141
144
|
throw new Error('invalid Expo autolinking entry')
|
|
142
145
|
const sourceRoot = Reflect.get(entry, 'path')
|
|
@@ -149,7 +152,7 @@ export async function collectExpoSwiftSources(
|
|
|
149
152
|
(candidate) =>
|
|
150
153
|
candidate &&
|
|
151
154
|
typeof candidate === 'object' &&
|
|
152
|
-
Reflect.get(candidate, 'packageName') ===
|
|
155
|
+
Reflect.get(candidate, 'packageName') === packageName,
|
|
153
156
|
)
|
|
154
157
|
const namespaces =
|
|
155
158
|
resolution && typeof resolution === 'object'
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { insertBundleGlobalFooter, readBundleGlobalFooter } from './bundleGlobalFooter'
|
|
2
|
+
import { isRecord } from './is-record'
|
|
2
3
|
|
|
3
4
|
// what the dev server answers for an app's local Expo Kotlin modules on an
|
|
4
5
|
// Android device: one linked Kotlin/Wasm process, or each module's diagnostic.
|
|
@@ -10,14 +11,10 @@ export interface ExpoKotlinCompilationResult {
|
|
|
10
11
|
failures: { namespace: string; diagnostic: string }[]
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
function record(value: unknown): value is Record<string, unknown> {
|
|
14
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
14
|
export function readExpoKotlinMetadata(value: unknown): ExpoKotlinCompilationResult {
|
|
18
15
|
const invalid = () => new Error('invalid Expo Kotlin compilation metadata')
|
|
19
16
|
if (
|
|
20
|
-
!
|
|
17
|
+
!isRecord(value) ||
|
|
21
18
|
!Array.isArray(value.namespaces) ||
|
|
22
19
|
!Array.isArray(value.failures)
|
|
23
20
|
)
|
|
@@ -30,7 +27,7 @@ export function readExpoKotlinMetadata(value: unknown): ExpoKotlinCompilationRes
|
|
|
30
27
|
const failures: ExpoKotlinCompilationResult['failures'] = []
|
|
31
28
|
for (const failure of value.failures) {
|
|
32
29
|
if (
|
|
33
|
-
!
|
|
30
|
+
!isRecord(failure) ||
|
|
34
31
|
typeof failure.namespace !== 'string' ||
|
|
35
32
|
typeof failure.diagnostic !== 'string'
|
|
36
33
|
)
|
|
@@ -40,7 +37,7 @@ export function readExpoKotlinMetadata(value: unknown): ExpoKotlinCompilationRes
|
|
|
40
37
|
const module = value.module
|
|
41
38
|
if (module === null) return { namespaces, module: null, failures }
|
|
42
39
|
if (
|
|
43
|
-
!
|
|
40
|
+
!isRecord(module) ||
|
|
44
41
|
typeof module.url !== 'string' ||
|
|
45
42
|
typeof module.hash !== 'string' ||
|
|
46
43
|
typeof module.compose !== 'boolean'
|
package/src/expoSwiftContract.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { insertBundleGlobalFooter, readBundleGlobalFooter } from './bundleGlobalFooter'
|
|
2
|
+
import { isRecord } from './is-record'
|
|
2
3
|
|
|
3
4
|
export interface ExpoSwiftPreparationOptions {
|
|
4
5
|
signal?: AbortSignal
|
|
@@ -33,9 +34,6 @@ export interface ExpoSwiftCompilationResult {
|
|
|
33
34
|
failures: { namespace: string; diagnostic: string }[]
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
function record(value: unknown): value is Record<string, unknown> {
|
|
37
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
38
|
-
}
|
|
39
37
|
function strings(value: unknown): value is string[] {
|
|
40
38
|
return Array.isArray(value) && value.every((item) => typeof item === 'string')
|
|
41
39
|
}
|
|
@@ -44,7 +42,7 @@ export function isExpoSwiftCompilationResult(
|
|
|
44
42
|
value: unknown,
|
|
45
43
|
): value is ExpoSwiftCompilationResult {
|
|
46
44
|
if (
|
|
47
|
-
!
|
|
45
|
+
!isRecord(value) ||
|
|
48
46
|
!strings(value.namespaces) ||
|
|
49
47
|
!Array.isArray(value.modules) ||
|
|
50
48
|
value.modules.length > 1 ||
|
|
@@ -62,7 +60,7 @@ export function isExpoSwiftCompilationResult(
|
|
|
62
60
|
return false
|
|
63
61
|
for (const artifact of value.modules) {
|
|
64
62
|
if (
|
|
65
|
-
!
|
|
63
|
+
!isRecord(artifact) ||
|
|
66
64
|
!strings(artifact.namespaces) ||
|
|
67
65
|
artifact.namespaces.length === 0 ||
|
|
68
66
|
!artifact.namespaces.every((namespace) => remaining.delete(namespace)) ||
|
|
@@ -75,7 +73,7 @@ export function isExpoSwiftCompilationResult(
|
|
|
75
73
|
}
|
|
76
74
|
for (const failure of value.failures) {
|
|
77
75
|
if (
|
|
78
|
-
!
|
|
76
|
+
!isRecord(failure) ||
|
|
79
77
|
typeof failure.namespace !== 'string' ||
|
|
80
78
|
!remaining.delete(failure.namespace) ||
|
|
81
79
|
typeof failure.diagnostic !== 'string'
|
|
@@ -95,6 +95,15 @@ export async function handleStagedHostFileRequest(
|
|
|
95
95
|
res.end('staged file not found')
|
|
96
96
|
return
|
|
97
97
|
}
|
|
98
|
+
if (req.method === 'OPTIONS') {
|
|
99
|
+
res.writeHead(204, {
|
|
100
|
+
'Access-Control-Allow-Methods': 'GET, HEAD',
|
|
101
|
+
'Access-Control-Allow-Headers':
|
|
102
|
+
req.headers['access-control-request-headers'] ?? '*',
|
|
103
|
+
})
|
|
104
|
+
res.end()
|
|
105
|
+
return
|
|
106
|
+
}
|
|
98
107
|
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
99
108
|
res.writeHead(405, { Allow: 'GET, HEAD' })
|
|
100
109
|
res.end('method not allowed')
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs'
|
|
|
2
2
|
import { createRequire } from 'node:module'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { androidMergedResourceValuesDirectory } from './android-app-identity'
|
|
5
|
+
import { isRecord } from './is-record'
|
|
5
6
|
import {
|
|
6
7
|
extractNativeAppIdentityFromFiles,
|
|
7
8
|
type NativeAppIdentity,
|
|
@@ -17,10 +18,6 @@ import {
|
|
|
17
18
|
type XcodeAppTarget,
|
|
18
19
|
} from './xcode-project'
|
|
19
20
|
|
|
20
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
21
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
21
|
function nonEmptyString(value: unknown): string | undefined {
|
|
25
22
|
return typeof value === 'string' && value.length > 0 ? value : undefined
|
|
26
23
|
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { nativeAppIdentityFromAndroidManifest } from './android-app-identity'
|
|
12
12
|
import { findBundleFooter } from './bundleGlobalFooter'
|
|
13
|
+
import { isRecord } from './is-record'
|
|
13
14
|
import {
|
|
14
15
|
resolveNativeAppIconArtwork,
|
|
15
16
|
isNativeAppIconArtwork,
|
|
@@ -99,10 +100,6 @@ interface NativeAppIdentityGlobals {
|
|
|
99
100
|
__sootsim_native_app_identity?: NativeAppIdentity | null
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
103
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
103
|
function nonEmptyString(value: unknown): string | null {
|
|
107
104
|
return typeof value === 'string' && value.length > 0 ? value : null
|
|
108
105
|
}
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import { isSwiftImageVariants, type SwiftImageVariants } from './assetCatalogContract'
|
|
2
|
+
import { isRecord } from './is-record'
|
|
2
3
|
|
|
3
4
|
export interface NativeAppIconArtwork {
|
|
4
5
|
primary?: SwiftImageVariants
|
|
5
6
|
alternates: Record<string, SwiftImageVariants>
|
|
6
7
|
}
|
|
7
8
|
|
|
8
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
9
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
10
|
-
}
|
|
11
|
-
|
|
12
9
|
export function isNativeAppIconArtwork(value: unknown): value is NativeAppIconArtwork {
|
|
13
10
|
return (
|
|
14
11
|
isRecord(value) &&
|
|
@@ -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
|
+
}
|
|
@@ -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,
|
|
@@ -326,91 +330,115 @@ export function collectBridgeModuleSources(
|
|
|
326
330
|
signal?.throwIfAborted()
|
|
327
331
|
const iosRoot = path.join(appRoot, 'ios')
|
|
328
332
|
if (!fs.existsSync(iosRoot)) return null
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const targets = projects.flatMap((project) =>
|
|
334
|
-
listXcodeAppTargets(project, { requireSources: false }).map((name) => ({
|
|
335
|
-
project,
|
|
336
|
-
name,
|
|
337
|
-
})),
|
|
338
|
-
)
|
|
333
|
+
const projectRoot = fs.realpathSync(appRoot)
|
|
334
|
+
const project = findXcodeProject(iosRoot)
|
|
335
|
+
if (!project) return null
|
|
336
|
+
const targets = listXcodeAppTargets(project, { requireSources: false })
|
|
339
337
|
// RCTRegisterModule runs in the one app binary; several targets are
|
|
340
338
|
// several apps, and nothing here says which one this bundle runs in
|
|
341
339
|
if (targets.length !== 1) return null
|
|
342
|
-
const app = readXcodeAppTarget(
|
|
340
|
+
const app = readXcodeAppTarget(project, targets[0]!, 'Release', {
|
|
343
341
|
requireSources: false,
|
|
344
342
|
})
|
|
345
|
-
const own = (file: string) =>
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const namespace = app.moduleName.replace(/[^A-Za-z0-9_]/g, '_')
|
|
362
|
-
const swiftFiles: Record<string, string> = {}
|
|
363
|
-
for (const file of app.sources.filter(own)) {
|
|
364
|
-
const relative = path.relative(iosRoot, file)
|
|
365
|
-
if (relative.startsWith('..') || path.isAbsolute(relative))
|
|
366
|
-
throw new Error(`${file}: app Swift source outside ${iosRoot}`)
|
|
367
|
-
swiftFiles[`Sources/${namespace}/${relative.split(path.sep).join('/')}`] =
|
|
368
|
-
`#sourceLocation(file: ${JSON.stringify(file)}, line: 1)\n${fs.readFileSync(file, 'utf8')}`
|
|
343
|
+
const own = (file: string) => {
|
|
344
|
+
const resolved = fs.realpathSync(file)
|
|
345
|
+
if (
|
|
346
|
+
[file, resolved].some((name) =>
|
|
347
|
+
name.split(path.sep).some((part) => part === 'Pods' || part === 'node_modules'),
|
|
348
|
+
)
|
|
349
|
+
)
|
|
350
|
+
return false
|
|
351
|
+
const relative = path.relative(projectRoot, resolved)
|
|
352
|
+
if (
|
|
353
|
+
relative === '..' ||
|
|
354
|
+
relative.startsWith(`..${path.sep}`) ||
|
|
355
|
+
path.isAbsolute(relative)
|
|
356
|
+
)
|
|
357
|
+
throw new Error(`${file}: app native source outside ${projectRoot}`)
|
|
358
|
+
return true
|
|
369
359
|
}
|
|
370
360
|
const units: ExpoSwiftSourceUnit[] = []
|
|
371
361
|
const failures: { namespace: string; diagnostic: string }[] = []
|
|
372
|
-
|
|
373
|
-
|
|
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]) {
|
|
374
365
|
signal?.throwIfAborted()
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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')}`
|
|
388
396
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
+
]
|
|
408
439
|
}
|
|
409
|
-
unit
|
|
410
|
-
`RCTModuleHost.Registration(className: ${swiftString(module.className)}, moduleName: ${swiftString(module.jsName)}, methods: ${methodsLiteral(methods)}, objectiveCSelectors: ${JSON.stringify([...selectors].sort())}, superclass: { ${superclass}() })`,
|
|
411
|
-
]
|
|
440
|
+
units.push(unit)
|
|
412
441
|
}
|
|
413
|
-
units.push(unit)
|
|
414
442
|
}
|
|
415
|
-
return { units, failures }
|
|
443
|
+
return units.length === 0 && failures.length === 0 ? null : { units, failures }
|
|
416
444
|
}
|
|
@@ -43,7 +43,10 @@ export const REACT_NATIVE_PASSTHROUGH_SPECIFIERS: readonly string[] = [
|
|
|
43
43
|
]
|
|
44
44
|
|
|
45
45
|
export function isReactNativePassthroughSpecifier(specifier: string): boolean {
|
|
46
|
+
// checked in place: every module of a bundle asks at boot
|
|
46
47
|
return REACT_NATIVE_PASSTHROUGH_SPECIFIERS.some(
|
|
47
|
-
(passthrough) =>
|
|
48
|
+
(passthrough) =>
|
|
49
|
+
specifier.startsWith(passthrough) &&
|
|
50
|
+
(specifier.length === passthrough.length || specifier[passthrough.length] === '/'),
|
|
48
51
|
)
|
|
49
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
|
}
|