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.
- package/dist/backgroundComputation.mjs +145 -0
- package/dist/backgroundComputation.mjs.map +1 -0
- package/dist/backgroundComputation.native.js +163 -0
- package/dist/backgroundComputation.native.js.map +1 -0
- package/dist/backgroundComputationNative.mjs +16 -0
- package/dist/backgroundComputationNative.mjs.map +1 -0
- package/dist/backgroundComputationNative.native.js +18 -0
- package/dist/backgroundComputationNative.native.js.map +1 -0
- package/dist/config/getBaseViteConfigOnly.mjs +6 -10
- package/dist/config/getBaseViteConfigOnly.mjs.map +1 -1
- package/dist/config/getBaseViteConfigOnly.native.js +6 -10
- package/dist/config/getBaseViteConfigOnly.native.js.map +1 -1
- package/dist/config/getOptimizeDeps.mjs +2 -1
- package/dist/config/getOptimizeDeps.mjs.map +1 -1
- package/dist/config/getOptimizeDeps.native.js +2 -1
- package/dist/config/getOptimizeDeps.native.js.map +1 -1
- package/dist/config/getViteServerConfig.mjs +13 -0
- package/dist/config/getViteServerConfig.mjs.map +1 -1
- package/dist/config/getViteServerConfig.native.js +15 -1
- package/dist/config/getViteServerConfig.native.js.map +1 -1
- package/dist/constants/defaults.mjs +4 -0
- package/dist/constants/defaults.mjs.map +1 -1
- package/dist/constants/defaults.native.js +4 -0
- package/dist/constants/defaults.native.js.map +1 -1
- package/dist/exports/build.native.js +1 -1
- package/dist/exports/build.native.js.map +1 -1
- package/dist/exports/clean.native.js +1 -1
- package/dist/exports/clean.native.js.map +1 -1
- package/dist/exports/createServer.native.js +2 -2
- package/dist/exports/createServer.native.js.map +1 -1
- package/dist/exports/dev.mjs +3 -0
- package/dist/exports/dev.mjs.map +1 -1
- package/dist/exports/dev.native.js +6 -3
- package/dist/exports/dev.native.js.map +1 -1
- package/dist/exports/loadEnv.native.js +1 -1
- package/dist/exports/loadEnv.native.js.map +1 -1
- package/dist/exports/patch.native.js +1 -1
- package/dist/exports/patch.native.js.map +1 -1
- package/dist/exports/prebuild.mjs +0 -22
- package/dist/exports/prebuild.mjs.map +1 -1
- package/dist/exports/prebuild.native.js +1 -23
- package/dist/exports/prebuild.native.js.map +1 -1
- package/dist/exports/prebuildWithoutExpo.mjs +1116 -178
- package/dist/exports/prebuildWithoutExpo.mjs.map +1 -1
- package/dist/exports/prebuildWithoutExpo.native.js +1592 -243
- package/dist/exports/prebuildWithoutExpo.native.js.map +1 -1
- package/dist/exports/runAndroid.native.js +1 -1
- package/dist/exports/runAndroid.native.js.map +1 -1
- package/dist/exports/runIos.native.js +1 -1
- package/dist/exports/runIos.native.js.map +1 -1
- package/dist/exports/serve.native.js +1 -1
- package/dist/exports/serve.native.js.map +1 -1
- package/dist/exports/serveStaticAssets.native.js +1 -1
- package/dist/exports/serveStaticAssets.native.js.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.mjs +4 -1
- package/dist/index.native.js +4 -1
- package/dist/native-source-contract.mjs +3 -0
- package/dist/native-source-contract.native.js +3 -0
- package/dist/patches/builtInDepPatches.mjs +0 -13
- package/dist/patches/builtInDepPatches.mjs.map +1 -1
- package/dist/patches/builtInDepPatches.native.js +0 -13
- package/dist/patches/builtInDepPatches.native.js.map +1 -1
- package/dist/plugins/autoDepOptimizePlugin.mjs +3 -2
- package/dist/plugins/autoDepOptimizePlugin.mjs.map +1 -1
- package/dist/plugins/autoDepOptimizePlugin.native.js +6 -3
- package/dist/plugins/autoDepOptimizePlugin.native.js.map +1 -1
- package/dist/plugins/backgroundComputationPlugin.mjs +27 -0
- package/dist/plugins/backgroundComputationPlugin.mjs.map +1 -0
- package/dist/plugins/backgroundComputationPlugin.native.js +32 -0
- package/dist/plugins/backgroundComputationPlugin.native.js.map +1 -0
- package/dist/plugins/reactNativeCommonJsPlugin.native.js +3 -3
- package/dist/plugins/reactNativeCommonJsPlugin.native.js.map +1 -1
- package/dist/plugins/reactNativeDevAssetPlugin.mjs +2 -1
- package/dist/plugins/reactNativeDevAssetPlugin.mjs.map +1 -1
- package/dist/plugins/reactNativeDevAssetPlugin.native.js +3 -2
- package/dist/plugins/reactNativeDevAssetPlugin.native.js.map +1 -1
- package/dist/plugins/reactNativeDevServer.mjs +87 -24
- package/dist/plugins/reactNativeDevServer.mjs.map +1 -1
- package/dist/plugins/reactNativeDevServer.native.js +162 -45
- package/dist/plugins/reactNativeDevServer.native.js.map +1 -1
- package/dist/plugins/workletImportsPlugin.mjs +9 -30
- package/dist/plugins/workletImportsPlugin.mjs.map +1 -1
- package/dist/plugins/workletImportsPlugin.native.js +10 -35
- package/dist/plugins/workletImportsPlugin.native.js.map +1 -1
- package/dist/rn-commands/bundle/buildBundle.mjs +3 -3
- package/dist/rn-commands/bundle/buildBundle.mjs.map +1 -1
- package/dist/rn-commands/bundle/buildBundle.native.js +3 -3
- package/dist/rn-commands/bundle/buildBundle.native.js.map +1 -1
- package/dist/runtime/hmr-client.native.js +3 -7
- package/dist/runtime/hmr-client.native.js.map +1 -1
- package/dist/runtime/hmr-runtime.native.js +32 -40
- package/dist/runtime/hmr-runtime.native.js.map +1 -1
- package/dist/serve/node.native.js +2 -2
- package/dist/serve/node.native.js.map +1 -1
- package/dist/swift-package-id.mjs +3 -0
- package/dist/swift-package-id.native.js +3 -0
- package/dist/user-interface/index.native.js +6 -6
- package/dist/user-interface/index.native.js.map +1 -1
- package/dist/utils/compileNativeDevBytecode.mjs +93 -0
- package/dist/utils/compileNativeDevBytecode.mjs.map +1 -0
- package/dist/utils/compileNativeDevBytecode.native.js +98 -0
- package/dist/utils/compileNativeDevBytecode.native.js.map +1 -0
- package/dist/utils/createNativeDevEngine.mjs +227 -20
- package/dist/utils/createNativeDevEngine.mjs.map +1 -1
- package/dist/utils/createNativeDevEngine.native.js +249 -37
- package/dist/utils/createNativeDevEngine.native.js.map +1 -1
- package/dist/utils/execSync.native.js +1 -1
- package/dist/utils/execSync.native.js.map +1 -1
- package/dist/utils/getServerEntry.native.js +2 -2
- package/dist/utils/getServerEntry.native.js.map +1 -1
- package/dist/utils/nativeRun.mjs +32 -16
- package/dist/utils/nativeRun.mjs.map +1 -1
- package/dist/utils/nativeRun.native.js +43 -18
- package/dist/utils/nativeRun.native.js.map +1 -1
- package/dist/utils/nativeSourceContract.mjs +1823 -0
- package/dist/utils/nativeSourceContract.mjs.map +1 -0
- package/dist/utils/nativeSourceContract.native.js +2425 -0
- package/dist/utils/nativeSourceContract.native.js.map +1 -0
- package/dist/utils/patches.native.js +15 -24
- package/dist/utils/patches.native.js.map +1 -1
- package/dist/utils/printServerUrls.native.js +1 -1
- package/dist/utils/printServerUrls.native.js.map +1 -1
- package/dist/utils/scanDepsToOptimize.mjs +7 -1
- package/dist/utils/scanDepsToOptimize.mjs.map +1 -1
- package/dist/utils/scanDepsToOptimize.native.js +27 -2
- package/dist/utils/scanDepsToOptimize.native.js.map +1 -1
- package/dist/utils/swiftPackageId.mjs +31 -2
- package/dist/utils/swiftPackageId.mjs.map +1 -1
- package/dist/utils/swiftPackageId.native.js +50 -2
- package/dist/utils/swiftPackageId.native.js.map +1 -1
- package/dist/vxrn-vite-plugin.native.js +1 -1
- package/dist/vxrn-vite-plugin.native.js.map +1 -1
- package/expo-plugin.cjs +792 -2
- package/native-project-patches.cjs +201 -10
- package/package.json +34 -15
- package/src/backgroundComputation.ts +220 -0
- package/src/backgroundComputationNative.ts +23 -0
- package/src/config/getBaseViteConfigOnly.ts +8 -9
- package/src/config/getOptimizeDeps.ts +3 -1
- package/src/config/getViteServerConfig.ts +16 -0
- package/src/config/glbAssets.test.ts +228 -0
- package/src/constants/defaults.ts +6 -0
- package/src/exports/dev.ts +5 -1
- package/src/exports/expoPlugin.test.mjs +337 -15
- package/src/exports/prebuild.ts +0 -31
- package/src/exports/prebuildWithoutExpo.test.ts +1588 -48
- package/src/exports/prebuildWithoutExpo.ts +1573 -213
- package/src/index.ts +8 -0
- package/src/native-source-contract.ts +16 -0
- package/src/patches/builtInDepPatches.ts +0 -21
- package/src/plugins/autoDepOptimizePlugin.ts +7 -2
- package/src/plugins/backgroundComputationPlugin.test.ts +77 -0
- package/src/plugins/backgroundComputationPlugin.ts +29 -0
- package/src/plugins/reactNativeDevAssetPlugin.ts +5 -1
- package/src/plugins/reactNativeDevServer.test.ts +74 -0
- package/src/plugins/reactNativeDevServer.ts +145 -28
- package/src/plugins/rnCodegenPlugin.test.ts +30 -0
- package/src/plugins/workletImportsPlugin.ts +19 -37
- package/src/rn-commands/bundle/buildBundle.ts +5 -3
- package/src/swift-package-id.ts +1 -0
- package/src/utils/compileNativeDevBytecode.test.ts +168 -0
- package/src/utils/compileNativeDevBytecode.ts +117 -0
- package/src/utils/createNativeDevEngine.test.ts +643 -18
- package/src/utils/createNativeDevEngine.ts +278 -48
- package/src/utils/nativeRun.test.ts +125 -23
- package/src/utils/nativeRun.ts +51 -23
- package/src/utils/nativeSourceArtifacts.test.ts +39 -0
- package/src/utils/nativeSourceContract.test.ts +600 -0
- package/src/utils/nativeSourceContract.ts +1897 -0
- package/src/utils/scanDepsToOptimize.test.ts +80 -0
- package/src/utils/scanDepsToOptimize.ts +18 -7
- package/src/utils/swiftPackageId.ts +25 -1
- package/types/backgroundComputation.d.ts +28 -0
- package/types/backgroundComputation.d.ts.map +1 -0
- package/types/backgroundComputationNative.d.ts +2 -0
- package/types/backgroundComputationNative.d.ts.map +1 -0
- package/types/config/getBaseViteConfigOnly.d.ts.map +1 -1
- package/types/config/getOptimizeDeps.d.ts.map +1 -1
- package/types/config/getViteServerConfig.d.ts.map +1 -1
- package/types/constants/defaults.d.ts.map +1 -1
- package/types/exports/dev.d.ts.map +1 -1
- package/types/exports/prebuild.d.ts.map +1 -1
- package/types/exports/prebuildWithoutExpo.d.ts +6 -0
- package/types/exports/prebuildWithoutExpo.d.ts.map +1 -1
- package/types/index.d.ts +2 -0
- package/types/index.d.ts.map +1 -1
- package/types/native-source-contract.d.ts +2 -0
- package/types/native-source-contract.d.ts.map +1 -0
- package/types/patches/builtInDepPatches.d.ts.map +1 -1
- package/types/plugins/autoDepOptimizePlugin.d.ts.map +1 -1
- package/types/plugins/backgroundComputationPlugin.d.ts +3 -0
- package/types/plugins/backgroundComputationPlugin.d.ts.map +1 -0
- package/types/plugins/reactNativeDevAssetPlugin.d.ts.map +1 -1
- package/types/plugins/reactNativeDevServer.d.ts.map +1 -1
- package/types/plugins/workletImportsPlugin.d.ts.map +1 -1
- package/types/rn-commands/bundle/buildBundle.d.ts.map +1 -1
- package/types/swift-package-id.d.ts +2 -0
- package/types/swift-package-id.d.ts.map +1 -0
- package/types/utils/compileNativeDevBytecode.d.ts +3 -0
- package/types/utils/compileNativeDevBytecode.d.ts.map +1 -0
- package/types/utils/createNativeDevEngine.d.ts +7 -22
- package/types/utils/createNativeDevEngine.d.ts.map +1 -1
- package/types/utils/nativeRun.d.ts.map +1 -1
- package/types/utils/nativeSourceContract.d.ts +121 -0
- package/types/utils/nativeSourceContract.d.ts.map +1 -0
- package/types/utils/scanDepsToOptimize.d.ts +1 -0
- package/types/utils/scanDepsToOptimize.d.ts.map +1 -1
- package/types/utils/swiftPackageId.d.ts +1 -0
- 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 {
|
|
9
|
+
import { swiftPackageDirectories } from "../utils/swiftPackageId.mjs";
|
|
9
10
|
|
|
10
|
-
const
|
|
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
|
|
77
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
371
|
-
|
|
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"),
|
|
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
|
|
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
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
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
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
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
|
-
|
|
958
|
-
|
|
959
|
-
const
|
|
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="
|
|
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"
|
|
978
|
-
<constraints
|
|
979
|
-
<constraint firstItem="splash-
|
|
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
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
const
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
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
|
|
1529
|
+
stamps.push(` <key>${notificationsHost.enabledInfoPlistKey}</key>
|
|
1104
1530
|
<true/>`);
|
|
1105
1531
|
}
|
|
1106
1532
|
if (app.notifications?.push === true) {
|
|
1107
|
-
stamps.push(` <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
|
-
|
|
1114
|
-
|
|
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
|
-
|
|
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,
|
|
1137
|
-
|
|
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>",
|
|
1143
|
-
|
|
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 =
|
|
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>",
|
|
1153
|
-
|
|
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 =
|
|
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(
|
|
1930
|
+
throw new Error(`[vxrn] cannot stamp ${additions[0][0]}: expected LSRequiresIPhoneOS in Info.plist`);
|
|
1220
1931
|
}
|
|
1221
|
-
rendered = rendered.replace(anchor, ` <key
|
|
1222
|
-
<string>${escapeXml(
|
|
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.
|
|
1248
|
-
|
|
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?.
|
|
1291
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
1375
|
-
const
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
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
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
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 = '
|
|
2345
|
+
s.swift_version = '${manifest.languageMode}.0'
|
|
1414
2346
|
s.source_files = 'Sources/**/*.swift', 'Register.m'
|
|
1415
|
-
s.dependency '
|
|
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
|
|
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
|
|
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")}`], {
|
|
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
|