peachsim 0.1.2186 → 0.1.2188

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 (96) hide show
  1. package/dist-lib/agent-daemon-client.cjs +1 -1
  2. package/dist-lib/agent-events.cjs +1 -1
  3. package/dist-lib/attached-projects.cjs +1 -1
  4. package/dist-lib/auth/shared-session.cjs +1 -1
  5. package/dist-lib/backend-origin.cjs +1 -1
  6. package/dist-lib/beta.cjs +1 -1
  7. package/dist-lib/beta.mjs +1 -1
  8. package/dist-lib/bridge-constants.cjs +1 -1
  9. package/dist-lib/bridge-contract-input.cjs +1 -1
  10. package/dist-lib/bridge-contract-input.mjs +1 -1
  11. package/dist-lib/bridge-contract.cjs +1 -1
  12. package/dist-lib/bridge-contract.mjs +1 -1
  13. package/dist-lib/bundle-channel.cjs +2708 -2383
  14. package/dist-lib/capture-contract.cjs +1 -1
  15. package/dist-lib/capture-contract.mjs +1 -1
  16. package/dist-lib/cloud-contract.cjs +1 -1
  17. package/dist-lib/cloud-contract.mjs +1 -1
  18. package/dist-lib/cloud-test-args.cjs +1 -1
  19. package/dist-lib/cloud-test-args.mjs +1 -1
  20. package/dist-lib/cloud.cjs +1 -1
  21. package/dist-lib/cloud.mjs +1 -1
  22. package/dist-lib/config.cjs +1 -1
  23. package/dist-lib/config.mjs +1 -1
  24. package/dist-lib/detox/index.cjs +1 -1
  25. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  26. package/dist-lib/expo-swift-bundle.cjs +3229 -3222
  27. package/dist-lib/expo-swift-metadata.cjs +1 -1
  28. package/dist-lib/expo-swift-metadata.mjs +1 -1
  29. package/dist-lib/home-paths.cjs +1 -1
  30. package/dist-lib/host/bridge-host.cjs +4540 -4215
  31. package/dist-lib/host/fetch-proxy-handler.cjs +8 -7
  32. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  33. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  34. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  35. package/dist-lib/host/websocket-proxy.cjs +1 -1
  36. package/dist-lib/index.cjs +431 -281
  37. package/dist-lib/jump-to-source-babel.cjs +1 -1
  38. package/dist-lib/jump-to-source-native.cjs +1 -1
  39. package/dist-lib/kotlin-host.cjs +645 -284
  40. package/dist-lib/menu.cjs +1 -1
  41. package/dist-lib/menu.mjs +1 -1
  42. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  43. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  44. package/dist-lib/metro-production-bundle.cjs +1 -1
  45. package/dist-lib/metro-production-bundle.mjs +1 -1
  46. package/dist-lib/metro.cjs +732 -582
  47. package/dist-lib/native-app-identity-project.cjs +3 -3
  48. package/dist-lib/native-app-identity-project.mjs +3 -3
  49. package/dist-lib/native-app-identity.cjs +1 -1
  50. package/dist-lib/native-app-identity.mjs +1 -1
  51. package/dist-lib/native-firebase-metadata.cjs +1 -1
  52. package/dist-lib/native-firebase-metadata.mjs +1 -1
  53. package/dist-lib/pdfium-raster.cjs +1 -1
  54. package/dist-lib/pdfium-raster.mjs +1 -1
  55. package/dist-lib/plist.cjs +1 -1
  56. package/dist-lib/plist.mjs +1 -1
  57. package/dist-lib/profiles.cjs +1 -1
  58. package/dist-lib/public-brand.cjs +1 -1
  59. package/dist-lib/public-origins-lib-esm.json +24 -24
  60. package/dist-lib/public-origins-lib.json +99 -59
  61. package/dist-lib/react-native-host-modules.cjs +1 -1
  62. package/dist-lib/react-native-host-modules.mjs +1 -1
  63. package/dist-lib/reap-browser.cjs +1 -1
  64. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  65. package/dist-lib/sdk.cjs +1 -1
  66. package/dist-lib/sdk.mjs +1 -1
  67. package/dist-lib/skills.cjs +5198 -4873
  68. package/dist-lib/swift-compile.cjs +2 -2
  69. package/dist-lib/swift-compile.mjs +2 -2
  70. package/dist-lib/swift-package.cjs +1 -1
  71. package/dist-lib/swift-package.mjs +1 -1
  72. package/dist-lib/swift-submit.cjs +1 -1
  73. package/dist-lib/swift-submit.mjs +1 -1
  74. package/dist-lib/swift-toolchain.cjs +3 -1
  75. package/dist-lib/swift.cjs +1 -1
  76. package/dist-lib/vite-swift.cjs +4 -4
  77. package/dist-lib/vite.cjs +8 -7
  78. package/dist-lib/worklet-executable.cjs +1 -1
  79. package/dist-lib/worklet-executable.mjs +1 -1
  80. package/dist-types/packages/peach-engine/src/engine/node.d.ts +1 -1
  81. package/kotlin/sdk.tar.gz +0 -0
  82. package/package.json +1 -1
  83. package/sdk/ios/System/Library/Frameworks/CFNetwork.framework/Headers/CFNetwork.h +10 -0
  84. package/src/android-autolinking.ts +45 -4
  85. package/src/android-bridge-packages.ts +2 -2
  86. package/src/android-gradle-sources.ts +143 -0
  87. package/src/androidAutolinkingContract.ts +8 -0
  88. package/src/androidGradleContract.ts +14 -0
  89. package/src/expo-kotlin-host.ts +3 -0
  90. package/src/java-module-abi.ts +145 -0
  91. package/src/javaAbiContract.ts +14 -0
  92. package/src/kotlin-host.ts +55 -3
  93. package/src/nitro-source.ts +2 -167
  94. package/src/react-native-autolinking.ts +153 -0
  95. package/src/swift-distribution-contract.ts +72 -0
  96. package/src/swift-toolchain.ts +1 -1
