peachsim 0.1.1625 → 0.1.1635
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 +10 -1
- package/cli/app-config.ts +4 -1
- package/cli/app-fonts.ts +37 -17
- package/cli/app-stubs.ts +224 -9
- package/cli/bridge-flow-runner.ts +64 -43
- package/cli/command-registry.ts +1 -0
- package/cli/commands/control.ts +283 -49
- package/cli/commands/doctor.ts +17 -184
- 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/wait-ready.ts +12 -0
- package/cli/commands/inspect.ts +26 -3
- 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/record.ts +21 -1
- 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 +5 -0
- package/cli/commands/upload.ts +46 -7
- package/cli/desktop-companion.ts +102 -17
- package/cli/drivers/desktop.ts +14 -1
- package/cli/drivers/playwright-sim-host.ts +38 -3
- 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 +2 -1
- package/cli/outbound-endpoints.ts +13 -1
- package/cli/pause-aware-timeout.ts +102 -0
- package/cli/scene-video.ts +89 -0
- package/cli/ws-bridge.ts +23 -14
- package/detox/attempt-events.cjs +4 -0
- package/detox/index.ts +167 -142
- package/detox/motion-change.cjs +57 -21
- package/detox/motion-change.d.cts +7 -2
- 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 +1 -1
- 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 +9536 -7604
- 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 +895 -664
- package/dist-lib/dev-bundle-resolution.cjs +14 -2
- package/dist-lib/expo-swift-bundle.cjs +7818 -5916
- package/dist-lib/expo-swift-metadata.cjs +3 -1
- package/dist-lib/expo-swift-metadata.mjs +3 -1
- package/dist-lib/home-paths.cjs +32 -3
- package/dist-lib/host/bridge-host.cjs +8340 -6392
- 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 +812 -339
- 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 +391 -28
- package/dist-lib/native-app-identity-project.cjs +55 -2
- package/dist-lib/native-app-identity-project.mjs +55 -2
- package/dist-lib/native-app-identity.cjs +53 -1
- package/dist-lib/native-app-identity.mjs +52 -1
- package/dist-lib/native-firebase-metadata.cjs +1 -1
- package/dist-lib/native-firebase-metadata.mjs +1 -1
- package/dist-lib/plist.cjs +1 -1
- package/dist-lib/plist.mjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +5 -1
- package/dist-lib/react-native-host-modules.mjs +5 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +15 -3
- package/dist-lib/sdk.cjs +27 -11
- package/dist-lib/sdk.mjs +27 -10
- package/dist-lib/skills.cjs +12276 -8947
- package/dist-lib/swift-compile.cjs +924 -118
- package/dist-lib/swift-compile.mjs +966 -152
- package/dist-lib/swift-package.cjs +12 -3
- package/dist-lib/swift-package.mjs +12 -3
- package/dist-lib/swift-submit.cjs +7 -1
- package/dist-lib/swift-submit.mjs +5 -1
- package/dist-lib/swift-toolchain.cjs +133 -18
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +1955 -498
- package/dist-lib/vite.cjs +3 -1
- package/dist-lib/worklet-executable.cjs +1 -1
- package/dist-lib/worklet-executable.mjs +1 -1
- 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/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 +191 -47
- package/src/expo-swift-ready.ts +78 -0
- package/src/expoSwiftContract.ts +13 -0
- package/src/home-paths.ts +48 -2
- package/src/host/bridge-host.ts +5 -2
- package/src/native-app-identity-project.ts +26 -2
- package/src/native-app-identity.ts +90 -0
- package/src/native-firebase-config.ts +209 -13
- package/src/process-group.ts +25 -2
- package/src/react-native-bridge-modules.ts +416 -0
- package/src/react-native-host-modules.ts +4 -0
- package/src/swift-binary-modules.ts +2 -2
- package/src/swift-compile.ts +835 -84
- package/src/swift-compiler-digest.ts +14 -0
- package/src/swift-distribution.lock.json +14 -12
- package/src/swift-host.ts +91 -29
- package/src/swift-icu.ts +14 -6
- package/src/swift-objc-sdk.ts +133 -10
- package/src/swift-objc-standin.ts +25 -3
- package/src/swift-package-manifest.ts +21 -6
- package/src/swift-package.ts +15 -1
- package/src/swift-packages.ts +119 -60
- package/src/swift-standin-emit.ts +57 -9
- package/src/swift-standins.ts +168 -20
- package/src/swift-submit.ts +10 -0
- package/src/swift-toolchain.ts +149 -23
- package/src/vite-plugin.ts +12 -1
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
extractNativeAppIdentityFromFiles,
|
|
7
7
|
type NativeAppIdentity,
|
|
8
8
|
readRootModuleName,
|
|
9
|
+
readGoogleSignInConfiguration,
|
|
9
10
|
selectNativeAppIdentityReader,
|
|
10
11
|
} from './native-app-identity'
|
|
11
12
|
import { resolveNativeAppIconArtwork } from './nativeAppIconContract'
|
|
@@ -90,6 +91,11 @@ export function readNativeAppIdentityFromXcodeTarget(
|
|
|
90
91
|
permissionUsageDescriptions[key] = value
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
const infoPlistStrings: Record<string, string> = {}
|
|
95
|
+
for (const [key, value] of Object.entries(dictionary)) {
|
|
96
|
+
if (typeof value === 'string') infoPlistStrings[key] = value
|
|
97
|
+
}
|
|
98
|
+
|
|
93
99
|
const querySchemes = dictionary.LSApplicationQueriesSchemes
|
|
94
100
|
const urlQuerySchemes = Array.isArray(querySchemes) ? querySchemes : []
|
|
95
101
|
|
|
@@ -146,6 +152,15 @@ export function readNativeAppIdentityFromXcodeTarget(
|
|
|
146
152
|
...(rootModuleName ? { rootModuleName } : {}),
|
|
147
153
|
...(rootViewBackgroundColor !== undefined ? { rootViewBackgroundColor } : {}),
|
|
148
154
|
...(designRequiresCompatibility ? { designRequiresCompatibility } : {}),
|
|
155
|
+
...(typeof dictionary.UIStatusBarHidden === 'boolean'
|
|
156
|
+
? { statusBarHidden: dictionary.UIStatusBarHidden }
|
|
157
|
+
: {}),
|
|
158
|
+
...(typeof dictionary.UIViewControllerBasedStatusBarAppearance === 'boolean'
|
|
159
|
+
? {
|
|
160
|
+
viewControllerBasedStatusBarAppearance:
|
|
161
|
+
dictionary.UIViewControllerBasedStatusBarAppearance,
|
|
162
|
+
}
|
|
163
|
+
: {}),
|
|
149
164
|
...(typeof dictionary.DTPlatformVersion === 'string'
|
|
150
165
|
? { dtPlatformVersion: dictionary.DTPlatformVersion }
|
|
151
166
|
: {}),
|
|
@@ -154,6 +169,14 @@ export function readNativeAppIdentityFromXcodeTarget(
|
|
|
154
169
|
buildVersion,
|
|
155
170
|
bundleIdentifier,
|
|
156
171
|
platform: 'ios',
|
|
172
|
+
googleSignIn: readGoogleSignInConfiguration(
|
|
173
|
+
dictionary,
|
|
174
|
+
Object.fromEntries(
|
|
175
|
+
app.resources
|
|
176
|
+
.filter((file) => file.endsWith('.plist'))
|
|
177
|
+
.map((file) => [file, parseInfoPlist(fs.readFileSync(file, 'utf8'))]),
|
|
178
|
+
),
|
|
179
|
+
),
|
|
157
180
|
supportedInterfaceOrientations,
|
|
158
181
|
ipadSupportedInterfaceOrientations,
|
|
159
182
|
...(defaultScreenOrientationMask ? { defaultScreenOrientationMask } : {}),
|
|
@@ -177,6 +200,7 @@ export function readNativeAppIdentityFromXcodeTarget(
|
|
|
177
200
|
(value) => value,
|
|
178
201
|
),
|
|
179
202
|
permissionUsageDescriptions,
|
|
203
|
+
infoPlistStrings,
|
|
180
204
|
urlQuerySchemes,
|
|
181
205
|
...(shareIntoAppGroupId ? { shareIntoAppGroupId } : {}),
|
|
182
206
|
...(rtcAppGroupIdentifier ? { rtcAppGroupIdentifier } : {}),
|
|
@@ -364,8 +388,8 @@ function readIosNativeAppIdentity(
|
|
|
364
388
|
? nonEmptyString(installedManifest.version)
|
|
365
389
|
: undefined
|
|
366
390
|
if (!installedVersion) throw new Error('Reanimated package version is malformed')
|
|
367
|
-
//
|
|
368
|
-
if (
|
|
391
|
+
// the static feature flag schema starts with reanimated 4.
|
|
392
|
+
if (Number.parseInt(installedVersion, 10) < 4) return identity
|
|
369
393
|
const defaults: unknown = JSON.parse(
|
|
370
394
|
fs.readFileSync(
|
|
371
395
|
path.join(
|
|
@@ -48,6 +48,9 @@ export interface NativeAppIdentity {
|
|
|
48
48
|
// UIDesignRequiresCompatibility: UIKit draws its pre-liquid-glass chrome.
|
|
49
49
|
// carried only when the plist sets it true.
|
|
50
50
|
designRequiresCompatibility?: true
|
|
51
|
+
// initial UIKit visibility and the owner of subsequent status bar preferences.
|
|
52
|
+
statusBarHidden?: boolean
|
|
53
|
+
viewControllerBasedStatusBarAppearance?: boolean
|
|
51
54
|
// the literal DTPlatformVersion stamp when the app plist supplies one.
|
|
52
55
|
dtPlatformVersion?: string
|
|
53
56
|
// CFBundleName, which CFNetwork stamps into the default User-Agent and iOS's
|
|
@@ -62,6 +65,10 @@ export interface NativeAppIdentity {
|
|
|
62
65
|
// SDK filtering belongs to the evaluation's device, not the bundle builder.
|
|
63
66
|
androidManifestPermissions?: readonly AndroidManifestPermission[]
|
|
64
67
|
permissionUsageDescriptions?: Readonly<Record<string, unknown>>
|
|
68
|
+
// every top-level string the app's Info.plist declares, build settings
|
|
69
|
+
// resolved, for native code that reads its own plist keys.
|
|
70
|
+
infoPlistStrings?: Readonly<Record<string, string>>
|
|
71
|
+
googleSignIn?: Readonly<Record<string, unknown>>
|
|
65
72
|
urlQuerySchemes?: readonly unknown[]
|
|
66
73
|
appIntentIdentifiers?: readonly string[]
|
|
67
74
|
backgroundTaskIdentifiers?: readonly string[]
|
|
@@ -102,6 +109,9 @@ function nonEmptyString(value: unknown): string | null {
|
|
|
102
109
|
|
|
103
110
|
export function isNativeAppIdentity(value: unknown): value is NativeAppIdentity {
|
|
104
111
|
if (!isRecord(value)) return false
|
|
112
|
+
for (const key of ['statusBarHidden', 'viewControllerBasedStatusBarAppearance']) {
|
|
113
|
+
if (value[key] !== undefined && typeof value[key] !== 'boolean') return false
|
|
114
|
+
}
|
|
105
115
|
const oneUpdates = value.oneUpdates
|
|
106
116
|
if (
|
|
107
117
|
oneUpdates !== undefined &&
|
|
@@ -143,6 +153,7 @@ export function isNativeAppIdentity(value: unknown): value is NativeAppIdentity
|
|
|
143
153
|
))
|
|
144
154
|
)
|
|
145
155
|
return false
|
|
156
|
+
if (value.googleSignIn !== undefined && !isRecord(value.googleSignIn)) return false
|
|
146
157
|
const descriptions = value.permissionUsageDescriptions
|
|
147
158
|
if (
|
|
148
159
|
value.reanimatedStaticFeatureFlags !== undefined &&
|
|
@@ -153,6 +164,12 @@ export function isNativeAppIdentity(value: unknown): value is NativeAppIdentity
|
|
|
153
164
|
)
|
|
154
165
|
return false
|
|
155
166
|
if (descriptions !== undefined && !isRecord(descriptions)) return false
|
|
167
|
+
if (
|
|
168
|
+
value.infoPlistStrings !== undefined &&
|
|
169
|
+
(!isRecord(value.infoPlistStrings) ||
|
|
170
|
+
!Object.values(value.infoPlistStrings).every((entry) => typeof entry === 'string'))
|
|
171
|
+
)
|
|
172
|
+
return false
|
|
156
173
|
if (value.urlQuerySchemes !== undefined && !Array.isArray(value.urlQuerySchemes))
|
|
157
174
|
return false
|
|
158
175
|
if (
|
|
@@ -267,6 +284,15 @@ export function readNativeAppIdentity(value: unknown): NativeAppIdentity | null
|
|
|
267
284
|
...(value.designRequiresCompatibility === undefined
|
|
268
285
|
? {}
|
|
269
286
|
: { designRequiresCompatibility: value.designRequiresCompatibility }),
|
|
287
|
+
...(value.statusBarHidden === undefined
|
|
288
|
+
? {}
|
|
289
|
+
: { statusBarHidden: value.statusBarHidden }),
|
|
290
|
+
...(value.viewControllerBasedStatusBarAppearance === undefined
|
|
291
|
+
? {}
|
|
292
|
+
: {
|
|
293
|
+
viewControllerBasedStatusBarAppearance:
|
|
294
|
+
value.viewControllerBasedStatusBarAppearance,
|
|
295
|
+
}),
|
|
270
296
|
...(value.dtPlatformVersion === undefined
|
|
271
297
|
? {}
|
|
272
298
|
: { dtPlatformVersion: value.dtPlatformVersion }),
|
|
@@ -291,11 +317,17 @@ export function readNativeAppIdentity(value: unknown): NativeAppIdentity | null
|
|
|
291
317
|
(permission) => ({ ...permission }),
|
|
292
318
|
),
|
|
293
319
|
}),
|
|
320
|
+
...(value.googleSignIn === undefined
|
|
321
|
+
? {}
|
|
322
|
+
: { googleSignIn: structuredClone(value.googleSignIn) }),
|
|
294
323
|
...(value.permissionUsageDescriptions === undefined
|
|
295
324
|
? {}
|
|
296
325
|
: {
|
|
297
326
|
permissionUsageDescriptions: { ...value.permissionUsageDescriptions },
|
|
298
327
|
}),
|
|
328
|
+
...(value.infoPlistStrings === undefined
|
|
329
|
+
? {}
|
|
330
|
+
: { infoPlistStrings: { ...value.infoPlistStrings } }),
|
|
299
331
|
...(value.appIntentIdentifiers === undefined
|
|
300
332
|
? {}
|
|
301
333
|
: { appIntentIdentifiers: [...value.appIntentIdentifiers] }),
|
|
@@ -496,6 +528,39 @@ export function insertNativeAppIdentityFooter(
|
|
|
496
528
|
|
|
497
529
|
// ── native project extraction ────────────────────────────────────────────────
|
|
498
530
|
|
|
531
|
+
// Info.plist and bundled service plists are native configuration, independent of Firebase initialization.
|
|
532
|
+
export function readGoogleSignInConfiguration(
|
|
533
|
+
plist: Record<string, unknown>,
|
|
534
|
+
resources: Readonly<Record<string, unknown>>,
|
|
535
|
+
): Record<string, unknown> {
|
|
536
|
+
const clientIds: Record<string, string> = {}
|
|
537
|
+
for (const [file, value] of Object.entries(resources)) {
|
|
538
|
+
if (isRecord(value) && typeof value.CLIENT_ID === 'string')
|
|
539
|
+
clientIds[
|
|
540
|
+
file
|
|
541
|
+
.split('/')
|
|
542
|
+
.pop()
|
|
543
|
+
?.replace(/\.plist$/, '') ?? file
|
|
544
|
+
] = value.CLIENT_ID
|
|
545
|
+
}
|
|
546
|
+
const urlSchemes: string[] = []
|
|
547
|
+
for (const entry of Array.isArray(plist.CFBundleURLTypes)
|
|
548
|
+
? plist.CFBundleURLTypes
|
|
549
|
+
: []) {
|
|
550
|
+
if (!isRecord(entry) || !Array.isArray(entry.CFBundleURLSchemes)) continue
|
|
551
|
+
for (const scheme of entry.CFBundleURLSchemes)
|
|
552
|
+
if (typeof scheme === 'string') urlSchemes.push(scheme)
|
|
553
|
+
}
|
|
554
|
+
return {
|
|
555
|
+
clientId: plist.GIDClientID,
|
|
556
|
+
serverClientId: plist.GIDServerClientID,
|
|
557
|
+
hostedDomain: plist.GIDHostedDomain,
|
|
558
|
+
openIDRealm: plist.GIDOpenIDRealm,
|
|
559
|
+
urlSchemes,
|
|
560
|
+
clientIds,
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
499
564
|
function plistString(plist: string, key: string): string | null {
|
|
500
565
|
const match = plist.match(new RegExp(`<key>${key}</key>\\s*<string>([^<]*)</string>`))
|
|
501
566
|
return match ? match[1].trim() : null
|
|
@@ -952,6 +1017,13 @@ function nativeAppIdentityFromIos(
|
|
|
952
1017
|
permissionUsageDescriptions[key] = resolved
|
|
953
1018
|
}
|
|
954
1019
|
|
|
1020
|
+
const infoPlistStrings: Record<string, string> = {}
|
|
1021
|
+
for (const [key, value] of Object.entries(dictionary)) {
|
|
1022
|
+
if (typeof value !== 'string') continue
|
|
1023
|
+
const resolved = resolveBuildSetting(value, settings)
|
|
1024
|
+
if (resolved !== null) infoPlistStrings[key] = resolved
|
|
1025
|
+
}
|
|
1026
|
+
|
|
955
1027
|
// Xcode's generated Info.plist uses these build settings only when enabled.
|
|
956
1028
|
// source: developer.apple.com/documentation/xcode/build-settings-reference.
|
|
957
1029
|
if (settings.GENERATE_INFOPLIST_FILE === 'YES') {
|
|
@@ -1025,12 +1097,29 @@ function nativeAppIdentityFromIos(
|
|
|
1025
1097
|
...(rootModuleName === undefined ? {} : { rootModuleName }),
|
|
1026
1098
|
...(rootViewBackgroundColor === undefined ? {} : { rootViewBackgroundColor }),
|
|
1027
1099
|
...(designRequiresCompatibility ? { designRequiresCompatibility } : {}),
|
|
1100
|
+
...(typeof dictionary.UIStatusBarHidden === 'boolean'
|
|
1101
|
+
? { statusBarHidden: dictionary.UIStatusBarHidden }
|
|
1102
|
+
: {}),
|
|
1103
|
+
...(typeof dictionary.UIViewControllerBasedStatusBarAppearance === 'boolean'
|
|
1104
|
+
? {
|
|
1105
|
+
viewControllerBasedStatusBarAppearance:
|
|
1106
|
+
dictionary.UIViewControllerBasedStatusBarAppearance,
|
|
1107
|
+
}
|
|
1108
|
+
: {}),
|
|
1028
1109
|
...(dtPlatformVersion === undefined ? {} : { dtPlatformVersion }),
|
|
1029
1110
|
bundleName,
|
|
1030
1111
|
appVersion,
|
|
1031
1112
|
buildVersion,
|
|
1032
1113
|
bundleIdentifier,
|
|
1033
1114
|
platform: 'ios',
|
|
1115
|
+
googleSignIn: readGoogleSignInConfiguration(
|
|
1116
|
+
dictionary,
|
|
1117
|
+
Object.fromEntries(
|
|
1118
|
+
[...resources]
|
|
1119
|
+
.filter((file) => file.endsWith('.plist') && files[file])
|
|
1120
|
+
.map((file) => [file, parseInfoPlist(files[file])]),
|
|
1121
|
+
),
|
|
1122
|
+
),
|
|
1034
1123
|
supportedInterfaceOrientations,
|
|
1035
1124
|
ipadSupportedInterfaceOrientations,
|
|
1036
1125
|
...(defaultScreenOrientationMask === undefined
|
|
@@ -1056,6 +1145,7 @@ function nativeAppIdentityFromIos(
|
|
|
1056
1145
|
}
|
|
1057
1146
|
: {}),
|
|
1058
1147
|
permissionUsageDescriptions,
|
|
1148
|
+
infoPlistStrings,
|
|
1059
1149
|
urlQuerySchemes,
|
|
1060
1150
|
...(shareIntoAppGroupId === undefined ? {} : { shareIntoAppGroupId }),
|
|
1061
1151
|
...(rtcAppGroupIdentifier === undefined ? {} : { rtcAppGroupIdentifier }),
|
|
@@ -106,6 +106,172 @@ export async function readExpoConfigAsync(
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
// a launch method may guard configure() with the safe-configure idiom: an
|
|
110
|
+
// `if` whose comma-separated clauses read the bundled GoogleService-Info.plist
|
|
111
|
+
// and test its API_KEY prefix. every clause is evaluated against the plist the
|
|
112
|
+
// target ships, so the answer is the one native launch reaches. any other
|
|
113
|
+
// condition cannot be proven and stays refused.
|
|
114
|
+
function evaluateFirebaseGuard(header: string, resources: string[]): boolean | null {
|
|
115
|
+
const condition = header.replace(/^\s*if\b/, '').replace(/\{\s*$/, '')
|
|
116
|
+
const clauses: string[] = []
|
|
117
|
+
let depth = 0
|
|
118
|
+
let start = 0
|
|
119
|
+
let quoted = false
|
|
120
|
+
for (let index = 0; index < condition.length; index++) {
|
|
121
|
+
const char = condition[index]
|
|
122
|
+
if (char === '"') quoted = !quoted
|
|
123
|
+
if (quoted) continue
|
|
124
|
+
if (char === '(' || char === '[') depth += 1
|
|
125
|
+
if (char === ')' || char === ']') depth -= 1
|
|
126
|
+
if (char === ',' && depth === 0) {
|
|
127
|
+
clauses.push(condition.slice(start, index).trim())
|
|
128
|
+
start = index + 1
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
clauses.push(condition.slice(start).trim())
|
|
132
|
+
const sources = resources.filter(
|
|
133
|
+
(file) => path.basename(file) === 'GoogleService-Info.plist',
|
|
134
|
+
)
|
|
135
|
+
const bound = new Map<string, unknown>()
|
|
136
|
+
for (const clause of clauses) {
|
|
137
|
+
if (/^FirebaseApp\s*\.\s*app\s*\(\s*\)\s*==\s*nil$/.test(clause)) continue
|
|
138
|
+
const resource =
|
|
139
|
+
/^let\s+(\w+)\s*=\s*Bundle\.main\.path\s*\(\s*forResource:\s*"GoogleService-Info"\s*,\s*ofType:\s*"plist"\s*\)$/.exec(
|
|
140
|
+
clause,
|
|
141
|
+
)
|
|
142
|
+
if (resource) {
|
|
143
|
+
if (sources.length !== 1) return false
|
|
144
|
+
bound.set(resource[1], sources[0])
|
|
145
|
+
continue
|
|
146
|
+
}
|
|
147
|
+
const dictionary =
|
|
148
|
+
/^let\s+(\w+)\s*=\s*NSDictionary\s*\(\s*contentsOfFile:\s*(\w+)\s*\)$/.exec(clause)
|
|
149
|
+
if (dictionary && typeof bound.get(dictionary[2]) === 'string') {
|
|
150
|
+
const parsed = parsePlistSource(
|
|
151
|
+
fs.readFileSync(String(bound.get(dictionary[2])), 'utf8'),
|
|
152
|
+
)
|
|
153
|
+
if (!isRecord(parsed)) return false
|
|
154
|
+
bound.set(dictionary[1], parsed)
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
const entry = /^let\s+(\w+)\s*=\s*(\w+)\s*\[\s*"(\w+)"\s*\]\s+as\?\s+String$/.exec(
|
|
158
|
+
clause,
|
|
159
|
+
)
|
|
160
|
+
const dictionaryValue = entry ? bound.get(entry[2]) : undefined
|
|
161
|
+
if (entry && isRecord(dictionaryValue)) {
|
|
162
|
+
const value = dictionaryValue[entry[3]]
|
|
163
|
+
if (typeof value !== 'string') return false
|
|
164
|
+
bound.set(entry[1], value)
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
const prefix = /^(\w+)\.hasPrefix\s*\(\s*"([^"]*)"\s*\)$/.exec(clause)
|
|
168
|
+
const subject = prefix ? bound.get(prefix[1]) : undefined
|
|
169
|
+
if (prefix && typeof subject === 'string') {
|
|
170
|
+
if (!subject.startsWith(prefix[2])) return false
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
173
|
+
return null
|
|
174
|
+
}
|
|
175
|
+
return true
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// native source with comments and string contents blanked in place, so brace
|
|
179
|
+
// and keyword scans see only code and offsets still index the raw text.
|
|
180
|
+
function blankSwiftObjcLiterals(raw: string) {
|
|
181
|
+
return raw.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*|"(?:\\.|[^"\\])*"/g, (token) =>
|
|
182
|
+
token.startsWith('"')
|
|
183
|
+
? `"${' '.repeat(token.length - 2)}"`
|
|
184
|
+
: token.replace(/[^\n]/g, ' '),
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// the text of the brace block opening at `open`, and the index after its end.
|
|
189
|
+
function braceBlock(source: string, open: number) {
|
|
190
|
+
let depth = 0
|
|
191
|
+
for (let index = open; index < source.length; index += 1) {
|
|
192
|
+
if (source[index] === '{') depth += 1
|
|
193
|
+
if (source[index] === '}') depth -= 1
|
|
194
|
+
if (depth === 0) return { body: source.slice(open + 1, index), end: index + 1 }
|
|
195
|
+
}
|
|
196
|
+
return null
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// whether every path through the code configures the default Firebase app: a
|
|
200
|
+
// top-level configure call, or an if/else chain ending in else whose blocks each
|
|
201
|
+
// do.
|
|
202
|
+
function configuresDefaultApp(code: string): boolean {
|
|
203
|
+
const configureCall = /\bFirebaseApp\s*\.\s*configure\s*\(/
|
|
204
|
+
let topLevel = ''
|
|
205
|
+
let depth = 0
|
|
206
|
+
for (const char of code) {
|
|
207
|
+
if (char === '{') depth += 1
|
|
208
|
+
if (depth === 0) topLevel += char
|
|
209
|
+
if (char === '}') depth -= 1
|
|
210
|
+
}
|
|
211
|
+
if (configureCall.test(topLevel)) return true
|
|
212
|
+
for (const chain of code.matchAll(/\bif\b[^{]*\{/g)) {
|
|
213
|
+
let open = chain.index + chain[0].length - 1
|
|
214
|
+
const blocks: string[] = []
|
|
215
|
+
let complete = false
|
|
216
|
+
for (;;) {
|
|
217
|
+
const block = braceBlock(code, open)
|
|
218
|
+
if (!block) break
|
|
219
|
+
blocks.push(block.body)
|
|
220
|
+
const next = /^\s*else\s*(if\b[^{]*)?\{/.exec(code.slice(block.end))
|
|
221
|
+
if (!next) break
|
|
222
|
+
open = block.end + next[0].length - 1
|
|
223
|
+
if (!next[1]) {
|
|
224
|
+
const last = braceBlock(code, open)
|
|
225
|
+
if (last) {
|
|
226
|
+
blocks.push(last.body)
|
|
227
|
+
complete = true
|
|
228
|
+
}
|
|
229
|
+
break
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (complete && blocks.every(configuresDefaultApp)) return true
|
|
233
|
+
}
|
|
234
|
+
return false
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// where a launch method configures the default Firebase app: directly, or by
|
|
238
|
+
// calling a static no-argument function of an app target type whose body
|
|
239
|
+
// configures it on every path. returns a match-like result carrying the call's
|
|
240
|
+
// index, or null.
|
|
241
|
+
function findDefaultConfigureCall(body: string, sources: readonly string[]) {
|
|
242
|
+
const direct =
|
|
243
|
+
/^\s*(?:FirebaseApp\s*\.\s*configure\s*\(\s*\)|\[FIRApp\s+configure\])/m.exec(body)
|
|
244
|
+
if (direct) return direct
|
|
245
|
+
for (const call of body.matchAll(/^\s*([A-Z]\w*)\s*\.\s*(\w+)\s*\(\s*\)/gm)) {
|
|
246
|
+
const [, type, name] = call
|
|
247
|
+
for (const file of sources) {
|
|
248
|
+
if (!file.endsWith('.swift')) continue
|
|
249
|
+
const source = blankSwiftObjcLiterals(fs.readFileSync(file, 'utf8'))
|
|
250
|
+
const declaration = new RegExp(
|
|
251
|
+
`\\b(?:enum|struct|class)\\s+${type}\\b[^{]*\\{`,
|
|
252
|
+
).exec(source)
|
|
253
|
+
if (!declaration) continue
|
|
254
|
+
const owner = braceBlock(source, declaration.index + declaration[0].length - 1)
|
|
255
|
+
const method = owner
|
|
256
|
+
? new RegExp(`\\bstatic\\s+func\\s+${name}\\s*\\(\\s*\\)[^{]*\\{`).exec(
|
|
257
|
+
owner.body,
|
|
258
|
+
)
|
|
259
|
+
: null
|
|
260
|
+
const helper =
|
|
261
|
+
owner && method
|
|
262
|
+
? braceBlock(owner.body, method.index + method[0].length - 1)
|
|
263
|
+
: null
|
|
264
|
+
if (
|
|
265
|
+
helper &&
|
|
266
|
+
!/\b(?:return|throw)\b/.test(helper.body) &&
|
|
267
|
+
configuresDefaultApp(helper.body)
|
|
268
|
+
)
|
|
269
|
+
return call
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return null
|
|
273
|
+
}
|
|
274
|
+
|
|
109
275
|
// the GoogleService-Info.plist an app target's launch method configures as the
|
|
110
276
|
// default app, relative to the project root, or null when it configures none.
|
|
111
277
|
function readTargetGoogleServicesFile(
|
|
@@ -120,27 +286,43 @@ function readTargetGoogleServicesFile(
|
|
|
120
286
|
/^AppDelegate\.(swift|mm|m)$/.test(path.basename(file)),
|
|
121
287
|
)
|
|
122
288
|
const defaultConfigured = delegates.some((file) => {
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*|"(?:\\.|[^"\\])*"/g, (token) =>
|
|
126
|
-
token.startsWith('"') ? '""' : token.replace(/[^\n]/g, ' '),
|
|
127
|
-
)
|
|
289
|
+
const raw = fs.readFileSync(file, 'utf8')
|
|
290
|
+
const source = blankSwiftObjcLiterals(raw)
|
|
128
291
|
const launch =
|
|
129
292
|
/(?:\bfunc\s+application\s*\([\s\S]*?didFinishLaunchingWithOptions[\s\S]*?\)\s*->\s*Bool|^-\s*\(BOOL\)\s*application\s*:[\s\S]*?didFinishLaunchingWithOptions[^{]*)\s*\{/m.exec(
|
|
130
293
|
source,
|
|
131
294
|
)
|
|
132
295
|
if (!launch) return false
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
|
|
296
|
+
const bodyStart = launch.index + launch[0].length
|
|
297
|
+
const body = source.slice(bodyStart)
|
|
298
|
+
const configure = findDefaultConfigureCall(body, app.allSources)
|
|
136
299
|
if (!configure) return false
|
|
137
|
-
|
|
300
|
+
let before = body.slice(0, configure.index)
|
|
138
301
|
let depth = 1
|
|
139
|
-
|
|
140
|
-
|
|
302
|
+
let guardOpen = -1
|
|
303
|
+
for (let index = 0; index < before.length; index++) {
|
|
304
|
+
const token = before[index]
|
|
305
|
+
if (token === '{') {
|
|
306
|
+
depth += 1
|
|
307
|
+
if (depth === 2) guardOpen = index
|
|
308
|
+
}
|
|
141
309
|
if (token === '}') depth -= 1
|
|
142
310
|
if (depth === 0) return false
|
|
143
311
|
}
|
|
312
|
+
let guard: string | null = null
|
|
313
|
+
if (depth === 2) {
|
|
314
|
+
const headers = [...before.slice(0, guardOpen).matchAll(/^[ \t]*if\b/gm)]
|
|
315
|
+
const header = headers.at(-1)
|
|
316
|
+
if (header) {
|
|
317
|
+
guard = raw.slice(bodyStart + header.index, bodyStart + guardOpen + 1)
|
|
318
|
+
before = before.slice(0, header.index)
|
|
319
|
+
depth = 1
|
|
320
|
+
for (const token of before) {
|
|
321
|
+
if (token === '{') depth += 1
|
|
322
|
+
if (token === '}') depth -= 1
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
144
326
|
let conditionalDepth = 0
|
|
145
327
|
for (const directive of before.matchAll(
|
|
146
328
|
/^\s*#\s*(if|ifdef|ifndef|elseif|elif|else|endif)\b/gm,
|
|
@@ -155,11 +337,25 @@ function readTargetGoogleServicesFile(
|
|
|
155
337
|
conditionalDepth -= 1
|
|
156
338
|
}
|
|
157
339
|
}
|
|
158
|
-
|
|
340
|
+
// a return inside a nested closure leaves the launch method running
|
|
341
|
+
let topLevelBefore = ''
|
|
342
|
+
let nesting = 0
|
|
343
|
+
for (const token of before) {
|
|
344
|
+
if (token === '{') nesting += 1
|
|
345
|
+
if (nesting === 0) topLevelBefore += token
|
|
346
|
+
if (token === '}') nesting -= 1
|
|
347
|
+
}
|
|
348
|
+
const guarded = guard === null ? true : evaluateFirebaseGuard(guard, app.resources)
|
|
349
|
+
if (
|
|
350
|
+
guarded === null ||
|
|
351
|
+
depth !== 1 ||
|
|
352
|
+
conditionalDepth !== 0 ||
|
|
353
|
+
/\b(?:return|throw)\b/.test(topLevelBefore)
|
|
354
|
+
)
|
|
159
355
|
throw new Error(
|
|
160
356
|
'native Firebase initialization must be unconditional in the app launch method',
|
|
161
357
|
)
|
|
162
|
-
return
|
|
358
|
+
return guarded
|
|
163
359
|
})
|
|
164
360
|
if (!defaultConfigured) return null
|
|
165
361
|
const resources = app.resources.filter((file) =>
|
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
|