peachsim 0.1.2185 → 0.1.2187

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.
Files changed (103) hide show
  1. package/cli/commands/cpu-profile.ts +6 -2
  2. package/cli/commands/shell-url.ts +3 -6
  3. package/dist-lib/agent-daemon-client.cjs +1 -1
  4. package/dist-lib/agent-events.cjs +1 -1
  5. package/dist-lib/attached-projects.cjs +1 -1
  6. package/dist-lib/auth/shared-session.cjs +1 -1
  7. package/dist-lib/backend-origin.cjs +1 -1
  8. package/dist-lib/beta.cjs +1 -1
  9. package/dist-lib/beta.mjs +1 -1
  10. package/dist-lib/bridge-constants.cjs +1 -1
  11. package/dist-lib/bridge-contract-input.cjs +1 -1
  12. package/dist-lib/bridge-contract-input.mjs +1 -1
  13. package/dist-lib/bridge-contract.cjs +1 -1
  14. package/dist-lib/bridge-contract.mjs +1 -1
  15. package/dist-lib/bundle-channel.cjs +2755 -2385
  16. package/dist-lib/capture-contract.cjs +1 -1
  17. package/dist-lib/capture-contract.mjs +1 -1
  18. package/dist-lib/cloud-contract.cjs +1 -1
  19. package/dist-lib/cloud-contract.mjs +1 -1
  20. package/dist-lib/cloud-test-args.cjs +1 -1
  21. package/dist-lib/cloud-test-args.mjs +1 -1
  22. package/dist-lib/cloud.cjs +1 -1
  23. package/dist-lib/cloud.mjs +1 -1
  24. package/dist-lib/config.cjs +1 -1
  25. package/dist-lib/config.mjs +1 -1
  26. package/dist-lib/detox/index.cjs +2 -2
  27. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  28. package/dist-lib/expo-swift-bundle.cjs +3265 -3223
  29. package/dist-lib/expo-swift-metadata.cjs +2 -1
  30. package/dist-lib/expo-swift-metadata.mjs +2 -1
  31. package/dist-lib/home-paths.cjs +1 -1
  32. package/dist-lib/host/bridge-host.cjs +4587 -4217
  33. package/dist-lib/host/fetch-proxy-handler.cjs +8 -7
  34. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  35. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  36. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  37. package/dist-lib/host/websocket-proxy.cjs +1 -1
  38. package/dist-lib/index.cjs +474 -283
  39. package/dist-lib/jump-to-source-babel.cjs +1 -1
  40. package/dist-lib/jump-to-source-native.cjs +1 -1
  41. package/dist-lib/kotlin-host.cjs +647 -284
  42. package/dist-lib/menu.cjs +1 -1
  43. package/dist-lib/menu.mjs +1 -1
  44. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  45. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  46. package/dist-lib/metro-production-bundle.cjs +1 -1
  47. package/dist-lib/metro-production-bundle.mjs +1 -1
  48. package/dist-lib/metro.cjs +775 -584
  49. package/dist-lib/native-app-identity-project.cjs +45 -4
  50. package/dist-lib/native-app-identity-project.mjs +45 -4
  51. package/dist-lib/native-app-identity.cjs +40 -2
  52. package/dist-lib/native-app-identity.mjs +40 -2
  53. package/dist-lib/native-firebase-metadata.cjs +1 -1
  54. package/dist-lib/native-firebase-metadata.mjs +1 -1
  55. package/dist-lib/pdfium-raster.cjs +1 -1
  56. package/dist-lib/pdfium-raster.mjs +1 -1
  57. package/dist-lib/plist.cjs +1 -1
  58. package/dist-lib/plist.mjs +1 -1
  59. package/dist-lib/profiles.cjs +1 -1
  60. package/dist-lib/public-brand.cjs +1 -1
  61. package/dist-lib/public-origins-lib-esm.json +24 -24
  62. package/dist-lib/public-origins-lib.json +99 -59
  63. package/dist-lib/react-native-host-modules.cjs +1 -1
  64. package/dist-lib/react-native-host-modules.mjs +1 -1
  65. package/dist-lib/reap-browser.cjs +1 -1
  66. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  67. package/dist-lib/sdk.cjs +1 -1
  68. package/dist-lib/sdk.mjs +1 -1
  69. package/dist-lib/skills.cjs +5257 -4878
  70. package/dist-lib/swift-compile.cjs +2 -2
  71. package/dist-lib/swift-compile.mjs +2 -2
  72. package/dist-lib/swift-package.cjs +1 -1
  73. package/dist-lib/swift-package.mjs +1 -1
  74. package/dist-lib/swift-submit.cjs +1 -1
  75. package/dist-lib/swift-submit.mjs +1 -1
  76. package/dist-lib/swift-toolchain.cjs +1 -1
  77. package/dist-lib/swift.cjs +1 -1
  78. package/dist-lib/vite-swift.cjs +3 -3
  79. package/dist-lib/vite.cjs +8 -7
  80. package/dist-lib/worklet-executable.cjs +1 -1
  81. package/dist-lib/worklet-executable.mjs +1 -1
  82. package/dist-types/packages/peach-engine/src/render-worker/compositor-backend.d.ts +1 -0
  83. package/kotlin/sdk.tar.gz +0 -0
  84. package/package.json +1 -1
  85. package/runtime-companion.json +1 -1
  86. package/sdk/ios/System/Library/Frameworks/CFNetwork.framework/Headers/CFNetwork.h +10 -0
  87. package/sdk/ios/System/Library/Frameworks/UIKit.framework/Headers/UIImageAsset.h +1 -1
  88. package/src/android-autolinking.ts +45 -4
  89. package/src/android-bridge-packages.ts +2 -2
  90. package/src/android-gradle-sources.ts +143 -0
  91. package/src/androidAutolinkingContract.ts +8 -0
  92. package/src/androidGradleContract.ts +14 -0
  93. package/src/expo-kotlin-host.ts +3 -0
  94. package/src/expo-native-workspace.ts +7 -1
  95. package/src/java-module-abi.ts +145 -0
  96. package/src/javaAbiContract.ts +14 -0
  97. package/src/kotlin-host.ts +57 -3
  98. package/src/native-app-identity-project.ts +10 -2
  99. package/src/native-app-identity.ts +15 -1
  100. package/src/nativeAppIconContract.ts +37 -0
  101. package/src/nitro-source.ts +2 -167
  102. package/src/react-native-autolinking.ts +153 -0
  103. package/src/swift-installed-targets.ts +8 -0
