vxrn 2.0.0-beta.99.1 → 2.6.1

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 (210) hide show
  1. package/dist/backgroundComputation.mjs +145 -0
  2. package/dist/backgroundComputation.mjs.map +1 -0
  3. package/dist/backgroundComputation.native.js +163 -0
  4. package/dist/backgroundComputation.native.js.map +1 -0
  5. package/dist/backgroundComputationNative.mjs +16 -0
  6. package/dist/backgroundComputationNative.mjs.map +1 -0
  7. package/dist/backgroundComputationNative.native.js +18 -0
  8. package/dist/backgroundComputationNative.native.js.map +1 -0
  9. package/dist/config/getBaseViteConfigOnly.mjs +6 -10
  10. package/dist/config/getBaseViteConfigOnly.mjs.map +1 -1
  11. package/dist/config/getBaseViteConfigOnly.native.js +6 -10
  12. package/dist/config/getBaseViteConfigOnly.native.js.map +1 -1
  13. package/dist/config/getOptimizeDeps.mjs +2 -1
  14. package/dist/config/getOptimizeDeps.mjs.map +1 -1
  15. package/dist/config/getOptimizeDeps.native.js +2 -1
  16. package/dist/config/getOptimizeDeps.native.js.map +1 -1
  17. package/dist/config/getViteServerConfig.mjs +13 -0
  18. package/dist/config/getViteServerConfig.mjs.map +1 -1
  19. package/dist/config/getViteServerConfig.native.js +15 -1
  20. package/dist/config/getViteServerConfig.native.js.map +1 -1
  21. package/dist/constants/defaults.mjs +4 -0
  22. package/dist/constants/defaults.mjs.map +1 -1
  23. package/dist/constants/defaults.native.js +4 -0
  24. package/dist/constants/defaults.native.js.map +1 -1
  25. package/dist/exports/build.native.js +1 -1
  26. package/dist/exports/build.native.js.map +1 -1
  27. package/dist/exports/clean.native.js +1 -1
  28. package/dist/exports/clean.native.js.map +1 -1
  29. package/dist/exports/createServer.native.js +2 -2
  30. package/dist/exports/createServer.native.js.map +1 -1
  31. package/dist/exports/dev.mjs +3 -0
  32. package/dist/exports/dev.mjs.map +1 -1
  33. package/dist/exports/dev.native.js +6 -3
  34. package/dist/exports/dev.native.js.map +1 -1
  35. package/dist/exports/loadEnv.native.js +1 -1
  36. package/dist/exports/loadEnv.native.js.map +1 -1
  37. package/dist/exports/patch.native.js +1 -1
  38. package/dist/exports/patch.native.js.map +1 -1
  39. package/dist/exports/prebuild.mjs +0 -22
  40. package/dist/exports/prebuild.mjs.map +1 -1
  41. package/dist/exports/prebuild.native.js +1 -23
  42. package/dist/exports/prebuild.native.js.map +1 -1
  43. package/dist/exports/prebuildWithoutExpo.mjs +1116 -178
  44. package/dist/exports/prebuildWithoutExpo.mjs.map +1 -1
  45. package/dist/exports/prebuildWithoutExpo.native.js +1592 -243
  46. package/dist/exports/prebuildWithoutExpo.native.js.map +1 -1
  47. package/dist/exports/runAndroid.native.js +1 -1
  48. package/dist/exports/runAndroid.native.js.map +1 -1
  49. package/dist/exports/runIos.native.js +1 -1
  50. package/dist/exports/runIos.native.js.map +1 -1
  51. package/dist/exports/serve.native.js +1 -1
  52. package/dist/exports/serve.native.js.map +1 -1
  53. package/dist/exports/serveStaticAssets.native.js +1 -1
  54. package/dist/exports/serveStaticAssets.native.js.map +1 -1
  55. package/dist/index.js +4 -1
  56. package/dist/index.mjs +4 -1
  57. package/dist/index.native.js +4 -1
  58. package/dist/native-source-contract.mjs +3 -0
  59. package/dist/native-source-contract.native.js +3 -0
  60. package/dist/patches/builtInDepPatches.mjs +0 -13
  61. package/dist/patches/builtInDepPatches.mjs.map +1 -1
  62. package/dist/patches/builtInDepPatches.native.js +0 -13
  63. package/dist/patches/builtInDepPatches.native.js.map +1 -1
  64. package/dist/plugins/autoDepOptimizePlugin.mjs +3 -2
  65. package/dist/plugins/autoDepOptimizePlugin.mjs.map +1 -1
  66. package/dist/plugins/autoDepOptimizePlugin.native.js +6 -3
  67. package/dist/plugins/autoDepOptimizePlugin.native.js.map +1 -1
  68. package/dist/plugins/backgroundComputationPlugin.mjs +27 -0
  69. package/dist/plugins/backgroundComputationPlugin.mjs.map +1 -0
  70. package/dist/plugins/backgroundComputationPlugin.native.js +32 -0
  71. package/dist/plugins/backgroundComputationPlugin.native.js.map +1 -0
  72. package/dist/plugins/reactNativeCommonJsPlugin.native.js +3 -3
  73. package/dist/plugins/reactNativeCommonJsPlugin.native.js.map +1 -1
  74. package/dist/plugins/reactNativeDevAssetPlugin.mjs +2 -1
  75. package/dist/plugins/reactNativeDevAssetPlugin.mjs.map +1 -1
  76. package/dist/plugins/reactNativeDevAssetPlugin.native.js +3 -2
  77. package/dist/plugins/reactNativeDevAssetPlugin.native.js.map +1 -1
  78. package/dist/plugins/reactNativeDevServer.mjs +87 -24
  79. package/dist/plugins/reactNativeDevServer.mjs.map +1 -1
  80. package/dist/plugins/reactNativeDevServer.native.js +162 -45
  81. package/dist/plugins/reactNativeDevServer.native.js.map +1 -1
  82. package/dist/plugins/workletImportsPlugin.mjs +9 -30
  83. package/dist/plugins/workletImportsPlugin.mjs.map +1 -1
  84. package/dist/plugins/workletImportsPlugin.native.js +10 -35
  85. package/dist/plugins/workletImportsPlugin.native.js.map +1 -1
  86. package/dist/rn-commands/bundle/buildBundle.mjs +3 -3
  87. package/dist/rn-commands/bundle/buildBundle.mjs.map +1 -1
  88. package/dist/rn-commands/bundle/buildBundle.native.js +3 -3
  89. package/dist/rn-commands/bundle/buildBundle.native.js.map +1 -1
  90. package/dist/runtime/hmr-client.native.js +3 -7
  91. package/dist/runtime/hmr-client.native.js.map +1 -1
  92. package/dist/runtime/hmr-runtime.native.js +32 -40
  93. package/dist/runtime/hmr-runtime.native.js.map +1 -1
  94. package/dist/serve/node.native.js +2 -2
  95. package/dist/serve/node.native.js.map +1 -1
  96. package/dist/swift-package-id.mjs +3 -0
  97. package/dist/swift-package-id.native.js +3 -0
  98. package/dist/user-interface/index.native.js +6 -6
  99. package/dist/user-interface/index.native.js.map +1 -1
  100. package/dist/utils/compileNativeDevBytecode.mjs +93 -0
  101. package/dist/utils/compileNativeDevBytecode.mjs.map +1 -0
  102. package/dist/utils/compileNativeDevBytecode.native.js +98 -0
  103. package/dist/utils/compileNativeDevBytecode.native.js.map +1 -0
  104. package/dist/utils/createNativeDevEngine.mjs +227 -20
  105. package/dist/utils/createNativeDevEngine.mjs.map +1 -1
  106. package/dist/utils/createNativeDevEngine.native.js +249 -37
  107. package/dist/utils/createNativeDevEngine.native.js.map +1 -1
  108. package/dist/utils/execSync.native.js +1 -1
  109. package/dist/utils/execSync.native.js.map +1 -1
  110. package/dist/utils/getServerEntry.native.js +2 -2
  111. package/dist/utils/getServerEntry.native.js.map +1 -1
  112. package/dist/utils/nativeRun.mjs +32 -16
  113. package/dist/utils/nativeRun.mjs.map +1 -1
  114. package/dist/utils/nativeRun.native.js +43 -18
  115. package/dist/utils/nativeRun.native.js.map +1 -1
  116. package/dist/utils/nativeSourceContract.mjs +1823 -0
  117. package/dist/utils/nativeSourceContract.mjs.map +1 -0
  118. package/dist/utils/nativeSourceContract.native.js +2425 -0
  119. package/dist/utils/nativeSourceContract.native.js.map +1 -0
  120. package/dist/utils/patches.native.js +15 -24
  121. package/dist/utils/patches.native.js.map +1 -1
  122. package/dist/utils/printServerUrls.native.js +1 -1
  123. package/dist/utils/printServerUrls.native.js.map +1 -1
  124. package/dist/utils/scanDepsToOptimize.mjs +7 -1
  125. package/dist/utils/scanDepsToOptimize.mjs.map +1 -1
  126. package/dist/utils/scanDepsToOptimize.native.js +27 -2
  127. package/dist/utils/scanDepsToOptimize.native.js.map +1 -1
  128. package/dist/utils/swiftPackageId.mjs +31 -2
  129. package/dist/utils/swiftPackageId.mjs.map +1 -1
  130. package/dist/utils/swiftPackageId.native.js +50 -2
  131. package/dist/utils/swiftPackageId.native.js.map +1 -1
  132. package/dist/vxrn-vite-plugin.native.js +1 -1
  133. package/dist/vxrn-vite-plugin.native.js.map +1 -1
  134. package/expo-plugin.cjs +792 -2
  135. package/native-project-patches.cjs +201 -10
  136. package/package.json +34 -15
  137. package/src/backgroundComputation.ts +220 -0
  138. package/src/backgroundComputationNative.ts +23 -0
  139. package/src/config/getBaseViteConfigOnly.ts +8 -9
  140. package/src/config/getOptimizeDeps.ts +3 -1
  141. package/src/config/getViteServerConfig.ts +16 -0
  142. package/src/config/glbAssets.test.ts +228 -0
  143. package/src/constants/defaults.ts +6 -0
  144. package/src/exports/dev.ts +5 -1
  145. package/src/exports/expoPlugin.test.mjs +337 -15
  146. package/src/exports/prebuild.ts +0 -31
  147. package/src/exports/prebuildWithoutExpo.test.ts +1588 -48
  148. package/src/exports/prebuildWithoutExpo.ts +1573 -213
  149. package/src/index.ts +8 -0
  150. package/src/native-source-contract.ts +16 -0
  151. package/src/patches/builtInDepPatches.ts +0 -21
  152. package/src/plugins/autoDepOptimizePlugin.ts +7 -2
  153. package/src/plugins/backgroundComputationPlugin.test.ts +77 -0
  154. package/src/plugins/backgroundComputationPlugin.ts +29 -0
  155. package/src/plugins/reactNativeDevAssetPlugin.ts +5 -1
  156. package/src/plugins/reactNativeDevServer.test.ts +74 -0
  157. package/src/plugins/reactNativeDevServer.ts +145 -28
  158. package/src/plugins/rnCodegenPlugin.test.ts +30 -0
  159. package/src/plugins/workletImportsPlugin.ts +19 -37
  160. package/src/rn-commands/bundle/buildBundle.ts +5 -3
  161. package/src/swift-package-id.ts +1 -0
  162. package/src/utils/compileNativeDevBytecode.test.ts +168 -0
  163. package/src/utils/compileNativeDevBytecode.ts +117 -0
  164. package/src/utils/createNativeDevEngine.test.ts +643 -18
  165. package/src/utils/createNativeDevEngine.ts +278 -48
  166. package/src/utils/nativeRun.test.ts +125 -23
  167. package/src/utils/nativeRun.ts +51 -23
  168. package/src/utils/nativeSourceArtifacts.test.ts +39 -0
  169. package/src/utils/nativeSourceContract.test.ts +600 -0
  170. package/src/utils/nativeSourceContract.ts +1897 -0
  171. package/src/utils/scanDepsToOptimize.test.ts +80 -0
  172. package/src/utils/scanDepsToOptimize.ts +18 -7
  173. package/src/utils/swiftPackageId.ts +25 -1
  174. package/types/backgroundComputation.d.ts +28 -0
  175. package/types/backgroundComputation.d.ts.map +1 -0
  176. package/types/backgroundComputationNative.d.ts +2 -0
  177. package/types/backgroundComputationNative.d.ts.map +1 -0
  178. package/types/config/getBaseViteConfigOnly.d.ts.map +1 -1
  179. package/types/config/getOptimizeDeps.d.ts.map +1 -1
  180. package/types/config/getViteServerConfig.d.ts.map +1 -1
  181. package/types/constants/defaults.d.ts.map +1 -1
  182. package/types/exports/dev.d.ts.map +1 -1
  183. package/types/exports/prebuild.d.ts.map +1 -1
  184. package/types/exports/prebuildWithoutExpo.d.ts +6 -0
  185. package/types/exports/prebuildWithoutExpo.d.ts.map +1 -1
  186. package/types/index.d.ts +2 -0
  187. package/types/index.d.ts.map +1 -1
  188. package/types/native-source-contract.d.ts +2 -0
  189. package/types/native-source-contract.d.ts.map +1 -0
  190. package/types/patches/builtInDepPatches.d.ts.map +1 -1
  191. package/types/plugins/autoDepOptimizePlugin.d.ts.map +1 -1
  192. package/types/plugins/backgroundComputationPlugin.d.ts +3 -0
  193. package/types/plugins/backgroundComputationPlugin.d.ts.map +1 -0
  194. package/types/plugins/reactNativeDevAssetPlugin.d.ts.map +1 -1
  195. package/types/plugins/reactNativeDevServer.d.ts.map +1 -1
  196. package/types/plugins/workletImportsPlugin.d.ts.map +1 -1
  197. package/types/rn-commands/bundle/buildBundle.d.ts.map +1 -1
  198. package/types/swift-package-id.d.ts +2 -0
  199. package/types/swift-package-id.d.ts.map +1 -0
  200. package/types/utils/compileNativeDevBytecode.d.ts +3 -0
  201. package/types/utils/compileNativeDevBytecode.d.ts.map +1 -0
  202. package/types/utils/createNativeDevEngine.d.ts +7 -22
  203. package/types/utils/createNativeDevEngine.d.ts.map +1 -1
  204. package/types/utils/nativeRun.d.ts.map +1 -1
  205. package/types/utils/nativeSourceContract.d.ts +121 -0
  206. package/types/utils/nativeSourceContract.d.ts.map +1 -0
  207. package/types/utils/scanDepsToOptimize.d.ts +1 -0
  208. package/types/utils/scanDepsToOptimize.d.ts.map +1 -1
  209. package/types/utils/swiftPackageId.d.ts +1 -0
  210. package/types/utils/swiftPackageId.d.ts.map +1 -1
