janela 0.9.0 → 0.10.1

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 (46) hide show
  1. package/README.md +19 -1
  2. package/bin/janela.mjs +262 -12
  3. package/package.json +4 -3
  4. package/runtime/ios.ts +483 -0
  5. package/shim/ios/app.cc +330 -0
  6. package/templates/janela.conf.json +5 -0
  7. package/templates/react/files/janela.conf.json +5 -0
  8. package/templates/solid/files/janela.conf.json +5 -0
  9. package/templates/svelte/files/janela.conf.json +5 -0
  10. package/templates/vue/files/janela.conf.json +5 -0
  11. package/vendor-webview/core/include/webview/backends.hh +1 -0
  12. package/vendor-webview/core/include/webview/detail/backends/cocoa_webkit.hh +4 -1
  13. package/vendor-webview/core/include/webview/detail/backends/uikit_webkit.hh +460 -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 +12 -2
package/README.md CHANGED
@@ -542,10 +542,28 @@ no `-framework` support) and the binary is wrapped into an ad-hoc-signed
542
542
  - `console.log` from commands goes to stdout — visible under `janela dev`,
543
543
  not when launched from Finder.
544
544
 
545
+ ## iOS
546
+
547
+ An iOS build runs the same app from the same source — same `main.ts`, same
548
+ contract, same frontend:
549
+
550
+ ```bash
551
+ janela build --target ios # -> .janela/out-ios/<name>.app (simulator)
552
+ janela dev --target ios # build, boot a simulator, install, launch
553
+ ```
554
+
555
+ It is **not part of a release yet** and is simulator-only. Commands, the typed
556
+ contract, events, Vite frontends, async commands (`commandAsync`, `defer`,
557
+ `sleep`) and file I/O all work the same as on desktop — the shell owns the
558
+ clock and the file queue on both. File dialogs are not on iOS yet and report
559
+ clearly when called; window control is a no-op there by nature. See
560
+ [docs/ios.md](../../docs/ios.md).
561
+
545
562
  ## Status
546
563
 
547
564
  Early proof of concept, on macOS (arm64), Linux (WebKitGTK) and Windows
548
- (WebView2). The design notes and scriptc findings behind it are in
565
+ (WebView2), with iOS on a branch (above). The design notes and scriptc
566
+ findings behind it are in
549
567
  [docs/findings.md](../../docs/findings.md). Not yet: async commands that run in
550
568
  parallel (host code is single-threaded; `commandAsync` interleaves instead),
551
569
  tray icons and menus, multi-window, directory picking on Windows,
package/bin/janela.mjs CHANGED
@@ -263,8 +263,215 @@ function webview2Include(cacheDir) {
263
263
  return incDir;
264
264
  }
265
265
 
