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 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
- * Initialize a new Android project
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: 26,
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 || 24)).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")
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
  };