janela 0.10.1 → 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.
- package/README.md +20 -0
- package/bin/janela.mjs +283 -12
- package/package.json +5 -3
- package/shim/android/app.cc +435 -0
- package/shim/android/java/dev/janela/host/JanelaActivity.java +26 -0
- package/shim/android/java/dev/webview/WebviewBridge.java +136 -0
- package/vendor-webview/core/include/webview/backends.hh +1 -0
- package/vendor-webview/core/include/webview/detail/backends/android_webkit.hh +541 -0
- package/vendor-webview/core/include/webview/detail/platform/android/jni.hh +167 -0
- package/vendor-webview/core/include/webview/detail/platform/android/webkit.hh +118 -0
- package/vendor-webview/core/include/webview/macros.h +8 -1
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
|
|
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(
|
|
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"], {
|
|
@@ -463,6 +463,264 @@ async function devIos(root) {
|
|
|
463
463
|
]);
|
|
464
464
|
}
|
|
465
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
|
+
|
|
466
724
|
// ---- shim ----------------------------------------------------------------
|
|
467
725
|
|
|
468
726
|
function which(bin) {
|
|
@@ -680,20 +938,28 @@ function makeGuiSubsystem(exePath) {
|
|
|
680
938
|
function build(root, { devUrl = null, gui = true, target = "desktop" } = {}) {
|
|
681
939
|
const conf = loadConf(root);
|
|
682
940
|
const ios = target === "ios";
|
|
683
|
-
const
|
|
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}`);
|
|
684
948
|
const cacheDir = join(root, ".janela", "cache");
|
|
685
|
-
const outDir = join(root, ".janela",
|
|
949
|
+
const outDir = join(root, ".janela", `out${suffix}`);
|
|
686
950
|
for (const d of [buildDir, cacheDir, outDir]) mkdirSync(d, { recursive: true });
|
|
687
951
|
|
|
688
952
|
// Desktop links a C shim over webview.h; iOS links no shim at all — the
|
|
689
953
|
// UIKit shell is the program, and it links this build's output instead.
|
|
690
|
-
const shimLib =
|
|
954
|
+
const shimLib = mobile ? null : buildShim(cacheDir);
|
|
691
955
|
|
|
692
956
|
// Assemble the compile unit: runtime + user's commands + generated modules.
|
|
693
957
|
// Which runtime lane lands here as "./janela" is the whole difference
|
|
694
958
|
// between the two targets — a project's main.ts is compiled unchanged
|
|
695
959
|
// against either.
|
|
696
|
-
|
|
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"));
|
|
697
963
|
cpSync(join(KIT, "runtime", "types.ts"), join(buildDir, "types.ts"));
|
|
698
964
|
const mainSrc = join(root, "src-host", "main.ts");
|
|
699
965
|
if (!existsSync(mainSrc)) fail("missing src-host/main.ts");
|
|
@@ -728,7 +994,7 @@ function build(root, { devUrl = null, gui = true, target = "desktop" } = {}) {
|
|
|
728
994
|
// The iOS entry exports the library's two entry points instead of running a
|
|
729
995
|
// loop: UIKit owns the loop and calls in. Everything above this line — the
|
|
730
996
|
// contract, main.ts, the flattened frontend — is identical to desktop.
|
|
731
|
-
const entryTail =
|
|
997
|
+
const entryTail = mobile
|
|
732
998
|
? `const app = createApp<CmdsOf<typeof setup>, EvtsOf<typeof setup>>(WINDOW);\n` +
|
|
733
999
|
`setup(app);\n` +
|
|
734
1000
|
`app.setHtml(INDEX_HTML);\n\n` +
|
|
@@ -774,6 +1040,7 @@ function build(root, { devUrl = null, gui = true, target = "desktop" } = {}) {
|
|
|
774
1040
|
);
|
|
775
1041
|
|
|
776
1042
|
if (ios) return buildIos(root, conf, buildDir, outDir);
|
|
1043
|
+
if (android) return buildAndroid(root, conf, buildDir, outDir);
|
|
777
1044
|
|
|
778
1045
|
writeFileSync(join(buildDir, "janela.ffi.json"), JSON.stringify(ffiManifest(shimLib), null, 2) + "\n");
|
|
779
1046
|
|
|
@@ -970,7 +1237,7 @@ function positionals() {
|
|
|
970
1237
|
return out;
|
|
971
1238
|
}
|
|
972
1239
|
|
|
973
|
-
const TARGETS = ["desktop", "ios"];
|
|
1240
|
+
const TARGETS = ["desktop", "ios", "android"];
|
|
974
1241
|
|
|
975
1242
|
function targetOrFail() {
|
|
976
1243
|
const t = flag("target", "desktop");
|
|
@@ -986,14 +1253,18 @@ switch (cmd) {
|
|
|
986
1253
|
build(process.cwd(), { target: targetOrFail() });
|
|
987
1254
|
break;
|
|
988
1255
|
case "dev":
|
|
989
|
-
|
|
990
|
-
|
|
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
|
+
}
|
|
991
1262
|
break;
|
|
992
1263
|
default:
|
|
993
1264
|
console.log(
|
|
994
1265
|
"usage: janela init <name> [--template vanilla|vue|react|svelte|solid]\n" +
|
|
995
|
-
" janela build [--target desktop|ios]\n" +
|
|
996
|
-
" janela dev [--target desktop|ios]",
|
|
1266
|
+
" janela build [--target desktop|ios|android]\n" +
|
|
1267
|
+
" janela dev [--target desktop|ios|android]",
|
|
997
1268
|
);
|
|
998
1269
|
process.exit(cmd ? 1 : 0);
|
|
999
1270
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "janela",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Desktop and
|
|
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"
|