@@ -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,11 +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, settings: 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)',
138
143
  ' dev.peach.android.ProcessSettings.configure(settings)',
144
+ ' android.os.SystemProperties.configure(properties)',
139
145
  '}',
140
146
  '',
141
147
  '@OptIn(kotlin.js.ExperimentalJsExport::class, kotlinx.coroutines.DelicateCoroutinesApi::class)',
@@ -703,6 +709,25 @@ export async function expoKotlinArtifact(
703
709
  origin: string,
704
710
  modules: ExpoKotlinModules,
705
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
+ }
706
731
  const toolchain = readToolchain(moduleRoot)
707
732
  const language = appKotlinLanguage(root)
708
733
  const scanned = await scanExpoTypes(toolchain, moduleRoot, modules.sources)
@@ -731,11 +756,12 @@ export async function expoKotlinArtifact(
731
756
  ' ))',
732
757
  '}',
733
758
  '',
734
- '@JsExport fun peachSetAndroidDeviceInfo(model: String, release: String, apiLevel: Int, locale: String, timeZone: String, settings: String) {',
759
+ '@JsExport fun peachSetAndroidDeviceInfo(model: String, release: String, apiLevel: Int, locale: String, timeZone: String, settings: String, properties: String) {',
735
760
  ' android.os.Build.setDevice(model, release, apiLevel)',
736
761
  ' java.util.Locale.setDefault(java.util.Locale.forLanguageTag(locale))',
737
762
  ' java.util.TimeZone.setDevice(timeZone)',
738
763
  ' dev.peach.android.ProcessSettings.configure(settings)',
764
+ ' android.os.SystemProperties.configure(properties)',
739
765
  ' PeachExpoRuntime.configureProcessLocale(locale)',
740
766
  '}',
741
767
  '',
@@ -757,6 +783,7 @@ export async function expoKotlinArtifact(
757
783
  toolchain,
758
784
  language,
759
785
  sources: modules.sources.filter((file) => !rewritten.has(file)),
786
+ javaSpecs: modules.javaSpecs,
760
787
  generated: {
761
788
  ...types.generated,
762
789
  'PeachExpoEntry.kt': entry,
@@ -1109,6 +1136,7 @@ async function compileKotlin(
1109
1136
  toolchain: KotlinToolchain
1110
1137
  language: string
1111
1138
  sources: string[]
1139
+ javaSpecs?: string[]
1112
1140
  generated: Record<string, string>
1113
1141
  // the app source a generated file rewrites, which its diagnostics name
1114
1142
  origins?: Record<string, string>
@@ -1137,10 +1165,14 @@ async function compileKotlin(
1137
1165
  usesCompose,
1138
1166
  usesSerialization = false,
1139
1167
  viewModels,
1168
+ javaSpecs = [],
1140
1169
  } = input
1141
1170
  const sdk = await loadKotlinSdk(moduleRoot, toolchain)
1142
1171
  const plugin = path.join(sdk.root, 'plugin.jar')
1143
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
1144
1176
  const generated = {
1145
1177
  ...sourceGenerated,
1146
1178
  'PeachBindingsInitialization.kt': [
@@ -1152,6 +1184,7 @@ async function compileKotlin(
1152
1184
  '@JsExport fun peachInitializeModuleBindings() {',
1153
1185
  ' if (!initialized) {',
1154
1186
  ' requireIncomingBindings()',
1187
+ ...(java ? [` ${JAVA_BINDINGS_INITIALIZER}()`] : []),
1155
1188
  ' initializeApplicationIncomingBindings()',
1156
1189
  ...(sourceGenerated['PeachViews.kt']
1157
1190
  ? [' initializeApplicationViews()']
@@ -1175,6 +1208,7 @@ async function compileKotlin(
1175
1208
  sdk: sdk.sha256,
1176
1209
  sdkProfile,
1177
1210
  usesSerialization,
1211
+ java: java?.sha256,
1178
1212
  }),
1179
1213
  )
1180
1214
  for (const { file, bytes } of inputs)
@@ -1210,6 +1244,8 @@ async function compileKotlin(
1210
1244
  }
1211
1245
  fs.mkdirSync(buildsDir(), { recursive: true })
1212
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)
1213
1249
  const generatedDir = path.join(work, 'generated')
1214
1250
  fs.mkdirSync(generatedDir)
1215
1251
  for (const [name, text] of Object.entries(generated))
@@ -1234,7 +1270,7 @@ async function compileKotlin(
1234
1270
  [
1235
1271
  ...common,
1236
1272
  `-Xplugin=${plugin}`,
1237
- `-Xfriend-modules=${sdkLibrary}`,
1273
+ `-Xfriend-modules=${[sdkLibrary, ...(javaLibrary ? [javaLibrary] : [])].join(path.delimiter)}`,
1238
1274
  ...(usesCompose
1239
1275
  ? [
1240
1276
  `-Xplugin=${path.resolve(path.dirname(kotlinc), '../lib/compose-compiler-plugin.jar')}`,
@@ -1261,6 +1297,17 @@ async function compileKotlin(
1261
1297
  { maxBuffer: 16 * 1024 * 1024 },
1262
1298
  )
1263
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
+ })
1264
1311
  // the compiler resolves the app's classes and constructors: a first
1265
1312
  // klib's ABI names the view models the generated factory builds. an app
1266
1313
  // whose sources never name ViewModel declares none and skips it
@@ -1397,6 +1444,11 @@ async function compileKotlin(
1397
1444
  output = output
1398
1445
  .replaceAll(target, file)
1399
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
+ )
1400
1452
  throw new Error(
1401
1453
  `kotlin compile failed:\n${output.trim() || (error instanceof Error ? error.message : String(error))}`,
1402
1454
  )
@@ -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
@@ -0,0 +1,153 @@
1
+ import { execFile } from 'node:child_process'
2
+ import fs from 'node:fs'
3
+ import { createRequire } from 'node:module'
4
+ import path from 'node:path'
5
+ import { promisify } from 'node:util'
6
+ import { PEACH_FORKED_SCRIPT_ENV } from './forked-script-contract'
7
+ import { isRecord } from './is-record'
8
+
9
+ const run = promisify(execFile)
10
+ function autolinkerJson(
11
+ command: string,
12
+ output: { stdout: string; stderr: string },
13
+ ): unknown {
14
+ try {
15
+ return JSON.parse(output.stdout)
16
+ } catch (cause) {
17
+ throw new Error(
18
+ `${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)'}`,
19
+ { cause },
20
+ )
21
+ }
22
+ }
23
+
24
+ // old CLI releases lack platform flags and Expo’s React Native command.
25
+ async function cliAdvertises(
26
+ cli: string,
27
+ args: string[],
28
+ token: string,
29
+ appRoot: string,
30
+ signal?: AbortSignal,
31
+ ): Promise<boolean> {
32
+ const { stdout, stderr } = await run('node', [cli, ...args], {
33
+ cwd: appRoot,
34
+ signal,
35
+ timeout: 30_000,
36
+ maxBuffer: 1 * 1024 * 1024,
37
+ env: { ...process.env, [PEACH_FORKED_SCRIPT_ENV]: '1' },
38
+ })
39
+ if (!stdout.trim())
40
+ throw new Error(
41
+ `${cli} ${args.join(' ')} returned empty help output${stderr.trim() ? `: ${stderr.trim()}` : ''}`,
42
+ )
43
+ return stdout.includes(token)
44
+ }
45
+
46
+ function declaresDependency(appRoot: string, name: string): boolean {
47
+ const manifest: unknown = JSON.parse(
48
+ fs.readFileSync(path.join(appRoot, 'package.json'), 'utf8'),
49
+ )
50
+ return (
51
+ isRecord(manifest) &&
52
+ ['dependencies', 'devDependencies'].some((field) => {
53
+ const group = manifest[field]
54
+ return isRecord(group) && Object.hasOwn(group, name)
55
+ })
56
+ )
57
+ }
58
+
59
+ export async function reactNativeAutolinkingConfig(
60
+ appRoot: string,
61
+ platform: 'ios' | 'android',
62
+ signal?: AbortSignal,
63
+ ): Promise<unknown> {
64
+ appRoot = fs.realpathSync(appRoot)
65
+ const appRequire = createRequire(path.join(appRoot, 'package.json'))
66
+ const expoManifest = appRequire.resolve
67
+ .paths('expo')
68
+ ?.map((directory) => path.join(directory, 'expo/package.json'))
69
+ .find((manifest) => fs.existsSync(manifest))
70
+ const expoRequire = expoManifest
71
+ ? createRequire(fs.realpathSync(expoManifest))
72
+ : appRequire
73
+ const nativeFile = (
74
+ platform === 'ios'
75
+ ? ['ios/Podfile']
76
+ : ['android/settings.gradle', 'android/settings.gradle.kts']
77
+ )
78
+ .map((name) => path.join(appRoot, name))
79
+ .find((file) => fs.existsSync(file))
80
+ const nativeSource = nativeFile ? fs.readFileSync(nativeFile, 'utf8') : null
81
+ const asksExpo =
82
+ process.env.EXPO_USE_COMMUNITY_AUTOLINKING !== '1' &&
83
+ (nativeSource === null
84
+ ? declaresDependency(appRoot, 'expo')
85
+ : nativeSource.includes('expo-modules-autolinking') ||
86
+ (platform === 'android' &&
87
+ nativeSource.includes('expoAutolinking.rnConfigCommand')))
88
+ const expoAutolinker = asksExpo
89
+ ? [appRequire, expoRequire]
90
+ .flatMap((req) => req.resolve.paths('expo-modules-autolinking') ?? [])
91
+ .map((directory) => path.join(directory, 'expo-modules-autolinking/package.json'))
92
+ .find((manifest) => fs.existsSync(manifest))
93
+ : undefined
94
+ if (expoAutolinker) {
95
+ const cli = path.join(path.dirname(expoAutolinker), 'bin/expo-modules-autolinking.js')
96
+ if (await cliAdvertises(cli, ['--help'], 'react-native-config', appRoot, signal)) {
97
+ const args = [
98
+ cli,
99
+ 'react-native-config',
100
+ '--json',
101
+ '--platform',
102
+ platform,
103
+ '--project-root',
104
+ appRoot,
105
+ ]
106
+ return autolinkerJson(
107
+ 'expo-modules-autolinking react-native-config',
108
+ await run('node', args, {
109
+ timeout: 30_000,
110
+ maxBuffer: 8 * 1024 * 1024,
111
+ env: { ...process.env, [PEACH_FORKED_SCRIPT_ENV]: '1' },
112
+ // expo configuration resolves its project from cwd.
113
+ cwd: appRoot,
114
+ signal,
115
+ }),
116
+ )
117
+ }
118
+ }
119
+ const cliRequire = declaresDependency(appRoot, 'one')
120
+ ? createRequire(
121
+ createRequire(appRequire.resolve('one/package.json')).resolve(
122
+ 'vxrn/package.json',
123
+ ),
124
+ )
125
+ : appRequire
126
+ const cliManifest = cliRequire.resolve('@react-native-community/cli/package.json')
127
+ const cli: unknown = JSON.parse(fs.readFileSync(cliManifest, 'utf8'))
128
+ const bins = isRecord(cli) && isRecord(cli.bin) ? cli.bin : undefined
129
+ const bin = [bins?.['rnc-cli'], bins?.['react-native']].find(
130
+ (entry): entry is string => typeof entry === 'string',
131
+ )
132
+ if (bin === undefined) throw new Error(`${cliManifest} declares no CLI bin`)
133
+ const cliPath = path.join(path.dirname(cliManifest), bin)
134
+ const configArgs = (await cliAdvertises(
135
+ cliPath,
136
+ ['config', '--help'],
137
+ '--platform',
138
+ appRoot,
139
+ signal,
140
+ ))
141
+ ? ['config', '--platform', platform]
142
+ : ['config']
143
+ return autolinkerJson(
144
+ `react-native ${configArgs.join(' ')}`,
145
+ await run('node', [cliPath, ...configArgs], {
146
+ cwd: appRoot,
147
+ signal,
148
+ timeout: 30_000,
149
+ maxBuffer: 16 * 1024 * 1024,
150
+ env: { ...process.env, [PEACH_FORKED_SCRIPT_ENV]: '1', NO_COLOR: '1' },
151
+ }),
152
+ )
153
+ }