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.
- package/README.md +20 -0
- package/bin/janela.mjs +289 -16
- 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/shim/ios/app.cc +330 -0
- package/vendor-webview/core/include/webview/backends.hh +2 -0
- package/vendor-webview/core/include/webview/detail/backends/android_webkit.hh +541 -0
- package/vendor-webview/core/include/webview/detail/backends/cocoa_webkit.hh +4 -1
- package/vendor-webview/core/include/webview/detail/backends/uikit_webkit.hh +460 -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/detail/platform/darwin/cocoa/NSBundle.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSInvocation.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSMethodSignature.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSNotification.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSNumber.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSObject.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSPoint.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSRect.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSSize.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSString.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSURL.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSURLRequest.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSValue.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/types.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/objc/Class.hh +4 -1
- package/vendor-webview/core/include/webview/detail/platform/darwin/objc/autoreleasepool.hh +4 -1
- package/vendor-webview/core/include/webview/detail/platform/darwin/objc/invoke.hh +4 -1
- package/vendor-webview/core/include/webview/detail/platform/darwin/objc/memory.hh +4 -1
- package/vendor-webview/core/include/webview/detail/platform/darwin/objc/objc.hh +4 -1
- package/vendor-webview/core/include/webview/detail/platform/darwin/uikit/UIApplication.hh +80 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/uikit/UIScreen.hh +57 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/uikit/UIView.hh +79 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/uikit/UIViewController.hh +70 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/uikit/UIWindow.hh +67 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/uikit/uikit.hh +43 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKOpenPanelParameters.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKScriptMessage.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKUserContentController.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKUserScript.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKWebView.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKWebViewConfiguration.hh +4 -2
- package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/webkit.hh +4 -2
- package/vendor-webview/core/include/webview/macros.h +19 -2
- 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
|
|
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"], {
|
|
@@ -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
|
-
//
|
|
422
|
-
//
|
|
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.
|
|
426
|
+
join(KIT, "shim", "ios", "app.cc"),
|
|
426
427
|
"-target", `arm64-apple-ios${ios.minimumVersion}-simulator`,
|
|
427
428
|
"-isysroot", sdk,
|
|
428
|
-
"-
|
|
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
|
|
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",
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
988
|
-
|
|
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.
|
|
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"
|