janela 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +20 -0
  2. package/bin/janela.mjs +289 -16
  3. package/package.json +5 -3
  4. package/shim/android/app.cc +435 -0
  5. package/shim/android/java/dev/janela/host/JanelaActivity.java +26 -0
  6. package/shim/android/java/dev/webview/WebviewBridge.java +136 -0
  7. package/shim/ios/app.cc +330 -0
  8. package/vendor-webview/core/include/webview/backends.hh +2 -0
  9. package/vendor-webview/core/include/webview/detail/backends/android_webkit.hh +541 -0
  10. package/vendor-webview/core/include/webview/detail/backends/cocoa_webkit.hh +4 -1
  11. package/vendor-webview/core/include/webview/detail/backends/uikit_webkit.hh +460 -0
  12. package/vendor-webview/core/include/webview/detail/platform/android/jni.hh +167 -0
  13. package/vendor-webview/core/include/webview/detail/platform/android/webkit.hh +118 -0
  14. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSBundle.hh +4 -2
  15. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSInvocation.hh +4 -2
  16. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSMethodSignature.hh +4 -2
  17. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSNotification.hh +4 -2
  18. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSNumber.hh +4 -2
  19. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSObject.hh +4 -2
  20. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSPoint.hh +4 -2
  21. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSRect.hh +4 -2
  22. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSSize.hh +4 -2
  23. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSString.hh +4 -2
  24. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSURL.hh +4 -2
  25. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSURLRequest.hh +4 -2
  26. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSValue.hh +4 -2
  27. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/types.hh +4 -2
  28. package/vendor-webview/core/include/webview/detail/platform/darwin/objc/Class.hh +4 -1
  29. package/vendor-webview/core/include/webview/detail/platform/darwin/objc/autoreleasepool.hh +4 -1
  30. package/vendor-webview/core/include/webview/detail/platform/darwin/objc/invoke.hh +4 -1
  31. package/vendor-webview/core/include/webview/detail/platform/darwin/objc/memory.hh +4 -1
  32. package/vendor-webview/core/include/webview/detail/platform/darwin/objc/objc.hh +4 -1
  33. package/vendor-webview/core/include/webview/detail/platform/darwin/uikit/UIApplication.hh +80 -0
  34. package/vendor-webview/core/include/webview/detail/platform/darwin/uikit/UIScreen.hh +57 -0
  35. package/vendor-webview/core/include/webview/detail/platform/darwin/uikit/UIView.hh +79 -0
  36. package/vendor-webview/core/include/webview/detail/platform/darwin/uikit/UIViewController.hh +70 -0
  37. package/vendor-webview/core/include/webview/detail/platform/darwin/uikit/UIWindow.hh +67 -0
  38. package/vendor-webview/core/include/webview/detail/platform/darwin/uikit/uikit.hh +43 -0
  39. package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKOpenPanelParameters.hh +4 -2
  40. package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKScriptMessage.hh +4 -2
  41. package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKUserContentController.hh +4 -2
  42. package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKUserScript.hh +4 -2
  43. package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKWebView.hh +4 -2
  44. package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKWebViewConfiguration.hh +4 -2
  45. package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/webkit.hh +4 -2
  46. package/vendor-webview/core/include/webview/macros.h +19 -2
  47. package/shim/ios/app.mm +0 -386
package/README.md CHANGED
@@ -559,6 +559,26 @@ clock and the file queue on both. File dialogs are not on iOS yet and report
559
559
  clearly when called; window control is a no-op there by nature. See
560
560
  [docs/ios.md](../../docs/ios.md).
561
561
 
562
+ ## Android
563
+
564
+ Same again: same `main.ts`, same contract, same frontend.
565
+
566
+ ```bash
567
+ janela build --target android # -> .janela/out-android/<name>.apk
568
+ janela dev --target android # build, boot an emulator, install, launch, follow logcat
569
+ ```
570
+
571
+ Needs a JDK, the Android SDK, the NDK and zig; there is no Gradle in the build.
572
+ Commands, events, `commandAsync`/`sleep`/`defer` and file I/O all behave as
573
+ they do on desktop and iOS — the shell owns the clock on each. Native dialogs
574
+ are not on Android yet and report clearly when called; `setTitle` sets the
575
+ Activity label and the other window controls are no-ops by nature. See
576
+ [docs/android.md](../../docs/android.md).
577
+
578
+ Unlike every other platform an APK also carries a little Java: the webview
579
+ backend needs a companion class, because `android.webkit.WebView` is a Java API
580
+ whose callbacks native code cannot receive on its own.
581
+
562
582
  ## Status
