janela 0.8.0 → 0.10.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 +23 -4
- package/bin/janela.mjs +266 -18
- package/package.json +4 -3
- package/runtime/ios.ts +483 -0
- package/runtime/janela.ts +88 -82
- package/shim/ios/app.mm +386 -0
- package/shim/wvshim.cc +166 -59
- package/templates/janela.conf.json +5 -0
- package/templates/react/files/janela.conf.json +5 -0
- package/templates/solid/files/janela.conf.json +5 -0
- package/templates/svelte/files/janela.conf.json +5 -0
- package/templates/vue/files/janela.conf.json +5 -0
package/README.md
CHANGED
|
@@ -279,9 +279,10 @@ in scriptc and can cost far more than the read did.
|
|
|
279
279
|
**Use `app.sleep`, not `setTimeout`.** scriptc's own event loop is parked for
|
|
280
280
|
as long as the program sits inside the `run()` FFI call, so `setTimeout`,
|
|
281
281
|
`queueMicrotask` and `await` in host code never fire while the window is open
|
|
282
|
-
(they all run after it closes). janela
|
|
283
|
-
|
|
284
|
-
|
|
282
|
+
(they all run after it closes). janela schedules through the shell instead: the
|
|
283
|
+
runtime parks a continuation under an id, the shell keeps the clock and calls
|
|
284
|
+
it back on the UI thread when it comes due. Nothing polls, so an idle app
|
|
285
|
+
costs nothing at all.
|
|
285
286
|
|
|
286
287
|
**Still single-threaded.** scriptc's runtime is not thread-safe (concurrent
|
|
287
288
|
calls from several threads abort the process), so host code always runs on the
|
|
@@ -541,10 +542,28 @@ no `-framework` support) and the binary is wrapped into an ad-hoc-signed
|
|
|
541
542
|
- `console.log` from commands goes to stdout — visible under `janela dev`,
|
|
542
543
|
not when launched from Finder.
|
|
543
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
|
+
|
|
544
562
|
## Status
|
|
545
563
|
|
|
546
564
|
Early proof of concept, on macOS (arm64), Linux (WebKitGTK) and Windows
|
|
547
|
-
(WebView2). The design notes and scriptc
|
|
565
|
+
(WebView2), with iOS on a branch (above). The design notes and scriptc
|
|
566
|
+
findings behind it are in
|
|
548
567
|
[docs/findings.md](../../docs/findings.md). Not yet: async commands that run in
|
|
549
568
|
parallel (host code is single-threaded; `commandAsync` interleaves instead),
|
|
550
569
|
tray icons and menus, multi-window, directory picking on Windows,
|
package/bin/janela.mjs
CHANGED
|
@@ -263,8 +263,213 @@ 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
|
+
// The .mm extension picks Objective-C++; an explicit `-x objective-c++`
|
|
422
|
+
// would leak onto the archive that follows and be compiled as source.
|
|
423
|
+
run([
|
|
424
|
+
"xcrun", "clang++",
|
|
425
|
+
join(KIT, "shim", "ios", "app.mm"),
|
|
426
|
+
"-target", `arm64-apple-ios${ios.minimumVersion}-simulator`,
|
|
427
|
+
"-isysroot", sdk,
|
|
428
|
+
"-fobjc-arc", "-std=c++17", "-O2",
|
|
429
|
+
"-framework", "UIKit", "-framework", "WebKit", "-framework", "Foundation",
|
|
430
|
+
lib,
|
|
431
|
+
"-o", join(bundle, conf.name),
|
|
432
|
+
]);
|
|
433
|
+
run(["strip", join(bundle, conf.name)]);
|
|
434
|
+
writeFileSync(join(bundle, "Info.plist"), iosPlist(conf));
|
|
435
|
+
|
|
436
|
+
console.log(
|
|
437
|
+
`janela: built ${relative(root, bundle)} ` +
|
|
438
|
+
`(${statSync(join(bundle, conf.name)).size} bytes, iOS Simulator)`,
|
|
439
|
+
);
|
|
440
|
+
return bundle;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async function devIos(root) {
|
|
444
|
+
const conf = loadConf(root);
|
|
445
|
+
const device = iosDeviceOrFail(iosConf(conf).device);
|
|
446
|
+
const bundle = build(root, { target: "ios" });
|
|
447
|
+
|
|
448
|
+
if (device.state !== "Booted") {
|
|
449
|
+
console.log(`janela: booting ${device.name}`);
|
|
450
|
+
spawnSync("xcrun", ["simctl", "boot", device.udid]);
|
|
451
|
+
spawnSync("xcrun", ["simctl", "bootstatus", device.udid, "-b"]);
|
|
452
|
+
}
|
|
453
|
+
spawnSync("open", ["-a", "Simulator"]);
|
|
454
|
+
|
|
455
|
+
console.log(`janela: installing on ${device.name}`);
|
|
456
|
+
run(["xcrun", "simctl", "install", device.udid, bundle]);
|
|
457
|
+
console.log("janela: launching (Ctrl-C to stop following the log)");
|
|
458
|
+
run([
|
|
459
|
+
"xcrun", "simctl", "launch", "--console-pty",
|
|
460
|
+
device.udid, iosConf(conf).identifier,
|
|
461
|
+
]);
|
|
462
|
+
}
|
|
463
|
+
|
|
266
464
|
// ---- shim ----------------------------------------------------------------
|
|
267
465
|
|
|
466
|
+
function which(bin) {
|
|
467
|
+
const r = spawnSync(process.platform === "win32" ? "where" : "which", [bin], {
|
|
468
|
+
encoding: "utf8",
|
|
469
|
+
});
|
|
470
|
+
return r.status === 0 ? r.stdout.trim().split("\n")[0] : null;
|
|
471
|
+
}
|
|
472
|
+
|
|
268
473
|
function buildShim(cacheDir) {
|
|
269
474
|
const src = join(KIT, "shim", "wvshim.cc");
|
|
270
475
|
const win = process.platform === "win32";
|
|
@@ -324,21 +529,21 @@ function ffiManifest(shimLib) {
|
|
|
324
529
|
returns: "i32",
|
|
325
530
|
},
|
|
326
531
|
{
|
|
327
|
-
name: "
|
|
532
|
+
name: "wvOnTimer", symbol: "wv_on_timer",
|
|
328
533
|
params: [
|
|
329
534
|
"i32",
|
|
330
|
-
{ callback: { id: "
|
|
331
|
-
{ context: "
|
|
535
|
+
{ callback: { id: "timer", params: ["i32", { context: "timer" }], returns: "void", lifetime: "retained" } },
|
|
536
|
+
{ context: "timer" },
|
|
332
537
|
],
|
|
333
538
|
returns: "i32",
|
|
334
539
|
},
|
|
335
540
|
{ name: "wvRun", symbol: "wv_run", params: ["i32"], returns: "i32" },
|
|
336
541
|
{ name: "wvTerminate", symbol: "wv_terminate", params: ["i32"], returns: "i32" },
|
|
337
|
-
// async: deferred returns
|
|
542
|
+
// async: the held-reply table (deferred returns) plus shell-owned
|
|
543
|
+
// scheduling — TS parks a continuation id, the shell calls it back due.
|
|
338
544
|
{ name: "wvDefer", symbol: "wv_defer", params: ["i32"], returns: "i32" },
|
|
339
545
|
{ name: "wvResolve", symbol: "wv_resolve", params: ["i32", "i32", "i32"], returns: "i32" },
|
|
340
|
-
{ name: "
|
|
341
|
-
{ name: "wvTickStop", symbol: "wv_tick_stop", params: ["i32"], returns: "i32" },
|
|
546
|
+
{ name: "wvSchedule", symbol: "wv_schedule", params: ["i32", "i32", "i32"], returns: "i32" },
|
|
342
547
|
// async file I/O: the blocking syscall runs on a shim worker thread
|
|
343
548
|
{ name: "wvFsRead", symbol: "wv_fs_read", params: ["i32", "string"], returns: "i32" },
|
|
344
549
|
{ name: "wvFsWrite", symbol: "wv_fs_write", params: ["i32", "string", "string"], returns: "i32" },
|
|
@@ -470,17 +675,23 @@ function makeGuiSubsystem(exePath) {
|
|
|
470
675
|
|
|
471
676
|
// `devUrl` points the window at a running vite server instead of inlining the
|
|
472
677
|
// frontend; `gui` asks for a GUI-subsystem .exe on Windows (build, not dev).
|
|
473
|
-
function build(root, { devUrl = null, gui = true } = {}) {
|
|
678
|
+
function build(root, { devUrl = null, gui = true, target = "desktop" } = {}) {
|
|
474
679
|
const conf = loadConf(root);
|
|
475
|
-
const
|
|
680
|
+
const ios = target === "ios";
|
|
681
|
+
const buildDir = join(root, ".janela", ios ? "build-ios" : "build");
|
|
476
682
|
const cacheDir = join(root, ".janela", "cache");
|
|
477
|
-
const outDir = join(root, ".janela", "out");
|
|
683
|
+
const outDir = join(root, ".janela", ios ? "out-ios" : "out");
|
|
478
684
|
for (const d of [buildDir, cacheDir, outDir]) mkdirSync(d, { recursive: true });
|
|
479
685
|
|
|
480
|
-
|
|
686
|
+
// Desktop links a C shim over webview.h; iOS links no shim at all — the
|
|
687
|
+
// UIKit shell is the program, and it links this build's output instead.
|
|
688
|
+
const shimLib = ios ? null : buildShim(cacheDir);
|
|
481
689
|
|
|
482
690
|
// Assemble the compile unit: runtime + user's commands + generated modules.
|
|
483
|
-
|
|
691
|
+
// Which runtime lane lands here as "./janela" is the whole difference
|
|
692
|
+
// between the two targets — a project's main.ts is compiled unchanged
|
|
693
|
+
// against either.
|
|
694
|
+
cpSync(join(KIT, "runtime", ios ? "ios.ts" : "janela.ts"), join(buildDir, "janela.ts"));
|
|
484
695
|
cpSync(join(KIT, "runtime", "types.ts"), join(buildDir, "types.ts"));
|
|
485
696
|
const mainSrc = join(root, "src-host", "main.ts");
|
|
486
697
|
if (!existsSync(mainSrc)) fail("missing src-host/main.ts");
|
|
@@ -512,6 +723,34 @@ function build(root, { devUrl = null, gui = true } = {}) {
|
|
|
512
723
|
`width: ${Number(w.width ?? 800)}, height: ${Number(w.height ?? 600)} };\n`,
|
|
513
724
|
);
|
|
514
725
|
|
|
726
|
+
// The iOS entry exports the library's two entry points instead of running a
|
|
727
|
+
// loop: UIKit owns the loop and calls in. Everything above this line — the
|
|
728
|
+
// contract, main.ts, the flattened frontend — is identical to desktop.
|
|
729
|
+
const entryTail = ios
|
|
730
|
+
? `const app = createApp<CmdsOf<typeof setup>, EvtsOf<typeof setup>>(WINDOW);\n` +
|
|
731
|
+
`setup(app);\n` +
|
|
732
|
+
`app.setHtml(INDEX_HTML);\n\n` +
|
|
733
|
+
`/** One page invoke; the UIKit shell calls this through the library ABI. */\n` +
|
|
734
|
+
`export function handleInvoke(cmd: string, argsJson: string): string {\n` +
|
|
735
|
+
` return app.dispatch(cmd, argsJson);\n` +
|
|
736
|
+
`}\n\n` +
|
|
737
|
+
`/** The document the shell loads into its WKWebView. */\n` +
|
|
738
|
+
`export function indexHtml(): string {\n` +
|
|
739
|
+
` return app.indexHtml();\n` +
|
|
740
|
+
`}\n\n` +
|
|
741
|
+
`/** A continuation the shell parked has come due (main queue). */\n` +
|
|
742
|
+
`export function onTimer(id: number): void {\n` +
|
|
743
|
+
` app.onTimer(id);\n` +
|
|
744
|
+
`}\n\n` +
|
|
745
|
+
`/** A file job the shell owns has finished (main queue). */\n` +
|
|
746
|
+
`export function onFsDone(id: number, ok: boolean, payload: string): void {\n` +
|
|
747
|
+
` app.onFsDone(id, ok, payload);\n` +
|
|
748
|
+
`}\n`
|
|
749
|
+
: `const app = createApp<CmdsOf<typeof setup>, EvtsOf<typeof setup>>(WINDOW);\n` +
|
|
750
|
+
`setup(app);\n` +
|
|
751
|
+
`const rc = app.run(INDEX_HTML) + 0;\n` +
|
|
752
|
+
`console.log("[janela] run returned", rc);\n`;
|
|
753
|
+
|
|
515
754
|
writeFileSync(
|
|
516
755
|
join(buildDir, "entry.ts"),
|
|
517
756
|
`// Generated by janela — do not edit.\n` +
|
|
@@ -529,12 +768,11 @@ function build(root, { devUrl = null, gui = true } = {}) {
|
|
|
529
768
|
`// and what is wanted here is the normalised table anyway.\n` +
|
|
530
769
|
`type CmdsOf<F> = F extends (app: JanelaAppImpl<infer C, infer _E>) => void ? C : CommandShapes;\n` +
|
|
531
770
|
`type EvtsOf<F> = F extends (app: JanelaAppImpl<infer _C, infer E>) => void ? E : Record<string, unknown>;\n\n` +
|
|
532
|
-
|
|
533
|
-
`setup(app);\n` +
|
|
534
|
-
`const rc = app.run(INDEX_HTML) + 0;\n` +
|
|
535
|
-
`console.log("[janela] run returned", rc);\n`,
|
|
771
|
+
entryTail,
|
|
536
772
|
);
|
|
537
773
|
|
|
774
|
+
if (ios) return buildIos(root, conf, buildDir, outDir);
|
|
775
|
+
|
|
538
776
|
writeFileSync(join(buildDir, "janela.ffi.json"), JSON.stringify(ffiManifest(shimLib), null, 2) + "\n");
|
|
539
777
|
|
|
540
778
|
console.log("janela: compiling TypeScript to a native binary");
|
|
@@ -730,20 +968,30 @@ function positionals() {
|
|
|
730
968
|
return out;
|
|
731
969
|
}
|
|
732
970
|
|
|
971
|
+
const TARGETS = ["desktop", "ios"];
|
|
972
|
+
|
|
973
|
+
function targetOrFail() {
|
|
974
|
+
const t = flag("target", "desktop");
|
|
975
|
+
if (!TARGETS.includes(t)) fail(`unknown target '${t}' (${TARGETS.join(", ")})`);
|
|
976
|
+
return t;
|
|
977
|
+
}
|
|
978
|
+
|
|
733
979
|
switch (cmd) {
|
|
734
980
|
case "init":
|
|
735
981
|
init(positionals()[0], flag("template", "vanilla"));
|
|
736
982
|
break;
|
|
737
983
|
case "build":
|
|
738
|
-
build(process.cwd());
|
|
984
|
+
build(process.cwd(), { target: targetOrFail() });
|
|
739
985
|
break;
|
|
740
986
|
case "dev":
|
|
741
|
-
await
|
|
987
|
+
if (targetOrFail() === "ios") await devIos(process.cwd());
|
|
988
|
+
else await dev(process.cwd());
|
|
742
989
|
break;
|
|
743
990
|
default:
|
|
744
991
|
console.log(
|
|
745
992
|
"usage: janela init <name> [--template vanilla|vue|react|svelte|solid]\n" +
|
|
746
|
-
" janela build |
|
|
993
|
+
" janela build [--target desktop|ios]\n" +
|
|
994
|
+
" janela dev [--target desktop|ios]",
|
|
747
995
|
);
|
|
748
996
|
process.exit(cmd ? 1 : 0);
|
|
749
997
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "janela",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Desktop apps in pure TypeScript, compiled to native. No Rust, no Node, no Electron.",
|
|
3
|
+
"version": "0.10.0",
|
|
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"
|