266
+ // ---- iOS ------------------------------------------------------------------
267
+ //
268
+ // The second build lane. Desktop compiles TypeScript to an EXECUTABLE that
269
+ // drives a C library over FFI; iOS compiles it to a LIBRARY that a UIKit shell
270
+ // drives. scriptc refuses executables for iOS targets, so the inversion is not
271
+ // a preference — and library mode links no event loop (SC4005), which is why
272
+ // the async surface is desktop-only there.
273
+ //
274
+ // Everything below is simulator-only: a device build additionally needs a
275
+ // signing identity and a provisioning profile, which is its own project.
276
+
277
+ const IOS_MIN_VERSION = "15.0";
278
+ const IOS_PREFIX = "jl_";
279
+
280
+ function iosDeviceOrFail(name) {
281
+ const json = capture(["xcrun", "simctl", "list", "devices", "available", "--json"]);
282
+ const devices = JSON.parse(json).devices ?? {};
283
+ const runtimes = Object.keys(devices).filter((k) => /iOS/i.test(k));
284
+ if (runtimes.length === 0) {
285
+ fail(
286
+ "no iOS simulator runtime is installed — open Xcode > Settings > Components " +
287
+ "and get an iOS simulator, or run `xcodebuild -downloadPlatform iOS` in a terminal " +
288
+ "(it needs admin rights, so it cannot run unattended)",
289
+ );
290
+ }
291
+ const all = runtimes.flatMap((k) => devices[k]);
292
+ const pick = name
293
+ ? all.find((d) => d.name === name)
294
+ : all.find((d) => /^iPhone/.test(d.name)) ?? all[0];
295
+ if (!pick) fail(`no simulator named '${name}' — see \`xcrun simctl list devices\``);
296
+ return pick;
297
+ }
298
+
299
+ function iosConf(conf) {
300
+ const ios = conf.ios ?? {};
301
+ return {
302
+ identifier: ios.identifier ?? conf.identifier,
303
+ displayName: ios.displayName ?? conf.window?.title ?? conf.name,
304
+ minimumVersion: String(ios.minimumVersion ?? IOS_MIN_VERSION),
305
+ device: ios.device ?? null,
306
+ };
307
+ }
308
+
309
+ // The library profile: one export carries every command, so a project's own
310
+ // commands need no ABI of their own. `janelaEmit` is the reverse channel the
311
+ // shell registers before init.
312
+ function iosProfile() {
313
+ return {
314
+ profile_format: 1,
315
+ name: "janela",
316
+ entry: "./entry.ts",
317
+ emission: "llvm",
318
+ abi: {
319
+ prefix: IOS_PREFIX,
320
+ init_symbol: `${IOS_PREFIX}init`,
321
+ sink_register_symbol: `${IOS_PREFIX}set_panic_sink`,
322
+ collect_symbol: `${IOS_PREFIX}collect`,
323
+ result_reset_symbol: `${IOS_PREFIX}reset`,
324
+ callback_register_symbol: `${IOS_PREFIX}set_callback`,
325
+ },
326
+ exports: [
327
+ {
328
+ export: "handleInvoke",
329
+ symbol: `${IOS_PREFIX}handle_invoke`,
330
+ params: ["string", "string"],
331
+ returns: "string",
332
+ },
333
+ {
334
+ export: "indexHtml",
335
+ symbol: `${IOS_PREFIX}index_html`,
336
+ params: [],
337
+ returns: "string",
338
+ },
339
+ // The shell calls these back on the main queue when work it owns comes
340
+ // due. They are the mirror of the desktop shim's wv_on_timer: the
341
+ // library parks a continuation under an id and is re-entered with it.
342
+ {
343
+ export: "onTimer",
344
+ symbol: `${IOS_PREFIX}on_timer`,
345
+ params: ["f64"],
346
+ returns: "void",
347
+ },
348
+ {
349
+ export: "onFsDone",
350
+ symbol: `${IOS_PREFIX}on_fs_done`,
351
+ params: ["f64", "bool", "string"],
352
+ returns: "void",
353
+ },
354
+ ],
355
+ // TS -> shell. A channel handler must never re-enter the library (see
356
+ // upstream #263: violations silently appear to work), so every one of
357
+ // these only records the request; the shell acts on its own queue and
358
+ // re-enters through an export above on a later turn.
359
+ callbacks: [
360
+ { name: "janelaEmit", params: ["string", "string"], returns: "void" },
361
+ { name: "hostSchedule", params: ["f64", "f64"], returns: "void" },
362
+ { name: "hostSettle", params: ["f64", "string"], returns: "void" },
363
+ { name: "hostReadFile", params: ["f64", "string"], returns: "void" },
364
+ { name: "hostWriteFile", params: ["f64", "string", "string"], returns: "void" },
365
+ ],
366
+ };
367
+ }
368
+
369
+ function iosPlist(conf) {
370
+ const ios = iosConf(conf);
371
+ return `<?xml version="1.0" encoding="UTF-8"?>
372
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
373
+ <plist version="1.0">
374
+ <dict>
375
+ <key>CFBundleName</key><string>${conf.name}</string>
376
+ <key>CFBundleDisplayName</key><string>${ios.displayName}</string>
377
+ <key>CFBundleIdentifier</key><string>${ios.identifier}</string>
378
+ <key>CFBundleExecutable</key><string>${conf.name}</string>
379
+ <key>CFBundlePackageType</key><string>APPL</string>
380
+ <key>CFBundleVersion</key><string>${conf.version ?? "0.1.0"}</string>
381
+ <key>CFBundleShortVersionString</key><string>${conf.version ?? "0.1.0"}</string>
382
+ <key>LSRequiresIPhoneOS</key><true/>
383
+ <key>UILaunchScreen</key><dict/>
384
+ <key>MinimumOSVersion</key><string>${ios.minimumVersion}</string>
385
+ <key>CFBundleSupportedPlatforms</key><array><string>iPhoneSimulator</string></array>
386
+ </dict>
387
+ </plist>
388
+ `;
389
+ }
390
+
391
+ function buildIos(root, conf, buildDir, outDir) {
392
+ if (process.platform !== "darwin") {
393
+ fail("iOS builds need macOS with Xcode — `--target ios` is only available there");
394
+ }
395
+ if (!which("zig")) {
396
+ fail(
397
+ "iOS builds need zig on PATH: scriptc routes mobile targets through `zig cc`. " +
398
+ "Install it with `brew install zig`",
399
+ );
400
+ }
401
+ const ios = iosConf(conf);
402
+
403
+ writeFileSync(join(buildDir, "profile.json"), JSON.stringify(iosProfile(), null, 2) + "\n");
404
+
405
+ console.log("janela: compiling TypeScript to an iOS library");
406
+ run(["node", scriptcBin(), "build", "--lib", "--profile", "profile.json"], {
407
+ cwd: buildDir,
408
+ env: {
409
+ ...process.env,
410
+ SCRIPTC_CC: "zigcc",
411
+ SCRIPTC_TARGET: "aarch64-apple-ios-simulator",
412
+ },
413
+ });
414
+ const lib = join(buildDir, ".scriptc", "entry.lib.a");
415
+ if (!existsSync(lib)) fail(`scriptc produced no library at ${lib}`);
416
+
417
+ console.log("janela: compiling the UIKit shell");
418
+ const bundle = join(outDir, `${conf.name}.app`);
419
+ mkdirSync(bundle, { recursive: true });
420
+ const sdk = capture(["xcrun", "--sdk", "iphonesimulator", "--show-sdk-path"]);
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.
424
+ run([
425
+ "xcrun", "clang++",
426
+ join(KIT, "shim", "ios", "app.cc"),
427
+ "-target", `arm64-apple-ios${ios.minimumVersion}-simulator`,
428
+ "-isysroot", sdk,
429
+ "-std=c++17", "-O2", "-fblocks",
430
+ `-I${join(KIT, "vendor-webview", "core", "include")}`,
431
+ "-framework", "UIKit", "-framework", "WebKit", "-framework", "Foundation",
432
+ lib,
433
+ "-o", join(bundle, conf.name),
434
+ ]);
435
+ run(["strip", join(bundle, conf.name)]);
436
+ writeFileSync(join(bundle, "Info.plist"), iosPlist(conf));
437
+
438
+ console.log(
439
+ `janela: built ${relative(root, bundle)} ` +
440
+ `(${statSync(join(bundle, conf.name)).size} bytes, iOS Simulator)`,
441
+ );
442
+ return bundle;
443
+ }
444
+
445
+ async function devIos(root) {
446
+ const conf = loadConf(root);
447
+ const device = iosDeviceOrFail(iosConf(conf).device);
448
+ const bundle = build(root, { target: "ios" });
449
+
450
+ if (device.state !== "Booted") {
451
+ console.log(`janela: booting ${device.name}`);
452
+ spawnSync("xcrun", ["simctl", "boot", device.udid]);
453
+ spawnSync("xcrun", ["simctl", "bootstatus", device.udid, "-b"]);
454
+ }
455
+ spawnSync("open", ["-a", "Simulator"]);
456
+
457
+ console.log(`janela: installing on ${device.name}`);
458
+ run(["xcrun", "simctl", "install", device.udid, bundle]);
459
+ console.log("janela: launching (Ctrl-C to stop following the log)");
460
+ run([
461
+ "xcrun", "simctl", "launch", "--console-pty",
462
+ device.udid, iosConf(conf).identifier,
463
+ ]);
464
+ }
465
+
266
466
  // ---- shim ----------------------------------------------------------------