563
583
 
564
584
  Early proof of concept, on macOS (arm64), Linux (WebKitGTK) and Windows
package/bin/janela.mjs CHANGED
@@ -309,7 +309,7 @@ function iosConf(conf) {
309
309
  // The library profile: one export carries every command, so a project's own
310
310
  // commands need no ABI of their own. `janelaEmit` is the reverse channel the
311
311
  // shell registers before init.
312
- function iosProfile() {
312
+ function libraryProfile() {
313
313
  return {
314
314
  profile_format: 1,
315
315
  name: "janela",
@@ -400,7 +400,7 @@ function buildIos(root, conf, buildDir, outDir) {
400
400
  }
401
401
  const ios = iosConf(conf);
402
402
 
403
- writeFileSync(join(buildDir, "profile.json"), JSON.stringify(iosProfile(), null, 2) + "\n");
403
+ writeFileSync(join(buildDir, "profile.json"), JSON.stringify(libraryProfile(), null, 2) + "\n");
404
404
 
405
405
  console.log("janela: compiling TypeScript to an iOS library");
406
406
  run(["node", scriptcBin(), "build", "--lib", "--profile", "profile.json"], {
@@ -418,14 +418,16 @@ function buildIos(root, conf, buildDir, outDir) {
418
418
  const bundle = join(outDir, `${conf.name}.app`);
419
419
  mkdirSync(bundle, { recursive: true });
420
420
  const sdk = capture(["xcrun", "--sdk", "iphonesimulator", "--show-sdk-path"]);
421
- // The .mm extension picks Objective-C++; an explicit `-x objective-c++`
422
- // would leak onto the archive that follows and be compiled as source.
421
+ // Plain C++: the shell drives the webview through webview.h's UIKit
422
+ // backend, which uses the Objective-C runtime rather than Objective-C, so
423
+ // no .mm and no -fobjc-arc. Blocks still need -fblocks for libdispatch.
423
424
  run([
424
425
  "xcrun", "clang++",
425
- join(KIT, "shim", "ios", "app.mm"),
426
+ join(KIT, "shim", "ios", "app.cc"),
426
427
  "-target", `arm64-apple-ios${ios.minimumVersion}-simulator`,
427
428
  "-isysroot", sdk,
428
- "-fobjc-arc", "-std=c++17", "-O2",
429
+ "-std=c++17", "-O2", "-fblocks",
430
+ `-I${join(KIT, "vendor-webview", "core", "include")}`,
429
431
  "-framework", "UIKit", "-framework", "WebKit", "-framework", "Foundation",
430
432
  lib,
431
433
  "-o", join(bundle, conf.name),
@@ -461,6 +463,264 @@ async function devIos(root) {
461
463
  ]);
462
464
  }
463
465
 
466
+
467
+ // ---- Android ---------------------------------------------------------------
468
+ //
469
+ // Android is library-mode like iOS: the system owns the Activity and its
470
+ // Looper, and the app's TypeScript is a linked scriptc library the shell calls
471
+ // into. Unlike every other target the APK also carries Java — the webview
472
+ // backend needs a companion class because android.webkit.WebView is a Java API
473
+ // and native code cannot define a class to receive its callbacks.
474
+
475
+ const ANDROID_MIN_SDK = 26;
476
+ const ANDROID_TARGET_SDK = 34;
477
+ const ANDROID_ABI = "arm64-v8a";
478
+
479
+ /// Android package names are Java package names: dot-separated identifiers,
480
+ /// so no hyphens. A janela project may be called `my-app`, which makes the
481
+ /// default identifier `dev.janela.my-app` — legal everywhere else and not
482
+ /// here, so each segment is coerced rather than failing the build.
483
+ function androidPackage(id) {
484
+ return id
485
+ .split(".")
486
+ .map((seg) => {
487
+ const cleaned = seg.replace(/[^A-Za-z0-9_]/g, "_");
488
+ return /^[A-Za-z_]/.test(cleaned) ? cleaned : `_${cleaned}`;
489
+ })
490
+ .join(".");
491
+ }
492
+
493
+ function androidConf(conf) {
494
+ const a = conf.android ?? {};
495
+ return {
496
+ applicationId: androidPackage(a.applicationId ?? a.identifier ?? conf.identifier),
497
+ label: a.label ?? conf.window?.title ?? conf.name,
498
+ minSdk: String(a.minSdk ?? ANDROID_MIN_SDK),
499
+ device: a.device ?? null,
500
+ };
501
+ }
502
+
503
+ /// The SDK pieces an Android build needs, or a message saying which is absent.
504
+ function androidSdk() {
505
+ const home =
506
+ process.env.ANDROID_HOME ??
507
+ process.env.ANDROID_SDK_ROOT ??
508
+ join(process.env.HOME ?? "", "Library", "Android", "sdk");
509
+ if (!existsSync(home)) {
510
+ fail(
511
+ "Android builds need the Android SDK. Install it (Android Studio, or " +
512
+ "`sdkmanager`) and set ANDROID_HOME",
513
+ );
514
+ }
515
+ const ndkRoot = process.env.ANDROID_NDK_ROOT ?? null;
516
+ const ndks = existsSync(join(home, "ndk"))
517
+ ? readdirSync(join(home, "ndk")).sort()
518
+ : [];
519
+ const ndk = ndkRoot ?? (ndks.length ? join(home, "ndk", ndks[ndks.length - 1]) : null);
520
+ if (!ndk || !existsSync(ndk)) {
521
+ fail(
522
+ "Android builds need the NDK: `sdkmanager --install 'ndk;27.0.12077973'`, " +
523
+ "or set ANDROID_NDK_ROOT",
524
+ );
525
+ }
526
+ const buildToolsDir = join(home, "build-tools");
527
+ const versions = existsSync(buildToolsDir) ? readdirSync(buildToolsDir).sort() : [];
528
+ if (!versions.length) fail("Android builds need build-tools: `sdkmanager --install 'build-tools;36.0.0'`");
529
+ const bt = join(buildToolsDir, versions[versions.length - 1]);
530
+
531
+ const platformsDir = join(home, "platforms");
532
+ const platforms = existsSync(platformsDir) ? readdirSync(platformsDir).sort() : [];
533
+ if (!platforms.length) fail("Android builds need a platform: `sdkmanager --install 'platforms;android-36'`");
534
+ const androidJar = join(platformsDir, platforms[platforms.length - 1], "android.jar");
535
+
536
+ // The NDK ships darwin-x86_64 host binaries even on Apple silicon.
537
+ const hosts = readdirSync(join(ndk, "toolchains", "llvm", "prebuilt"));
538
+ const toolchain = join(ndk, "toolchains", "llvm", "prebuilt", hosts[0], "bin");
539
+
540
+ return { home, ndk, bt, androidJar, toolchain, adb: join(home, "platform-tools", "adb") };
541
+ }
542
+
543
+ function javaHome() {
544
+ if (process.env.JAVA_HOME) return process.env.JAVA_HOME;
545
+ const brew = "/opt/homebrew/opt/openjdk/libexec/openjdk.jdk/Contents/Home";
546
+ if (existsSync(brew)) return brew;
547
+ fail("Android builds need a JDK. Install one (`brew install openjdk`) and set JAVA_HOME");
548
+ }
549
+
550
+ function androidManifest(conf) {
551
+ const a = androidConf(conf);
552
+ return `<?xml version="1.0" encoding="utf-8"?>
553
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
554
+ package="${a.applicationId}"
555
+ android:versionCode="1"
556
+ android:versionName="${conf.version ?? "0.1.0"}">
557
+ <uses-permission android:name="android.permission.INTERNET"/>
558
+ <application android:label="${a.label}" android:hasCode="true">
559
+ <activity android:name="dev.janela.host.JanelaActivity" android:exported="true">
560
+ <intent-filter>
561
+ <action android:name="android.intent.action.MAIN"/>
562
+ <category android:name="android.intent.category.LAUNCHER"/>
563
+ </intent-filter>
564
+ </activity>
565
+ </application>
566
+ </manifest>
567
+ `;
568
+ }
569
+
570
+ /// A debug keystore, generated once and cached. Android refuses to install an
571
+ /// unsigned APK; a debug key is enough for a simulator or a developer device.
572
+ function debugKeystore(cacheDir, jdk) {
573
+ const ks = join(cacheDir, "debug.keystore");
574
+ if (existsSync(ks)) return ks;
575
+ run([
576
+ join(jdk, "bin", "keytool"), "-genkeypair", "-keystore", ks,
577
+ "-storepass", "android", "-keypass", "android", "-alias", "androiddebugkey",
578
+ "-dname", "CN=Android Debug,O=janela,C=US",
579
+ "-keyalg", "RSA", "-keysize", "2048", "-validity", "10000",
580
+ ]);
581
+ return ks;
582
+ }
583
+
584
+ function buildAndroid(root, conf, buildDir, outDir) {
585
+ const sdk = androidSdk();
586
+ const jdk = javaHome();
587
+ if (!which("zig")) {
588
+ fail(
589
+ "Android builds need zig on PATH: scriptc routes mobile targets through " +
590
+ "`zig cc`. Install it with `brew install zig`",
591
+ );
592
+ }
593
+ const a = androidConf(conf);
594
+ const cacheDir = join(root, ".janela", "cache");
595
+
596
+ writeFileSync(join(buildDir, "profile.json"), JSON.stringify(libraryProfile(), null, 2) + "\n");
597
+
598
+ console.log("janela: compiling TypeScript to an Android library");
599
+ run(["node", scriptcBin(), "build", "--lib", "--profile", "profile.json"], {
600
+ cwd: buildDir,
601
+ env: {
602
+ ...process.env,
603
+ SCRIPTC_CC: "zigcc",
604
+ SCRIPTC_TARGET: "aarch64-linux-android",
605
+ ANDROID_NDK_ROOT: sdk.ndk,
606
+ },
607
+ });
608
+ const lib = join(buildDir, ".scriptc", "entry.lib.a");
609
+ if (!existsSync(lib)) fail(`scriptc produced no library at ${lib}`);
610
+
611
+ // The shell is a shared library the Activity loads; Android has no main().
612
+ console.log("janela: compiling the Android shell");
613
+ const stage = join(buildDir, "apk");
614
+ const jniDir = join(stage, "lib", ANDROID_ABI);
615
+ mkdirSync(jniDir, { recursive: true });
616
+ const so = join(jniDir, "libjanela.so");
617
+ run([
618
+ join(sdk.toolchain, `aarch64-linux-android${a.minSdk}-clang++`),
619
+ join(KIT, "shim", "android", "app.cc"),
620
+ "-shared", "-fPIC", "-std=c++17", "-O2",
621
+ // The NDK links libc++ dynamically by default, which would mean shipping
622
+ // libc++_shared.so beside ours; static keeps the APK to one library.
623
+ "-static-libstdc++",
624
+ `-I${join(KIT, "vendor-webview", "core", "include")}`,
625
+ lib, "-llog", "-o", so,
626
+ ]);
627
+ run([join(sdk.toolchain, "llvm-strip"), so]);
628
+
629
+ // The companion Java class the backend requires, plus janela's Activity.
630
+ console.log("janela: compiling the Java bridge");
631
+ const classes = join(buildDir, "classes");
632
+ mkdirSync(classes, { recursive: true });
633
+ const javaSrc = join(KIT, "shim", "android", "java");
634
+ const sources = [
635
+ join(javaSrc, "dev", "webview", "WebviewBridge.java"),
636
+ join(javaSrc, "dev", "janela", "host", "JanelaActivity.java"),
637
+ ];
638
+ run([
639
+ join(jdk, "bin", "javac"), "-source", "11", "-target", "11", "-nowarn",
640
+ "-classpath", sdk.androidJar, "-d", classes, ...sources,
641
+ ]);
642
+ // d8 needs every class file named, including the inner classes javac
643
+ // emitted for the @JavascriptInterface object and the WebViewClient.
644
+ const classFiles = [];
645
+ const collect = (dir) => {
646
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
647
+ const p = join(dir, entry.name);
648
+ if (entry.isDirectory()) collect(p);
649
+ else if (entry.name.endsWith(".class")) classFiles.push(p);
650
+ }
651
+ };
652
+ collect(classes);
653
+ run([join(sdk.bt, "d8"), "--min-api", a.minSdk, "--output", stage, ...classFiles]);
654
+
655
+ writeFileSync(join(buildDir, "AndroidManifest.xml"), androidManifest(conf));
656
+
657
+ console.log("janela: packaging the APK");
658
+ const unsigned = join(buildDir, "unsigned.apk");
659
+ run([
660
+ join(sdk.bt, "aapt2"), "link", "-o", unsigned, "-I", sdk.androidJar,
661
+ "--manifest", join(buildDir, "AndroidManifest.xml"),
662
+ "--min-sdk-version", a.minSdk,
663
+ "--target-sdk-version", String(ANDROID_TARGET_SDK),
664
+ ]);
665
+ // aapt2 emits the manifest and resources; the code and the shared library
666
+ // are added to the same zip afterwards.
667
+ run(["zip", "-q", "-r", unsigned, "classes.dex", "lib"], { cwd: stage });
668
+
669
+ const aligned = join(buildDir, "aligned.apk");
670
+ run([join(sdk.bt, "zipalign"), "-f", "4", unsigned, aligned]);
671
+ const apk = join(outDir, `${conf.name}.apk`);
672
+ run([
673
+ join(sdk.bt, "apksigner"), "sign",
674
+ "--ks", debugKeystore(cacheDir, jdk),
675
+ "--ks-pass", "pass:android", "--key-pass", "pass:android",
676
+ "--out", apk, aligned,
677
+ ]);
678
+
679
+ console.log(
680
+ `janela: built ${relative(root, apk)} ` +
681
+ `(${statSync(apk).size} bytes, ${ANDROID_ABI}; .so ${statSync(so).size} bytes)`,
682
+ );
683
+ return apk;
684
+ }
685
+
686
+ /// Boots an emulator if none is running, then installs and launches.
687
+ async function devAndroid(root) {
688
+ const sdk = androidSdk();
689
+ const conf = loadConf(root);
690
+ const a = androidConf(conf);
691
+ const apk = build(root, { target: "android" });
692
+
693
+ const devices = capture([sdk.adb, "devices"]).split("\n").slice(1)
694
+ .filter((l) => l.trim().endsWith("device"));
695
+ if (!devices.length) {
696
+ const avds = capture([join(sdk.home, "emulator", "emulator"), "-list-avds"])
697
+ .split("\n").map((s) => s.trim()).filter(Boolean);
698
+ if (!avds.length) {
699
+ fail("no Android device or emulator. Create an AVD in Android Studio, or attach a device");
700
+ }
701
+ const avd = a.device ?? avds[0];
702
+ console.log(`janela: booting ${avd}`);
703
+ spawn(join(sdk.home, "emulator", "emulator"), ["-avd", avd, "-no-snapshot-save"], {
704
+ detached: true, stdio: "ignore",
705
+ }).unref();
706
+ run([sdk.adb, "wait-for-device"]);
707
+ // wait-for-device returns as soon as adb can talk to it; the package
708
+ // manager is not up until the boot animation has finished.
709
+ for (;;) {
710
+ const done = capture([sdk.adb, "shell", "getprop", "sys.boot_completed"]).trim();
711
+ if (done === "1") break;
712
+ await new Promise((r) => setTimeout(r, 2000));
713
+ }
714
+ }
715
+
716
+ console.log("janela: installing");
717
+ run([sdk.adb, "install", "-r", apk]);
718
+ run([sdk.adb, "shell", "am", "start", "-n",
719
+ `${a.applicationId}/dev.janela.host.JanelaActivity`]);
720
+ console.log("janela: running (Ctrl-C to stop following the log)");
721
+ run([sdk.adb, "logcat", "-s", "janela:*", "chromium:*", "AndroidRuntime:*"]);
722
+ }
723
+
464
724
  // ---- shim ----------------------------------------------------------------
465
725
 
466
726
  function which(bin) {
@@ -678,20 +938,28 @@ function makeGuiSubsystem(exePath) {
678
938
  function build(root, { devUrl = null, gui = true, target = "desktop" } = {}) {
679
939
  const conf = loadConf(root);
680
940
  const ios = target === "ios";
681
- const buildDir = join(root, ".janela", ios ? "build-ios" : "build");
941
+ const android = target === "android";
942
+ // Both mobile targets are library-mode: the platform owns the loop and the
943
+ // shell calls into a linked scriptc library. Everything above the shell is
944
+ // shared between them.
945
+ const mobile = ios || android;
946
+ const suffix = ios ? "-ios" : android ? "-android" : "";
947
+ const buildDir = join(root, ".janela", `build${suffix}`);
682
948
  const cacheDir = join(root, ".janela", "cache");
683
- const outDir = join(root, ".janela", ios ? "out-ios" : "out");
949
+ const outDir = join(root, ".janela", `out${suffix}`);
684
950
  for (const d of [buildDir, cacheDir, outDir]) mkdirSync(d, { recursive: true });
685
951
 
686
952
  // Desktop links a C shim over webview.h; iOS links no shim at all — the
687
953
  // UIKit shell is the program, and it links this build's output instead.
688
- const shimLib = ios ? null : buildShim(cacheDir);
954
+ const shimLib = mobile ? null : buildShim(cacheDir);
689
955
 
690
956
  // Assemble the compile unit: runtime + user's commands + generated modules.
691
957
  // Which runtime lane lands here as "./janela" is the whole difference
692
958
  // between the two targets — a project's main.ts is compiled unchanged
693
959
  // against either.
694
- cpSync(join(KIT, "runtime", ios ? "ios.ts" : "janela.ts"), join(buildDir, "janela.ts"));
960
+ // ios.ts is the library-mode runtime: Android uses it unchanged, which is
961
+ // the point — the same TypeScript serves both mobile shells.
962
+ cpSync(join(KIT, "runtime", mobile ? "ios.ts" : "janela.ts"), join(buildDir, "janela.ts"));
695
963
  cpSync(join(KIT, "runtime", "types.ts"), join(buildDir, "types.ts"));
696
964
  const mainSrc = join(root, "src-host", "main.ts");
697
965
  if (!existsSync(mainSrc)) fail("missing src-host/main.ts");
@@ -726,7 +994,7 @@ function build(root, { devUrl = null, gui = true, target = "desktop" } = {}) {
726
994
  // The iOS entry exports the library's two entry points instead of running a
727
995
  // loop: UIKit owns the loop and calls in. Everything above this line — the
728
996
  // contract, main.ts, the flattened frontend — is identical to desktop.
729
- const entryTail = ios
997
+ const entryTail = mobile
730
998
  ? `const app = createApp<CmdsOf<typeof setup>, EvtsOf<typeof setup>>(WINDOW);\n` +
731
999
  `setup(app);\n` +
732
1000
  `app.setHtml(INDEX_HTML);\n\n` +
@@ -772,6 +1040,7 @@ function build(root, { devUrl = null, gui = true, target = "desktop" } = {}) {
772
1040
  );
773
1041
 
774
1042
  if (ios) return buildIos(root, conf, buildDir, outDir);
1043
+ if (android) return buildAndroid(root, conf, buildDir, outDir);
775
1044
 
776
1045
  writeFileSync(join(buildDir, "janela.ffi.json"), JSON.stringify(ffiManifest(shimLib), null, 2) + "\n");
777
1046
 
@@ -968,7 +1237,7 @@ function positionals() {
968
1237
  return out;
969
1238
  }
970
1239
 
971
- const TARGETS = ["desktop", "ios"];
1240
+ const TARGETS = ["desktop", "ios", "android"];
972
1241
 
973
1242
  function targetOrFail() {
974
1243
  const t = flag("target", "desktop");
@@ -984,14 +1253,18 @@ switch (cmd) {
984
1253
  build(process.cwd(), { target: targetOrFail() });
985
1254
  break;
986
1255
  case "dev":
987
- if (targetOrFail() === "ios") await devIos(process.cwd());
988
- else await dev(process.cwd());
1256
+ {
1257
+ const t = targetOrFail();
1258
+ if (t === "ios") await devIos(process.cwd());
1259
+ else if (t === "android") await devAndroid(process.cwd());
1260
+ else await dev(process.cwd());
1261
+ }
989
1262
  break;
990
1263
  default:
991
1264
  console.log(
992
1265
  "usage: janela init <name> [--template vanilla|vue|react|svelte|solid]\n" +
993
- " janela build [--target desktop|ios]\n" +
994
- " janela dev [--target desktop|ios]",
1266
+ " janela build [--target desktop|ios|android]\n" +
1267
+ " janela dev [--target desktop|ios|android]",
995
1268
  );
996
1269
  process.exit(cmd ? 1 : 0);
997
1270
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "janela",
3
- "version": "0.10.0",
4
- "description": "Desktop and iOS apps in pure TypeScript, compiled to native. No Rust, no Node, no Electron.",
3
+ "version": "0.11.0",
4
+ "description": "Desktop, iOS and Android apps in pure TypeScript, compiled to native. No Rust, no Node, no Electron.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "janela": "bin/janela.mjs",
@@ -41,7 +41,9 @@
41
41
  "tauri",
42
42
  "electron-alternative",
43
43
  "scriptc",
44
- "ios"
44
+ "ios",
45
+ "android",
46
+ "mobile"
45
47
  ],
46
48
  "engines": {
47
49
  "node": ">=24"