@@ -0,0 +1,145 @@
1
+ import { execFile } from 'node:child_process'
2
+ import { createHash } from 'node:crypto'
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import { promisify } from 'node:util'
6
+ import { readBridgeMethod } from './android-bridge-packages'
7
+ import { isRecord } from './is-record'
8
+ import { kotlinHome } from './kotlin-toolchain'
9
+ import type { JavaModuleSpec, JavaModuleSpecs } from './javaAbiContract'
10
+ import type { KotlinToolchainInstallation } from './kotlinSdkContract'
11
+
12
+ const run = promisify(execFile)
13
+ export const JAVA_BINDINGS_INITIALIZER = 'initializeJavaBindings'
14
+
15
+ export async function readJavaModuleSpecs(
16
+ files: string[],
17
+ sdkRoot: string,
18
+ toolchain: KotlinToolchainInstallation,
19
+ ): Promise<JavaModuleSpecs> {
20
+ const digest = createHash('sha256')
21
+ const parent = path.join(kotlinHome(), 'java-specs')
22
+ fs.mkdirSync(parent, { recursive: true })
23
+ const work = fs.mkdtempSync(path.join(parent, '.read-'))
24
+ try {
25
+ const origins = new Map<string, string>()
26
+ for (const [index, file] of files.entries()) {
27
+ const bytes = fs.readFileSync(file)
28
+ digest.update(file).update('\0').update(bytes).update('\0')
29
+ const target = path.join(work, String(index), path.basename(file))
30
+ fs.mkdirSync(path.dirname(target))
31
+ fs.writeFileSync(target, bytes)
32
+ origins.set(target, file)
33
+ }
34
+ const { stdout } = await run(
35
+ path.join(toolchain.javaHome, 'bin', 'java'),
36
+ [
37
+ '-cp',
38
+ [
39
+ path.join(sdkRoot, 'tools.jar'),
40
+ path.resolve(path.dirname(toolchain.kotlinc), '../lib/kotlin-compiler.jar'),
41
+ path.resolve(path.dirname(toolchain.kotlinc), '../lib/kotlin-stdlib.jar'),
42
+ ].join(path.delimiter),
43
+ 'dev.peach.tools.javaabi.JavaModuleSpecsKt',
44
+ ...origins.keys(),
45
+ ],
46
+ { maxBuffer: 16 * 1024 * 1024 },
47
+ )
48
+ const specs = stdout
49
+ .split('\n')
50
+ .filter(Boolean)
51
+ .map((line): JavaModuleSpec => {
52
+ const row: unknown = JSON.parse(line)
53
+ if (
54
+ !isRecord(row) ||
55
+ typeof row.file !== 'string' ||
56
+ typeof row.className !== 'string' ||
57
+ (row.name !== null && typeof row.name !== 'string') ||
58
+ typeof row.source !== 'string' ||
59
+ !Array.isArray(row.methods)
60
+ )
61
+ throw new Error('Java module reader printed an invalid spec')
62
+ const file = origins.get(row.file)
63
+ if (!file) throw new Error('Java module reader named an unknown input')
64
+ return {
65
+ file,
66
+ className: row.className,
67
+ name: row.name,
68
+ source: row.source,
69
+ methods: row.methods.map(readBridgeMethod),
70
+ }
71
+ })
72
+ return { sha256: digest.digest('hex'), specs }
73
+ } finally {
74
+ fs.rmSync(work, { recursive: true, force: true })
75
+ }
76
+ }
77
+
78
+ export async function compileJavaModuleAbi(input: {
79
+ specs: JavaModuleSpecs
80
+ work: string
81
+ hash: string
82
+ toolchain: KotlinToolchainInstallation
83
+ libraries: string[]
84
+ sdkRoot: string
85
+ sdkLibrary: string
86
+ language: string
87
+ }): Promise<string> {
88
+ const { specs, work, hash, toolchain, libraries, sdkRoot, sdkLibrary, language } = input
89
+ const directory = path.join(work, 'java-abi')
90
+ fs.mkdirSync(directory)
91
+ const sources = specs.specs.map((spec, index) => {
92
+ const target = path.join(directory, `Spec${index}.kt`)
93
+ fs.writeFileSync(target, spec.source)
94
+ return target
95
+ })
96
+ const initializer = path.join(directory, 'Bindings.kt')
97
+ fs.writeFileSync(
98
+ initializer,
99
+ [
100
+ 'package dev.peach.jni',
101
+ `internal fun ${JAVA_BINDINGS_INITIALIZER}() { error("Java bindings require the compiler plugin") }`,
102
+ ].join('\n'),
103
+ )
104
+ const name = `peach-java-abi-${hash}`
105
+ await run(
106
+ toolchain.kotlinc,
107
+ [
108
+ '-J-Xmx2g',
109
+ '-Xwasm-target=js',
110
+ '-libraries',
111
+ libraries.join(path.delimiter),
112
+ `-Xplugin=${path.join(sdkRoot, 'plugin.jar')}`,
113
+ `-Xfriend-modules=${sdkLibrary}`,
114
+ '-language-version',
115
+ language,
116
+ '-api-version',
117
+ language,
118
+ '-nowarn',
119
+ '-ir-output-dir',
120
+ directory,
121
+ '-ir-output-name',
122
+ name,
123
+ ...sources,
124
+ initializer,
125
+ ],
126
+ { maxBuffer: 16 * 1024 * 1024 },
127
+ )
128
+ const library = path.join(directory, 'java-abi.klib')
129
+ await run(
130
+ path.join(toolchain.javaHome, 'bin', 'java'),
131
+ [
132
+ '-cp',
133
+ [
134
+ path.join(sdkRoot, 'tools.jar'),
135
+ path.resolve(path.dirname(toolchain.kotlinc), '../lib/kotlin-compiler.jar'),
136
+ path.resolve(path.dirname(toolchain.kotlinc), '../lib/kotlin-stdlib.jar'),
137
+ ].join(path.delimiter),
138
+ 'dev.peach.tools.platform.JavaPlatformTypesKt',
139
+ path.join(directory, `${name}.klib`),
140
+ library,
141
+ ],
142
+ { maxBuffer: 16 * 1024 * 1024 },
143
+ )
144
+ return library
145
+ }
@@ -0,0 +1,14 @@
1
+ import type { BridgeMethod } from './android-bridge-packages'
2
+
3
+ export interface JavaModuleSpec {
4
+ file: string
5
+ className: string
6
+ name: string | null
7
+ source: string
8
+ methods: BridgeMethod[]
9
+ }
10
+
11
+ export interface JavaModuleSpecs {
12
+ sha256: string
13
+ specs: JavaModuleSpec[]
14
+ }
@@ -20,6 +20,11 @@ import {
20
20
  } from './android-bridge-packages'
