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
package/src/home-paths.ts
CHANGED
|
@@ -185,6 +185,15 @@ export function cacheDir(): string {
|
|
|
185
185
|
return path.join(rnxHomeDir(), 'cache')
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
// swiftpm otherwise uses shared caches and configuration outside PEACH_HOME.
|
|
189
|
+
export function swiftPackageCacheOptions(): string[] {
|
|
190
|
+
const root = path.join(cacheDir(), 'swiftpm')
|
|
191
|
+
return ['cache', 'config', 'security'].flatMap((name) => [
|
|
192
|
+
`--${name}-path`,
|
|
193
|
+
path.join(root, name),
|
|
194
|
+
])
|
|
195
|
+
}
|
|
196
|
+
|
|
188
197
|
export function daemonLockfilePath(): string {
|
|
189
198
|
return path.join(rnxHomeDir(), DAEMON_LOCKFILE)
|
|
190
199
|
}
|
|
@@ -261,7 +270,10 @@ export function writeSharedConfig(patch: Partial<SharedConfig>): SharedConfig {
|
|
|
261
270
|
return next
|
|
262
271
|
}
|
|
263
272
|
|
|
264
|
-
|
|
273
|
+
// version 2 made the native app the default simulator window. version 1 setup
|
|
274
|
+
// saved the browser without asking on a Mac lacking the app, so its saved
|
|
275
|
+
// driver is not read back and those machines run setup again.
|
|
276
|
+
const CURRENT_ONBOARDING_VERSION = 2
|
|
265
277
|
|
|
266
278
|
export type SimulatorDriverPreference = DriverId
|
|
267
279
|
|
|
@@ -270,7 +282,9 @@ export function isOnboardingComplete(): boolean {
|
|
|
270
282
|
}
|
|
271
283
|
|
|
272
284
|
export function readSimulatorDriverPreference(): SimulatorDriverPreference | null {
|
|
273
|
-
const
|
|
285
|
+
const config = readSharedConfig()
|
|
286
|
+
if (config.onboardingVersion !== CURRENT_ONBOARDING_VERSION) return null
|
|
287
|
+
const driver = config.simulatorDriver
|
|
274
288
|
return driver === 'desktop' || driver === 'electron' || driver === 'playwright'
|
|
275
289
|
? driver
|
|
276
290
|
: null
|
|
@@ -634,6 +648,38 @@ export function isDevBridgeLockfileFresh(
|
|
|
634
648
|
}
|
|
635
649
|
}
|
|
636
650
|
|
|
651
|
+
function hostBridgePortNumber(value: number | null | undefined): number | null {
|
|
652
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 1 || value > 65535)
|
|
653
|
+
return null
|
|
654
|
+
return value
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/** port a desktop host's page should register on.
|
|
658
|
+
*
|
|
659
|
+
* the launch url's wsPort pin wins. an ambient PEACH_BRIDGE_PORT wins only
|
|
660
|
+
* when this home has no fresh daemon, the daemon already bound that port, or
|
|
661
|
+
* a fresh dev-bridge lockfile in this home claims it. otherwise the fresh
|
|
662
|
+
* daemon lock wins, so a second PEACH_HOME is not pulled onto another world's
|
|
663
|
+
* bridge. the mac user script and the native seed use the same rule. */
|
|
664
|
+
export function desktopHostBridgePort(input: {
|
|
665
|
+
pinnedPort?: number | null
|
|
666
|
+
daemonPort?: number | null
|
|
667
|
+
envPort?: number | null
|
|
668
|
+
devBridgePorts?: readonly number[]
|
|
669
|
+
}): number | null {
|
|
670
|
+
const pinned = hostBridgePortNumber(input.pinnedPort)
|
|
671
|
+
if (pinned != null) return pinned
|
|
672
|
+
const daemon = hostBridgePortNumber(input.daemonPort)
|
|
673
|
+
const env = hostBridgePortNumber(input.envPort)
|
|
674
|
+
const claimed = new Set(
|
|
675
|
+
(input.devBridgePorts ?? [])
|
|
676
|
+
.map((port) => hostBridgePortNumber(port))
|
|
677
|
+
.filter((port): port is number => port != null),
|
|
678
|
+
)
|
|
679
|
+
if (env != null && (daemon == null || env === daemon || claimed.has(env))) return env
|
|
680
|
+
return daemon
|
|
681
|
+
}
|
|
682
|
+
|
|
637
683
|
export function readLiveRuntimeVersions(): string[] {
|
|
638
684
|
const versions = new Set<string>()
|
|
639
685
|
const daemon = readDaemonLockfile()
|
package/src/host/bridge-host.ts
CHANGED
|
@@ -543,10 +543,13 @@ export class SootSimBridgeHost {
|
|
|
543
543
|
if (this.httpServer || this.wss) return this.effectivePort
|
|
544
544
|
|
|
545
545
|
// an installed runtime can serve while the startup update runs. only a
|
|
546
|
-
// machine without usable assets needs to wait for that download.
|
|
546
|
+
// machine without usable assets needs to wait for that download. only the
|
|
547
|
+
// daemon's runtime updater ever clears the flag, so a dev bridge (which
|
|
548
|
+
// serves the checkout build and runs no updater) never sets it.
|
|
547
549
|
const bootRuntimeDir = this.getActiveRuntime().runtimeDir
|
|
548
550
|
this.bootstrapping =
|
|
549
|
-
|
|
551
|
+
this.shouldWriteLockfile &&
|
|
552
|
+
(!bootRuntimeDir || !fs.existsSync(path.join(bootRuntimeDir, 'index.html')))
|
|
550
553
|
|
|
551
554
|
for (let attempt = 0; attempt < this.portFallbackCount; attempt++) {
|
|
552
555
|
const candidate = this.preferredPort === 0 ? 0 : this.preferredPort + attempt
|
|
@@ -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,10 +2,12 @@ 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,
|
|
8
9
|
readRootModuleName,
|
|
10
|
+
readGoogleSignInConfiguration,
|
|
9
11
|
selectNativeAppIdentityReader,
|
|
10
12
|
} from './native-app-identity'
|
|
11
13
|
import { resolveNativeAppIconArtwork } from './nativeAppIconContract'
|
|
@@ -16,10 +18,6 @@ import {
|
|
|
16
18
|
type XcodeAppTarget,
|
|
17
19
|
} from './xcode-project'
|
|
18
20
|
|
|
19
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
20
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
21
|
function nonEmptyString(value: unknown): string | undefined {
|
|
24
22
|
return typeof value === 'string' && value.length > 0 ? value : undefined
|
|
25
23
|
}
|
|
@@ -90,6 +88,11 @@ export function readNativeAppIdentityFromXcodeTarget(
|
|
|
90
88
|
permissionUsageDescriptions[key] = value
|
|
91
89
|
}
|
|
92
90
|
|
|
91
|
+
const infoPlistStrings: Record<string, string> = {}
|
|
92
|
+
for (const [key, value] of Object.entries(dictionary)) {
|
|
93
|
+
if (typeof value === 'string') infoPlistStrings[key] = value
|
|
94
|
+
}
|
|
95
|
+
|
|
93
96
|
const querySchemes = dictionary.LSApplicationQueriesSchemes
|
|
94
97
|
const urlQuerySchemes = Array.isArray(querySchemes) ? querySchemes : []
|
|
95
98
|
|
|
@@ -146,6 +149,15 @@ export function readNativeAppIdentityFromXcodeTarget(
|
|
|
146
149
|
...(rootModuleName ? { rootModuleName } : {}),
|
|
147
150
|
...(rootViewBackgroundColor !== undefined ? { rootViewBackgroundColor } : {}),
|
|
148
151
|
...(designRequiresCompatibility ? { designRequiresCompatibility } : {}),
|
|
152
|
+
...(typeof dictionary.UIStatusBarHidden === 'boolean'
|
|
153
|
+
? { statusBarHidden: dictionary.UIStatusBarHidden }
|
|
154
|
+
: {}),
|
|
155
|
+
...(typeof dictionary.UIViewControllerBasedStatusBarAppearance === 'boolean'
|
|
156
|
+
? {
|
|
157
|
+
viewControllerBasedStatusBarAppearance:
|
|
158
|
+
dictionary.UIViewControllerBasedStatusBarAppearance,
|
|
159
|
+
}
|
|
160
|
+
: {}),
|
|
149
161
|
...(typeof dictionary.DTPlatformVersion === 'string'
|
|
150
162
|
? { dtPlatformVersion: dictionary.DTPlatformVersion }
|
|
151
163
|
: {}),
|
|
@@ -154,6 +166,14 @@ export function readNativeAppIdentityFromXcodeTarget(
|
|
|
154
166
|
buildVersion,
|
|
155
167
|
bundleIdentifier,
|
|
156
168
|
platform: 'ios',
|
|
169
|
+
googleSignIn: readGoogleSignInConfiguration(
|
|
170
|
+
dictionary,
|
|
171
|
+
Object.fromEntries(
|
|
172
|
+
app.resources
|
|
173
|
+
.filter((file) => file.endsWith('.plist'))
|
|
174
|
+
.map((file) => [file, parseInfoPlist(fs.readFileSync(file, 'utf8'))]),
|
|
175
|
+
),
|
|
176
|
+
),
|
|
157
177
|
supportedInterfaceOrientations,
|
|
158
178
|
ipadSupportedInterfaceOrientations,
|
|
159
179
|
...(defaultScreenOrientationMask ? { defaultScreenOrientationMask } : {}),
|
|
@@ -177,6 +197,7 @@ export function readNativeAppIdentityFromXcodeTarget(
|
|
|
177
197
|
(value) => value,
|
|
178
198
|
),
|
|
179
199
|
permissionUsageDescriptions,
|
|
200
|
+
infoPlistStrings,
|
|
180
201
|
urlQuerySchemes,
|
|
181
202
|
...(shareIntoAppGroupId ? { shareIntoAppGroupId } : {}),
|
|
182
203
|
...(rtcAppGroupIdentifier ? { rtcAppGroupIdentifier } : {}),
|
|
@@ -364,8 +385,8 @@ function readIosNativeAppIdentity(
|
|
|
364
385
|
? nonEmptyString(installedManifest.version)
|
|
365
386
|
: undefined
|
|
366
387
|
if (!installedVersion) throw new Error('Reanimated package version is malformed')
|
|
367
|
-
//
|
|
368
|
-
if (
|
|
388
|
+
// the static feature flag schema starts with reanimated 4.
|
|
389
|
+
if (Number.parseInt(installedVersion, 10) < 4) return identity
|
|
369
390
|
const defaults: unknown = JSON.parse(
|
|
370
391
|
fs.readFileSync(
|
|
371
392
|
path.join(
|
|
@@ -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,
|
|
@@ -48,6 +49,9 @@ export interface NativeAppIdentity {
|
|
|
48
49
|
// UIDesignRequiresCompatibility: UIKit draws its pre-liquid-glass chrome.
|
|
49
50
|
// carried only when the plist sets it true.
|
|
50
51
|
designRequiresCompatibility?: true
|
|
52
|
+
// initial UIKit visibility and the owner of subsequent status bar preferences.
|
|
53
|
+
statusBarHidden?: boolean
|
|
54
|
+
viewControllerBasedStatusBarAppearance?: boolean
|
|
51
55
|
// the literal DTPlatformVersion stamp when the app plist supplies one.
|
|
52
56
|
dtPlatformVersion?: string
|
|
53
57
|
// CFBundleName, which CFNetwork stamps into the default User-Agent and iOS's
|
|
@@ -62,6 +66,10 @@ export interface NativeAppIdentity {
|
|
|
62
66
|
// SDK filtering belongs to the evaluation's device, not the bundle builder.
|
|
63
67
|
androidManifestPermissions?: readonly AndroidManifestPermission[]
|
|
64
68
|
permissionUsageDescriptions?: Readonly<Record<string, unknown>>
|
|
69
|
+
// every top-level string the app's Info.plist declares, build settings
|
|
70
|
+
// resolved, for native code that reads its own plist keys.
|
|
71
|
+
infoPlistStrings?: Readonly<Record<string, string>>
|
|
72
|
+
googleSignIn?: Readonly<Record<string, unknown>>
|
|
65
73
|
urlQuerySchemes?: readonly unknown[]
|
|
66
74
|
appIntentIdentifiers?: readonly string[]
|
|
67
75
|
backgroundTaskIdentifiers?: readonly string[]
|
|
@@ -92,16 +100,15 @@ interface NativeAppIdentityGlobals {
|
|
|
92
100
|
__sootsim_native_app_identity?: NativeAppIdentity | null
|
|
93
101
|
}
|
|
94
102
|
|
|
95
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
96
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
103
|
function nonEmptyString(value: unknown): string | null {
|
|
100
104
|
return typeof value === 'string' && value.length > 0 ? value : null
|
|
101
105
|
}
|
|
102
106
|
|
|
103
107
|
export function isNativeAppIdentity(value: unknown): value is NativeAppIdentity {
|
|
104
108
|
if (!isRecord(value)) return false
|
|
109
|
+
for (const key of ['statusBarHidden', 'viewControllerBasedStatusBarAppearance']) {
|
|
110
|
+
if (value[key] !== undefined && typeof value[key] !== 'boolean') return false
|
|
111
|
+
}
|
|
105
112
|
const oneUpdates = value.oneUpdates
|
|
106
113
|
if (
|
|
107
114
|
oneUpdates !== undefined &&
|
|
@@ -143,6 +150,7 @@ export function isNativeAppIdentity(value: unknown): value is NativeAppIdentity
|
|
|
143
150
|
))
|
|
144
151
|
)
|
|
145
152
|
return false
|
|
153
|
+
if (value.googleSignIn !== undefined && !isRecord(value.googleSignIn)) return false
|
|
146
154
|
const descriptions = value.permissionUsageDescriptions
|
|
147
155
|
if (
|
|
148
156
|
value.reanimatedStaticFeatureFlags !== undefined &&
|
|
@@ -153,6 +161,12 @@ export function isNativeAppIdentity(value: unknown): value is NativeAppIdentity
|
|
|
153
161
|
)
|
|
154
162
|
return false
|
|
155
163
|
if (descriptions !== undefined && !isRecord(descriptions)) return false
|
|
164
|
+
if (
|
|
165
|
+
value.infoPlistStrings !== undefined &&
|
|
166
|
+
(!isRecord(value.infoPlistStrings) ||
|
|
167
|
+
!Object.values(value.infoPlistStrings).every((entry) => typeof entry === 'string'))
|
|
168
|
+
)
|
|
169
|
+
return false
|
|
156
170
|
if (value.urlQuerySchemes !== undefined && !Array.isArray(value.urlQuerySchemes))
|
|
157
171
|
return false
|
|
158
172
|
if (
|
|
@@ -267,6 +281,15 @@ export function readNativeAppIdentity(value: unknown): NativeAppIdentity | null
|
|
|
267
281
|
...(value.designRequiresCompatibility === undefined
|
|
268
282
|
? {}
|
|
269
283
|
: { designRequiresCompatibility: value.designRequiresCompatibility }),
|
|
284
|
+
...(value.statusBarHidden === undefined
|
|
285
|
+
? {}
|
|
286
|
+
: { statusBarHidden: value.statusBarHidden }),
|
|
287
|
+
...(value.viewControllerBasedStatusBarAppearance === undefined
|
|
288
|
+
? {}
|
|
289
|
+
: {
|
|
290
|
+
viewControllerBasedStatusBarAppearance:
|
|
291
|
+
value.viewControllerBasedStatusBarAppearance,
|
|
292
|
+
}),
|
|
270
293
|
...(value.dtPlatformVersion === undefined
|
|
271
294
|
? {}
|
|
272
295
|
: { dtPlatformVersion: value.dtPlatformVersion }),
|
|
@@ -291,11 +314,17 @@ export function readNativeAppIdentity(value: unknown): NativeAppIdentity | null
|
|
|
291
314
|
(permission) => ({ ...permission }),
|
|
292
315
|
),
|
|
293
316
|
}),
|
|
317
|
+
...(value.googleSignIn === undefined
|
|
318
|
+
? {}
|
|
319
|
+
: { googleSignIn: structuredClone(value.googleSignIn) }),
|
|
294
320
|
...(value.permissionUsageDescriptions === undefined
|
|
295
321
|
? {}
|
|
296
322
|
: {
|
|
297
323
|
permissionUsageDescriptions: { ...value.permissionUsageDescriptions },
|
|
298
324
|
}),
|
|
325
|
+
...(value.infoPlistStrings === undefined
|
|
326
|
+
? {}
|
|
327
|
+
: { infoPlistStrings: { ...value.infoPlistStrings } }),
|
|
299
328
|
...(value.appIntentIdentifiers === undefined
|
|
300
329
|
? {}
|
|
301
330
|
: { appIntentIdentifiers: [...value.appIntentIdentifiers] }),
|
|
@@ -496,6 +525,39 @@ export function insertNativeAppIdentityFooter(
|
|
|
496
525
|
|
|
497
526
|
// ── native project extraction ────────────────────────────────────────────────
|
|
498
527
|
|
|
528
|
+
// Info.plist and bundled service plists are native configuration, independent of Firebase initialization.
|
|
529
|
+
export function readGoogleSignInConfiguration(
|
|
530
|
+
plist: Record<string, unknown>,
|
|
531
|
+
resources: Readonly<Record<string, unknown>>,
|
|
532
|
+
): Record<string, unknown> {
|
|
533
|
+
const clientIds: Record<string, string> = {}
|
|
534
|
+
for (const [file, value] of Object.entries(resources)) {
|
|
535
|
+
if (isRecord(value) && typeof value.CLIENT_ID === 'string')
|
|
536
|
+
clientIds[
|
|
537
|
+
file
|
|
538
|
+
.split('/')
|
|
539
|
+
.pop()
|
|
540
|
+
?.replace(/\.plist$/, '') ?? file
|
|
541
|
+
] = value.CLIENT_ID
|
|
542
|
+
}
|
|
543
|
+
const urlSchemes: string[] = []
|
|
544
|
+
for (const entry of Array.isArray(plist.CFBundleURLTypes)
|
|
545
|
+
? plist.CFBundleURLTypes
|
|
546
|
+
: []) {
|
|
547
|
+
if (!isRecord(entry) || !Array.isArray(entry.CFBundleURLSchemes)) continue
|
|
548
|
+
for (const scheme of entry.CFBundleURLSchemes)
|
|
549
|
+
if (typeof scheme === 'string') urlSchemes.push(scheme)
|
|
550
|
+
}
|
|
551
|
+
return {
|
|
552
|
+
clientId: plist.GIDClientID,
|
|
553
|
+
serverClientId: plist.GIDServerClientID,
|
|
554
|
+
hostedDomain: plist.GIDHostedDomain,
|
|
555
|
+
openIDRealm: plist.GIDOpenIDRealm,
|
|
556
|
+
urlSchemes,
|
|
557
|
+
clientIds,
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
499
561
|
function plistString(plist: string, key: string): string | null {
|
|
500
562
|
const match = plist.match(new RegExp(`<key>${key}</key>\\s*<string>([^<]*)</string>`))
|
|
501
563
|
return match ? match[1].trim() : null
|
|
@@ -952,6 +1014,13 @@ function nativeAppIdentityFromIos(
|
|
|
952
1014
|
permissionUsageDescriptions[key] = resolved
|
|
953
1015
|
}
|
|
954
1016
|
|
|
1017
|
+
const infoPlistStrings: Record<string, string> = {}
|
|
1018
|
+
for (const [key, value] of Object.entries(dictionary)) {
|
|
1019
|
+
if (typeof value !== 'string') continue
|
|
1020
|
+
const resolved = resolveBuildSetting(value, settings)
|
|
1021
|
+
if (resolved !== null) infoPlistStrings[key] = resolved
|
|
1022
|
+
}
|
|
1023
|
+
|
|
955
1024
|
// Xcode's generated Info.plist uses these build settings only when enabled.
|
|
956
1025
|
// source: developer.apple.com/documentation/xcode/build-settings-reference.
|
|
957
1026
|
if (settings.GENERATE_INFOPLIST_FILE === 'YES') {
|
|
@@ -1025,12 +1094,29 @@ function nativeAppIdentityFromIos(
|
|
|
1025
1094
|
...(rootModuleName === undefined ? {} : { rootModuleName }),
|
|
1026
1095
|
...(rootViewBackgroundColor === undefined ? {} : { rootViewBackgroundColor }),
|
|
1027
1096
|
...(designRequiresCompatibility ? { designRequiresCompatibility } : {}),
|
|
1097
|
+
...(typeof dictionary.UIStatusBarHidden === 'boolean'
|
|
1098
|
+
? { statusBarHidden: dictionary.UIStatusBarHidden }
|
|
1099
|
+
: {}),
|
|
1100
|
+
...(typeof dictionary.UIViewControllerBasedStatusBarAppearance === 'boolean'
|
|
1101
|
+
? {
|
|
1102
|
+
viewControllerBasedStatusBarAppearance:
|
|
1103
|
+
dictionary.UIViewControllerBasedStatusBarAppearance,
|
|
1104
|
+
}
|
|
1105
|
+
: {}),
|
|
1028
1106
|
...(dtPlatformVersion === undefined ? {} : { dtPlatformVersion }),
|
|
1029
1107
|
bundleName,
|
|
1030
1108
|
appVersion,
|
|
1031
1109
|
buildVersion,
|
|
1032
1110
|
bundleIdentifier,
|
|
1033
1111
|
platform: 'ios',
|
|
1112
|
+
googleSignIn: readGoogleSignInConfiguration(
|
|
1113
|
+
dictionary,
|
|
1114
|
+
Object.fromEntries(
|
|
1115
|
+
[...resources]
|
|
1116
|
+
.filter((file) => file.endsWith('.plist') && files[file])
|
|
1117
|
+
.map((file) => [file, parseInfoPlist(files[file])]),
|
|
1118
|
+
),
|
|
1119
|
+
),
|
|
1034
1120
|
supportedInterfaceOrientations,
|
|
1035
1121
|
ipadSupportedInterfaceOrientations,
|
|
1036
1122
|
...(defaultScreenOrientationMask === undefined
|
|
@@ -1056,6 +1142,7 @@ function nativeAppIdentityFromIos(
|
|
|
1056
1142
|
}
|
|
1057
1143
|
: {}),
|
|
1058
1144
|
permissionUsageDescriptions,
|
|
1145
|
+
infoPlistStrings,
|
|
1059
1146
|
urlQuerySchemes,
|
|
1060
1147
|
...(shareIntoAppGroupId === undefined ? {} : { shareIntoAppGroupId }),
|
|
1061
1148
|
...(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) =>
|
|
@@ -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) &&
|