267
467
 
468
+ function which(bin) {
469
+ const r = spawnSync(process.platform === "win32" ? "where" : "which", [bin], {
470
+ encoding: "utf8",
471
+ });
472
+ return r.status === 0 ? r.stdout.trim().split("\n")[0] : null;
473
+ }
474
+
268
475
  function buildShim(cacheDir) {
269
476
  const src = join(KIT, "shim", "wvshim.cc");
270
477
  const win = process.platform === "win32";
@@ -470,17 +677,23 @@ function makeGuiSubsystem(exePath) {
470
677
 
471
678
  // `devUrl` points the window at a running vite server instead of inlining the
472
679
  // frontend; `gui` asks for a GUI-subsystem .exe on Windows (build, not dev).
473
- function build(root, { devUrl = null, gui = true } = {}) {
680
+ function build(root, { devUrl = null, gui = true, target = "desktop" } = {}) {
474
681
  const conf = loadConf(root);
475
- const buildDir = join(root, ".janela", "build");
682
+ const ios = target === "ios";
683
+ const buildDir = join(root, ".janela", ios ? "build-ios" : "build");
476
684
  const cacheDir = join(root, ".janela", "cache");
477
- const outDir = join(root, ".janela", "out");
685
+ const outDir = join(root, ".janela", ios ? "out-ios" : "out");
478
686
  for (const d of [buildDir, cacheDir, outDir]) mkdirSync(d, { recursive: true });
479
687
 
480
- const shimLib = buildShim(cacheDir);
688
+ // Desktop links a C shim over webview.h; iOS links no shim at all — the
689
+ // UIKit shell is the program, and it links this build's output instead.
690
+ const shimLib = ios ? null : buildShim(cacheDir);
481
691
 
482
692
  // Assemble the compile unit: runtime + user's commands + generated modules.
483
- cpSync(join(KIT, "runtime", "janela.ts"), join(buildDir, "janela.ts"));
693
+ // Which runtime lane lands here as "./janela" is the whole difference
694
+ // between the two targets — a project's main.ts is compiled unchanged
695
+ // against either.
696
+ cpSync(join(KIT, "runtime", ios ? "ios.ts" : "janela.ts"), join(buildDir, "janela.ts"));
484
697
  cpSync(join(KIT, "runtime", "types.ts"), join(buildDir, "types.ts"));
485
698
  const mainSrc = join(root, "src-host", "main.ts");
486
699
  if (!existsSync(mainSrc)) fail("missing src-host/main.ts");
@@ -512,6 +725,34 @@ function build(root, { devUrl = null, gui = true } = {}) {
512
725
  `width: ${Number(w.width ?? 800)}, height: ${Number(w.height ?? 600)} };\n`,
513
726
  );
514
727
 
728
+ // The iOS entry exports the library's two entry points instead of running a
729
+ // loop: UIKit owns the loop and calls in. Everything above this line — the
730
+ // contract, main.ts, the flattened frontend — is identical to desktop.
731
+ const entryTail = ios
732
+ ? `const app = createApp<CmdsOf<typeof setup>, EvtsOf<typeof setup>>(WINDOW);\n` +
733
+ `setup(app);\n` +
734
+ `app.setHtml(INDEX_HTML);\n\n` +
735
+ `/** One page invoke; the UIKit shell calls this through the library ABI. */\n` +
736
+ `export function handleInvoke(cmd: string, argsJson: string): string {\n` +
737
+ ` return app.dispatch(cmd, argsJson);\n` +
738
+ `}\n\n` +
739
+ `/** The document the shell loads into its WKWebView. */\n` +
740
+ `export function indexHtml(): string {\n` +
741
+ ` return app.indexHtml();\n` +
742
+ `}\n\n` +
743
+ `/** A continuation the shell parked has come due (main queue). */\n` +
744
+ `export function onTimer(id: number): void {\n` +
745
+ ` app.onTimer(id);\n` +
746
+ `}\n\n` +
747
+ `/** A file job the shell owns has finished (main queue). */\n` +
748
+ `export function onFsDone(id: number, ok: boolean, payload: string): void {\n` +
749
+ ` app.onFsDone(id, ok, payload);\n` +
750
+ `}\n`
751
+ : `const app = createApp<CmdsOf<typeof setup>, EvtsOf<typeof setup>>(WINDOW);\n` +
752
+ `setup(app);\n` +
753
+ `const rc = app.run(INDEX_HTML) + 0;\n` +
754
+ `console.log("[janela] run returned", rc);\n`;
755
+
515
756
  writeFileSync(
516
757
  join(buildDir, "entry.ts"),
517
758
  `// Generated by janela — do not edit.\n` +
@@ -529,12 +770,11 @@ function build(root, { devUrl = null, gui = true } = {}) {
529
770
  `// and what is wanted here is the normalised table anyway.\n` +
530
771
  `type CmdsOf<F> = F extends (app: JanelaAppImpl<infer C, infer _E>) => void ? C : CommandShapes;\n` +
531
772
  `type EvtsOf<F> = F extends (app: JanelaAppImpl<infer _C, infer E>) => void ? E : Record<string, unknown>;\n\n` +
532
- `const app = createApp<CmdsOf<typeof setup>, EvtsOf<typeof setup>>(WINDOW);\n` +
533
- `setup(app);\n` +
534
- `const rc = app.run(INDEX_HTML) + 0;\n` +
535
- `console.log("[janela] run returned", rc);\n`,
773
+ entryTail,
536
774
  );
537
775
 
776
+ if (ios) return buildIos(root, conf, buildDir, outDir);
777
+
538
778
  writeFileSync(join(buildDir, "janela.ffi.json"), JSON.stringify(ffiManifest(shimLib), null, 2) + "\n");
539
779
 
540
780
  console.log("janela: compiling TypeScript to a native binary");
@@ -730,20 +970,30 @@ function positionals() {
730
970
  return out;
731
971
  }
732
972
 
973
+ const TARGETS = ["desktop", "ios"];
974
+
975
+ function targetOrFail() {
976
+ const t = flag("target", "desktop");
977
+ if (!TARGETS.includes(t)) fail(`unknown target '${t}' (${TARGETS.join(", ")})`);
978
+ return t;
979
+ }
980
+
733
981
  switch (cmd) {
734
982
  case "init":
735
983
  init(positionals()[0], flag("template", "vanilla"));
736
984
  break;
737
985
  case "build":
738
- build(process.cwd());
986
+ build(process.cwd(), { target: targetOrFail() });
739
987
  break;
740
988
  case "dev":
741
- await dev(process.cwd());
989
+ if (targetOrFail() === "ios") await devIos(process.cwd());
990
+ else await dev(process.cwd());
742
991
  break;
743
992
  default:
744
993
  console.log(
745
994
  "usage: janela init <name> [--template vanilla|vue|react|svelte|solid]\n" +
746
- " janela build | janela dev",
995
+ " janela build [--target desktop|ios]\n" +
996
+ " janela dev [--target desktop|ios]",
747
997
  );
748
998
  process.exit(cmd ? 1 : 0);
749
999
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "janela",
3
- "version": "0.9.0",
4
- "description": "Desktop apps in pure TypeScript, compiled to native. No Rust, no Node, no Electron.",
3
+ "version": "0.10.1",
4
+ "description": "Desktop and iOS 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",
@@ -40,7 +40,8 @@
40
40
  "native",
41
41
  "tauri",
42
42
  "electron-alternative",
43
- "scriptc"
43
+ "scriptc",
44
+ "ios"
44
45
  ],
45
46
  "engines": {
46
47
  "node": ">=24"