craft-native 0.0.91 → 0.0.94
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/README.md +17 -0
- package/dist/android/src/index.d.ts +39 -1
- package/dist/android/src/index.js +55 -4
- package/dist/android/templates/CraftBridge.kt.template +534 -103
- package/dist/android/templates/CraftNative.kt.template +38 -13
- package/dist/android/templates/MainActivity.kt.template +98 -10
- package/dist/api/mobile.d.ts +44 -2
- package/dist/binary-resolver.d.ts +4 -0
- package/dist/cli.js +65 -9
- package/dist/index.cjs +89 -54
- package/dist/index.d.cts +20 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +90 -55
- package/dist/ios/src/index.d.ts +1 -1
- package/dist/ios/src/index.js +1 -1
- package/dist/ios/templates/CraftApp.swift +681 -57
- package/dist/mobile.js +40 -9
- package/dist/sdk-version.d.ts +1 -0
- package/package.json +2 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package com.craft.runtime
|
|
2
2
|
|
|
3
3
|
import android.app.Activity
|
|
4
|
+
import android.content.IntentSender
|
|
4
5
|
import com.google.android.gms.location.Priority
|
|
5
6
|
import com.google.android.gms.location.LocationServices
|
|
6
7
|
import com.google.android.gms.location.LocationResult
|
|
@@ -110,10 +111,24 @@ object CraftNative {
|
|
|
110
111
|
*/
|
|
111
112
|
val isAvailable: Boolean = try {
|
|
112
113
|
System.loadLibrary("craft")
|
|
114
|
+
// Said out loud, because the two outcomes below are otherwise
|
|
115
|
+
// indistinguishable from outside: a library that never shipped and a
|
|
116
|
+
// library that loaded and then failed to bind both end with the shim
|
|
117
|
+
// answering every call. Zig logs its own registration under the same
|
|
118
|
+
// tag, so the pair reads as a sequence.
|
|
119
|
+
android.util.Log.i("CraftNative", "craft: libcraft.so loaded")
|
|
113
120
|
true
|
|
114
121
|
} catch (e: UnsatisfiedLinkError) {
|
|
122
|
+
// The reason, not just the fact. This used to be a bare `false`, and
|
|
123
|
+
// the first time the runtime was genuinely shipped and genuinely
|
|
124
|
+
// failed to load, the only evidence anywhere was that nothing
|
|
125
|
+
// happened — no linker line, no exception, nothing in logcat at all.
|
|
126
|
+
// The message carries the dynamic linker's own complaint, which is the
|
|
127
|
+
// one thing that says *why*.
|
|
128
|
+
android.util.Log.w("CraftNative", "craft: libcraft.so did not load: ${e.message}")
|
|
115
129
|
false
|
|
116
130
|
} catch (e: SecurityException) {
|
|
131
|
+
android.util.Log.w("CraftNative", "craft: libcraft.so was refused: ${e.message}")
|
|
117
132
|
false
|
|
118
133
|
}
|
|
119
134
|
|
|
@@ -197,14 +212,20 @@ object CraftNative {
|
|
|
197
212
|
/**
|
|
198
213
|
* Run `script` in the page. Called from Zig, on any thread.
|
|
199
214
|
*
|
|
200
|
-
*
|
|
201
|
-
* WebView exists
|
|
202
|
-
*
|
|
203
|
-
*
|
|
215
|
+
* Returns whether a deliverer took it. False is every moment before the
|
|
216
|
+
* WebView exists and after it is torn down, and it used to be
|
|
217
|
+
* indistinguishable from a delivery: the script was dropped, Zig's caller
|
|
218
|
+
* reported success, and the page waited on a promise nothing would settle
|
|
219
|
+
* (#231). Zig counts a false, and says so once.
|
|
220
|
+
*
|
|
221
|
+
* Still no throw. A throw here would cross back into Zig as a pending
|
|
222
|
+
* exception on a thread with nowhere to report it; false says the same
|
|
223
|
+
* thing and the caller can act on it.
|
|
204
224
|
*/
|
|
205
225
|
@JvmStatic
|
|
206
|
-
fun deliver(script: String) {
|
|
207
|
-
|
|
226
|
+
fun deliver(script: String): Boolean {
|
|
227
|
+
val target = deliverer ?: return false
|
|
228
|
+
return runCatching { target(script); true }.getOrDefault(false)
|
|
208
229
|
}
|
|
209
230
|
|
|
210
231
|
private external fun nativeGetDeviceInfo(activity: Activity): String?
|
|
@@ -213,7 +234,7 @@ object CraftNative {
|
|
|
213
234
|
private external fun nativeClipboardRead(activity: Activity): String?
|
|
214
235
|
private external fun nativeClipboardWrite(activity: Activity, text: String): Boolean
|
|
215
236
|
private external fun nativeOpenUrl(activity: Activity, url: String): Boolean
|
|
216
|
-
private external fun nativeShare(activity: Activity, text: String, title: String): Boolean
|
|
237
|
+
private external fun nativeShare(activity: Activity, text: String, title: String, chosen: IntentSender, requestCode: Int): Boolean
|
|
217
238
|
private external fun nativeOpenCamera(activity: Activity): Boolean
|
|
218
239
|
private external fun nativePickImage(activity: Activity): Boolean
|
|
219
240
|
private external fun nativePickFile(activity: Activity): Boolean
|
|
@@ -235,7 +256,7 @@ object CraftNative {
|
|
|
235
256
|
private external fun nativeResetDeepLinks(): Boolean
|
|
236
257
|
private external fun nativeSetInitialURL(url: String?): Boolean
|
|
237
258
|
private external fun nativeGetInitialURL(): Boolean
|
|
238
|
-
private external fun nativeDispatchDeepLink(url: String): Boolean
|
|
259
|
+
private external fun nativeDispatchDeepLink(url: String, initial: Boolean): Boolean
|
|
239
260
|
private external fun nativeScreenshotReady(bytes: ByteArray)
|
|
240
261
|
private external fun nativeScreenshotError(message: String)
|
|
241
262
|
private external fun nativeTakeScreenshot(activity: Activity, webView: WebView): Boolean
|
|
@@ -411,11 +432,15 @@ object CraftNative {
|
|
|
411
432
|
}
|
|
412
433
|
}
|
|
413
434
|
|
|
414
|
-
/**
|
|
415
|
-
|
|
435
|
+
/**
|
|
436
|
+
* Returns whether Zig launched the share menu for a result; false falls
|
|
437
|
+
* through. `chosen` fires on a pick and `requestCode` routes the result
|
|
438
|
+
* back to CraftBridge, which settles the page's promise from the two.
|
|
439
|
+
*/
|
|
440
|
+
fun share(activity: Activity, text: String, title: String, chosen: IntentSender, requestCode: Int): Boolean {
|
|
416
441
|
if (!isAvailable) return false
|
|
417
442
|
return try {
|
|
418
|
-
nativeShare(activity, text, title)
|
|
443
|
+
nativeShare(activity, text, title, chosen, requestCode)
|
|
419
444
|
} catch (e: UnsatisfiedLinkError) {
|
|
420
445
|
false
|
|
421
446
|
}
|
|
@@ -697,9 +722,9 @@ object CraftNative {
|
|
|
697
722
|
return try { nativeGetInitialURL() } catch (e: UnsatisfiedLinkError) { false }
|
|
698
723
|
}
|
|
699
724
|
|
|
700
|
-
fun dispatchDeepLink(url: String): Boolean {
|
|
725
|
+
fun dispatchDeepLink(url: String, initial: Boolean): Boolean {
|
|
701
726
|
if (!isAvailable) return false
|
|
702
|
-
return try { nativeDispatchDeepLink(url) } catch (e: UnsatisfiedLinkError) { false }
|
|
727
|
+
return try { nativeDispatchDeepLink(url, initial) } catch (e: UnsatisfiedLinkError) { false }
|
|
703
728
|
}
|
|
704
729
|
|
|
705
730
|
@JvmStatic
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
package {{PACKAGE_NAME}}
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
|
+
import android.content.Context
|
|
4
5
|
import android.os.Bundle
|
|
5
6
|
import android.content.Intent
|
|
6
7
|
import android.graphics.Bitmap
|
|
8
|
+
import android.net.ConnectivityManager
|
|
9
|
+
import android.net.Network
|
|
7
10
|
import android.net.Uri
|
|
11
|
+
import android.util.Log
|
|
8
12
|
import android.webkit.WebResourceRequest
|
|
9
13
|
import android.webkit.WebResourceError
|
|
10
14
|
import android.webkit.WebResourceResponse
|
|
@@ -19,6 +23,20 @@ import org.json.JSONObject
|
|
|
19
23
|
import java.io.FileNotFoundException
|
|
20
24
|
import java.net.URLConnection
|
|
21
25
|
|
|
26
|
+
/** Main-frame failures for which the bundled app is an honest fallback (#254). */
|
|
27
|
+
object CraftLoadFailure {
|
|
28
|
+
fun isUnreachable(errorCode: Int): Boolean = when (errorCode) {
|
|
29
|
+
WebViewClient.ERROR_HOST_LOOKUP,
|
|
30
|
+
WebViewClient.ERROR_CONNECT,
|
|
31
|
+
WebViewClient.ERROR_TIMEOUT,
|
|
32
|
+
// Android uses ERROR_IO for connection interruptions as well as local
|
|
33
|
+
// I/O failures. Keep it here deliberately: it is the closest Android
|
|
34
|
+
// equivalent to iOS's NSURLErrorNetworkConnectionLost.
|
|
35
|
+
WebViewClient.ERROR_IO -> true
|
|
36
|
+
else -> false
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
22
40
|
class MainActivity : AppCompatActivity() {
|
|
23
41
|
private lateinit var webView: WebView
|
|
24
42
|
private lateinit var craftBridge: CraftBridge
|
|
@@ -32,6 +50,28 @@ class MainActivity : AppCompatActivity() {
|
|
|
32
50
|
.build()
|
|
33
51
|
}
|
|
34
52
|
private var loadedBundledFallback = false
|
|
53
|
+
private lateinit var connectivityManager: ConnectivityManager
|
|
54
|
+
@Volatile
|
|
55
|
+
private var isConnected = false
|
|
56
|
+
private var networkMonitoringRegistered = false
|
|
57
|
+
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
|
58
|
+
override fun onAvailable(network: Network) {
|
|
59
|
+
val wasConnected = isConnected
|
|
60
|
+
isConnected = true
|
|
61
|
+
if (!wasConnected && isConnected) {
|
|
62
|
+
runOnUiThread {
|
|
63
|
+
returnFromBundledFallback("the network came back")
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
override fun onLost(network: Network) {
|
|
69
|
+
// A second transport can already be the default by the time the
|
|
70
|
+
// old one reports its loss, so read current state instead of
|
|
71
|
+
// blindly turning the latch off.
|
|
72
|
+
isConnected = connectivityManager.activeNetwork != null
|
|
73
|
+
}
|
|
74
|
+
}
|
|
35
75
|
|
|
36
76
|
@SuppressLint("SetJavaScriptEnabled")
|
|
37
77
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
@@ -44,11 +84,31 @@ class MainActivity : AppCompatActivity() {
|
|
|
44
84
|
|
|
45
85
|
webView = findViewById(R.id.webview)
|
|
46
86
|
craftBridge = CraftBridge(this, webView, CraftHealthConnect(this, webView))
|
|
47
|
-
|
|
48
|
-
|
|
87
|
+
// The link that launched the app (#215). Queued until the page's bridge
|
|
88
|
+
// is injected, then dispatched once with `initial: true`, so a page that
|
|
89
|
+
// subscribes rather than calling getInitialURL still hears it.
|
|
90
|
+
//
|
|
91
|
+
// A relaunch from Recents carries the intent the app was first opened
|
|
92
|
+
// with. The person picked the task out of a list, not the link or the
|
|
93
|
+
// shortcut inside it, and neither is news the second time — so both
|
|
94
|
+
// are skipped, not just the link (#229).
|
|
95
|
+
//
|
|
96
|
+
// A recreated activity restores what the bridge saved rather than
|
|
97
|
+
// reading its intent, which onNewIntent may have replaced with a
|
|
98
|
+
// later warm link.
|
|
99
|
+
val launchedFromHistory = intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0
|
|
100
|
+
if (savedInstanceState == null) {
|
|
101
|
+
if (!launchedFromHistory) {
|
|
102
|
+
intent.data?.toString()?.let(craftBridge::receiveDeepLink)
|
|
103
|
+
handleIncomingShortcut(intent)
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
craftBridge.restoreDeepLinks(savedInstanceState)
|
|
107
|
+
}
|
|
49
108
|
|
|
50
109
|
setupWebView()
|
|
51
110
|
loadContent()
|
|
111
|
+
setupNetworkMonitoring()
|
|
52
112
|
}
|
|
53
113
|
|
|
54
114
|
@SuppressLint("SetJavaScriptEnabled")
|
|
@@ -94,7 +154,13 @@ class MainActivity : AppCompatActivity() {
|
|
|
94
154
|
|
|
95
155
|
override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
|
|
96
156
|
super.onReceivedError(view, request, error)
|
|
97
|
-
if (
|
|
157
|
+
if (
|
|
158
|
+
request?.isForMainFrame == true &&
|
|
159
|
+
error != null &&
|
|
160
|
+
CraftLoadFailure.isUnreachable(error.errorCode) &&
|
|
161
|
+
hasBundledFallback &&
|
|
162
|
+
!loadedBundledFallback
|
|
163
|
+
) {
|
|
98
164
|
loadedBundledFallback = true
|
|
99
165
|
webView.loadUrl(BUNDLED_APP_URL)
|
|
100
166
|
}
|
|
@@ -107,6 +173,23 @@ class MainActivity : AppCompatActivity() {
|
|
|
107
173
|
webView.addJavascriptInterface(craftBridge, "CraftAndroid")
|
|
108
174
|
}
|
|
109
175
|
|
|
176
|
+
private fun setupNetworkMonitoring() {
|
|
177
|
+
if (!hasBundledFallback || devServerUrl == null) return
|
|
178
|
+
connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
|
179
|
+
isConnected = connectivityManager.activeNetwork != null
|
|
180
|
+
connectivityManager.registerDefaultNetworkCallback(networkCallback)
|
|
181
|
+
networkMonitoringRegistered = true
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Leave the bundled copy for the configured remote origin again. */
|
|
185
|
+
private fun returnFromBundledFallback(reason: String) {
|
|
186
|
+
val remoteUrl = devServerUrl ?: return
|
|
187
|
+
if (!loadedBundledFallback) return
|
|
188
|
+
loadedBundledFallback = false
|
|
189
|
+
Log.i(TAG, "$reason; loading $remoteUrl again instead of the bundled copy")
|
|
190
|
+
webView.loadUrl(remoteUrl)
|
|
191
|
+
}
|
|
192
|
+
|
|
110
193
|
private fun loadContent() {
|
|
111
194
|
// Check for dev server URL in config
|
|
112
195
|
val remoteUrl = devServerUrl
|
|
@@ -150,12 +233,6 @@ class MainActivity : AppCompatActivity() {
|
|
|
150
233
|
}
|
|
151
234
|
}
|
|
152
235
|
|
|
153
|
-
private fun handleIncomingDeepLink(source: Intent?, dispatch: Boolean) {
|
|
154
|
-
val url = source?.data?.toString() ?: return
|
|
155
|
-
craftBridge.setInitialURL(url)
|
|
156
|
-
if (dispatch) craftBridge.dispatchDeepLink(url)
|
|
157
|
-
}
|
|
158
|
-
|
|
159
236
|
private fun handleIncomingShortcut(source: Intent?) {
|
|
160
237
|
source?.getStringExtra(SHORTCUT_TYPE_EXTRA)?.let(craftBridge::dispatchShortcut)
|
|
161
238
|
}
|
|
@@ -198,10 +275,15 @@ class MainActivity : AppCompatActivity() {
|
|
|
198
275
|
return (uri.scheme == "https" || localDevelopment) && origin in trustedOrigins
|
|
199
276
|
}
|
|
200
277
|
|
|
278
|
+
override fun onSaveInstanceState(outState: Bundle) {
|
|
279
|
+
super.onSaveInstanceState(outState)
|
|
280
|
+
craftBridge.saveDeepLinks(outState)
|
|
281
|
+
}
|
|
282
|
+
|
|
201
283
|
override fun onNewIntent(intent: Intent) {
|
|
202
284
|
super.onNewIntent(intent)
|
|
203
285
|
setIntent(intent)
|
|
204
|
-
|
|
286
|
+
intent.data?.toString()?.let(craftBridge::receiveDeepLink)
|
|
205
287
|
handleIncomingShortcut(intent)
|
|
206
288
|
}
|
|
207
289
|
|
|
@@ -216,6 +298,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
216
298
|
override fun onResume() {
|
|
217
299
|
super.onResume()
|
|
218
300
|
webView.onResume()
|
|
301
|
+
returnFromBundledFallback("the app returned to the foreground")
|
|
219
302
|
}
|
|
220
303
|
|
|
221
304
|
override fun onPause() {
|
|
@@ -224,6 +307,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
224
307
|
}
|
|
225
308
|
|
|
226
309
|
override fun onDestroy() {
|
|
310
|
+
if (networkMonitoringRegistered) {
|
|
311
|
+
connectivityManager.unregisterNetworkCallback(networkCallback)
|
|
312
|
+
networkMonitoringRegistered = false
|
|
313
|
+
}
|
|
227
314
|
craftBridge.close()
|
|
228
315
|
webView.destroy()
|
|
229
316
|
super.onDestroy()
|
|
@@ -245,6 +332,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
245
332
|
}
|
|
246
333
|
|
|
247
334
|
companion object {
|
|
335
|
+
private const val TAG = "CraftMainActivity"
|
|
248
336
|
private const val BUNDLED_APP_HOST = "appassets.androidplatform.net"
|
|
249
337
|
private const val BUNDLED_APP_ORIGIN = "https://appassets.androidplatform.net"
|
|
250
338
|
private const val BUNDLED_APP_URL = "$BUNDLED_APP_ORIGIN/"
|
package/dist/api/mobile.d.ts
CHANGED
|
@@ -550,9 +550,12 @@ export declare const share: {
|
|
|
550
550
|
/**
|
|
551
551
|
* Open native share dialog.
|
|
552
552
|
*
|
|
553
|
+
* Resolves `true` when the person shared and `false` when they dismissed the
|
|
554
|
+
* dialog, on the native bridges and in the browser alike.
|
|
555
|
+
*
|
|
553
556
|
* @param options - Share options
|
|
554
557
|
*/
|
|
555
|
-
share(options: ShareOptions): Promise<
|
|
558
|
+
share(options: ShareOptions): Promise<boolean>;
|
|
556
559
|
/**
|
|
557
560
|
* Check if sharing is available.
|
|
558
561
|
*/
|
|
@@ -614,6 +617,22 @@ export interface NotificationOptions {
|
|
|
614
617
|
/** Schedule for future (timestamp) */
|
|
615
618
|
scheduleAt?: number;
|
|
616
619
|
}
|
|
620
|
+
/** A notification as the bridges read it: `delay` in place of `scheduleAt`. */
|
|
621
|
+
export type BridgeNotification = Omit<NotificationOptions, 'scheduleAt'> & {
|
|
622
|
+
delay?: number;
|
|
623
|
+
};
|
|
624
|
+
/**
|
|
625
|
+
* The bridges' spelling of a notification: `scheduleAt` becomes `delay`.
|
|
626
|
+
*
|
|
627
|
+
* Both bridges schedule from `delay`, milliseconds from now, and iOS also from
|
|
628
|
+
* `timestamp`; neither reads `scheduleAt`, so a reminder meant for an hour
|
|
629
|
+
* from now fired at once (#261). `delay` rather than `timestamp` because
|
|
630
|
+
* Android reads only `delay`, and because an interval is what an instant
|
|
631
|
+
* means, where iOS's calendar trigger would move with the device's time zone.
|
|
632
|
+
* A moment already past sends no delay at all, which both bridges deliver
|
|
633
|
+
* now: a zero delay raises inside UserNotifications on the Swift side.
|
|
634
|
+
*/
|
|
635
|
+
export declare function bridgeNotification(options: NotificationOptions, now?: number): BridgeNotification;
|
|
617
636
|
/**
|
|
618
637
|
* Local notifications API.
|
|
619
638
|
*
|
|
@@ -670,7 +689,14 @@ export declare const keepAwake: {
|
|
|
670
689
|
};
|
|
671
690
|
export declare const deepLinks: {
|
|
672
691
|
getInitialURL(): Promise<string | null>;
|
|
673
|
-
|
|
692
|
+
/**
|
|
693
|
+
* The second argument says whether this is the link that launched the app.
|
|
694
|
+
* A page that also calls `getInitialURL()` after an await gets that link
|
|
695
|
+
* from both, and can skip it here when `initial` is true.
|
|
696
|
+
*/
|
|
697
|
+
onLink(callback: (url: string, link: {
|
|
698
|
+
initial: boolean;
|
|
699
|
+
}) => void): () => void;
|
|
674
700
|
};
|
|
675
701
|
export declare function normalizeDeepLinkURL(value: unknown): string | null;
|
|
676
702
|
export declare const network: {
|
|
@@ -683,7 +709,23 @@ export declare const appReview: {
|
|
|
683
709
|
export declare const pushNotifications: {
|
|
684
710
|
register(): Promise<string>;
|
|
685
711
|
onToken(callback: (token: string) => void): () => void;
|
|
712
|
+
/**
|
|
713
|
+
* A tap on a notification, with the payload it was sent with.
|
|
714
|
+
*
|
|
715
|
+
* Goes through the bridge's replay where there is one, so a tap that
|
|
716
|
+
* launched the app still reaches a page that subscribes once it has
|
|
717
|
+
* hydrated — rather than only one listening before the bridge was ready.
|
|
718
|
+
*/
|
|
686
719
|
onNotification(callback: (data: Record<string, unknown>) => void): () => void;
|
|
720
|
+
/**
|
|
721
|
+
* A notification that arrived while the app was open, with the payload it
|
|
722
|
+
* was sent with — the same shape `onNotification` hands a tap.
|
|
723
|
+
*
|
|
724
|
+
* The banner still shows. This is for a page that wants to react while it
|
|
725
|
+
* is on screen, such as refreshing what the push was about. Live only: an
|
|
726
|
+
* arrival before the page subscribed is not held for it. iOS only for now.
|
|
727
|
+
*/
|
|
728
|
+
onReceive(callback: (data: Record<string, unknown>) => void): () => void;
|
|
687
729
|
};
|
|
688
730
|
export type HealthDataType = 'steps' | 'heartRate' | 'activeEnergy' | 'distance' | 'workouts';
|
|
689
731
|
export interface HealthDataOptions {
|
|
@@ -46,6 +46,10 @@ export declare function resolveCraftBinary(explicit?: string): string;
|
|
|
46
46
|
* path-matrix behaviour the pantry contract exists to remove. So instead the
|
|
47
47
|
* spawn is marked, and a CLI that starts up already marked knows it *is* the
|
|
48
48
|
* thing that was spawned, and says so.
|
|
49
|
+
*
|
|
50
|
+
* Both spawners set it: `runCraftBinary()` in the CLI, and `CraftApp.show()`
|
|
51
|
+
* in the SDK. The SDK one was missing until #236, so every SDK caller on such
|
|
52
|
+
* a machine still met the raw parser error this marker exists to replace.
|
|
49
53
|
*/
|
|
50
54
|
export declare const CRAFT_CLI_SPAWN_MARKER = "CRAFT_CLI_SPAWNED_FROM";
|
|
51
55
|
/**
|
package/dist/cli.js
CHANGED
|
@@ -1575,7 +1575,7 @@ async function open(options) {
|
|
|
1575
1575
|
await $`open ${projectPath}`;
|
|
1576
1576
|
}
|
|
1577
1577
|
function orderSimulators(devices) {
|
|
1578
|
-
return
|
|
1578
|
+
return devices.filter((device) => device.runtime.startsWith("iOS")).sort((left, right) => {
|
|
1579
1579
|
const booted = Number(right.state === "Booted") - Number(left.state === "Booted");
|
|
1580
1580
|
if (booted !== 0)
|
|
1581
1581
|
return booted;
|
|
@@ -1730,11 +1730,14 @@ var exports_src2 = {};
|
|
|
1730
1730
|
__export(exports_src2, {
|
|
1731
1731
|
syncAndroidWebAssets: () => syncAndroidWebAssets,
|
|
1732
1732
|
run: () => run2,
|
|
1733
|
+
resolveRuntimeDir: () => resolveRuntimeDir2,
|
|
1733
1734
|
renderAndroidPermissions: () => renderAndroidPermissions,
|
|
1734
1735
|
renderAndroidDeepLinks: () => renderAndroidDeepLinks,
|
|
1735
1736
|
open: () => open2,
|
|
1737
|
+
installRuntime: () => installRuntime2,
|
|
1736
1738
|
init: () => init2,
|
|
1737
|
-
build: () => build2
|
|
1739
|
+
build: () => build2,
|
|
1740
|
+
ANDROID_MIN_SDK: () => ANDROID_MIN_SDK
|
|
1738
1741
|
});
|
|
1739
1742
|
import { cpSync as cpSync3, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
1740
1743
|
import { dirname as dirname2, extname, isAbsolute, join as join3, relative, resolve as resolve2, sep } from "path";
|
|
@@ -1838,6 +1841,9 @@ function validateAndroidConfig(config) {
|
|
|
1838
1841
|
throw new Error(`Android ${name} must be a positive integer`);
|
|
1839
1842
|
}
|
|
1840
1843
|
}
|
|
1844
|
+
if (Number(config.minSdk) < ANDROID_MIN_SDK) {
|
|
1845
|
+
throw new Error(`Android minSdk must be at least ${ANDROID_MIN_SDK} (Android 8.0) for the Craft runtime`);
|
|
1846
|
+
}
|
|
1841
1847
|
if (Number(config.minSdk) > Number(config.targetSdk)) {
|
|
1842
1848
|
throw new Error("Android minSdk must not exceed targetSdk");
|
|
1843
1849
|
}
|
|
@@ -1928,6 +1934,36 @@ function renderAndroidDeepLinks(config) {
|
|
|
1928
1934
|
</intent-filter>`).join(`
|
|
1929
1935
|
`);
|
|
1930
1936
|
}
|
|
1937
|
+
function resolveRuntimeDir2(override) {
|
|
1938
|
+
if (override === null)
|
|
1939
|
+
return null;
|
|
1940
|
+
const dir = override ?? process.env.CRAFT_ANDROID_RUNTIME;
|
|
1941
|
+
if (!dir)
|
|
1942
|
+
return null;
|
|
1943
|
+
if (!existsSync5(dir)) {
|
|
1944
|
+
const source = override === undefined ? "CRAFT_ANDROID_RUNTIME points at" : "runtimeDir is";
|
|
1945
|
+
throw new Error(`${source} ${dir}, which does not exist.`);
|
|
1946
|
+
}
|
|
1947
|
+
return dir;
|
|
1948
|
+
}
|
|
1949
|
+
function installRuntime2(output, runtimeDir) {
|
|
1950
|
+
const found = RUNTIME_ABIS.map((abi) => ({ abi, source: join3(runtimeDir, abi, "libcraft.so") })).filter((entry) => existsSync5(entry.source));
|
|
1951
|
+
if (found.length === 0) {
|
|
1952
|
+
throw new Error(`${runtimeDir} has no <abi>/libcraft.so for any of ${RUNTIME_ABIS.join(", ")}. ` + "Run `zig build build-android-all -Doptimize=ReleaseSafe` in packages/zig and point at its zig-out/android.");
|
|
1953
|
+
}
|
|
1954
|
+
if (found.length < RUNTIME_ABIS.length) {
|
|
1955
|
+
const missing = RUNTIME_ABIS.filter((abi) => !found.some((entry) => entry.abi === abi));
|
|
1956
|
+
console.warn(` \u26A0 only ${found.map((entry) => entry.abi).join(", ")} was found; ${missing.join(", ")} is missing. ` + "The app will fall back to the Kotlin shim on those devices.");
|
|
1957
|
+
}
|
|
1958
|
+
const dest = join3(output, "app/src/main/jniLibs");
|
|
1959
|
+
rmSync3(dest, { force: true, recursive: true });
|
|
1960
|
+
for (const { abi, source } of found) {
|
|
1961
|
+
const abiDir = join3(dest, abi);
|
|
1962
|
+
mkdirSync3(abiDir, { recursive: true });
|
|
1963
|
+
cpSync3(source, join3(abiDir, "libcraft.so"));
|
|
1964
|
+
}
|
|
1965
|
+
return true;
|
|
1966
|
+
}
|
|
1931
1967
|
async function init2(options) {
|
|
1932
1968
|
const { name, packageName, output } = options;
|
|
1933
1969
|
console.log(`
|
|
@@ -1945,7 +1981,6 @@ async function init2(options) {
|
|
|
1945
1981
|
if (config.enableBackgroundLocation)
|
|
1946
1982
|
config.enableGeolocation = true;
|
|
1947
1983
|
if (config.enableHealthConnect) {
|
|
1948
|
-
config.minSdk = Math.max(config.minSdk ?? 26, 26);
|
|
1949
1984
|
config.compileSdk = Math.max(config.compileSdk ?? 36, 36);
|
|
1950
1985
|
}
|
|
1951
1986
|
normalizeAndroidNetworkConfig(config);
|
|
@@ -2029,7 +2064,7 @@ async function init2(options) {
|
|
|
2029
2064
|
const projectGradleTemplate = readFileSync4(join3(TEMPLATES_DIR2, "build.gradle.kts.project.template"), "utf-8");
|
|
2030
2065
|
writeFileSync4(join3(output, "build.gradle.kts"), projectGradleTemplate.replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services") version "4.4.2" apply false' : ""));
|
|
2031
2066
|
const appGradleTemplate = readFileSync4(join3(TEMPLATES_DIR2, "build.gradle.kts.app.template"), "utf-8");
|
|
2032
|
-
const appGradle = appGradleTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{VERSION_NAME\}\}/g, escapeKotlinString(config.version || "1.0.0")).replace(/\{\{VERSION_CODE\}\}/g, String(config.versionCode || 1)).replace(/\{\{MIN_SDK\}\}/g, String(config.minSdk
|
|
2067
|
+
const appGradle = appGradleTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{VERSION_NAME\}\}/g, escapeKotlinString(config.version || "1.0.0")).replace(/\{\{VERSION_CODE\}\}/g, String(config.versionCode || 1)).replace(/\{\{MIN_SDK\}\}/g, String(config.minSdk ?? ANDROID_MIN_SDK)).replace(/\{\{COMPILE_SDK\}\}/g, String(config.compileSdk || 36)).replace(/\{\{TARGET_SDK\}\}/g, String(config.targetSdk || 35)).replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services")' : "").replace(/\{\{FIREBASE_MESSAGING_DEPENDENCY\}\}/g, config.enablePushNotifications ? ' implementation("com.google.firebase:firebase-messaging:24.1.0")' : "").replace(/\{\{HEALTH_CONNECT_DEPENDENCIES\}\}/g, config.enableHealthConnect ? ` implementation("androidx.health.connect:connect-client:1.1.0")
|
|
2033
2068
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")` : "");
|
|
2034
2069
|
writeFileSync4(join3(output, "app/build.gradle.kts"), appGradle);
|
|
2035
2070
|
const proguardTemplate = readFileSync4(join3(TEMPLATES_DIR2, "proguard-rules.pro.template"), "utf-8");
|
|
@@ -2147,6 +2182,11 @@ zipStorePath=wrapper/dists
|
|
|
2147
2182
|
</body>
|
|
2148
2183
|
</html>`;
|
|
2149
2184
|
writeFileSync4(join3(output, "app/src/main/assets/index.html"), placeholderHtml);
|
|
2185
|
+
const runtimeDir = resolveRuntimeDir2(options.runtimeDir);
|
|
2186
|
+
if (runtimeDir) {
|
|
2187
|
+
installRuntime2(output, runtimeDir);
|
|
2188
|
+
console.log(" Installed the Zig runtime from", runtimeDir);
|
|
2189
|
+
}
|
|
2150
2190
|
console.log("\u2705 Project initialized");
|
|
2151
2191
|
console.log("");
|
|
2152
2192
|
console.log("Next steps:");
|
|
@@ -2165,6 +2205,15 @@ async function build2(options) {
|
|
|
2165
2205
|
throw new Error(`No craft.config.json found in ${output}. Run 'craft android init' first.`);
|
|
2166
2206
|
}
|
|
2167
2207
|
const config = JSON.parse(readFileSync4(configPath, "utf-8"));
|
|
2208
|
+
if (existsSync5(join3(output, "app/src/main/jniLibs"))) {
|
|
2209
|
+
const runtimeDir = resolveRuntimeDir2(options.runtimeDir);
|
|
2210
|
+
if (runtimeDir) {
|
|
2211
|
+
installRuntime2(output, runtimeDir);
|
|
2212
|
+
console.log(" Refreshed the Zig runtime from", runtimeDir);
|
|
2213
|
+
} else {
|
|
2214
|
+
console.log(" Keeping the Zig runtime installed at init (no runtime directory configured)");
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2168
2217
|
if (devServer) {
|
|
2169
2218
|
const url = androidWebUrl(devServer, "Android dev server URL");
|
|
2170
2219
|
config.devServerURL = url.toString();
|
|
@@ -2313,7 +2362,7 @@ window.__craftRejectPendingPromises = function(message) {
|
|
|
2313
2362
|
if (entry) entry.settle(false, new Error(message || 'Android bridge closed'));
|
|
2314
2363
|
});
|
|
2315
2364
|
};
|
|
2316
|
-
`, TEMPLATES_DIR2, LOCAL_DEVELOPMENT_HOSTS, KOTLIN_KEYWORDS, DEFAULT_CONFIG2;
|
|
2365
|
+
`, TEMPLATES_DIR2, LOCAL_DEVELOPMENT_HOSTS, KOTLIN_KEYWORDS, ANDROID_MIN_SDK = 26, DEFAULT_CONFIG2, RUNTIME_ABIS;
|
|
2317
2366
|
var init_src2 = __esm(() => {
|
|
2318
2367
|
({ $: $2 } = globalThis.Bun);
|
|
2319
2368
|
TEMPLATES_DIR2 = join3(dirname2(import.meta.dir), "templates");
|
|
@@ -2367,10 +2416,11 @@ var init_src2 = __esm(() => {
|
|
|
2367
2416
|
enableDeepLinks: false,
|
|
2368
2417
|
urlSchemes: [],
|
|
2369
2418
|
trustedOrigins: [],
|
|
2370
|
-
minSdk:
|
|
2419
|
+
minSdk: ANDROID_MIN_SDK,
|
|
2371
2420
|
compileSdk: 36,
|
|
2372
2421
|
targetSdk: 35
|
|
2373
2422
|
};
|
|
2423
|
+
RUNTIME_ABIS = ["arm64-v8a", "x86_64"];
|
|
2374
2424
|
});
|
|
2375
2425
|
|
|
2376
2426
|
// ../../node_modules/@stacksjs/clapp/dist/index.js
|
|
@@ -4483,7 +4533,7 @@ function craftBinaryIsCliShimMessage(spawnedFrom) {
|
|
|
4483
4533
|
return [
|
|
4484
4534
|
`"${spawnedFrom}" on PATH is the Craft CLI, not the native binary.`,
|
|
4485
4535
|
"",
|
|
4486
|
-
"
|
|
4536
|
+
"Craft spawned it expecting the native build and reached this CLI instead \u2014",
|
|
4487
4537
|
"which is why an option you never typed (--url) came back as unknown.",
|
|
4488
4538
|
"",
|
|
4489
4539
|
"Fix it in one of two ways:",
|
|
@@ -4537,7 +4587,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
|
|
|
4537
4587
|
`);
|
|
4538
4588
|
}
|
|
4539
4589
|
// package.json
|
|
4540
|
-
var version = "0.0.
|
|
4590
|
+
var version = "0.0.94";
|
|
4541
4591
|
|
|
4542
4592
|
// bin/cli.ts
|
|
4543
4593
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
|
@@ -5025,10 +5075,16 @@ cli.command("publish", "Publish app to App Store or Play Store").option("--ios",
|
|
|
5025
5075
|
}
|
|
5026
5076
|
});
|
|
5027
5077
|
cli.command("init <name>", "Initialize a new Craft project").option("--template <type>", "Project template (blank, tabs, drawer, dashboard, desktop, ios, android, all)", { default: "blank" }).option("--bundle-id <id>", "Bundle identifier for mobile").example("craft init MyApp").example("craft init MyApp --template tabs").example("craft init MyApp --template dashboard").example("craft init MyApp --template all").action(async (name, options) => {
|
|
5078
|
+
const template = options?.template || "blank";
|
|
5079
|
+
const templates = ["blank", "tabs", "drawer", "dashboard", "desktop", "ios", "android", "all"];
|
|
5080
|
+
if (!templates.includes(template)) {
|
|
5081
|
+
console.error(`Error: Unknown template "${template}"`);
|
|
5082
|
+
console.error(`Available templates: ${templates.join(", ")}`);
|
|
5083
|
+
process6.exit(1);
|
|
5084
|
+
}
|
|
5028
5085
|
console.log(`
|
|
5029
5086
|
\u26A1 Creating new Craft project: ${name}
|
|
5030
5087
|
`);
|
|
5031
|
-
const template = options?.template || "blank";
|
|
5032
5088
|
const bundleId = options?.bundleId || `com.example.${name.toLowerCase().replace(/[^a-z0-9]/g, "")}`;
|
|
5033
5089
|
const appNameSlug = name.toLowerCase().replace(/[^a-z0-9]/g, "-");
|
|
5034
5090
|
const { mkdirSync: mkdirSync4, writeFileSync: writeFileSync5, existsSync: existsSync7, readdirSync: readdirSync3, readFileSync: readFileSync5, cpSync: cpSync4 } = await import("fs");
|