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
package/README.md
CHANGED
|
@@ -453,6 +453,23 @@ See [benchmarks](../../benchmarks) for detailed performance comparisons.
|
|
|
453
453
|
- **Zero dependencies** - Pure Node.js APIs only
|
|
454
454
|
- **Better DX** - Hot reload and dev tools built-in
|
|
455
455
|
|
|
456
|
+
## Native bridge error codes
|
|
457
|
+
|
|
458
|
+
The mobile bridge declarations in `types/craft.d.ts` distinguish canonical
|
|
459
|
+
`NativeBridgeErrorCode` values from the broader compatible `CraftErrorCode` union.
|
|
460
|
+
The native wire vocabulary comes from `packages/zig/src/bridge_error.zig`; SDK
|
|
461
|
+
parity tests fail if the declaration drifts. It includes `INVALID_PARAMETER`,
|
|
462
|
+
`PLATFORM_NOT_SUPPORTED`, `NATIVE_CALL_FAILED`, `WEBVIEW_HANDLE_NOT_SET`, `BUSY`,
|
|
463
|
+
and `CAPABILITY_DISABLED`.
|
|
464
|
+
|
|
465
|
+
Existing mobile/shim names such as `INVALID_PARAMS` and `NOT_AVAILABLE` remain
|
|
466
|
+
accepted by `CraftErrorCode` for source compatibility. They are not aliases that
|
|
467
|
+
the SDK rewrites on the wire. Applications supporting multiple host versions
|
|
468
|
+
should handle the codes those hosts actually return; new native error handling
|
|
469
|
+
should use the canonical spellings. A capability disabled by app configuration
|
|
470
|
+
is distinct from a user denying permission, and `BUSY` is distinct from a failed
|
|
471
|
+
native call.
|
|
472
|
+
|
|
456
473
|
## Requirements
|
|
457
474
|
|
|
458
475
|
- Bun >= 1.0.0 (for development)
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Generates native Android apps from web content using WebView.
|
|
5
5
|
*/
|
|
6
|
+
/** Oldest Android API level supported by the generated Kotlin runtime. */
|
|
7
|
+
export declare const ANDROID_MIN_SDK = 26;
|
|
6
8
|
export interface CraftAndroidConfig {
|
|
7
9
|
appName: string;
|
|
8
10
|
packageName: string;
|
|
@@ -28,6 +30,7 @@ export interface CraftAndroidConfig {
|
|
|
28
30
|
googleServicesFile?: string;
|
|
29
31
|
devServerURL?: string;
|
|
30
32
|
hasBundledFallback?: boolean;
|
|
33
|
+
/** Android API level, at least ANDROID_MIN_SDK (26). */
|
|
31
34
|
minSdk?: number;
|
|
32
35
|
compileSdk?: number;
|
|
33
36
|
targetSdk?: number;
|
|
@@ -37,6 +40,14 @@ export interface InitOptions {
|
|
|
37
40
|
packageName?: string;
|
|
38
41
|
output: string;
|
|
39
42
|
config?: Partial<CraftAndroidConfig>;
|
|
43
|
+
/**
|
|
44
|
+
* Where to find the Zig runtime's `<abi>/libcraft.so`.
|
|
45
|
+
*
|
|
46
|
+
* `null` means no runtime regardless of the environment; `undefined` falls
|
|
47
|
+
* through to `CRAFT_ANDROID_RUNTIME`. Same shape as the iOS builder's
|
|
48
|
+
* `runtimeDir`.
|
|
49
|
+
*/
|
|
50
|
+
runtimeDir?: string | null;
|
|
40
51
|
}
|
|
41
52
|
export interface BuildOptions {
|
|
42
53
|
htmlPath?: string;
|
|
@@ -44,6 +55,8 @@ export interface BuildOptions {
|
|
|
44
55
|
output: string;
|
|
45
56
|
release?: boolean;
|
|
46
57
|
compile?: boolean;
|
|
58
|
+
/** Same meaning as `InitOptions.runtimeDir`; refreshes what init installed. */
|
|
59
|
+
runtimeDir?: string | null;
|
|
47
60
|
}
|
|
48
61
|
export interface OpenOptions {
|
|
49
62
|
output: string;
|
|
@@ -56,8 +69,33 @@ export declare function syncAndroidWebAssets(source: string, output: string): vo
|
|
|
56
69
|
export declare function renderAndroidPermissions(config: CraftAndroidConfig): string;
|
|
57
70
|
export declare function renderAndroidDeepLinks(config: CraftAndroidConfig): string;
|
|
58
71
|
/**
|
|
59
|
-
*
|
|
72
|
+
* Where the runtime directory comes from when the caller does not say.
|
|
73
|
+
*
|
|
74
|
+
* The same shape as `CRAFT_BIN` and the iOS builder's `CRAFT_IOS_RUNTIME`: an
|
|
75
|
+
* explicit override for the monorepo dev loop, not a lookup path. Shipping the
|
|
76
|
+
* runtime to real apps means putting these libraries in the pantry package
|
|
77
|
+
* beside the `craft` binary, which is a distribution decision this function
|
|
78
|
+
* does not make.
|
|
79
|
+
*/
|
|
80
|
+
export declare function resolveRuntimeDir(override?: string | null): string | null;
|
|
81
|
+
/**
|
|
82
|
+
* Copy the Zig runtime into the generated project as
|
|
83
|
+
* `app/src/main/jniLibs/<abi>/libcraft.so`.
|
|
84
|
+
*
|
|
85
|
+
* That path is AGP's default `jniLibs.srcDirs`, so nothing in the Gradle
|
|
86
|
+
* templates has to know about it — the library is packaged into the APK and
|
|
87
|
+
* `System.loadLibrary("craft")` finds it because the file is named for the
|
|
88
|
+
* `craft` it asks for.
|
|
89
|
+
*
|
|
90
|
+
* Until this existed, nothing put the library anywhere. `CraftNative` caught
|
|
91
|
+
* the `UnsatisfiedLinkError`, set `isAvailable = false`, and every action fell
|
|
92
|
+
* through to the Kotlin shim — by design, so an app with no runtime still
|
|
93
|
+
* works, which is also why nobody noticed that *every* generated app was in
|
|
94
|
+
* that state and the whole Android half of the Zig bridge had never run.
|
|
95
|
+
*
|
|
96
|
+
* Returns true when a runtime was installed.
|
|
60
97
|
*/
|
|
98
|
+
export declare function installRuntime(output: string, runtimeDir: string): boolean;
|
|
61
99
|
export declare function init(options: InitOptions): Promise<void>;
|
|
62
100
|
/**
|
|
63
101
|
* Build Android project
|
|
@@ -110,6 +110,7 @@ var KOTLIN_KEYWORDS = new Set([
|
|
|
110
110
|
"when",
|
|
111
111
|
"while"
|
|
112
112
|
]);
|
|
113
|
+
var ANDROID_MIN_SDK = 26;
|
|
113
114
|
var DEFAULT_CONFIG = {
|
|
114
115
|
version: "1.0.0",
|
|
115
116
|
versionCode: 1,
|
|
@@ -129,7 +130,7 @@ var DEFAULT_CONFIG = {
|
|
|
129
130
|
enableDeepLinks: false,
|
|
130
131
|
urlSchemes: [],
|
|
131
132
|
trustedOrigins: [],
|
|
132
|
-
minSdk:
|
|
133
|
+
minSdk: ANDROID_MIN_SDK,
|
|
133
134
|
compileSdk: 36,
|
|
134
135
|
targetSdk: 35
|
|
135
136
|
};
|
|
@@ -228,6 +229,9 @@ function validateAndroidConfig(config) {
|
|
|
228
229
|
throw new Error(`Android ${name} must be a positive integer`);
|
|
229
230
|
}
|
|
230
231
|
}
|
|
232
|
+
if (Number(config.minSdk) < ANDROID_MIN_SDK) {
|
|
233
|
+
throw new Error(`Android minSdk must be at least ${ANDROID_MIN_SDK} (Android 8.0) for the Craft runtime`);
|
|
234
|
+
}
|
|
231
235
|
if (Number(config.minSdk) > Number(config.targetSdk)) {
|
|
232
236
|
throw new Error("Android minSdk must not exceed targetSdk");
|
|
233
237
|
}
|
|
@@ -318,6 +322,37 @@ function renderAndroidDeepLinks(config) {
|
|
|
318
322
|
</intent-filter>`).join(`
|
|
319
323
|
`);
|
|
320
324
|
}
|
|
325
|
+
var RUNTIME_ABIS = ["arm64-v8a", "x86_64"];
|
|
326
|
+
function resolveRuntimeDir(override) {
|
|
327
|
+
if (override === null)
|
|
328
|
+
return null;
|
|
329
|
+
const dir = override ?? process.env.CRAFT_ANDROID_RUNTIME;
|
|
330
|
+
if (!dir)
|
|
331
|
+
return null;
|
|
332
|
+
if (!existsSync(dir)) {
|
|
333
|
+
const source = override === undefined ? "CRAFT_ANDROID_RUNTIME points at" : "runtimeDir is";
|
|
334
|
+
throw new Error(`${source} ${dir}, which does not exist.`);
|
|
335
|
+
}
|
|
336
|
+
return dir;
|
|
337
|
+
}
|
|
338
|
+
function installRuntime(output, runtimeDir) {
|
|
339
|
+
const found = RUNTIME_ABIS.map((abi) => ({ abi, source: join(runtimeDir, abi, "libcraft.so") })).filter((entry) => existsSync(entry.source));
|
|
340
|
+
if (found.length === 0) {
|
|
341
|
+
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.");
|
|
342
|
+
}
|
|
343
|
+
if (found.length < RUNTIME_ABIS.length) {
|
|
344
|
+
const missing = RUNTIME_ABIS.filter((abi) => !found.some((entry) => entry.abi === abi));
|
|
345
|
+
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.");
|
|
346
|
+
}
|
|
347
|
+
const dest = join(output, "app/src/main/jniLibs");
|
|
348
|
+
rmSync(dest, { force: true, recursive: true });
|
|
349
|
+
for (const { abi, source } of found) {
|
|
350
|
+
const abiDir = join(dest, abi);
|
|
351
|
+
mkdirSync(abiDir, { recursive: true });
|
|
352
|
+
cpSync(source, join(abiDir, "libcraft.so"));
|
|
353
|
+
}
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
321
356
|
async function init(options) {
|
|
322
357
|
const { name, packageName, output } = options;
|
|
323
358
|
console.log(`
|
|
@@ -335,7 +370,6 @@ async function init(options) {
|
|
|
335
370
|
if (config.enableBackgroundLocation)
|
|
336
371
|
config.enableGeolocation = true;
|
|
337
372
|
if (config.enableHealthConnect) {
|
|
338
|
-
config.minSdk = Math.max(config.minSdk ?? 26, 26);
|
|
339
373
|
config.compileSdk = Math.max(config.compileSdk ?? 36, 36);
|
|
340
374
|
}
|
|
341
375
|
normalizeAndroidNetworkConfig(config);
|
|
@@ -419,7 +453,7 @@ async function init(options) {
|
|
|
419
453
|
const projectGradleTemplate = readFileSync(join(TEMPLATES_DIR, "build.gradle.kts.project.template"), "utf-8");
|
|
420
454
|
writeFileSync(join(output, "build.gradle.kts"), projectGradleTemplate.replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services") version "4.4.2" apply false' : ""));
|
|
421
455
|
const appGradleTemplate = readFileSync(join(TEMPLATES_DIR, "build.gradle.kts.app.template"), "utf-8");
|
|
422
|
-
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
|
|
456
|
+
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")
|
|
423
457
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")` : "");
|
|
424
458
|
writeFileSync(join(output, "app/build.gradle.kts"), appGradle);
|
|
425
459
|
const proguardTemplate = readFileSync(join(TEMPLATES_DIR, "proguard-rules.pro.template"), "utf-8");
|
|
@@ -537,6 +571,11 @@ zipStorePath=wrapper/dists
|
|
|
537
571
|
</body>
|
|
538
572
|
</html>`;
|
|
539
573
|
writeFileSync(join(output, "app/src/main/assets/index.html"), placeholderHtml);
|
|
574
|
+
const runtimeDir = resolveRuntimeDir(options.runtimeDir);
|
|
575
|
+
if (runtimeDir) {
|
|
576
|
+
installRuntime(output, runtimeDir);
|
|
577
|
+
console.log(" Installed the Zig runtime from", runtimeDir);
|
|
578
|
+
}
|
|
540
579
|
console.log("\u2705 Project initialized");
|
|
541
580
|
console.log("");
|
|
542
581
|
console.log("Next steps:");
|
|
@@ -555,6 +594,15 @@ async function build(options) {
|
|
|
555
594
|
throw new Error(`No craft.config.json found in ${output}. Run 'craft android init' first.`);
|
|
556
595
|
}
|
|
557
596
|
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
597
|
+
if (existsSync(join(output, "app/src/main/jniLibs"))) {
|
|
598
|
+
const runtimeDir = resolveRuntimeDir(options.runtimeDir);
|
|
599
|
+
if (runtimeDir) {
|
|
600
|
+
installRuntime(output, runtimeDir);
|
|
601
|
+
console.log(" Refreshed the Zig runtime from", runtimeDir);
|
|
602
|
+
} else {
|
|
603
|
+
console.log(" Keeping the Zig runtime installed at init (no runtime directory configured)");
|
|
604
|
+
}
|
|
605
|
+
}
|
|
558
606
|
if (devServer) {
|
|
559
607
|
const url = androidWebUrl(devServer, "Android dev server URL");
|
|
560
608
|
config.devServerURL = url.toString();
|
|
@@ -641,9 +689,12 @@ async function run(options) {
|
|
|
641
689
|
export {
|
|
642
690
|
syncAndroidWebAssets,
|
|
643
691
|
run,
|
|
692
|
+
resolveRuntimeDir,
|
|
644
693
|
renderAndroidPermissions,
|
|
645
694
|
renderAndroidDeepLinks,
|
|
646
695
|
open,
|
|
696
|
+
installRuntime,
|
|
647
697
|
init,
|
|
648
|
-
build
|
|
698
|
+
build,
|
|
699
|
+
ANDROID_MIN_SDK
|
|
649
700
|
};
|