21
21
  import { rnxHomeDir } from './home-paths'
22
22
  import { isRecord } from './is-record'
23
+ import {
24
+ compileJavaModuleAbi,
25
+ JAVA_BINDINGS_INITIALIZER,
26
+ readJavaModuleSpecs,
27
+ } from './java-module-abi'
23
28
  import { kotlinInstanceSource } from './kotlin-instance-source'
24
29
  import { installKotlinSdkArchive } from './kotlin-sdk'
25
30
  import {
@@ -131,10 +136,12 @@ function entrySource(ids: string[]): string {
131
136
  '',
132
137
  '@OptIn(kotlin.js.ExperimentalJsExport::class)',
133
138
  '@JsExport',
134
- 'fun peachSetAndroidDeviceInfo(model: String, release: String, apiLevel: Int, locale: String, timeZone: String) {',
139
+ 'fun peachSetAndroidDeviceInfo(model: String, release: String, apiLevel: Int, locale: String, timeZone: String, settings: String, properties: String) {',
135
140
  ' Build.setDevice(model, release, apiLevel)',
136
141
  ' java.util.Locale.setDefault(java.util.Locale.forLanguageTag(locale))',
137
142
  ' java.util.TimeZone.setDevice(timeZone)',
143
+ ' dev.peach.android.ProcessSettings.configure(settings)',
144
+ ' android.os.SystemProperties.configure(properties)',
138
145
  '}',
139
146
  '',
140
147
  '@OptIn(kotlin.js.ExperimentalJsExport::class, kotlinx.coroutines.DelicateCoroutinesApi::class)',
@@ -702,6 +709,25 @@ export async function expoKotlinArtifact(
702
709
  origin: string,
703
710
  modules: ExpoKotlinModules,
704
711
  ): Promise<KotlinArtifact & { compose: boolean }> {
712
+ if (modules.gradleSources) {
713
+ const { resolveAndroidGradleSources } = await import('./android-gradle-sources')
714
+ const projects = await resolveAndroidGradleSources(modules.gradleSources)
715
+ modules = {
716
+ ...modules,
717
+ sources: [
718
+ ...new Set([
719
+ ...modules.sources,
720
+ ...projects.flatMap((project) => project.kotlin),
721
+ ]),
722
+ ],
723
+ javaSpecs: [
724
+ ...new Set([
725
+ ...(modules.javaSpecs ?? []),
726
+ ...projects.flatMap((project) => project.java),
727
+ ]),
728
+ ],
729
+ }
730
+ }
705
731
  const toolchain = readToolchain(moduleRoot)
706
732
  const language = appKotlinLanguage(root)
707
733
  const scanned = await scanExpoTypes(toolchain, moduleRoot, modules.sources)
@@ -730,10 +756,12 @@ export async function expoKotlinArtifact(
730
756
  ' ))',
731
757
  '}',
732
758
  '',
733
- '@JsExport fun peachSetAndroidDeviceInfo(model: String, release: String, apiLevel: Int, locale: String, timeZone: String) {',
759
+ '@JsExport fun peachSetAndroidDeviceInfo(model: String, release: String, apiLevel: Int, locale: String, timeZone: String, settings: String, properties: String) {',
734
760
  ' android.os.Build.setDevice(model, release, apiLevel)',
735
761
  ' java.util.Locale.setDefault(java.util.Locale.forLanguageTag(locale))',
736
762
  ' java.util.TimeZone.setDevice(timeZone)',
763
+ ' dev.peach.android.ProcessSettings.configure(settings)',
764
+ ' android.os.SystemProperties.configure(properties)',
737
765
  ' PeachExpoRuntime.configureProcessLocale(locale)',
738
766
  '}',
739
767
  '',
@@ -755,6 +783,7 @@ export async function expoKotlinArtifact(
755
783
  toolchain,
756
784
  language,
757
785
  sources: modules.sources.filter((file) => !rewritten.has(file)),
786
+ javaSpecs: modules.javaSpecs,
758
787
  generated: {
759
788
  ...types.generated,
760
789
  'PeachExpoEntry.kt': entry,
@@ -1107,6 +1136,7 @@ async function compileKotlin(
1107
1136
  toolchain: KotlinToolchain
1108
1137
  language: string
1109
1138
  sources: string[]
1139
+ javaSpecs?: string[]
1110
1140
  generated: Record<string, string>
1111
1141
  // the app source a generated file rewrites, which its diagnostics name
1112
1142
  origins?: Record<string, string>
@@ -1135,10 +1165,14 @@ async function compileKotlin(
1135
1165
  usesCompose,
1136
1166
  usesSerialization = false,
1137
1167
  viewModels,
1168
+ javaSpecs = [],
1138
1169
  } = input
1139
1170
  const sdk = await loadKotlinSdk(moduleRoot, toolchain)
1140
1171
  const plugin = path.join(sdk.root, 'plugin.jar')
1141
1172
  const sdkLibrary = path.join(sdk.root, `${sdkProfile}.klib`)
1173
+ const java = javaSpecs.length
1174
+ ? await readJavaModuleSpecs(javaSpecs, sdk.root, await ensureToolchain(toolchain))
1175
+ : null
1142
1176
  const generated = {
1143
1177
  ...sourceGenerated,
1144
1178
  'PeachBindingsInitialization.kt': [
@@ -1150,6 +1184,7 @@ async function compileKotlin(
1150
1184
  '@JsExport fun peachInitializeModuleBindings() {',
1151
1185
  ' if (!initialized) {',
1152
1186
  ' requireIncomingBindings()',
1187
+ ...(java ? [` ${JAVA_BINDINGS_INITIALIZER}()`] : []),
1153
1188
  ' initializeApplicationIncomingBindings()',
1154
1189
  ...(sourceGenerated['PeachViews.kt']
1155
1190
  ? [' initializeApplicationViews()']
@@ -1173,6 +1208,7 @@ async function compileKotlin(
1173
1208
  sdk: sdk.sha256,
1174
1209
  sdkProfile,
1175
1210
  usesSerialization,
1211
+ java: java?.sha256,
1176
1212
  }),
1177
1213
  )
1178
1214
  for (const { file, bytes } of inputs)
@@ -1208,6 +1244,8 @@ async function compileKotlin(
1208
1244
  }
1209
1245
  fs.mkdirSync(buildsDir(), { recursive: true })
1210
1246
  const work = fs.mkdtempSync(path.join(buildsDir(), `${hash}-`))
1247
+ const javaLibrary = java ? path.join(work, 'java-abi', 'java-abi.klib') : null
1248
+ if (javaLibrary) libraries.push(javaLibrary)
1211
1249
  const generatedDir = path.join(work, 'generated')
1212
1250
  fs.mkdirSync(generatedDir)
1213
1251
  for (const [name, text] of Object.entries(generated))
@@ -1232,7 +1270,7 @@ async function compileKotlin(
1232
1270
  [
1233
1271
  ...common,
1234
1272
  `-Xplugin=${plugin}`,
1235
- `-Xfriend-modules=${sdkLibrary}`,
1273
+ `-Xfriend-modules=${[sdkLibrary, ...(javaLibrary ? [javaLibrary] : [])].join(path.delimiter)}`,
1236
1274
  ...(usesCompose
1237
1275
  ? [
1238
1276
  `-Xplugin=${path.resolve(path.dirname(kotlinc), '../lib/compose-compiler-plugin.jar')}`,
@@ -1259,6 +1297,17 @@ async function compileKotlin(
1259
1297
  { maxBuffer: 16 * 1024 * 1024 },
1260
1298
  )
1261
1299
  try {
1300
+ if (java)
1301
+ await compileJavaModuleAbi({
1302
+ specs: java,
1303
+ work,
1304
+ hash,
1305
+ toolchain: ready,
1306
+ libraries: libraries.filter((library) => library !== javaLibrary),
1307
+ sdkRoot: sdk.root,
1308
+ sdkLibrary,
1309
+ language,
1310
+ })
1262
1311
  // the compiler resolves the app's classes and constructors: a first
1263
1312
  // klib's ABI names the view models the generated factory builds. an app
1264
1313
  // whose sources never name ViewModel declares none and skips it
@@ -1395,6 +1444,11 @@ async function compileKotlin(
1395
1444
  output = output
1396
1445
  .replaceAll(target, file)
1397
1446
  .replaceAll(path.relative(process.cwd(), target), file)
1447
+ for (const [index, spec] of (java?.specs ?? []).entries())
1448
+ output = output.replaceAll(
1449
+ path.join(work, 'java-abi', `Spec${index}.kt`),
1450
+ spec.file,
1451
+ )
1398
1452
  throw new Error(
1399
1453
  `kotlin compile failed:\n${output.trim() || (error instanceof Error ? error.message : String(error))}`,
1400
1454
  )
@@ -10,7 +10,10 @@ import {
10
10
  readGoogleSignInConfiguration,
11
11
  selectNativeAppIdentityReader,
12
12
  } from './native-app-identity'
13
- import { resolveNativeAppIconArtwork } from './nativeAppIconContract'
13
+ import {
14
+ resolveNativeAppIconArtwork,
15
+ resolveNativeBundleIcons,
16
+ } from './nativeAppIconContract'
14
17
  import { parsePlistSource } from './plist'
15
18
  import { NATIVE_GRAPH_SDK_VERSION } from './swift-objc-standin'
16
19
  import {
@@ -91,11 +94,15 @@ export function readNativeAppIdentityFromXcodeTarget(
91
94
  }
92
95
  const alternateSetting =
93
96
  settings?.('ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES') ?? ''
97
+ const catalogAlternateNames: string[] = []
94
98
  for (const match of alternateSetting
95
99
  .replace(/\\"/g, '"')
96
100
  .matchAll(/"([^"]*)"|([^\s(),]+)/g)) {
97
101
  const val = match[1] ?? match[2]
98
- if (val) alternateIconNames.add(val)
102
+ if (val) {
103
+ alternateIconNames.add(val)
104
+ catalogAlternateNames.push(val)
105
+ }
99
106
  }
100
107
 
101
108
  const permissionUsageDescriptions: Record<string, unknown> = {}
@@ -204,6 +211,7 @@ export function readNativeAppIdentityFromXcodeTarget(
204
211
  ipadSupportedInterfaceOrientations,
205
212
  ...(defaultScreenOrientationMask ? { defaultScreenOrientationMask } : {}),
206
213
  alternateIconNames: [...alternateIconNames],
214
+ bundleIcons: resolveNativeBundleIcons(icons, catalogAlternateNames, (value) => value),
207
215
  appIconArtwork: resolveNativeAppIconArtwork(
208
216
  app.resources,
209
217
  (file) => fs.readFileSync(file, 'utf8'),
@@ -13,6 +13,7 @@ import { nativeAppIdentityFromAndroidManifest } from './android-app-identity'
13
13
  import { isRecord } from './is-record'
14
14
  import {
15
15
  resolveNativeAppIconArtwork,
16
+ resolveNativeBundleIcons,
16
17
  isNativeAppIconArtwork,
17
18
  readNativeAppIconArtwork,
18
19
  type NativeAppIconArtwork,
@@ -87,6 +88,7 @@ export interface NativeAppIdentity {
87
88
  rtcAppGroupIdentifier?: string
88
89
  screenShareExtensionBundleId?: string
89
90
  alternateIconNames?: readonly string[]
91
+ bundleIcons?: Readonly<Record<string, unknown>>
90
92
  appIconArtwork?: NativeAppIconArtwork
91
93
  // resolved Reanimated compile flags from the native build inputs.
92
94
  reanimatedStaticFeatureFlags?: Readonly<Record<string, boolean>>
@@ -183,6 +185,7 @@ export function isNativeAppIdentity(value: unknown): value is NativeAppIdentity
183
185
  )
184
186
  return false
185
187
  if (value.googleSignIn !== undefined && !isRecord(value.googleSignIn)) return false
188
+ if (value.bundleIcons !== undefined && !isRecord(value.bundleIcons)) return false
186
189
  const descriptions = value.permissionUsageDescriptions
187
190
  if (
188
191
  value.reanimatedStaticFeatureFlags !== undefined &&
@@ -355,6 +358,9 @@ export function readNativeAppIdentity(value: unknown): NativeAppIdentity | null
355
358
  ...(value.alternateIconNames === undefined
356
359
  ? {}
357
360
  : { alternateIconNames: [...value.alternateIconNames] }),
361
+ ...(value.bundleIcons === undefined
362
+ ? {}
363
+ : { bundleIcons: structuredClone(value.bundleIcons) }),
358
364
  ...(value.androidManifestPermissions === undefined
359
365
  ? {}
360
366
  : {
@@ -1003,10 +1009,13 @@ function nativeAppIdentityFromIos(
1003
1009
  )
1004
1010
  if (alternateSetting === null)
1005
1011
  throw new Error('native alternate icon setting is unresolved')
1012
+ const catalogAlternateNames: string[] = []
1006
1013
  for (const match of alternateSetting
1007
1014
  .replace(/\\"/g, '"')
1008
1015
  .matchAll(/"([^"]*)"|([^\s(),]+)/g)) {
1009
- alternateIconNames.add(match[1] ?? match[2])
1016
+ const name = match[1] ?? match[2]
1017
+ alternateIconNames.add(name)
1018
+ catalogAlternateNames.push(name)
1010
1019
  }
1011
1020
  // swift casts the whole configured value to [String]; a mixed array grants none.
1012
1021
  const intentIds = dictionary.OneAppIntentIdentifiers
@@ -1208,6 +1217,11 @@ function nativeAppIdentityFromIos(
1208
1217
  ? {}
1209
1218
  : { defaultScreenOrientationMask }),
1210
1219
  alternateIconNames: [...alternateIconNames],
1220
+ bundleIcons: resolveNativeBundleIcons(icons, catalogAlternateNames, (value) => {
1221
+ const resolved = resolveBuildSetting(value, settings)
1222
+ if (resolved === null) throw new Error('native bundle icon reference is unresolved')
1223
+ return resolved
1224
+ }),
1211
1225
  ...(resourceUri
1212
1226
  ? {
1213
1227
  appIconArtwork: resolveNativeAppIconArtwork(
@@ -6,6 +6,43 @@ export interface NativeAppIconArtwork {
6
6
  alternates: Record<string, SwiftImageVariants>
7
7
  }
8
8
 
9
+ export function resolveNativeBundleIcons(
10
+ icons: unknown,
11
+ catalogNames: readonly string[],
12
+ resolveReference: (value: string) => string,
13
+ ): Record<string, unknown> | undefined {
14
+ if (!isRecord(icons) && catalogNames.length === 0) return undefined
15
+ const resolve = (value: unknown): unknown => {
16
+ if (typeof value === 'string') return resolveReference(value)
17
+ if (Array.isArray(value)) return value.map(resolve)
18
+ if (isRecord(value))
19
+ return Object.fromEntries(
20
+ Object.entries(value).map(([key, entry]) => [
21
+ resolveReference(key),
22
+ resolve(entry),
23
+ ]),
24
+ )
25
+ return value
26
+ }
27
+ const dictionary = isRecord(icons)
28
+ ? Object.fromEntries(
29
+ Object.entries(icons).map(([key, value]) => [key, resolve(value)]),
30
+ )
31
+ : {}
32
+ if (catalogNames.length === 0) return dictionary
33
+ return {
34
+ ...dictionary,
35
+ CFBundleAlternateIcons: {
36
+ ...Object.fromEntries(
37
+ catalogNames.map((name) => [name, { CFBundleIconName: name }]),
38
+ ),
39
+ ...(isRecord(dictionary.CFBundleAlternateIcons)
40
+ ? dictionary.CFBundleAlternateIcons
41
+ : {}),
42
+ },
43
+ }
44
+ }
45
+
9
46
  export function isNativeAppIconArtwork(value: unknown): value is NativeAppIconArtwork {
10
47
  return (
11
48
  isRecord(value) &&
@@ -1,15 +1,13 @@
1
- import { execFile } from 'node:child_process'
2
1
  import { createHash } from 'node:crypto'
3
2
  import fs from 'node:fs'
4
3
  import { createRequire } from 'node:module'
5
4
  import path from 'node:path'
6
- import { promisify } from 'node:util'
7
- import { PEACH_FORKED_SCRIPT_ENV } from './forked-script-contract'
8
5
  import {
9
6
  nitroDependencyIncludes,
10
7
  nitroDependencyReader,
11
8
  prepareNitroDependencies,
12
9
  } from './nitro-dependencies'
10
+ import { reactNativeAutolinkingConfig } from './react-native-autolinking'
13
11
  import { projectSourceKey, readPackageFile } from './swift-source-file'
14
12
  import { MAX_SWIFT_COMPILE_SOURCE_BYTES } from './swift-submit'
15
13
  import type { NitroView } from './nitroContract'
@@ -33,24 +31,6 @@ export type NitroSourceOptions = { packageRoots: string[]; answers?: NitroAnswer
33
31
  }
34
32
  )
35
33
 
36
- const run = promisify(execFile)
37
-
38
- // an autolinker that exits 0 with unparseable stdout names what it printed,
39
- // so a truncated or empty pipe read is distinguishable from a bad command.
40
- function autolinkerJson(
41
- command: string,
42
- output: { stdout: string; stderr: string },
43
- ): unknown {
44
- try {
45
- return JSON.parse(output.stdout)
46
- } catch (cause) {
47
- throw new Error(
48
- `${command} exited 0 with ${output.stdout.length} bytes of stdout that is not JSON (${cause instanceof Error ? cause.message : cause}); stderr: ${output.stderr.trim().slice(0, 500) || '(empty)'}; stdout: ${output.stdout.slice(0, 200) || '(empty)'}`,
49
- { cause },
50
- )
51
- }
52
- }
53
-
54
34
  type NitroPackage = { name: string; version: string; root: string }
55
35
 
56
36
  export const nitroProviderTarget = (provider: Pick<NitroPackage, 'name'>) =>
@@ -378,150 +358,6 @@ export function selectNitroProviders(
378
358
  .filter(({ selected }) => selected.length > 0)
379
359
  }
380
360
 
381
- // whether the app's installed CLI advertises a command or flag. old Expo
382
- // autolinkers predate `react-native-config` and old React Native CLIs predate
383
- // `config --platform`, so both are probed rather than assumed.
384
- // the cli runs from the app root, as its real command does: react-native cli
385
- // 6 exits 1 with no output when it finds no project in its cwd
386
- async function cliAdvertises(
387
- cli: string,
388
- args: string[],
389
- token: string,
390
- appRoot: string,
391
- signal?: AbortSignal,
392
- ): Promise<boolean> {
393
- const { stdout, stderr } = await run('node', [cli, ...args], {
394
- cwd: appRoot,
395
- signal,
396
- timeout: 30_000,
397
- maxBuffer: 1 * 1024 * 1024,
398
- env: { ...process.env, [PEACH_FORKED_SCRIPT_ENV]: '1' },
399
- })
400
- if (!stdout.trim())
401
- throw new Error(
402
- `${cli} ${args.join(' ')} returned empty help output${stderr.trim() ? `: ${stderr.trim()}` : ''}`,
403
- )
404
- return stdout.includes(token)
405
- }
406
-
407
- function declaresDependency(appRoot: string, name: string): boolean {
408
- const manifest: unknown = JSON.parse(
409
- fs.readFileSync(path.join(appRoot, 'package.json'), 'utf8'),
410
- )
411
- return (
412
- object(manifest) &&
413
- ['dependencies', 'devDependencies'].some((field) => {
414
- const group = manifest[field]
415
- return object(group) && Object.hasOwn(group, name)
416
- })
417
- )
418
- }
419
-
420
- async function autolinkingConfig(
421
- appRoot: string,
422
- appRequire: NodeJS.Require,
423
- signal?: AbortSignal,
424
- ): Promise<unknown> {
425
- // a pnpm app reaches the autolinker only through expo's own dependencies
426
- const expoManifest = appRequire.resolve
427
- .paths('expo')
428
- ?.map((directory) => path.join(directory, 'expo/package.json'))
429
- .find((manifest) => fs.existsSync(manifest))
430
- const expoRequire = expoManifest
431
- ? createRequire(fs.realpathSync(expoManifest))
432
- : appRequire
433
- // the app's autolinking contract is the command its Podfile hands
434
- // use_native_modules!: expo's react-native-config, or the React Native CLI
435
- // when it passes none (expo's loader ignores react-native.config.cjs, which
436
- // the CLI reads). a gitignored ios project is prebuild output, so without a
437
- // Podfile the prebuild that would write it answers: expo's for an app that
438
- // declares expo, One's bare use_native_modules! otherwise. an Expo Podfile
439
- // holds both commands and runs the CLI's when EXPO_USE_COMMUNITY_AUTOLINKING
440
- // is 1.
441
- const podfile = path.join(appRoot, 'ios/Podfile')
442
- const asksExpo =
443
- process.env.EXPO_USE_COMMUNITY_AUTOLINKING !== '1' &&
444
- (fs.existsSync(podfile)
445
- ? fs.readFileSync(podfile, 'utf8').includes('expo-modules-autolinking')
446
- : declaresDependency(appRoot, 'expo'))
447
- const expoAutolinker = asksExpo
448
- ? [appRequire, expoRequire]
449
- .flatMap((req) => req.resolve.paths('expo-modules-autolinking') ?? [])
450
- .map((directory) => path.join(directory, 'expo-modules-autolinking/package.json'))
451
- .find((manifest) => fs.existsSync(manifest))
452
- : undefined
453
- if (expoAutolinker) {
454
- const cli = path.join(path.dirname(expoAutolinker), 'bin/expo-modules-autolinking.js')
455
- // autolinkers before the `react-native-config` command (Expo SDK 48 ships
456
- // 1.2.0) cannot answer the React Native dependency map; those apps fall
457
- // through to their own `react-native config`, the command their Podfile's
458
- // use_native_modules! runs.
459
- if (await cliAdvertises(cli, ['--help'], 'react-native-config', appRoot, signal)) {
460
- const args = [
461
- cli,
462
- 'react-native-config',
463
- '--json',
464
- '--platform',
465
- 'ios',
466
- '--project-root',
467
- appRoot,
468
- ]
469
- return autolinkerJson(
470
- 'expo-modules-autolinking react-native-config',
471
- await run('node', args, {
472
- timeout: 30_000,
473
- maxBuffer: 8 * 1024 * 1024,
474
- env: { ...process.env, [PEACH_FORKED_SCRIPT_ENV]: '1' },
475
- // expo's react-native.config.js calls findProjectRootSync() on the
476
- // process cwd and ignores --project-root. the corpus bridge starts
477
- // outside the app, where that walk finds no package.json.
478
- cwd: appRoot,
479
- signal,
480
- }),
481
- )
482
- }
483
- }
484
- // the app's own `react-native config`, the command its Podfile runs, so any
485
- // CLI release answers through one interface
486
- // One's generated Podfile resolves the community CLI from vxrn.
487
- const cliRequire = declaresDependency(appRoot, 'one')
488
- ? createRequire(
489
- createRequire(appRequire.resolve('one/package.json')).resolve(
490
- 'vxrn/package.json',
491
- ),
492
- )
493
- : appRequire
494
- const cliManifest = cliRequire.resolve('@react-native-community/cli/package.json')
495
- const cli: unknown = JSON.parse(fs.readFileSync(cliManifest, 'utf8'))
496
- const bins = object(cli) && object(cli.bin) ? cli.bin : undefined
497
- // the bin was renamed from `react-native` to `rnc-cli` along the way; both
498
- // point at the same entry.
499
- const bin = [bins?.['rnc-cli'], bins?.['react-native']].find(
500
- (entry): entry is string => typeof entry === 'string',
501
- )
502
- if (bin === undefined) throw new Error(`${cliManifest} declares no CLI bin`)
503
- const cliPath = path.join(path.dirname(cliManifest), bin)
504
- const configArgs = (await cliAdvertises(
505
- cliPath,
506
- ['config', '--help'],
507
- '--platform',
508
- appRoot,
509
- signal,
510
- ))
511
- ? ['config', '--platform', 'ios']
512
- : ['config']
513
- return autolinkerJson(
514
- `react-native ${configArgs.join(' ')}`,
515
- await run('node', [cliPath, ...configArgs], {
516
- cwd: appRoot,
517
- signal,
518
- timeout: 30_000,
519
- maxBuffer: 16 * 1024 * 1024,
520
- env: { ...process.env, [PEACH_FORKED_SCRIPT_ENV]: '1', NO_COLOR: '1' },
521
- }),
522
- )
523
- }
524
-
525
361
  /** the packages react native's iOS autolinking links into the app that ship a
526
362
  * generated Nitro iOS initializer. The app's own autolinker resolves the
527
363
  * dependencies, so local roots from react-native.config and platform exclusions
@@ -541,8 +377,7 @@ export async function iosAutolinkingConfig(
541
377
  appRoot: string,
542
378
  signal?: AbortSignal,
543
379
  ): Promise<unknown> {
544
- const root = fs.realpathSync(appRoot)
545
- return autolinkingConfig(root, createRequire(path.join(root, 'package.json')), signal)
380
+ return reactNativeAutolinkingConfig(appRoot, 'ios', signal)
546
381
  }
547
382
 
548
383
  /** every dependency React Native iOS autolinking links into the app, with the