@@ -1,13 +1,19 @@
1
1
  import { execFileSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
2
3
  import module from "node:module";
3
4
  import path from "node:path";
4
5
  import { pathToFileURL } from "node:url";
5
6
  import { validateNativeApp } from "@vxrn/utils/nativeAppManifest";
6
7
  import FSExtra from "fs-extra";
7
8
  import sharp from "sharp";
8
- import { swiftPackageId } from "../utils/swiftPackageId.mjs";
9
+ import { swiftPackageDirectories } from "../utils/swiftPackageId.mjs";
9
10
 
10
- const nativeProjectPatches = module.createRequire(import.meta.url)("../../native-project-patches.cjs");
11
+ const requireVxrn = module.createRequire(import.meta.url);
12
+ const nativeProjectPatches = requireVxrn("../../native-project-patches.cjs");
13
+ const notificationsHost = nativeProjectPatches.ONE_NOTIFICATIONS;
14
+ const updatesHost = nativeProjectPatches.ONE_UPDATES;
15
+ const PORTRAIT_ORIENTATIONS = ["UIInterfaceOrientationPortrait", "UIInterfaceOrientationPortraitUpsideDown"];
16
+ const LANDSCAPE_ORIENTATIONS = ["UIInterfaceOrientationLandscapeLeft", "UIInterfaceOrientationLandscapeRight"];
11
17
  const validatePrebuildApp = validateNativeApp;
12
18
  const ANDROID_DENSITIES = {
13
19
  mdpi: 1,
@@ -19,7 +25,7 @@ const ANDROID_DENSITIES = {
19
25
  const IOS_BUNDLE_PLACEHOLDER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
20
26
  const ANDROID_PACKAGE_PLACEHOLDER = "com.helloworld";
21
27
  const ANDROID_PACKAGE_PATH = "com/helloworld";
22
- function patchIosBundlePhase(project) {
28
+ function patchIosBundlePhase(project, updatesRuntimeVersion) {
23
29
  let patchedBundlePhases = 0;
24
30
  const patchedProject = project.replace(/shellScript = ("(?:\\.|[^"\\])*");/g, (assignment, serializedScript) => {
25
31
  let script;
@@ -38,6 +44,12 @@ function patchIosBundlePhase(project) {
38
44
  if (!patched.includes("[vxrn/one] React Native now defaults CLI_PATH") || !patched.includes("[vxrn/one] use the hermes-engine pod") || !patched.includes("[vxrn/one] ensure patches are applied")) {
39
45
  throw new Error("[vxrn] failed to apply required iOS bundle phase patches");
40
46
  }
47
+ if (updatesRuntimeVersion !== void 0) {
48
+ patched = nativeProjectPatches.addEmbeddedUpdatesManifestToBundleReactNativeShellScript(patched, updatesRuntimeVersion);
49
+ if (!patched.includes("[vxrn/one] the embedded update manifest lands beside the release bundle")) {
50
+ throw new Error("[vxrn] failed to apply the iOS embedded updates manifest patch");
51
+ }
52
+ }
41
53
  patchedBundlePhases++;
42
54
  return `shellScript = ${JSON.stringify(patched)};`;
43
55
  });
@@ -64,21 +76,14 @@ import ReactAppDependencyProvider
64
76
  class SceneDelegate: UIResponder, UIWindowSceneDelegate {
65
77
  var window: UIWindow?
66
78
 
67
- var reactNativeDelegate: ReactNativeDelegate?
68
- var reactNativeFactory: RCTReactNativeFactory?
69
-
70
79
  func scene(
71
80
  _ scene: UIScene,
72
81
  willConnectTo session: UISceneSession,
73
82
  options connectionOptions: UIScene.ConnectionOptions
74
83
  ) {
75
84
  guard let windowScene = scene as? UIWindowScene else { return }
76
- let delegate = ReactNativeDelegate()
77
- let factory = RCTReactNativeFactory(delegate: delegate)
78
- delegate.dependencyProvider = RCTAppDependencyProvider()
79
-
80
- reactNativeDelegate = delegate
81
- reactNativeFactory = factory
85
+ guard let appDelegate = UIApplication.shared.delegate as? AppDelegate,
86
+ let factory = appDelegate.reactNativeFactory else { return }
82
87
 
83
88
  let window = UIWindow(windowScene: windowScene)
84
89
  self.window = window
@@ -154,7 +159,7 @@ function patchIosInfoPlistSceneManifest(rendered) {
154
159
  </dict>
155
160
  ${anchor}`);
156
161
  }
157
- function patchIosAppDelegateSceneLifecycle(rendered, appName) {
162
+ function patchIosAppDelegateSceneLifecycle(rendered, appName, backgroundTasks, appIntents) {
158
163
  const anchor = `@main
159
164
  class AppDelegate: UIResponder, UIApplicationDelegate {
160
165
  var window: UIWindow?
@@ -189,38 +194,48 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
189
194
  }
190
195
  return rendered.replace(anchor, `@main
191
196
  class AppDelegate: UIResponder, UIApplicationDelegate {
197
+ var reactNativeDelegate: ReactNativeDelegate?
198
+ var reactNativeFactory: RCTReactNativeFactory?
199
+
192
200
  func application(
193
201
  _ application: UIApplication,
194
202
  didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
195
203
  ) -> Bool {
196
- true
197
- }
198
-
199
- // remote push answers land here; the notifications module observes the
200
- // forward. inert unless something calls registerForRemoteNotifications.
201
- func application(
202
- _ application: UIApplication,
203
- didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
204
- ) {
205
- NotificationCenter.default.post(
206
- name: NSNotification.Name("OneNativePushTokenDidRegister"),
207
- object: nil,
208
- userInfo: ["deviceToken": deviceToken]
209
- )
210
- }
211
-
212
- func application(
213
- _ application: UIApplication,
214
- didFailToRegisterForRemoteNotificationsWithError error: Error
215
- ) {
216
- NotificationCenter.default.post(
217
- name: NSNotification.Name("OneNativePushTokenDidFail"),
218
- object: nil,
219
- userInfo: ["error": error.localizedDescription]
220
- )
204
+ let delegate = ReactNativeDelegate()
205
+ let factory = RCTReactNativeFactory(delegate: delegate)
206
+ delegate.dependencyProvider = RCTAppDependencyProvider()
207
+ reactNativeDelegate = delegate
208
+ reactNativeFactory = factory
209
+ ${backgroundTasks ? ` OneBackgroundTasksRegisterHost(factory, launchOptions)
210
+ ` : ""}${appIntents ? ` OneAppIntentsRegisterHost(factory, launchOptions)
211
+ ` : ""} return true
221
212
  }
222
213
  }`);
223
214
  }
215
+ const ONE_BRIDGING_HEADER_NAME = "One-Bridging-Header.h";
216
+ function renderOneBridgingHeader(app) {
217
+ const imports = [nativeProjectPatches.ONE_LAUNCH_SCREEN.bridgingHeaderImport];
218
+ if (app.updates !== void 0) imports.push(updatesHost.bridgingHeaderImport);
219
+ if (app.ios?.backgroundTasks !== void 0) imports.push("#import \"OneBackgroundTasksBridge.h\"");
220
+ if (app.ios?.appIntents !== void 0) imports.push("#import \"OneAppIntentsBridge.h\"");
221
+ return `// generated by one prebuild: exposes One's c entry points to AppDelegate
222
+ // without importing the One module.
223
+ ${imports.join("\n")}
224
+ `;
225
+ }
226
+ function generateOneBridgingHeader(dest, app) {
227
+ FSExtra.writeFileSync(path.join(dest, app.name, ONE_BRIDGING_HEADER_NAME), renderOneBridgingHeader(app));
228
+ }
229
+ function patchIosPbxprojOneBridgingHeader(project, appName) {
230
+ const anchor = `INFOPLIST_FILE = ${appName}/Info.plist;`;
231
+ const setting = `SWIFT_OBJC_BRIDGING_HEADER = "${appName}/${ONE_BRIDGING_HEADER_NAME}";`;
232
+ const occurrences = project.split(anchor).length - 1;
233
+ if (occurrences !== 2) {
234
+ throw new Error("[vxrn] prebuild template project.pbxproj changed shape: cannot point the app target at the One bridging header");
235
+ }
236
+ return project.split(anchor).join(`${anchor}
237
+ ${setting}`);
238
+ }
224
239
  function patchIosPbxprojSceneDelegate(rendered, appName) {
225
240
  const edits = [
226
241
  ["/* AppDelegate.swift in Sources */ = {isa = PBXBuildFile;", ` ${SCENE_DELEGATE_BUILD_FILE_ID} /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = ${SCENE_DELEGATE_FILE_REF_ID} /* SceneDelegate.swift */; };`],
@@ -238,7 +253,128 @@ function patchIosPbxprojSceneDelegate(rendered, appName) {
238
253
  }
239
254
  return patched;
240
255
  }
241
- function patchIosPbxprojPushEntitlements(rendered, appName) {
256
+ function patchIosPbxprojAppIntents(rendered, appName) {
257
+ const name = "OneAppIntents.swift";
258
+ const fileRef = pbxprojId("one-app-intents-file");
259
+ const buildFile = pbxprojId("one-app-intents-build");
260
+ const edits = [
261
+ ["/* AppDelegate.swift in Sources */ = {isa = PBXBuildFile;", ` ${buildFile} /* ${name} in Sources */ = {isa = PBXBuildFile; fileRef = ${fileRef} /* ${name} */; };`],
262
+ ["/* AppDelegate.swift */ = {isa = PBXFileReference;", ` ${fileRef} /* ${name} */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ${name}; path = ${appName}/${name}; sourceTree = "<group>"; };`],
263
+ ["/* AppDelegate.swift */,", ` ${fileRef} /* ${name} */,`],
264
+ ["/* AppDelegate.swift in Sources */,", ` ${buildFile} /* ${name} in Sources */,`]
265
+ ];
266
+ let patched = rendered;
267
+ for (const [anchor, insertion] of edits) {
268
+ const next = insertAfterLine(patched, anchor, insertion);
269
+ if (next === patched) {
270
+ throw new Error(`[vxrn] prebuild template project.pbxproj lost its AppDelegate.swift anchor (${anchor})`);
271
+ }
272
+ patched = next;
273
+ }
274
+ return patched;
275
+ }
276
+ function swiftLiteral(value) {
277
+ return JSON.stringify(value).replace(/\\u([0-9a-fA-F]{4})/g, (_match, hex) => `\\u{${hex}}`);
278
+ }
279
+ function swiftShortcutPhrase(value) {
280
+ return `"${value.split("{app}").map((part) => swiftLiteral(part).slice(1, -1)).join("\\(.applicationName)")}"`;
281
+ }
282
+ function renderIosAppIntentsSwift(actions) {
283
+ const types = actions.map((action, index) => {
284
+ const summaryTitle = swiftLiteral(action.title).slice(1, -1);
285
+ const text = action.textParameterTitle === void 0 ? "" : `
286
+ @Parameter(title: ${swiftLiteral(action.textParameterTitle)}) var text: String
287
+
288
+ static var parameterSummary: some ParameterSummary {
289
+ Summary("${summaryTitle} \\(\\.$text)")
290
+ }
291
+ `;
292
+ return `struct OneAppIntent${index}: AppIntent {
293
+ static let title: LocalizedStringResource = ${swiftLiteral(action.title)}
294
+ ${text}
295
+ func perform() async throws -> some IntentResult & ReturnsValue<String> {
296
+ let result = try await oneAppIntentsRun(${swiftLiteral(action.id)}, ${text ? "text" : "nil"})
297
+ return .result(value: result)
298
+ }
299
+ }`;
300
+ });
301
+ const shortcuts = actions.map((action, index) => ` AppShortcut(
302
+ intent: OneAppIntent${index}(),
303
+ phrases: [${swiftShortcutPhrase(action.shortcutPhrase)}],
304
+ shortTitle: ${swiftLiteral(action.title)},
305
+ systemImageName: "bolt.fill"
306
+ )`).join("\n");
307
+ return `import AppIntents
308
+ import Foundation
309
+
310
+ private func oneAppIntentsRun(_ identifier: String, _ text: String?) async throws -> String {
311
+ try await withCheckedThrowingContinuation { continuation in
312
+ OneAppIntentsPerform(identifier, text) { result, error in
313
+ if let error { continuation.resume(throwing: error) }
314
+ else if let result { continuation.resume(returning: result) }
315
+ else { continuation.resume(throwing: NSError(domain: "OneAppIntents", code: 3)) }
316
+ }
317
+ }
318
+ }
319
+
320
+ ${types.join("\n\n")}
321
+
322
+ struct OneAppShortcuts: AppShortcutsProvider {
323
+ static var appShortcuts: [AppShortcut] {
324
+ ${shortcuts}
325
+ }
326
+ }
327
+ `;
328
+ }
329
+ function fontFileName(font) {
330
+ return path.basename(font);
331
+ }
332
+ function pbxprojId(seed) {
333
+ return createHash("sha1").update(seed).digest("hex").slice(0, 24).toUpperCase();
334
+ }
335
+ const GOOGLE_SERVICES_PLIST = "GoogleService-Info.plist";
336
+ const GOOGLE_SERVICES_VERSION = "4.4.4";
337
+ function iosResourceFiles(app) {
338
+ return [...(app.fonts ?? []).map(fontFileName), ...app.ios?.googleServicesFile ? [GOOGLE_SERVICES_PLIST] : []];
339
+ }
340
+ function patchIosPbxprojResources(rendered, appName, names) {
341
+ let patched = rendered;
342
+ for (const name of names) {
343
+ const fileRef = pbxprojId(`one-resource-file:${name}`);
344
+ const buildFile = pbxprojId(`one-resource-build:${name}`);
345
+ const fileType = name.endsWith(".plist") ? "text.plist.xml" : "file";
346
+ const edits = [
347
+ ["/* Images.xcassets in Resources */ = {isa = PBXBuildFile;", ` ${buildFile} /* ${name} in Resources */ = {isa = PBXBuildFile; fileRef = ${fileRef} /* ${name} */; };`],
348
+ ["/* Images.xcassets */ = {isa = PBXFileReference;", ` ${fileRef} /* ${name} */ = {isa = PBXFileReference; lastKnownFileType = ${fileType}; name = ${name}; path = ${appName}/${name}; sourceTree = "<group>"; };`],
349
+ ["/* Images.xcassets */,", ` ${fileRef} /* ${name} */,`],
350
+ ["/* Images.xcassets in Resources */,", ` ${buildFile} /* ${name} in Resources */,`]
351
+ ];
352
+ for (const [anchor, insertion] of edits) {
353
+ const next = insertAfterLine(patched, anchor, insertion);
354
+ if (next === patched) {
355
+ throw new Error(`[vxrn] prebuild template project.pbxproj lost its Images.xcassets anchor (${anchor})`);
356
+ }
357
+ patched = next;
358
+ }
359
+ }
360
+ return patched;
361
+ }
362
+ function copyAppResources({ root, dest, platform, app }) {
363
+ const copies = (app.fonts ?? []).map((font) => [font, platform === "ios" ? path.join(dest, app.name, fontFileName(font)) : path.join(dest, "app", "src", "main", "assets", "fonts", fontFileName(font))]);
364
+ const googleServices = platform === "ios" ? app.ios?.googleServicesFile : app.android?.googleServicesFile;
365
+ if (googleServices) {
366
+ copies.push([googleServices, platform === "ios" ? path.join(dest, app.name, GOOGLE_SERVICES_PLIST) : path.join(dest, "app", "google-services.json")]);
367
+ }
368
+ for (const [file, target] of copies) {
369
+ const source = path.resolve(root, file);
370
+ if (!FSExtra.existsSync(source)) {
371
+ throw new Error(`[vxrn] native.app names ${file}, which does not exist`);
372
+ }
373
+ FSExtra.mkdirSync(path.dirname(target), { recursive: true });
374
+ FSExtra.copyFileSync(source, target);
375
+ }
376
+ }
377
+ function patchIosPbxprojAppEntitlements(rendered, appName) {
242
378
  const edits = [["/* AppDelegate.swift */ = {isa = PBXFileReference;", ` ${PUSH_ENTITLEMENTS_FILE_REF_ID} /* ${appName}.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = ${appName}.entitlements; path = ${appName}/${appName}.entitlements; sourceTree = "<group>"; };`], ["/* AppDelegate.swift */,", ` ${PUSH_ENTITLEMENTS_FILE_REF_ID} /* ${appName}.entitlements */,`]];
243
379
  let patched = rendered;
244
380
  for (const [anchor, insertion] of edits) {
@@ -255,17 +391,92 @@ function patchIosPbxprojPushEntitlements(rendered, appName) {
255
391
  return patched.split(setting).join(`${setting}
256
392
  CODE_SIGN_ENTITLEMENTS = ${appName}/${appName}.entitlements;`);
257
393
  }
258
- function renderPushEntitlements() {
394
+ function renderPlistValue(value, indent) {
395
+ if (typeof value === "string") return `${indent}<string>${escapeXml(value)}</string>`;
396
+ if (typeof value === "boolean") return `${indent}<${value}/>`;
397
+ if (typeof value === "number") {
398
+ return Number.isInteger(value) ? `${indent}<integer>${value}</integer>` : `${indent}<real>${value}</real>`;
399
+ }
400
+ if (Array.isArray(value)) {
401
+ return `${indent}<array>
402
+ ${value.map((item) => renderPlistValue(item, `${indent} `)).join("\n")}
403
+ ${indent}</array>`;
404
+ }
405
+ return `${indent}<dict>
406
+ ${renderPlistEntries(value, `${indent} `)}
407
+ ${indent}</dict>`;
408
+ }
409
+ function renderPlistEntries(entries, indent) {
410
+ return Object.entries(entries).map(([key, value]) => `${indent}<key>${escapeXml(key)}</key>
411
+ ${renderPlistValue(value, indent)}`).join("\n");
412
+ }
413
+ function appEntitlements(app) {
414
+ const modeled = {};
415
+ if (app.notifications?.push === true || app.ios?.widgets?.pushNotifications) {
416
+ modeled["aps-environment"] = app.notifications?.apsEnvironment ?? notificationsHost.apsEnvironment;
417
+ }
418
+ if (app.ios?.widgets) {
419
+ modeled["com.apple.security.application-groups"] = [app.ios.widgets.appGroup];
420
+ }
421
+ if (app.ios?.associatedDomains?.length) {
422
+ modeled["com.apple.developer.associated-domains"] = app.ios.associatedDomains;
423
+ }
424
+ if (app.ios?.usesAppleSignIn) {
425
+ modeled["com.apple.developer.applesignin"] = ["Default"];
426
+ }
427
+ for (const key of Object.keys(app.ios?.entitlements ?? {})) {
428
+ if (key in modeled) {
429
+ throw new Error(`[vxrn] native.app.ios.entitlements sets ${key}, which native.app already writes: use its field`);
430
+ }
431
+ }
432
+ return {
433
+ ...modeled,
434
+ ...app.ios?.entitlements
435
+ };
436
+ }
437
+ function renderEntitlements(entries) {
259
438
  return `<?xml version="1.0" encoding="UTF-8"?>
260
439
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
261
440
  <plist version="1.0">
262
441
  <dict>
263
- <key>aps-environment</key>
264
- <string>development</string>
442
+ ${renderPlistEntries(entries, " ")}
265
443
  </dict>
266
444
  </plist>
267
445
  `;
268
446
  }
447
+ function patchAndroidMainApplicationUpdatesHost(rendered) {
448
+ const anchor = "import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost";
449
+ if (!rendered.includes(anchor)) {
450
+ throw new Error("[vxrn] prebuild template MainApplication.kt changed shape: cannot point the ReactHost at One.Updates");
451
+ }
452
+ return rendered.replace(anchor, "import com.margelo.nitro.one.OneUpdatesReactHost.getDefaultReactHost");
453
+ }
454
+ function escapeGradleString(value) {
455
+ return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
456
+ }
457
+ function patchAndroidBuildGradleUpdatesManifest(rendered, runtimeVersion) {
458
+ const anchor = " autolinkLibrariesWithApp()\n}";
459
+ if (!rendered.includes(anchor)) {
460
+ throw new Error("[vxrn] cannot stamp the embedded updates manifest: expected the replaced react block in app/build.gradle");
461
+ }
462
+ const snippet = `${anchor}
463
+
464
+ // [vxrn/one] the embedded update manifest lands beside the release bundle
465
+ tasks.matching { it.name.startsWith("createBundle") && it.name.endsWith("JsAndAssets") }.configureEach { bundleTask ->
466
+ // capture the output directory provider at configuration time, so the
467
+ // action below holds no task reference at execution time.
468
+ def bundleDir = bundleTask.jsBundleDir
469
+ bundleTask.doLast {
470
+ def manifestFile = new File(bundleDir.get().asFile, "one-updates-embedded.json")
471
+ manifestFile.text = groovy.json.JsonOutput.toJson([
472
+ id: java.util.UUID.randomUUID().toString(),
473
+ createdAt: java.time.Instant.now().toString(),
474
+ runtimeVersion: "${escapeGradleString(runtimeVersion)}",
475
+ ])
476
+ }
477
+ }`;
478
+ return rendered.replace(anchor, snippet);
479
+ }
269
480
  function patchIosPbxprojWidgets(project, app) {
270
481
  const name = app.name;
271
482
  const bundleId = app.ios.bundleId;
@@ -367,10 +578,15 @@ function generateSceneDelegate(args) {
367
578
  const { dest, platform, app } = args;
368
579
  if (platform !== "ios") return;
369
580
  FSExtra.writeFileSync(path.join(dest, app.name, "SceneDelegate.swift"), renderSceneDelegateSwift(app.name));
370
- if (app.notifications?.push === true && !app.ios?.widgets) {
371
- FSExtra.writeFileSync(path.join(dest, app.name, `${app.name}.entitlements`), renderPushEntitlements());
581
+ const entitlements = appEntitlements(app);
582
+ if (Object.keys(entitlements).length && !app.ios?.widgets) {
583
+ FSExtra.writeFileSync(path.join(dest, app.name, `${app.name}.entitlements`), renderEntitlements(entitlements));
372
584
  }
373
585
  }
586
+ function generateIosAppIntents(dest, platform, app) {
587
+ if (platform !== "ios" || !app.ios?.appIntents) return;
588
+ FSExtra.writeFileSync(path.join(dest, app.name, "OneAppIntents.swift"), renderIosAppIntentsSwift(app.ios.appIntents.actions));
589
+ }
374
590
  function generateIosWidgets(dest, app) {
375
591
  const widgets = app.ios?.widgets;
376
592
  if (!widgets) return;
@@ -381,7 +597,7 @@ function generateIosWidgets(dest, app) {
381
597
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
382
598
  <plist version="1.0"><dict><key>com.apple.security.application-groups</key><array><string>${escapeXml(widgets.appGroup)}</string></array></dict></plist>
383
599
  `;
384
- FSExtra.writeFileSync(path.join(appDir, "OneAppWidgets.entitlements"), widgets.pushNotifications || app.notifications?.push ? entitlements.replace("</dict></plist>", "<key>aps-environment</key><string>development</string></dict></plist>") : entitlements);
600
+ FSExtra.writeFileSync(path.join(appDir, "OneAppWidgets.entitlements"), renderEntitlements(appEntitlements(app)));
385
601
  FSExtra.writeFileSync(path.join(extensionDir, "OneWidgets.entitlements"), entitlements);
386
602
  FSExtra.writeFileSync(path.join(extensionDir, "WidgetInfo.plist"), `<?xml version="1.0" encoding="UTF-8"?>
387
603
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -889,33 +1105,62 @@ class OneWidgetsBridge: RCTEventEmitter {
889
1105
  }
890
1106
  `);
891
1107
  }
1108
+ const IPAD_APP_ICON_SIZES = [
1109
+ ["20x20", ["1x", "2x"]],
1110
+ ["29x29", ["1x", "2x"]],
1111
+ ["40x40", ["1x", "2x"]],
1112
+ ["76x76", ["1x", "2x"]],
1113
+ ["83.5x83.5", ["2x"]]
1114
+ ];
892
1115
  async function generateAppIcons(args) {
893
1116
  const { root, dest, platform, app } = args;
894
1117
  if (!app.icon) return;
1118
+ if (platform === "ios") {
1119
+ const assetsDir = path.join(dest, app.name, "Images.xcassets");
1120
+ const template = JSON.parse(FSExtra.readFileSync(path.join(assetsDir, "AppIcon.appiconset", "Contents.json"), "utf8"));
1121
+ const icons = [["AppIcon", app.icon], ...Object.entries(app.ios?.alternateIcons ?? {})];
1122
+ for (const [name, icon] of icons) {
1123
+ const source2 = path.resolve(root, icon.source);
1124
+ if (!FSExtra.existsSync(source2)) {
1125
+ throw new Error(`[vxrn] native.app icon source does not exist: ${source2}`);
1126
+ }
1127
+ const metadata2 = await sharp(source2).metadata();
1128
+ if (metadata2.width !== metadata2.height || metadata2.width === void 0 || metadata2.width < 1024) {
1129
+ throw new Error(`[vxrn] native.app icon source must be a square image at least 1024px wide: ${source2}`);
1130
+ }
1131
+ const iconDir = path.join(assetsDir, `${name}.appiconset`);
1132
+ FSExtra.mkdirSync(iconDir, { recursive: true });
1133
+ const contents = structuredClone(template);
1134
+ if (app.ios?.tablet) {
1135
+ for (const [size, scales] of IPAD_APP_ICON_SIZES) {
1136
+ for (const scale of scales) contents.images.push({
1137
+ idiom: "ipad",
1138
+ scale,
1139
+ size
1140
+ });
1141
+ }
1142
+ }
1143
+ for (const image of contents.images) {
1144
+ const points = Number.parseFloat(image.size.split("x")[0]);
1145
+ const scale = Number.parseInt(image.scale, 10);
1146
+ const pixels = points * scale;
1147
+ const filename = image.idiom === "ios-marketing" ? "icon-1024.png" : `icon-${points}@${image.scale}.png`;
1148
+ image.filename = filename;
1149
+ await sharp(source2).rotate().resize(pixels, pixels, { fit: "cover" }).flatten({ background: icon.backgroundColor }).png().toFile(path.join(iconDir, filename));
1150
+ }
1151
+ FSExtra.writeFileSync(path.join(iconDir, "Contents.json"), `${JSON.stringify(contents, null, 2)}
1152
+ `);
1153
+ }
1154
+ return;
1155
+ }
895
1156
  const source = path.resolve(root, app.icon.source);
896
1157
  if (!FSExtra.existsSync(source)) {
897
1158
  throw new Error(`[vxrn] native.app.icon source does not exist: ${source}`);
898
1159
  }
899
1160
  const metadata = await sharp(source).metadata();
900
- if (metadata.width === void 0 || metadata.height === void 0 || metadata.width !== metadata.height || metadata.width < 1024) {
1161
+ if (metadata.width !== metadata.height || metadata.width === void 0 || metadata.width < 1024) {
901
1162
  throw new Error("[vxrn] native.app.icon source must be a square image at least 1024px wide");
902
1163
  }
903
- if (platform === "ios") {
904
- const iconDir = path.join(dest, app.name, "Images.xcassets", "AppIcon.appiconset");
905
- const contentsPath = path.join(iconDir, "Contents.json");
906
- const contents = JSON.parse(FSExtra.readFileSync(contentsPath, "utf8"));
907
- for (const image of contents.images) {
908
- const points = Number.parseFloat(image.size.split("x")[0]);
909
- const scale = Number.parseInt(image.scale, 10);
910
- const pixels = points * scale;
911
- const filename = image.idiom === "ios-marketing" ? "icon-1024.png" : `icon-${points}@${image.scale}.png`;
912
- image.filename = filename;
913
- await sharp(source).rotate().resize(pixels, pixels, { fit: "cover" }).flatten({ background: app.icon.backgroundColor }).png().toFile(path.join(iconDir, filename));
914
- }
915
- FSExtra.writeFileSync(contentsPath, `${JSON.stringify(contents, null, 2)}
916
- `);
917
- return;
918
- }
919
1164
  for (const [density, multiplier] of Object.entries(ANDROID_DENSITIES)) {
920
1165
  const pixels = 48 * multiplier;
921
1166
  const iconDir = path.join(dest, "app", "src", "main", "res", `mipmap-${density}`);
@@ -924,39 +1169,175 @@ async function generateAppIcons(args) {
924
1169
  }
925
1170
  }
926
1171
  }
1172
+ async function generateAdaptiveIcon(args) {
1173
+ const { root, dest, app } = args;
1174
+ const adaptiveIcon = app.android?.adaptiveIcon;
1175
+ if (!adaptiveIcon) return;
1176
+ const res = path.join(dest, "app", "src", "main", "res");
1177
+ const layers = {
1178
+ foreground: adaptiveIcon.foreground,
1179
+ background: adaptiveIcon.background,
1180
+ monochrome: adaptiveIcon.monochrome
1181
+ };
1182
+ for (const [layer, file] of Object.entries(layers)) {
1183
+ if (!file) continue;
1184
+ const source = path.resolve(root, file);
1185
+ if (!FSExtra.existsSync(source)) {
1186
+ throw new Error(`[vxrn] native.app.android.adaptiveIcon.${layer} does not exist: ${source}`);
1187
+ }
1188
+ for (const [density, multiplier] of Object.entries(ANDROID_DENSITIES)) {
1189
+ const pixels = 108 * multiplier;
1190
+ await sharp(source).rotate().resize(pixels, pixels, { fit: "cover" }).png().toFile(path.join(res, `mipmap-${density}`, `ic_launcher_${layer}.png`));
1191
+ }
1192
+ }
1193
+ if (!adaptiveIcon.background) {
1194
+ FSExtra.writeFileSync(path.join(res, "values", "ic_launcher_background.xml"), `<?xml version="1.0" encoding="utf-8"?>
1195
+ <resources>
1196
+ <color name="ic_launcher_background">${adaptiveIcon.backgroundColor ?? "#FFFFFF"}</color>
1197
+ </resources>
1198
+ `);
1199
+ }
1200
+ const adaptiveXml = `<?xml version="1.0" encoding="utf-8"?>
1201
+ <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
1202
+ <background android:drawable="${adaptiveIcon.background ? "@mipmap/ic_launcher_background" : "@color/ic_launcher_background"}"/>
1203
+ <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
1204
+ ${adaptiveIcon.monochrome ? " <monochrome android:drawable=\"@mipmap/ic_launcher_monochrome\"/>\n" : ""}</adaptive-icon>
1205
+ `;
1206
+ const anydpi = path.join(res, "mipmap-anydpi-v26");
1207
+ FSExtra.mkdirSync(anydpi, { recursive: true });
1208
+ for (const filename of ["ic_launcher.xml", "ic_launcher_round.xml"]) {
1209
+ FSExtra.writeFileSync(path.join(anydpi, filename), adaptiveXml);
1210
+ }
1211
+ }
1212
+ const IOS_DARK_APPEARANCE = [{
1213
+ appearance: "luminosity",
1214
+ value: "dark"
1215
+ }];
1216
+ function srgb(hex) {
1217
+ return {
1218
+ red: Number.parseInt(hex.slice(1, 3), 16) / 255,
1219
+ green: Number.parseInt(hex.slice(3, 5), 16) / 255,
1220
+ blue: Number.parseInt(hex.slice(5, 7), 16) / 255
1221
+ };
1222
+ }
1223
+ function xcodeColorSet(light, dark) {
1224
+ const entry = (hex) => {
1225
+ const { red, green, blue } = srgb(hex);
1226
+ return {
1227
+ color: {
1228
+ "color-space": "srgb",
1229
+ components: {
1230
+ red: red.toFixed(3),
1231
+ green: green.toFixed(3),
1232
+ blue: blue.toFixed(3),
1233
+ alpha: "1.000"
1234
+ }
1235
+ },
1236
+ idiom: "universal"
1237
+ };
1238
+ };
1239
+ return `${JSON.stringify({
1240
+ colors: [entry(light), ...dark ? [{
1241
+ appearances: IOS_DARK_APPEARANCE,
1242
+ ...entry(dark)
1243
+ }] : []],
1244
+ info: {
1245
+ author: "xcode",
1246
+ version: 1
1247
+ }
1248
+ }, null, 2)}
1249
+ `;
1250
+ }
1251
+ function generateAccentColor(args) {
1252
+ const accent = args.app.ios?.accentColor;
1253
+ if (!accent) return;
1254
+ const colorDir = path.join(args.dest, args.app.name, "Images.xcassets", "AccentColor.colorset");
1255
+ FSExtra.mkdirSync(colorDir, { recursive: true });
1256
+ FSExtra.writeFileSync(path.join(colorDir, "Contents.json"), xcodeColorSet(accent.light, accent.dark));
1257
+ }
927
1258
  async function generateSplashScreen(args) {
928
1259
  const { root, dest, platform, app } = args;
929
1260
  if (!app.splash) return;
930
- const source = path.resolve(root, app.splash.source);
931
- if (!FSExtra.existsSync(source)) {
932
- throw new Error(`[vxrn] native.app.splash source does not exist: ${source}`);
933
- }
934
- const { data: artwork, info: metadata } = await sharp(source).rotate().trim({ background: app.splash.backgroundColor }).png().toBuffer({ resolveWithObject: true });
935
- if (!metadata.width || !metadata.height) {
936
- throw new Error("[vxrn] native.app.splash source must be an image");
937
- }
1261
+ const cover = app.splash.resizeMode === "cover";
1262
+ const prepareArtwork = async (sourcePath, backgroundColor) => {
1263
+ const source = path.resolve(root, sourcePath);
1264
+ if (!FSExtra.existsSync(source)) {
1265
+ throw new Error(`[vxrn] native.app.splash source does not exist: ${source}`);
1266
+ }
1267
+ const { width: sourceWidth, format } = await sharp(source).metadata();
1268
+ const density = format === "svg" && sourceWidth && !cover ? Math.min(2400, Math.ceil(72 * 4 * (app.splash?.width ?? 200) / sourceWidth)) : void 0;
1269
+ const image = sharp(source, density ? { density } : {}).rotate();
1270
+ const prepared = await (cover ? image : image.trim({ background: backgroundColor })).png().toBuffer({ resolveWithObject: true });
1271
+ if (!prepared.info.width || !prepared.info.height) {
1272
+ throw new Error("[vxrn] native.app.splash source must be an image");
1273
+ }
1274
+ return prepared;
1275
+ };
1276
+ const { data: artwork, info: metadata } = await prepareArtwork(app.splash.source, app.splash.backgroundColor);
1277
+ const dark = app.splash.dark;
1278
+ const darkArtwork = dark?.source ? (await prepareArtwork(dark.source, dark.backgroundColor)).data : void 0;
938
1279
  const artworkWidth = app.splash.width ?? 200;
939
1280
  const artworkHeight = Number((artworkWidth * (metadata.height / metadata.width)).toFixed(3));
1281
+ const readBackground = (sourcePath) => {
1282
+ const source = path.resolve(root, sourcePath);
1283
+ if (!FSExtra.existsSync(source)) {
1284
+ throw new Error(`[vxrn] native.app.splash backgroundImage does not exist: ${source}`);
1285
+ }
1286
+ return sharp(source).rotate().png().toBuffer({ resolveWithObject: true });
1287
+ };
940
1288
  if (platform === "ios") {
941
1289
  const appDir = path.join(dest, app.name);
942
- const splashDir = path.join(appDir, "Images.xcassets", "Splash.imageset");
943
- FSExtra.mkdirSync(splashDir, { recursive: true });
944
- await sharp(artwork).toFile(path.join(splashDir, "splash.png"));
945
- FSExtra.writeFileSync(path.join(splashDir, "Contents.json"), `${JSON.stringify({
946
- images: [{
947
- filename: "splash.png",
948
- idiom: "universal",
949
- scale: "1x"
950
- }],
951
- info: {
952
- author: "xcode",
953
- version: 1
954
- }
955
- }, null, 2)}
1290
+ const assets = path.join(appDir, "Images.xcassets");
1291
+ const writeImageSet = async (name, light, dark2, pointWidth) => {
1292
+ const dir = path.join(assets, `${name}.imageset`);
1293
+ FSExtra.mkdirSync(dir, { recursive: true });
1294
+ const scales = pointWidth === void 0 ? [1] : [
1295
+ 1,
1296
+ 2,
1297
+ 3
1298
+ ];
1299
+ const images = [];
1300
+ for (const [variant, suffix, appearances] of [[
1301
+ light,
1302
+ "",
1303
+ void 0
1304
+ ], [
1305
+ dark2,
1306
+ "-dark",
1307
+ IOS_DARK_APPEARANCE
1308
+ ]]) {
1309
+ if (!variant) continue;
1310
+ for (const scale of scales) {
1311
+ const filename = `${name.toLowerCase()}${suffix}${scale === 1 ? "" : `@${scale}x`}.png`;
1312
+ const output = sharp(variant);
1313
+ await (pointWidth === void 0 ? output : output.resize({ width: Math.round(pointWidth * scale) })).png().toFile(path.join(dir, filename));
1314
+ images.push({
1315
+ ...appearances ? { appearances } : {},
1316
+ filename,
1317
+ idiom: "universal",
1318
+ scale: `${scale}x`
1319
+ });
1320
+ }
1321
+ }
1322
+ FSExtra.writeFileSync(path.join(dir, "Contents.json"), `${JSON.stringify({
1323
+ images,
1324
+ info: {
1325
+ author: "xcode",
1326
+ version: 1
1327
+ }
1328
+ }, null, 2)}
956
1329
  `);
957
- const red = Number.parseInt(app.splash.backgroundColor.slice(1, 3), 16) / 255;
958
- const green = Number.parseInt(app.splash.backgroundColor.slice(3, 5), 16) / 255;
959
- const blue = Number.parseInt(app.splash.backgroundColor.slice(5, 7), 16) / 255;
1330
+ };
1331
+ await writeImageSet("Splash", artwork, darkArtwork, cover ? void 0 : artworkWidth);
1332
+ const background2 = app.splash.backgroundImage ? await readBackground(app.splash.backgroundImage) : void 0;
1333
+ const darkBackground2 = dark?.backgroundImage ? (await readBackground(dark.backgroundImage)).data : void 0;
1334
+ if (background2) {
1335
+ await writeImageSet("SplashBackgroundImage", background2.data, darkBackground2, void 0);
1336
+ }
1337
+ const colorDir = path.join(assets, "SplashBackground.colorset");
1338
+ FSExtra.mkdirSync(colorDir, { recursive: true });
1339
+ FSExtra.writeFileSync(path.join(colorDir, "Contents.json"), xcodeColorSet(app.splash.backgroundColor, dark?.backgroundColor));
1340
+ const { red, green, blue } = srgb(app.splash.backgroundColor);
960
1341
  FSExtra.writeFileSync(path.join(appDir, "LaunchScreen.storyboard"), `<?xml version="1.0" encoding="UTF-8"?>
961
1342
  <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="launch-controller">
962
1343
  <device id="retina6_12" orientation="portrait" appearance="light"/>
@@ -971,15 +1352,23 @@ async function generateSplashScreen(args) {
971
1352
  <viewController id="launch-controller" sceneMemberID="viewController">
972
1353
  <view key="view" contentMode="scaleToFill" id="launch-view">
973
1354
  <rect key="frame" x="0.0" y="0.0" width="390" height="844"/>
974
- <subviews>
975
- <imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" image="Splash" translatesAutoresizingMaskIntoConstraints="NO" id="splash-image"/>
1355
+ <subviews>${background2 ? `
1356
+ <imageView userInteractionEnabled="NO" contentMode="scaleAspectFill" image="SplashBackgroundImage" translatesAutoresizingMaskIntoConstraints="NO" id="splash-background"/>` : ""}
1357
+ <imageView userInteractionEnabled="NO" contentMode="${cover ? "scaleAspectFill" : "scaleAspectFit"}" image="Splash" translatesAutoresizingMaskIntoConstraints="NO" id="splash-image"/>
976
1358
  </subviews>
977
- <color key="backgroundColor" red="${red}" green="${green}" blue="${blue}" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
978
- <constraints>
979
- <constraint firstItem="splash-image" firstAttribute="centerX" secondItem="launch-view" secondAttribute="centerX" id="splash-center-x"/>
1359
+ <color key="backgroundColor" name="SplashBackground"/>
1360
+ <constraints>${background2 ? `
1361
+ <constraint firstItem="splash-background" firstAttribute="leading" secondItem="launch-view" secondAttribute="leading" id="splash-background-leading"/>
1362
+ <constraint firstItem="splash-background" firstAttribute="trailing" secondItem="launch-view" secondAttribute="trailing" id="splash-background-trailing"/>
1363
+ <constraint firstItem="splash-background" firstAttribute="top" secondItem="launch-view" secondAttribute="top" id="splash-background-top"/>
1364
+ <constraint firstItem="splash-background" firstAttribute="bottom" secondItem="launch-view" secondAttribute="bottom" id="splash-background-bottom"/>` : ""}
1365
+ ${cover ? ` <constraint firstItem="splash-image" firstAttribute="leading" secondItem="launch-view" secondAttribute="leading" id="splash-leading"/>
1366
+ <constraint firstItem="splash-image" firstAttribute="trailing" secondItem="launch-view" secondAttribute="trailing" id="splash-trailing"/>
1367
+ <constraint firstItem="splash-image" firstAttribute="top" secondItem="launch-view" secondAttribute="top" id="splash-top"/>
1368
+ <constraint firstItem="splash-image" firstAttribute="bottom" secondItem="launch-view" secondAttribute="bottom" id="splash-bottom"/>` : ` <constraint firstItem="splash-image" firstAttribute="centerX" secondItem="launch-view" secondAttribute="centerX" id="splash-center-x"/>
980
1369
  <constraint firstItem="splash-image" firstAttribute="centerY" secondItem="launch-view" secondAttribute="centerY" id="splash-center-y"/>
981
1370
  <constraint firstItem="splash-image" firstAttribute="width" constant="${artworkWidth}" id="splash-width"/>
982
- <constraint firstItem="splash-image" firstAttribute="height" constant="${artworkHeight}" id="splash-height"/>
1371
+ <constraint firstItem="splash-image" firstAttribute="height" constant="${artworkHeight}" id="splash-height"/>`}
983
1372
  </constraints>
984
1373
  </view>
985
1374
  </viewController>
@@ -988,7 +1377,11 @@ async function generateSplashScreen(args) {
988
1377
  </scene>
989
1378
  </scenes>
990
1379
  <resources>
991
- <image name="Splash" width="${metadata.width}" height="${metadata.height}"/>
1380
+ <image name="Splash" width="${cover ? metadata.width : artworkWidth}" height="${cover ? metadata.height : artworkHeight}"/>${background2 ? `
1381
+ <image name="SplashBackgroundImage" width="${background2.info.width}" height="${background2.info.height}"/>` : ""}
1382
+ <namedColor name="SplashBackground">
1383
+ <color red="${red}" green="${green}" blue="${blue}" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
1384
+ </namedColor>
992
1385
  </resources>
993
1386
  </document>
994
1387
  `);
@@ -996,40 +1389,59 @@ async function generateSplashScreen(args) {
996
1389
  }
997
1390
  const mainRes = path.join(dest, "app", "src", "main", "res");
998
1391
  const drawable = path.join(mainRes, "drawable");
999
- for (const [density, multiplier] of Object.entries(ANDROID_DENSITIES)) {
1000
- const canvasSize = 288 * multiplier;
1001
- const imageSize = Math.round(artworkWidth * multiplier);
1002
- const contained = await sharp(artwork).resize(imageSize, imageSize, {
1003
- fit: "contain",
1004
- background: {
1005
- r: 0,
1006
- g: 0,
1007
- b: 0,
1008
- alpha: 0
1009
- }
1010
- }).png().toBuffer();
1011
- const drawableDensity = path.join(mainRes, `drawable-${density}`);
1012
- FSExtra.mkdirSync(drawableDensity, { recursive: true });
1013
- await sharp({ create: {
1014
- width: canvasSize,
1015
- height: canvasSize,
1016
- channels: 4,
1017
- background: {
1018
- r: 0,
1019
- g: 0,
1020
- b: 0,
1021
- alpha: 0
1022
- }
1023
- } }).composite([{
1024
- input: contained,
1025
- left: Math.round((canvasSize - imageSize) / 2),
1026
- top: Math.round((canvasSize - imageSize) / 2)
1027
- }]).png().toFile(path.join(drawableDensity, "splash.png"));
1392
+ const variants = [[artwork, "drawable"]];
1393
+ if (darkArtwork) variants.push([darkArtwork, "drawable-night"]);
1394
+ for (const [variantArtwork, drawablePrefix] of variants) {
1395
+ for (const [density, multiplier] of Object.entries(ANDROID_DENSITIES)) {
1396
+ const canvasSize = 288 * multiplier;
1397
+ const imageSize = Math.round(artworkWidth * multiplier);
1398
+ const contained = await sharp(variantArtwork).resize(imageSize, imageSize, {
1399
+ fit: "contain",
1400
+ background: {
1401
+ r: 0,
1402
+ g: 0,
1403
+ b: 0,
1404
+ alpha: 0
1405
+ }
1406
+ }).png().toBuffer();
1407
+ const drawableDensity = path.join(mainRes, `${drawablePrefix}-${density}`);
1408
+ FSExtra.mkdirSync(drawableDensity, { recursive: true });
1409
+ await sharp({ create: {
1410
+ width: canvasSize,
1411
+ height: canvasSize,
1412
+ channels: 4,
1413
+ background: {
1414
+ r: 0,
1415
+ g: 0,
1416
+ b: 0,
1417
+ alpha: 0
1418
+ }
1419
+ } }).composite([{
1420
+ input: contained,
1421
+ left: Math.round((canvasSize - imageSize) / 2),
1422
+ top: Math.round((canvasSize - imageSize) / 2)
1423
+ }]).png().toFile(path.join(drawableDensity, "splash.png"));
1424
+ }
1425
+ }
1426
+ const background = app.splash.backgroundImage ? await readBackground(app.splash.backgroundImage) : void 0;
1427
+ const darkBackground = dark?.backgroundImage ? (await readBackground(dark.backgroundImage)).data : void 0;
1428
+ if (background) {
1429
+ const nodpi = path.join(mainRes, "drawable-nodpi");
1430
+ FSExtra.mkdirSync(nodpi, { recursive: true });
1431
+ FSExtra.writeFileSync(path.join(nodpi, "splash_background.png"), background.data);
1432
+ if (darkBackground) {
1433
+ const nightNodpi = path.join(mainRes, "drawable-night-nodpi");
1434
+ FSExtra.mkdirSync(nightNodpi, { recursive: true });
1435
+ FSExtra.writeFileSync(path.join(nightNodpi, "splash_background.png"), darkBackground);
1436
+ }
1028
1437
  }
1029
1438
  FSExtra.writeFileSync(path.join(drawable, "launch_screen.xml"), `<?xml version="1.0" encoding="utf-8"?>
1030
1439
  <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
1031
1440
  <item android:drawable="@color/splash_background" />
1032
- <item>
1441
+ ${background ? ` <item>
1442
+ <bitmap android:gravity="fill" android:src="@drawable/splash_background" />
1443
+ </item>
1444
+ ` : ""} <item>
1033
1445
  <bitmap android:gravity="center" android:src="@drawable/splash" />
1034
1446
  </item>
1035
1447
  </layer-list>
@@ -1039,6 +1451,14 @@ async function generateSplashScreen(args) {
1039
1451
  <color name="splash_background">${app.splash.backgroundColor}</color>
1040
1452
  </resources>
1041
1453
  `);
1454
+ if (dark) {
1455
+ FSExtra.mkdirSync(path.join(mainRes, "values-night"), { recursive: true });
1456
+ FSExtra.writeFileSync(path.join(mainRes, "values-night", "colors.xml"), `<?xml version="1.0" encoding="utf-8"?>
1457
+ <resources>
1458
+ <color name="splash_background">${dark.backgroundColor}</color>
1459
+ </resources>
1460
+ `);
1461
+ }
1042
1462
  const stylesPath = path.join(mainRes, "values", "styles.xml");
1043
1463
  const styles = FSExtra.readFileSync(stylesPath, "utf8").replace(" <!-- Customize your theme here. -->", " <item name=\"android:windowBackground\">@drawable/launch_screen</item>");
1044
1464
  FSExtra.writeFileSync(stylesPath, styles);
@@ -1093,25 +1513,92 @@ ${schemes.map((scheme) => ` <string>${scheme}</string>`).join("\n")}
1093
1513
  stamps.push(` <key>ITSAppUsesNonExemptEncryption</key>
1094
1514
  <${app.ios.usesNonExemptEncryption ? "true" : "false"}/>`);
1095
1515
  }
1516
+ if (app.ios?.accentColor) {
1517
+ stamps.push(" <key>NSAccentColorName</key>\n <string>AccentColor</string>");
1518
+ }
1096
1519
  if (app.ios?.fileSharing) {
1097
1520
  stamps.push(` <key>UIFileSharingEnabled</key>
1098
1521
  <true/>
1099
1522
  <key>LSSupportsOpeningDocumentsInPlace</key>
1100
1523
  <true/>`);
1101
1524
  }
1525
+ if (app.photoLibrary?.readWrite !== void 0) {
1526
+ stamps.push(" <key>PHPhotoLibraryPreventAutomaticLimitedAccessAlert</key>\n <true/>");
1527
+ }
1102
1528
  if (app.notifications !== void 0) {
1103
- stamps.push(` <key>OneNativeNotificationsEnabled</key>
1529
+ stamps.push(` <key>${notificationsHost.enabledInfoPlistKey}</key>
1104
1530
  <true/>`);
1105
1531
  }
1106
1532
  if (app.notifications?.push === true) {
1107
- stamps.push(` <key>OneNativeNotificationsPush</key>
1533
+ stamps.push(` <key>${notificationsHost.pushInfoPlistKey}</key>
1108
1534
  <true/>`);
1109
1535
  }
1110
1536
  if (app.ios?.widgets) {
1111
1537
  stamps.push(" <key>NSSupportsLiveActivities</key>\n <true/>");
1112
1538
  }
1113
- if (app.pictureInPicture) {
1114
- stamps.push(" <key>UIBackgroundModes</key>\n <array>\n <string>audio</string>\n </array>");
1539
+ const refreshTasks = app.ios?.backgroundTasks?.refresh ?? [];
1540
+ const processingTasks = app.ios?.backgroundTasks?.processing ?? [];
1541
+ if (refreshTasks.length || processingTasks.length) {
1542
+ const plistArray = (key, identifiers) => ` <key>${key}</key>
1543
+ <array>
1544
+ ${identifiers.map((identifier) => ` <string>${escapeXml(identifier)}</string>`).join("\n")}
1545
+ </array>`;
1546
+ stamps.push(plistArray("BGTaskSchedulerPermittedIdentifiers", [...refreshTasks, ...processingTasks]));
1547
+ stamps.push(plistArray("OneBackgroundRefreshTaskIdentifiers", refreshTasks));
1548
+ stamps.push(plistArray("OneBackgroundProcessingTaskIdentifiers", processingTasks));
1549
+ }
1550
+ if (app.ios?.appIntents) {
1551
+ stamps.push(` <key>OneAppIntentIdentifiers</key>
1552
+ <array>
1553
+ ${app.ios.appIntents.actions.map((action) => ` <string>${escapeXml(action.id)}</string>`).join("\n")}
1554
+ </array>`);
1555
+ }
1556
+ const backgroundModes = [
1557
+ ...app.pictureInPicture || app.audio?.background ? ["audio"] : [],
1558
+ ...app.location?.background ? ["location"] : [],
1559
+ ...refreshTasks.length ? ["fetch"] : [],
1560
+ ...processingTasks.length ? ["processing"] : []
1561
+ ];
1562
+ if (backgroundModes.length) {
1563
+ stamps.push(` <key>UIBackgroundModes</key>
1564
+ <array>
1565
+ ${backgroundModes.map((mode) => ` <string>${mode}</string>`).join("\n")}
1566
+ </array>`);
1567
+ }
1568
+ if (app.updates !== void 0) {
1569
+ if (app.updates.url !== void 0) {
1570
+ stamps.push(` <key>${updatesHost.urlInfoPlistKey}</key>
1571
+ <string>${escapeXml(app.updates.url)}</string>`);
1572
+ }
1573
+ stamps.push(` <key>${updatesHost.runtimeVersionInfoPlistKey}</key>
1574
+ <string>${escapeXml(app.updates.runtimeVersion)}</string>`);
1575
+ }
1576
+ if (app.fonts?.length) {
1577
+ stamps.push(` <key>UIAppFonts</key>
1578
+ <array>
1579
+ ${app.fonts.map((font) => ` <string>${escapeXml(fontFileName(font))}</string>`).join("\n")}
1580
+ </array>`);
1581
+ }
1582
+ if (app.userInterfaceStyle !== void 0) {
1583
+ stamps.push(` <key>UIUserInterfaceStyle</key>
1584
+ <string>${app.userInterfaceStyle === "light" ? "Light" : app.userInterfaceStyle === "dark" ? "Dark" : "Automatic"}</string>`);
1585
+ }
1586
+ if (app.orientation !== void 0) {
1587
+ const phoneOrientations = app.orientation === "portrait" ? PORTRAIT_ORIENTATIONS : app.orientation === "landscape" ? LANDSCAPE_ORIENTATIONS : [...PORTRAIT_ORIENTATIONS, ...LANDSCAPE_ORIENTATIONS];
1588
+ const phoneList = /\t<key>UISupportedInterfaceOrientations<\/key>\n\t<array>\n[\s\S]*?\t<\/array>/;
1589
+ if (!phoneList.test(rendered)) {
1590
+ throw new Error(`[vxrn] prebuild template ${relativePath} lost its UISupportedInterfaceOrientations list`);
1591
+ }
1592
+ rendered = rendered.replace(phoneList, ` <key>UISupportedInterfaceOrientations</key>
1593
+ <array>
1594
+ ${phoneOrientations.map((orientation) => ` <string>${orientation}</string>`).join("\n")}
1595
+ </array>`);
1596
+ }
1597
+ for (const [key, value] of Object.entries(app.ios?.infoPlist ?? {})) {
1598
+ if (rendered.includes(`<key>${key}</key>`) || stamps.some((stamp) => stamp.includes(`<key>${key}</key>`))) {
1599
+ throw new Error(`[vxrn] native.app.ios.infoPlist sets ${key}, which the template or native.app already writes: use its field`);
1600
+ }
1601
+ stamps.push(renderPlistEntries({ [key]: value }, " "));
1115
1602
  }
1116
1603
  if (stamps.length) {
1117
1604
  const anchor = " <key>LSRequiresIPhoneOS</key>";
@@ -1120,40 +1607,163 @@ ${schemes.map((scheme) => ` <string>${scheme}</string>`).join("\n")}
1120
1607
  ${anchor}`);
1121
1608
  }
1122
1609
  }
1123
- if (platform === "android" && relativePath === "app/src/main/AndroidManifest.xml" && app.imagePicker?.camera !== void 0) {
1610
+ if (platform === "android" && relativePath === "app/src/main/AndroidManifest.xml" && (app.imagePicker?.camera !== void 0 || app.speech !== void 0)) {
1124
1611
  const anchor = "<uses-permission android:name=\"android.permission.INTERNET\" />";
1125
1612
  if (!rendered.includes(anchor)) {
1126
- throw new Error("[vxrn] cannot stamp the camera permission: expected the INTERNET permission in app/src/main/AndroidManifest.xml");
1613
+ throw new Error("[vxrn] cannot stamp the camera or microphone permission: expected the INTERNET permission in app/src/main/AndroidManifest.xml");
1614
+ }
1615
+ const stamps = [];
1616
+ if (app.imagePicker?.camera !== void 0) {
1617
+ stamps.push(" <uses-permission android:name=\"android.permission.CAMERA\" />");
1618
+ }
1619
+ if (app.speech !== void 0) {
1620
+ stamps.push(" <uses-permission android:name=\"android.permission.RECORD_AUDIO\" />", " <queries>\n <intent>\n <action android:name=\"android.speech.RecognitionService\" />\n </intent>\n </queries>");
1127
1621
  }
1128
1622
  rendered = rendered.replace(anchor, `${anchor}
1129
- <uses-permission android:name="android.permission.CAMERA" />`);
1623
+ ${stamps.join("\n")}`);
1624
+ }
1625
+ if (platform === "android" && relativePath === "app/src/main/AndroidManifest.xml" && app.location !== void 0) {
1626
+ const anchor = "<uses-permission android:name=\"android.permission.INTERNET\" />";
1627
+ if (!rendered.includes(anchor)) {
1628
+ throw new Error("[vxrn] cannot stamp location permissions: expected the INTERNET permission in app/src/main/AndroidManifest.xml");
1629
+ }
1630
+ const stamps = [" <uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\" />", " <uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\" />"];
1631
+ if (app.location.background) {
1632
+ stamps.push(" <uses-permission android:name=\"android.permission.ACCESS_BACKGROUND_LOCATION\" />");
1633
+ }
1634
+ rendered = rendered.replace(anchor, `${anchor}
1635
+ ${stamps.join("\n")}`);
1636
+ }
1637
+ if (platform === "android" && relativePath === "app/src/main/AndroidManifest.xml" && app.pedometer !== void 0) {
1638
+ const anchor = "<uses-permission android:name=\"android.permission.INTERNET\" />";
1639
+ if (!rendered.includes(anchor)) {
1640
+ throw new Error("[vxrn] cannot stamp the pedometer permission: expected the INTERNET permission in app/src/main/AndroidManifest.xml");
1641
+ }
1642
+ rendered = rendered.replace(anchor, `${anchor}
1643
+ <uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />`);
1130
1644
  }
1131
1645
  if (platform === "android" && relativePath === "app/src/main/AndroidManifest.xml" && app.notifications !== void 0) {
1132
1646
  const anchor = "<uses-permission android:name=\"android.permission.INTERNET\" />";
1133
1647
  if (!rendered.includes(anchor)) {
1134
1648
  throw new Error("[vxrn] cannot stamp notification permissions: expected the INTERNET permission in app/src/main/AndroidManifest.xml");
1135
1649
  }
1136
- rendered = rendered.replace(anchor, `${anchor}
1137
- <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
1138
- <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />`);
1139
- if (!rendered.includes("android.permission.POST_NOTIFICATIONS")) {
1650
+ rendered = rendered.replace(anchor, [anchor, ...notificationsHost.androidPermissions.map((permission) => ` <uses-permission android:name="${permission}" />`)].join("\n"));
1651
+ if (!rendered.includes(notificationsHost.androidPermissions[0])) {
1140
1652
  throw new Error("[vxrn] failed to stamp notification permissions into app manifest");
1141
1653
  }
1142
- rendered = rendered.replace(" </activity>\n </application>", " </activity>\n <receiver android:name=\"dev.onejs.onenative.OneNativeNotificationsReceiver\" android:exported=\"false\">\n <intent-filter>\n <action android:name=\"android.intent.action.BOOT_COMPLETED\" />\n </intent-filter>\n </receiver>\n </application>");
1143
- if (!rendered.includes("OneNativeNotificationsReceiver")) {
1654
+ rendered = rendered.replace(" </activity>\n </application>", ` </activity>
1655
+ <receiver android:name="${notificationsHost.receiver}" android:exported="false">
1656
+ <intent-filter>
1657
+ <action android:name="${notificationsHost.receiverAction}" />
1658
+ </intent-filter>
1659
+ </receiver>
1660
+ </application>`);
1661
+ if (!rendered.includes(notificationsHost.receiver)) {
1144
1662
  throw new Error("[vxrn] failed to stamp the notification receiver into app manifest");
1145
1663
  }
1146
1664
  }
1147
1665
  if (platform === "android" && relativePath === "app/src/main/AndroidManifest.xml" && app.notifications?.push === true) {
1148
- const anchor = "dev.onejs.onenative.OneNativeNotificationsReceiver";
1666
+ const anchor = notificationsHost.receiver;
1149
1667
  if (!rendered.includes(anchor)) {
1150
1668
  throw new Error("[vxrn] cannot stamp the push service: expected the notification receiver in app/src/main/AndroidManifest.xml");
1151
1669
  }
1152
- rendered = rendered.replace(" </receiver>\n </application>", " </receiver>\n <service android:name=\"dev.onejs.onenative.OneNativePushService\" android:exported=\"false\">\n <intent-filter>\n <action android:name=\"com.google.firebase.MESSAGING_EVENT\" />\n </intent-filter>\n </service>\n </application>");
1153
- if (!rendered.includes("OneNativePushService")) {
1670
+ rendered = rendered.replace(" </receiver>\n </application>", ` </receiver>
1671
+ <service android:name="${notificationsHost.pushService}" android:exported="false">
1672
+ <intent-filter>
1673
+ <action android:name="${notificationsHost.pushServiceAction}" />
1674
+ </intent-filter>
1675
+ </service>
1676
+ </application>`);
1677
+ if (!rendered.includes(notificationsHost.pushService)) {
1154
1678
  throw new Error("[vxrn] failed to stamp the push service into app manifest");
1155
1679
  }
1156
1680
  }
1681
+ if (platform === "android" && relativePath === "app/src/main/AndroidManifest.xml" && (app.audio !== void 0 || app.photoLibrary !== void 0 || app.contacts !== void 0 || app.calendar !== void 0 || app.ios?.backgroundTasks !== void 0)) {
1682
+ const anchor = "<uses-permission android:name=\"android.permission.INTERNET\" />";
1683
+ if (!rendered.includes(anchor)) {
1684
+ throw new Error("[vxrn] cannot stamp media permissions: expected the INTERNET permission in app/src/main/AndroidManifest.xml");
1685
+ }
1686
+ const stamps = [];
1687
+ if (app.audio?.microphone !== void 0 && !rendered.includes("android.permission.RECORD_AUDIO")) {
1688
+ stamps.push(" <uses-permission android:name=\"android.permission.RECORD_AUDIO\" />");
1689
+ }
1690
+ if (app.photoLibrary?.readWrite !== void 0) {
1691
+ stamps.push(" <uses-permission android:name=\"android.permission.READ_MEDIA_IMAGES\" />", " <uses-permission android:name=\"android.permission.READ_MEDIA_VIDEO\" />", " <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" android:maxSdkVersion=\"32\" />");
1692
+ }
1693
+ if (app.photoLibrary?.addOnly !== void 0) {
1694
+ stamps.push(" <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" android:maxSdkVersion=\"28\" />");
1695
+ }
1696
+ if (app.contacts !== void 0) {
1697
+ stamps.push(" <uses-permission android:name=\"android.permission.READ_CONTACTS\" />", " <uses-permission android:name=\"android.permission.WRITE_CONTACTS\" />");
1698
+ }
1699
+ if (app.calendar?.usage !== void 0) {
1700
+ stamps.push(" <uses-permission android:name=\"android.permission.READ_CALENDAR\" />", " <uses-permission android:name=\"android.permission.WRITE_CALENDAR\" />");
1701
+ }
1702
+ if (app.calendar?.remindersUsage !== void 0) {
1703
+ if (!rendered.includes("android.permission.POST_NOTIFICATIONS")) {
1704
+ stamps.push(" <uses-permission android:name=\"android.permission.POST_NOTIFICATIONS\" />");
1705
+ }
1706
+ if (!rendered.includes("android.permission.RECEIVE_BOOT_COMPLETED")) {
1707
+ stamps.push(" <uses-permission android:name=\"android.permission.RECEIVE_BOOT_COMPLETED\" />");
1708
+ }
1709
+ }
1710
+ if (app.audio?.background === true) {
1711
+ stamps.push(" <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE\" />", " <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK\" />");
1712
+ }
1713
+ if (stamps.length) {
1714
+ rendered = rendered.replace(anchor, `${anchor}
1715
+ ${stamps.join("\n")}`);
1716
+ }
1717
+ const appStamps = [];
1718
+ if (app.calendar?.remindersUsage !== void 0) {
1719
+ appStamps.push(" <meta-data android:name=\"one.calendar.reminders\" android:value=\"true\" />");
1720
+ }
1721
+ if (app.photoLibrary?.addOnly !== void 0) {
1722
+ appStamps.push(" <meta-data android:name=\"one.photoLibrary.addOnly\" android:value=\"true\" />");
1723
+ }
1724
+ if (app.audio?.background === true) {
1725
+ appStamps.push(" <meta-data android:name=\"one.audio.background\" android:value=\"true\" />", " <service android:name=\"com.margelo.nitro.one.OneAudioService\" android:exported=\"false\" android:foregroundServiceType=\"mediaPlayback\" />");
1726
+ }
1727
+ if (app.ios?.backgroundTasks !== void 0) {
1728
+ const refreshTasks = app.ios.backgroundTasks.refresh ?? [];
1729
+ const processingTasks = app.ios.backgroundTasks.processing ?? [];
1730
+ if (refreshTasks.length) {
1731
+ appStamps.push(` <meta-data android:name="one.backgroundTasks.refresh" android:value="${escapeXml(refreshTasks.join(","))}" />`);
1732
+ }
1733
+ if (processingTasks.length) {
1734
+ appStamps.push(` <meta-data android:name="one.backgroundTasks.processing" android:value="${escapeXml(processingTasks.join(","))}" />`);
1735
+ }
1736
+ }
1737
+ if (appStamps.length) {
1738
+ const appAnchor = " </application>";
1739
+ if (!rendered.includes(appAnchor)) {
1740
+ throw new Error("[vxrn] cannot stamp media components: expected </application> in app/src/main/AndroidManifest.xml");
1741
+ }
1742
+ rendered = rendered.replace(appAnchor, `${appStamps.join("\n")}
1743
+ ${appAnchor}`);
1744
+ }
1745
+ }
1746
+ if (platform === "android" && relativePath === "app/src/main/java/com/helloworld/MainApplication.kt" && app.updates !== void 0) {
1747
+ rendered = patchAndroidMainApplicationUpdatesHost(rendered);
1748
+ }
1749
+ if (platform === "android" && relativePath === "app/src/main/AndroidManifest.xml" && app.updates !== void 0) {
1750
+ const anchor = " </application>";
1751
+ if (!rendered.includes(anchor)) {
1752
+ throw new Error("[vxrn] cannot stamp the updates config: expected </application> in app/src/main/AndroidManifest.xml");
1753
+ }
1754
+ const stamps = [];
1755
+ if (app.updates.url !== void 0) {
1756
+ stamps.push(` <meta-data android:name="dev.onejs.updates.url" android:value="${escapeXml(app.updates.url)}" />`);
1757
+ }
1758
+ stamps.push(" <meta-data android:name=\"dev.onejs.updates.runtimeVersion\" android:value=\"@string/one_updates_runtime_version\" />");
1759
+ rendered = rendered.replace(anchor, `${stamps.join("\n")}
1760
+ ${anchor}`);
1761
+ }
1762
+ if (platform === "android" && relativePath === "app/src/main/res/values/strings.xml" && app.updates !== void 0) {
1763
+ const version = app.updates.runtimeVersion.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
1764
+ rendered = rendered.replace("</resources>", ` <string name="one_updates_runtime_version" translatable="false">"${escapeXml(version)}"</string>
1765
+ </resources>`);
1766
+ }
1157
1767
  if (platform === "android" && relativePath === "app/src/main/AndroidManifest.xml" && app.android?.googleMapsApiKey !== void 0) {
1158
1768
  const anchor = " </application>";
1159
1769
  if (!rendered.includes(anchor)) {
@@ -1163,7 +1773,7 @@ ${anchor}`);
1163
1773
  ${anchor}`);
1164
1774
  }
1165
1775
  if (platform === "android" && relativePath === "gradle.properties" && app.notifications?.push === true) {
1166
- const line = "oneNativePush=true";
1776
+ const line = `${notificationsHost.pushGradleProperty}=true`;
1167
1777
  if (!rendered.includes(line)) {
1168
1778
  const trailed = rendered.endsWith("\n") ? rendered : `${rendered}
1169
1779
  `;
@@ -1184,6 +1794,44 @@ ${line}
1184
1794
  `;
1185
1795
  }
1186
1796
  }
1797
+ if (platform === "android" && relativePath === "app/src/main/res/values/styles.xml" && (app.userInterfaceStyle === "light" || app.userInterfaceStyle === "dark")) {
1798
+ const dayNight = "parent=\"Theme.AppCompat.DayNight.NoActionBar\"";
1799
+ if (!rendered.includes(dayNight)) {
1800
+ throw new Error(`[vxrn] cannot lock userInterfaceStyle: expected ${dayNight} in app/src/main/res/values/styles.xml`);
1801
+ }
1802
+ rendered = rendered.replace(dayNight, app.userInterfaceStyle === "light" ? "parent=\"Theme.AppCompat.Light.NoActionBar\"" : "parent=\"Theme.AppCompat.NoActionBar\"");
1803
+ }
1804
+ if (platform === "android" && relativePath === "app/src/main/AndroidManifest.xml" && (app.android?.permissions?.length || app.android?.blockedPermissions?.length)) {
1805
+ const anchor = "<uses-permission android:name=\"android.permission.INTERNET\" />";
1806
+ if (!rendered.includes(anchor)) {
1807
+ throw new Error("[vxrn] cannot stamp permissions: expected the INTERNET permission in app/src/main/AndroidManifest.xml");
1808
+ }
1809
+ const permissionName = (name) => name.includes(".") ? name : `android.permission.${name}`;
1810
+ const lines = [...(app.android?.permissions ?? []).map((name) => ` <uses-permission android:name="${permissionName(name)}" />`), ...(app.android?.blockedPermissions ?? []).map((name) => ` <uses-permission android:name="${permissionName(name)}" tools:node="remove" />`)];
1811
+ rendered = rendered.replace(anchor, `${anchor}
1812
+ ${lines.join("\n")}`);
1813
+ if (app.android?.blockedPermissions?.length && !rendered.includes("xmlns:tools=")) {
1814
+ rendered = rendered.replace("xmlns:android=\"http://schemas.android.com/apk/res/android\"", "xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:tools=\"http://schemas.android.com/tools\"");
1815
+ }
1816
+ }
1817
+ if (platform === "android" && relativePath === "app/src/main/AndroidManifest.xml" && app.android?.appLinks?.length) {
1818
+ rendered = rendered.replace(" </activity>", ` <intent-filter android:autoVerify="true">
1819
+ <action android:name="android.intent.action.VIEW" />
1820
+ <category android:name="android.intent.category.DEFAULT" />
1821
+ <category android:name="android.intent.category.BROWSABLE" />
1822
+ <data android:scheme="https" />
1823
+ ${app.android.appLinks.map((link) => ` <data android:host="${link.host}"${link.pathPrefix ? ` android:pathPrefix="${link.pathPrefix}"` : ""} />`).join("\n")}
1824
+ </intent-filter>
1825
+ </activity>`);
1826
+ }
1827
+ if (platform === "android" && relativePath === "app/src/main/AndroidManifest.xml" && app.orientation !== void 0) {
1828
+ const anchor = "android:name=\".MainActivity\"";
1829
+ if (!rendered.includes(anchor)) {
1830
+ throw new Error("[vxrn] cannot stamp the orientation: expected .MainActivity in app/src/main/AndroidManifest.xml");
1831
+ }
1832
+ rendered = rendered.replace(anchor, `${anchor}
1833
+ android:screenOrientation="${app.orientation === "default" ? "unspecified" : app.orientation}"`);
1834
+ }
1187
1835
  if (platform === "android" && relativePath === "app/src/main/AndroidManifest.xml" && app.pictureInPicture) {
1188
1836
  const anchor = "android:name=\".MainActivity\"";
1189
1837
  if (!rendered.includes(anchor)) {
@@ -1212,20 +1860,97 @@ ${schemes.map((scheme) => ` <data android:scheme="${scheme}" />`).joi
1212
1860
  rendered = rendered.replace(/CURRENT_PROJECT_VERSION = [^;]+;/g, `CURRENT_PROJECT_VERSION = ${app.ios.buildNumber};`);
1213
1861
  }
1214
1862
  }
1863
+ if (platform === "ios" && relativePath.endsWith(".xcodeproj/project.pbxproj") && app.ios?.alternateIcons !== void 0) {
1864
+ const anchor = "ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;";
1865
+ if (!rendered.includes(anchor)) {
1866
+ throw new Error("[vxrn] prebuild template lost its primary app icon build setting");
1867
+ }
1868
+ const names = Object.keys(app.ios.alternateIcons).sort().join(" ");
1869
+ rendered = rendered.replaceAll(anchor, `${anchor}
1870
+ ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "${names}";`);
1871
+ }
1215
1872
  if (platform === "ios" && relativePath.endsWith("/Info.plist")) {
1873
+ const usage = /* @__PURE__ */ new Map();
1216
1874
  if (app.imagePicker?.camera !== void 0) {
1875
+ usage.set("NSCameraUsageDescription", app.imagePicker.camera);
1876
+ }
1877
+ if (app.imagePicker?.microphone !== void 0) {
1878
+ usage.set("NSMicrophoneUsageDescription", app.imagePicker.microphone);
1879
+ }
1880
+ if (app.photoLibrary !== void 0) {
1881
+ if (app.photoLibrary.addOnly !== void 0) {
1882
+ usage.set("NSPhotoLibraryAddUsageDescription", app.photoLibrary.addOnly);
1883
+ }
1884
+ if (app.photoLibrary.readWrite !== void 0) {
1885
+ usage.set("NSPhotoLibraryUsageDescription", app.photoLibrary.readWrite);
1886
+ }
1887
+ }
1888
+ if (app.contacts !== void 0) {
1889
+ usage.set("NSContactsUsageDescription", app.contacts.usage);
1890
+ }
1891
+ if (app.calendar !== void 0) {
1892
+ if (app.calendar.usage !== void 0) {
1893
+ usage.set("NSCalendarsFullAccessUsageDescription", app.calendar.usage);
1894
+ }
1895
+ if (app.calendar.remindersUsage !== void 0) {
1896
+ usage.set("NSRemindersFullAccessUsageDescription", app.calendar.remindersUsage);
1897
+ }
1898
+ }
1899
+ if (app.location !== void 0) {
1900
+ usage.set("NSLocationWhenInUseUsageDescription", app.location.whenInUse);
1901
+ if (app.location.alwaysAndWhenInUse !== void 0) {
1902
+ usage.set("NSLocationAlwaysAndWhenInUseUsageDescription", app.location.alwaysAndWhenInUse);
1903
+ }
1904
+ }
1905
+ if (app.pedometer !== void 0) {
1906
+ usage.set("NSMotionUsageDescription", app.pedometer.usage);
1907
+ }
1908
+ if (app.ios?.faceIdUsageDescription !== void 0) {
1909
+ usage.set("NSFaceIDUsageDescription", app.ios.faceIdUsageDescription);
1910
+ }
1911
+ if (app.speech !== void 0) {
1912
+ usage.set("NSSpeechRecognitionUsageDescription", app.speech.recognition);
1913
+ usage.set("NSMicrophoneUsageDescription", app.speech.microphone);
1914
+ }
1915
+ if (app.audio?.microphone !== void 0) {
1916
+ usage.set("NSMicrophoneUsageDescription", app.audio.microphone);
1917
+ }
1918
+ const additions = [];
1919
+ for (const [key, text] of usage) {
1920
+ const existing = new RegExp(`(<key>${key}</key>\\s*<string>)[^<]*(</string>)`);
1921
+ if (existing.test(rendered)) {
1922
+ rendered = rendered.replace(existing, (_match, before, after) => `${before}${escapeXml(text)}${after}`);
1923
+ } else {
1924
+ additions.push([key, text]);
1925
+ }
1926
+ }
1927
+ if (additions.length) {
1217
1928
  const anchor = " <key>LSRequiresIPhoneOS</key>";
1218
1929
  if (!rendered.includes(anchor)) {
1219
- throw new Error("[vxrn] cannot stamp NSCameraUsageDescription: expected LSRequiresIPhoneOS in Info.plist");
1930
+ throw new Error(`[vxrn] cannot stamp ${additions[0][0]}: expected LSRequiresIPhoneOS in Info.plist`);
1220
1931
  }
1221
- rendered = rendered.replace(anchor, ` <key>NSCameraUsageDescription</key>
1222
- <string>${escapeXml(app.imagePicker.camera)}</string>
1223
- ${anchor}`);
1932
+ rendered = rendered.replace(anchor, `${additions.map(([key, text]) => ` <key>${key}</key>
1933
+ <string>${escapeXml(text)}</string>
1934
+ `).join("")}${anchor}`);
1224
1935
  }
1936
+ rendered = rendered.replace(/\t<key>NS\w+UsageDescription<\/key>\s*<string><\/string>\n/g, "");
1225
1937
  rendered = patchIosInfoPlistSceneManifest(rendered);
1226
1938
  }
1227
1939
  if (platform === "ios" && relativePath.endsWith("/AppDelegate.swift")) {
1228
- rendered = patchIosAppDelegateSceneLifecycle(rendered, appName);
1940
+ rendered = patchIosAppDelegateSceneLifecycle(rendered, appName, !!app.ios?.backgroundTasks, !!app.ios?.appIntents);
1941
+ rendered = nativeProjectPatches.holdLaunchScreenOverRootView(rendered);
1942
+ const bundleURLProvider = "RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: \"index\")";
1943
+ if (!rendered.includes(bundleURLProvider)) {
1944
+ throw new Error("[vxrn] cannot request hermes dev bytecode: expected the template bundle URL provider");
1945
+ }
1946
+ rendered = rendered.replace(bundleURLProvider, `guard let bundleURL = RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") else { return nil }
1947
+ if bundleURL.isFileURL { return bundleURL }
1948
+ var components = URLComponents(url: bundleURL, resolvingAgainstBaseURL: false)!
1949
+ components.queryItems!.append(URLQueryItem(name: "bytecode", value: "hermes"))
1950
+ return components.url`);
1951
+ if (app.updates !== void 0) {
1952
+ rendered = nativeProjectPatches.pointReleaseBundleURLAtOneUpdates(rendered);
1953
+ }
1229
1954
  }
1230
1955
  if (platform === "ios" && app.ios?.deploymentTarget) {
1231
1956
  rendered = rendered.replace("platform :ios, min_ios_version_supported", `platform :ios, '${app.ios.deploymentTarget}'`).replace(/IPHONEOS_DEPLOYMENT_TARGET = \d+(?:\.\d+)?;/g, `IPHONEOS_DEPLOYMENT_TARGET = ${app.ios.deploymentTarget};`);
@@ -1242,12 +1967,17 @@ end`);
1242
1967
  }
1243
1968
  }
1244
1969
  if (platform === "ios" && relativePath.endsWith(".xcodeproj/project.pbxproj")) {
1245
- rendered = patchIosBundlePhase(rendered);
1970
+ rendered = patchIosBundlePhase(rendered, app.updates?.runtimeVersion);
1246
1971
  rendered = patchIosPbxprojSceneDelegate(rendered, appName);
1247
- if (app.notifications?.push === true && !app.ios?.widgets) {
1248
- rendered = patchIosPbxprojPushEntitlements(rendered, appName);
1972
+ if (app.ios?.appIntents) rendered = patchIosPbxprojAppIntents(rendered, appName);
1973
+ rendered = patchIosPbxprojOneBridgingHeader(rendered, appName);
1974
+ if (Object.keys(appEntitlements(app)).length && !app.ios?.widgets) {
1975
+ rendered = patchIosPbxprojAppEntitlements(rendered, appName);
1249
1976
  }
1250
1977
  if (app.ios?.widgets) rendered = patchIosPbxprojWidgets(rendered, app);
1978
+ if (iosResourceFiles(app).length) {
1979
+ rendered = patchIosPbxprojResources(rendered, appName, iosResourceFiles(app));
1980
+ }
1251
1981
  }
1252
1982
  if (platform === "ios" && relativePath === "Podfile") {
1253
1983
  if (app.ios?.ccache) rendered = `ENV['USE_CCACHE'] ||= '1'
@@ -1260,9 +1990,15 @@ use_frameworks! :linkage => :${app.ios.useFrameworks}`);
1260
1990
  rendered = nativeProjectPatches.injectReactNativeScreensGammaIntoPodfile(rendered);
1261
1991
  }
1262
1992
  rendered = nativeProjectPatches.injectFmtCxx17FixIntoPodfile(rendered);
1993
+ rendered = nativeProjectPatches.injectWebGPUHeaderPathsIntoPodfile(rendered);
1263
1994
  rendered = nativeProjectPatches.injectOneSwiftPackagesIntoPodfile(rendered);
1264
1995
  if (nitroWebImage) rendered = nativeProjectPatches.injectNitroWebImageModularHeaderIntoPodfile(rendered);
1265
1996
  rendered = nativeProjectPatches.injectHermesMinificationPatchIntoPodfile(rendered);
1997
+ const nativeModulesCall = "config = use_native_modules!";
1998
+ if (rendered.split(nativeModulesCall).length !== 2) {
1999
+ throw new Error("[vxrn] expected one iOS native module discovery call");
2000
+ }
2001
+ rendered = rendered.replace(nativeModulesCall, `config = use_native_modules!(['node', '-e', "process.argv=['', '', 'config'];require(require('module').createRequire(require.resolve('vxrn/package.json')).resolve('@react-native-community/cli')).run()"])`);
1266
2002
  if (!rendered.includes("[vxrn/one] fmt c++17 fix") || !rendered.includes("[vxrn/one] minify iOS Hermes Release bundle input")) {
1267
2003
  throw new Error("[vxrn] failed to apply required iOS Podfile patches");
1268
2004
  }
@@ -1273,6 +2009,9 @@ use_frameworks! :linkage => :${app.ios.useFrameworks}`);
1273
2009
  if (!rendered.includes("entryFile = file(\"../../package.json\")") || !rendered.includes("[vxrn/one] ensure patches are applied")) {
1274
2010
  throw new Error("[vxrn] failed to apply required Android Gradle patches");
1275
2011
  }
2012
+ if (app.updates !== void 0) {
2013
+ rendered = patchAndroidBuildGradleUpdatesManifest(rendered, app.updates.runtimeVersion);
2014
+ }
1276
2015
  if (app.version !== void 0) {
1277
2016
  rendered = rendered.replace(/versionName "[^"]*"/g, `versionName "${app.version}"`);
1278
2017
  }
@@ -1286,9 +2025,57 @@ use_frameworks! :linkage => :${app.ios.useFrameworks}`);
1286
2025
  throw new Error("[vxrn] expected two React Native Gradle plugin paths");
1287
2026
  }
1288
2027
  rendered = rendered.replace(hardcodedGradlePlugin, `includeBuild(new File(["node", "--print", "require('module').createRequire(require.resolve('react-native/package.json')).resolve('@react-native/gradle-plugin/package.json')"].execute(null, settingsDir).text.trim()).parentFile.canonicalPath)`);
2028
+ const autolinkCommand = "ex.autolinkLibrariesFromCommand()";
2029
+ if (rendered.split(autolinkCommand).length !== 2) {
2030
+ throw new Error("[vxrn] expected one React Native autolinking command");
2031
+ }
2032
+ rendered = rendered.replace(autolinkCommand, `ex.autolinkLibrariesFromCommand(["node", ["node", "--print", "require('module').createRequire(require.resolve('vxrn/package.json')).resolve('@react-native-community/cli/build/bin.js')"].execute(null, settingsDir).text.trim(), "config"])`);
1289
2033
  }
1290
- if (platform === "android" && app.android?.minSdk !== void 0) {
1291
- rendered = rendered.replace(/minSdkVersion = \d+/g, `minSdkVersion = ${app.android.minSdk}`);
2034
+ if (platform === "android" && relativePath === "app/build.gradle" && app.android?.minify) {
2035
+ const minify = "def enableProguardInReleaseBuilds = false";
2036
+ const minifyEnabled = "minifyEnabled enableProguardInReleaseBuilds";
2037
+ if (!rendered.includes(minify) || !rendered.includes(minifyEnabled)) {
2038
+ throw new Error("[vxrn] prebuild template app/build.gradle lost its proguard switch");
2039
+ }
2040
+ rendered = rendered.replace(minify, "def enableProguardInReleaseBuilds = true");
2041
+ if (app.android.shrinkResources) {
2042
+ rendered = rendered.replace(minifyEnabled, `${minifyEnabled}
2043
+ shrinkResources true`);
2044
+ }
2045
+ }
2046
+ if (platform === "android" && app.android?.googleServicesFile) {
2047
+ const edits = [[
2048
+ "build.gradle",
2049
+ "classpath(\"org.jetbrains.kotlin:kotlin-gradle-plugin\")",
2050
+ ` classpath("com.google.gms:google-services:${GOOGLE_SERVICES_VERSION}")`
2051
+ ], [
2052
+ "app/build.gradle",
2053
+ "apply plugin: \"com.facebook.react\"",
2054
+ "apply plugin: \"com.google.gms.google-services\""
2055
+ ]];
2056
+ for (const [file, anchor, insertion] of edits) {
2057
+ if (relativePath !== file) continue;
2058
+ const next = insertAfterLine(rendered, anchor, insertion);
2059
+ if (next === rendered) {
2060
+ throw new Error(`[vxrn] prebuild template ${file} lost its ${anchor} anchor`);
2061
+ }
2062
+ rendered = next;
2063
+ }
2064
+ }
2065
+ if (platform === "android" && relativePath === "app/proguard-rules.pro" && app.android?.proguardRules) {
2066
+ rendered = `${rendered.trimEnd()}
2067
+
2068
+ ${app.android.proguardRules.trim()}
2069
+ `;
2070
+ }
2071
+ for (const [field, setting] of [
2072
+ ["minSdk", "minSdkVersion"],
2073
+ ["targetSdk", "targetSdkVersion"],
2074
+ ["compileSdk", "compileSdkVersion"]
2075
+ ]) {
2076
+ const value = platform === "android" ? app.android?.[field] : void 0;
2077
+ if (value === void 0) continue;
2078
+ rendered = rendered.replace(new RegExp(`${setting} = \\d+`, "g"), `${setting} = ${value}`);
1292
2079
  }
1293
2080
  }
1294
2081
  return {
@@ -1300,7 +2087,7 @@ const generateForPlatform = async (root, platform, app, outDir = path.resolve(ro
1300
2087
  validatePrebuildApp(app, platform);
1301
2088
  const dest = outDir;
1302
2089
  const require2 = module.createRequire(root + "/");
1303
- const importPath = require2.resolve("@react-native-community/cli/build/tools/walk.js", { paths: [root] });
2090
+ const importPath = requireVxrn.resolve("@react-native-community/cli/build/tools/walk.js");
1304
2091
  const src = path.join(path.dirname(require2.resolve("@react-native-community/template/template/package.json", { paths: [root] })), platform);
1305
2092
  const walkModule = await import(pathToFileURL(importPath).href);
1306
2093
  const walkFn = walkModule?.default?.default ?? walkModule?.default ?? walkModule;
@@ -1345,48 +2132,184 @@ const generateForPlatform = async (root, platform, app, outDir = path.resolve(ro
1345
2132
  platform,
1346
2133
  app
1347
2134
  });
2135
+ if (platform === "android") await generateAdaptiveIcon({
2136
+ root,
2137
+ dest,
2138
+ app
2139
+ });
1348
2140
  await generateSplashScreen({
1349
2141
  root,
1350
2142
  dest,
1351
2143
  platform,
1352
2144
  app
1353
2145
  });
2146
+ if (platform === "ios") generateAccentColor({
2147
+ dest,
2148
+ app
2149
+ });
2150
+ copyAppResources({
2151
+ root,
2152
+ dest,
2153
+ platform,
2154
+ app
2155
+ });
1354
2156
  generateSceneDelegate({
1355
2157
  dest,
1356
2158
  platform,
1357
2159
  app
1358
2160
  });
2161
+ generateIosAppIntents(dest, platform, app);
2162
+ if (platform === "ios") generateOneBridgingHeader(dest, app);
1359
2163
  if (platform === "ios") generateIosWidgets(dest, app);
1360
- if (platform === "ios") generateSwiftPackages({
2164
+ if (platform === "ios") await generateSwiftPackages({
2165
+ root,
2166
+ dest
2167
+ });
2168
+ if (platform === "android") await generateKotlinSources({
1361
2169
  root,
1362
2170
  dest
1363
2171
  });
1364
2172
  };
1365
- function generateSwiftPackages({ root, dest }) {
2173
+ const KOTLIN_IMPORT_ARTIFACTS = [{
2174
+ importPrefix: "androidx.compose.material.icons",
2175
+ artifact: "androidx.compose.material:material-icons-core",
2176
+ versionKey: "materialIconsVersion",
2177
+ version: "1.7.6"
2178
+ }];
2179
+ function kotlinSourceImports(dir) {
2180
+ const imports = /* @__PURE__ */ new Set();
2181
+ if (!FSExtra.existsSync(dir)) return imports;
2182
+ const visit = (entry) => {
2183
+ if (FSExtra.statSync(entry).isDirectory()) {
2184
+ for (const child of FSExtra.readdirSync(entry)) visit(path.join(entry, child));
2185
+ return;
2186
+ }
2187
+ if (!entry.endsWith(".kt")) return;
2188
+ for (const line of FSExtra.readFileSync(entry, "utf8").split("\n")) {
2189
+ const match = /^\s*import\s+([A-Za-z0-9_.]+)/.exec(line);
2190
+ if (match) imports.add(match[1]);
2191
+ }
2192
+ };
2193
+ visit(dir);
2194
+ return imports;
2195
+ }
2196
+ function enableAppComposeIntegration(dest) {
2197
+ const rootBuildGradlePath = path.join(dest, "build.gradle");
2198
+ if (FSExtra.existsSync(rootBuildGradlePath)) {
2199
+ let content = FSExtra.readFileSync(rootBuildGradlePath, "utf8");
2200
+ const composePluginClasspath = " classpath(\"org.jetbrains.kotlin.plugin.compose:org.jetbrains.kotlin.plugin.compose.gradle.plugin:$kotlinVersion\")";
2201
+ if (!content.includes("org.jetbrains.kotlin.plugin.compose")) {
2202
+ const anchor = content.includes("classpath(\"org.jetbrains.kotlin:kotlin-gradle-plugin\")") ? "classpath(\"org.jetbrains.kotlin:kotlin-gradle-plugin\")" : "classpath \"org.jetbrains.kotlin:kotlin-gradle-plugin\"";
2203
+ content = insertAfterLine(content, anchor, composePluginClasspath);
2204
+ FSExtra.writeFileSync(rootBuildGradlePath, content, "utf8");
2205
+ }
2206
+ }
2207
+ const appBuildGradlePath = path.join(dest, "app/build.gradle");
2208
+ if (FSExtra.existsSync(appBuildGradlePath)) {
2209
+ let content = FSExtra.readFileSync(appBuildGradlePath, "utf8");
2210
+ if (!content.includes("org.jetbrains.kotlin.plugin.compose")) {
2211
+ const pluginAnchor = content.includes("apply plugin: \"org.jetbrains.kotlin.android\"") ? "apply plugin: \"org.jetbrains.kotlin.android\"" : "apply plugin: 'org.jetbrains.kotlin.android'";
2212
+ content = insertAfterLine(content, pluginAnchor, "apply plugin: \"org.jetbrains.kotlin.plugin.compose\"");
2213
+ }
2214
+ if (!content.includes("compose true")) {
2215
+ content = insertAfterLine(content, "android {", " buildFeatures {\n compose true\n }");
2216
+ }
2217
+ if (!content.includes("androidx.compose.ui:ui")) {
2218
+ const composeDeps = [
2219
+ " def composeUiVersion = rootProject.ext.has(\"composeUiVersion\") ? rootProject.ext.get(\"composeUiVersion\") : \"1.11.4\"",
2220
+ " def material3Version = rootProject.ext.has(\"material3Version\") ? rootProject.ext.get(\"material3Version\") : \"1.5.0-alpha17\"",
2221
+ " implementation \"androidx.compose.ui:ui:$composeUiVersion\"",
2222
+ " implementation \"androidx.compose.foundation:foundation:$composeUiVersion\"",
2223
+ " implementation \"androidx.compose.material3:material3:$material3Version\"",
2224
+ " def navigationComposeVersion = rootProject.ext.has(\"navigationComposeVersion\") ? rootProject.ext.get(\"navigationComposeVersion\") : \"2.10.2\"",
2225
+ " implementation \"androidx.navigation:navigation-compose:$navigationComposeVersion\""
2226
+ ].join("\n");
2227
+ const depsAnchor = content.includes("implementation(\"com.facebook.react:react-android\")") ? "implementation(\"com.facebook.react:react-android\")" : "implementation \"com.facebook.react:react-android\"";
2228
+ content = insertAfterLine(content, depsAnchor, composeDeps);
2229
+ }
2230
+ const imports = kotlinSourceImports(path.join(dest, "app/src/main/java/one/source"));
2231
+ for (const rule of KOTLIN_IMPORT_ARTIFACTS) {
2232
+ const needed = [...imports].some((name) => name === rule.importPrefix || name.startsWith(`${rule.importPrefix}.`));
2233
+ if (!needed || content.includes(rule.artifact)) continue;
2234
+ const reactAnchor = content.includes("implementation(\"com.facebook.react:react-android\")") ? "implementation(\"com.facebook.react:react-android\")" : "implementation \"com.facebook.react:react-android\"";
2235
+ const anchor = content.includes("androidx.compose.material3:material3") ? "androidx.compose.material3:material3" : reactAnchor;
2236
+ content = insertAfterLine(content, anchor, [` def ${rule.versionKey} = rootProject.ext.has("${rule.versionKey}") ? rootProject.ext.get("${rule.versionKey}") : "${rule.version}"`, ` implementation "${rule.artifact}:$${rule.versionKey}"`].join("\n"));
2237
+ }
2238
+ FSExtra.writeFileSync(appBuildGradlePath, content, "utf8");
2239
+ }
2240
+ }
2241
+ async function generateKotlinSources({ root, dest }) {
1366
2242
  const skip = /* @__PURE__ */ new Set([
1367
2243
  "node_modules",
1368
2244
  "ios",
1369
2245
  "android",
1370
2246
  "dist",
1371
2247
  "types",
1372
- "build"
2248
+ "build",
2249
+ "tests",
2250
+ "__tests__",
2251
+ "scripts",
2252
+ "evidence"
1373
2253
  ]);
1374
- const packageDirs = [];
1375
- const walk = (dir) => {
1376
- const entries = FSExtra.readdirSync(dir, { withFileTypes: true });
1377
- if (entries.some((entry) => entry.isFile() && entry.name === "Package.swift")) {
1378
- packageDirs.push(dir);
1379
- return;
1380
- }
1381
- for (const entry of entries) {
1382
- if (!entry.isDirectory() || entry.name.startsWith(".") || skip.has(entry.name)) continue;
1383
- walk(path.join(dir, entry.name));
2254
+ const sources = [];
2255
+ const manifest = FSExtra.existsSync(path.join(root, "package.json")) ? FSExtra.readJsonSync(path.join(root, "package.json")) : {};
2256
+ const workspacePatterns = Array.isArray(manifest.workspaces) ? manifest.workspaces : manifest.workspaces?.packages ?? [];
2257
+ const { default: glob } = await import("fast-glob");
2258
+ const workspaceRoots = new Set(glob.sync(workspacePatterns, {
2259
+ cwd: root,
2260
+ absolute: true,
2261
+ onlyDirectories: true,
2262
+ ignore: [
2263
+ "**/node_modules/**",
2264
+ "**/ios/**",
2265
+ "**/android/**"
2266
+ ]
2267
+ }).map((directory) => path.resolve(directory)));
2268
+ const collect = (dir) => {
2269
+ if (dir !== root && FSExtra.existsSync(path.join(dir, "package.json")) && !workspaceRoots.has(dir)) return;
2270
+ for (const entry of FSExtra.readdirSync(dir, { withFileTypes: true })) {
2271
+ if (entry.name.startsWith(".") || skip.has(entry.name)) continue;
2272
+ const source = path.join(dir, entry.name);
2273
+ if (entry.isDirectory()) collect(source);
2274
+ else if (entry.isFile() && entry.name.endsWith(".kt")) sources.push(source);
1384
2275
  }
1385
2276
  };
1386
- walk(root);
1387
- for (const packageDir of packageDirs) {
1388
- const id = swiftPackageId(packageDir);
2277
+ collect(root);
2278
+ const output = path.join(dest, "app/src/main/java/one/source");
2279
+ FSExtra.removeSync(output);
2280
+ if (sources.length === 0) return;
2281
+ const { kotlinSourceId, renderKotlinSourceGlue, writeNativeSourceDeclaration } = await import("../utils/nativeSourceContract.mjs");
2282
+ let hasViews = false;
2283
+ for (const source of sources) {
2284
+ const id = kotlinSourceId(root, source);
2285
+ const target = path.join(output, id);
2286
+ FSExtra.mkdirSync(target, { recursive: true });
2287
+ FSExtra.copyFileSync(source, path.join(target, path.basename(source)));
2288
+ const contract = writeNativeSourceDeclaration(source);
2289
+ if (contract.views.length > 0) hasViews = true;
2290
+ if (contract.modules.length > 0 || contract.views.length > 0) {
2291
+ FSExtra.writeFileSync(path.join(target, `OneNativeSource_${id}.kt`), renderKotlinSourceGlue(id, contract).source);
2292
+ }
2293
+ }
2294
+ if (hasViews) {
2295
+ enableAppComposeIntegration(dest);
2296
+ }
2297
+ }
2298
+ async function generateSwiftPackages({ root, dest }) {
2299
+ const skip = /* @__PURE__ */ new Set([
2300
+ "node_modules",
2301
+ "ios",
2302
+ "android",
2303
+ "dist",
2304
+ "types",
2305
+ "build"
2306
+ ]);
2307
+ const packages = swiftPackageDirectories(root);
2308
+ if (packages.size === 0) return;
2309
+ const { swiftPodManifest, writeSwiftPackageArtifacts } = await import("../utils/nativeSourceContract.mjs");
2310
+ for (const [id, packageDir] of packages) {
1389
2311
  const podDir = path.join(dest, "OneSwiftPackages", id);
2312
+ const manifest = swiftPodManifest(path.join(packageDir, "Package.swift"), FSExtra.readFileSync(path.join(packageDir, "Package.swift"), "utf8"));
1390
2313
  const sources = [];
1391
2314
  const collect = (dir) => {
1392
2315
  for (const entry of FSExtra.readdirSync(dir, { withFileTypes: true })) {
@@ -1397,11 +2320,20 @@ function generateSwiftPackages({ root, dest }) {
1397
2320
  }
1398
2321
  };
1399
2322
  collect(packageDir);
2323
+ const artifacts = writeSwiftPackageArtifacts(packageDir);
2324
+ const contracts = artifacts.contracts;
2325
+ const hasView = contracts.some((contract) => contract.defaultView);
2326
+ if (contracts.filter((contract) => contract.defaultView).length > 1) {
2327
+ throw new Error(`[vxrn] swift package ${packageDir} has more than one @main type`);
2328
+ }
1400
2329
  for (const source of sources) {
1401
2330
  const link = path.join(podDir, "Sources", path.relative(packageDir, source));
1402
2331
  FSExtra.mkdirSync(path.dirname(link), { recursive: true });
1403
2332
  FSExtra.symlinkSync(FSExtra.realpathSync(source), link);
1404
2333
  }
2334
+ if (artifacts.glue) {
2335
+ FSExtra.writeFileSync(path.join(podDir, "Sources", "OneNativeSource.generated.swift"), artifacts.glue);
2336
+ }
1405
2337
  FSExtra.writeFileSync(path.join(podDir, `${id}.podspec`), `Pod::Spec.new do |s|
1406
2338
  s.name = '${id}'
1407
2339
  s.version = '0.0.0'
@@ -1410,15 +2342,15 @@ function generateSwiftPackages({ root, dest }) {
1410
2342
  s.license = 'MIT'
1411
2343
  s.author = 'one'
1412
2344
  s.source = { :path => '.' }
1413
- s.swift_version = '6.0'
2345
+ s.swift_version = '${manifest.languageMode}.0'
1414
2346
  s.source_files = 'Sources/**/*.swift', 'Register.m'
1415
- s.dependency 'VxrnNative'
2347
+ s.dependency 'One'
1416
2348
  s.pod_target_xcconfig = {
1417
- 'OTHER_SWIFT_FLAGS' => '$(inherited) -cxx-interoperability-mode=default -Xcc -std=c++20 -Xfrontend -import-module -Xfrontend VxrnNative -Xfrontend -entry-point-function-name -Xfrontend ${id}_main',
2349
+ 'OTHER_SWIFT_FLAGS' => '$(inherited) -cxx-interoperability-mode=default -Xcc -std=c++20 -Xfrontend -import-module -Xfrontend One${manifest.mainActorIsolation ? " -default-isolation MainActor" : ""}${hasView ? ` -Xfrontend -entry-point-function-name -Xfrontend ${id}_main` : ""}',
1418
2350
  }
1419
2351
  end
1420
2352
  `);
1421
- FSExtra.writeFileSync(path.join(podDir, "Register.m"), `#import <Foundation/Foundation.h>
2353
+ FSExtra.writeFileSync(path.join(podDir, "Register.m"), hasView ? `#import <Foundation/Foundation.h>
1422
2354
 
1423
2355
  extern int ${id}_main(int argc, char **argv);
1424
2356
  extern void OneSwiftRegisterPackage(const char *name, int (*entry)(int, char **));
@@ -1431,7 +2363,7 @@ extern void OneSwiftRegisterPackage(const char *name, int (*entry)(int, char **)
1431
2363
  OneSwiftRegisterPackage("${id}", ${id}_main);
1432
2364
  }
1433
2365
  @end
1434
- `);
2366
+ ` : "#import <Foundation/Foundation.h>\n");
1435
2367
  }
1436
2368
  }
1437
2369
  function applyAndroidDependencyPatches(args) {
@@ -1440,15 +2372,14 @@ function applyAndroidDependencyPatches(args) {
1440
2372
  return;
1441
2373
  }
1442
2374
  const activityPath = path.join(root, "android", "app", "src", "main", "java", ...app.android.applicationId.split("."), "MainActivity.kt");
1443
- const rendered = nativeProjectPatches.addReactNativeScreensFix(FSExtra.readFileSync(activityPath, "utf8"));
2375
+ const rendered = nativeProjectPatches.holdLaunchScreenInMainActivity(nativeProjectPatches.addReactNativeScreensFix(FSExtra.readFileSync(activityPath, "utf8")));
1444
2376
  if (!rendered.includes("RNScreensFragmentFactory")) {
1445
2377
  throw new Error("[vxrn] failed to apply the react-native-screens activity patch");
1446
2378
  }
1447
2379
  FSExtra.writeFileSync(activityPath, rendered);
1448
2380
  }
1449
2381
  async function getNativeDependencyInventory(root) {
1450
- const require2 = module.createRequire(root + "/");
1451
- const cliConfigPath = require2.resolve("@react-native-community/cli-config", { paths: [root] });
2382
+ const cliConfigPath = requireVxrn.resolve("@react-native-community/cli-config");
1452
2383
  const cliConfig = await import(pathToFileURL(cliConfigPath).href);
1453
2384
  const config = await cliConfig.loadConfigAsync({ projectRoot: root });
1454
2385
  const inventory = [];
@@ -1471,12 +2402,19 @@ async function getNativeDependencyInventory(root) {
1471
2402
  function installNativeDependencies(args) {
1472
2403
  const { root, platform } = args;
1473
2404
  if (!platform || platform === "ios") {
1474
- execFileSync("pod", ["install", `--project-directory=${path.join(root, "ios")}`], { stdio: "inherit" });
2405
+ execFileSync("pod", ["install", `--project-directory=${path.join(root, "ios")}`], {
2406
+ stdio: "inherit",
2407
+ env: {
2408
+ ...process.env,
2409
+ LC_ALL: "en_US.UTF-8",
2410
+ RCT_IGNORE_PODS_DEPRECATION: "1"
2411
+ }
2412
+ });
1475
2413
  }
1476
2414
  }
1477
2415
  const transformPath = (filePath) => {
1478
2416
  return filePath.replace("_BUCK", "BUCK").replace("_gitignore", ".gitignore").replace("_gitattributes", ".gitattributes").replace("_babelrc", ".babelrc").replace("_editorconfig", ".editorconfig").replace("_eslintrc.js", ".eslintrc.js").replace("_flowconfig", ".flowconfig").replace("_buckconfig", ".buckconfig").replace("_prettierrc.js", ".prettierrc.js").replace("_bundle", ".bundle").replace("_ruby-version", ".ruby-version").replace("_node-version", ".node-version").replace("_watchmanconfig", ".watchmanconfig").replace("_xcode.env", ".xcode.env");
1479
2417
  };
1480
2418
 
1481
- export { applyAndroidDependencyPatches, generateForPlatform, getNativeDependencyInventory, installNativeDependencies, renderPrebuildFile, renderSceneDelegateSwift, validatePrebuildApp };
2419
+ export { applyAndroidDependencyPatches, enableAppComposeIntegration, generateForPlatform, generateKotlinSources, getNativeDependencyInventory, installNativeDependencies, renderIosAppIntentsSwift, renderPrebuildFile, renderSceneDelegateSwift, validatePrebuildApp };
1482
2420
  //# sourceMappingURL=prebuildWithoutExpo.mjs.map