janela 0.13.1 → 0.14.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 CHANGED
@@ -5,8 +5,11 @@
5
5
  Desktop and mobile apps in pure TypeScript, compiled to native. No Rust, no
6
6
  Node, no Electron. The backend is TypeScript compiled to a native binary by
7
7
  [scriptc](https://scriptc.dev); the window is the OS webview via
8
- [webview/webview](https://github.com/webview/webview). Binaries come out
9
- around 400–500 KB, with no bundled browser and no bundled runtime.
8
+ [webview/webview](https://github.com/webview/webview). A desktop binary comes
9
+ out around 200–400 KB 191 KB for the smallest template — with no bundled
10
+ browser and no bundled runtime; iOS and Android bundles land around
11
+ 384–578 KB. Per-template figures are in
12
+ [docs/frontend.md](../../docs/frontend.md).
10
13
 
11
14
  Five targets, one runtime — the same `main.ts`, the same typed contract and the
12
15
  same frontend build for each:
@@ -542,7 +545,7 @@ app.command("add", (args) => {
542
545
  Mechanically: drop the `JSON.parse(argsJson)` (cast `args` instead), drop
543
546
  every `JSON.stringify` around a result, `resolve`/`reject`/`emit` payload, and
544
547
  return nothing at all where you used to return `"null"`. Requires Node 24 to
545
- build (scriptc 0.0.35's floor).
548
+ build (scriptc 0.0.36's floor).
546
549
 
547
550
  ## What the CLI hides
548
551
 
@@ -559,10 +562,6 @@ no `-framework` support) and the binary is wrapped into an ad-hoc-signed
559
562
  the boundary, so anything that survives `JSON.stringify`/`JSON.parse` round
560
563
  trips (including full Unicode). `args` is typed `unknown` — cast it to the
561
564
  shape you expect.
562
- - Never use a bare FFI call as a complete variable initializer or assignment
563
- RHS — it is silently miscompiled. Wrap it in any expression (`+ 0`). Plain
564
- TypeScript is unaffected; only the runtime does FFI, so app code rarely
565
- meets this.
566
565
  - One window per app for now. Host code is single-threaded: a synchronous
567
566
  command blocks the UI while it runs — use `commandAsync` + `defer`/`sleep`
568
567
  (see "Async commands") for anything slow.
@@ -583,8 +582,10 @@ It is **simulator-only** so far — device builds and code signing are not
583
582
  wired up yet. Commands, the typed
584
583
  contract, events, Vite frontends, async commands (`commandAsync`, `defer`,
585
584
  `sleep`) and file I/O all work the same as on desktop — the shell owns the
586
- clock and the file queue on both. File dialogs are not on iOS yet and report
587
- clearly when called; window control is a no-op there by nature. See
585
+ clock and the file queue on both. `openFileDialog` works as of 0.13.0 the
586
+ picked file is copied into the app container and that path returned, so
587
+ `readFileAsync` opens it exactly as on desktop. `saveFileDialog` reports
588
+ clearly when called, and window control is a no-op there by nature. See
588
589
  [docs/ios.md](../../docs/ios.md).
589
590
 
590
591
  ## Android
@@ -615,11 +616,18 @@ The design notes and scriptc findings behind it are in
615
616
  [docs/findings.md](../../docs/findings.md), with per-platform notes in
616
617
  [docs/ios.md](../../docs/ios.md) and [docs/android.md](../../docs/android.md).
617
618
 
618
- Not yet: native dialogs and window control on mobile; device builds and code
619
- signing; icons, installers and notarization; async commands that run in
620
- parallel (host code is single-threaded, so `commandAsync` interleaves and a
621
- CPU-bound handler still needs slicing); an async HTTP client; tray icons and
622
- menus; multi-window; directory picking on Windows; and `app.center()`.
619
+ Not yet: `saveFileDialog` and window control on mobile (both deliberate
620
+ mobile "save" means exporting a file that already exists, which the desktop
621
+ signature cannot express, and window geometry is meaningless on a phone);
622
+ iOS device builds and code signing; installers and notarization; async
623
+ commands that run in parallel (host code is single-threaded, so
624
+ `commandAsync` interleaves and a CPU-bound handler still needs slicing); an
625
+ async HTTP client; tray icons and menus; multi-window; directory picking on
626
+ Windows and iOS; and `app.center()`.
627
+
628
+ `openFileDialog` **does** work on iOS and Android as of 0.13.0 — a picked
629
+ file is copied into the app container and that path returned, so
630
+ `readFileAsync` opens it exactly as on desktop.
623
631
 
624
632
  ## Releasing
625
633
 
package/bin/janela.mjs CHANGED
@@ -994,7 +994,7 @@ function build(root, { devUrl = null, gui = true, target = "desktop" } = {}) {
994
994
  `}\n`
995
995
  : `const app = createApp<CmdsOf<typeof setup>, EvtsOf<typeof setup>>(WINDOW);\n` +
996
996
  `setup(app);\n` +
997
- `const rc = app.run(INDEX_HTML) + 0;\n` +
997
+ `const rc = app.run(INDEX_HTML);\n` +
998
998
  `console.log("[janela] run returned", rc);\n`;
999
999
 
1000
1000
  writeFileSync(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "janela",
3
- "version": "0.13.1",
3
+ "version": "0.14.0",
4
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": {
@@ -21,7 +21,7 @@
21
21
  "./package.json": "./package.json"
22
22
  },
23
23
  "dependencies": {
24
- "scriptc": "0.0.35"
24
+ "scriptc": "0.0.36"
25
25
  },
26
26
  "license": "MIT",
27
27
  "author": "Mateus Lage",
package/runtime/janela.ts CHANGED
@@ -4,11 +4,6 @@
4
4
  // (name, argsJson) envelope, dispatched to handlers registered on the app
5
5
  // object. Handlers see decoded values — the runtime owns JSON at the boundary.
6
6
  // Backend→frontend events ride wv_eval into the injected bootstrap.
7
- //
8
- // NOTE ON STYLE: every FFI call whose result initializes a variable is written
9
- // `f(...) + 0`. scriptc miscompiles a bare FFI call used as a complete
10
- // initializer/assignment RHS — still true in 0.0.35, and reported upstream as
11
- // vercel-labs/scriptc#21. Any enclosing expression is the workaround.
12
7
 
13
8
  declare function wvCreate(debug: number): number;
14
9
  declare function wvSetTitle(h: number, title: string): number;
@@ -202,7 +197,7 @@ export class JanelaAppImpl<
202
197
  drainSize: number[] = [];
203
198
 
204
199
  constructor(cfg: WindowConfig) {
205
- const h = wvCreate(0) + 0;
200
+ const h = wvCreate(0);
206
201
  this.handle = h;
207
202
  wvSetTitle(h, cfg.title);
208
203
  wvSetSize(h, cfg.width, cfg.height, 0);
@@ -221,9 +216,7 @@ export class JanelaAppImpl<
221
216
  this.contIds.push(id);
222
217
  this.contFns.push(fn);
223
218
  const delay = ms > 0 ? ms : 0;
224
- // `+ 0` per the note at the top of this file: a bare FFI call is not safe
225
- // in every position, and this one is silently dropped without it.
226
- const rc = wvSchedule(this.handle, id, delay) + 0;
219
+ const rc = wvSchedule(this.handle, id, delay);
227
220
  if (rc < 0) console.log("[janela] could not schedule continuation", id);
228
221
  }
229
222
 
@@ -254,14 +247,14 @@ export class JanelaAppImpl<
254
247
  */
255
248
  drainSome(): void {
256
249
  if (this.drainIds.length === 0) return;
257
- const started = Date.now() + 0;
250
+ const started = Date.now();
258
251
 
259
252
  while (this.drainIds.length > 0) {
260
253
  let chunk = "";
261
254
  const taken =
262
255
  wvJobTakeAt(this.handle, this.drainIds[0], this.drainOff[0], DRAIN_SLICE, (text) => {
263
256
  chunk = text;
264
- }) + 0;
257
+ });
265
258
 
266
259
  // A negative count means the job vanished; treat the payload as final
267
260
  // rather than spinning on it forever.
@@ -334,7 +327,7 @@ export class JanelaAppImpl<
334
327
  const doneCbs: FsCallback[] = [];
335
328
  const doneOk: boolean[] = [];
336
329
  for (let i = 0; i < this.jobIds.length; i++) {
337
- const st = wvJobStatus(this.handle, this.jobIds[i]) + 0;
330
+ const st = wvJobStatus(this.handle, this.jobIds[i]);
338
331
  if (st === JOB_PENDING) {
339
332
  keptIds.push(this.jobIds[i]);
340
333
  keptCbs.push(this.jobCbs[i]);
@@ -355,7 +348,7 @@ export class JanelaAppImpl<
355
348
  this.drainOk.push(doneOk[i]);
356
349
  this.drainParts.push([]);
357
350
  this.drainOff.push(0);
358
- this.drainSize.push(wvJobSize(this.handle, doneIds[i]) + 0);
351
+ this.drainSize.push(wvJobSize(this.handle, doneIds[i]));
359
352
  }
360
353
  }
361
354
 
@@ -381,7 +374,7 @@ export class JanelaAppImpl<
381
374
  defaultPath === undefined ? "" : defaultPath,
382
375
  defaultName === undefined ? "" : defaultName,
383
376
  encodeFilters(filters),
384
- ) + 0;
377
+ );
385
378
  if (id < 0) {
386
379
  this.defer(() => cb(null, "EAGAIN: could not open a dialog"));
387
380
  return;
@@ -452,7 +445,7 @@ export class JanelaAppImpl<
452
445
  * worker thread; the callback lands on the UI thread on a later turn.
453
446
  */
454
447
  readFileAsync(path: string, cb: FsCallback): void {
455
- const id = wvFsRead(this.handle, path) + 0;
448
+ const id = wvFsRead(this.handle, path);
456
449
  if (id < 0) {
457
450
  this.defer(() => cb("EAGAIN: could not start a read of '" + path + "'", ""));
458
451
  return;
@@ -463,7 +456,7 @@ export class JanelaAppImpl<
463
456
 
464
457
  /** Write a file without blocking the window; cb(null) on success. */
465
458
  writeFileAsync(path: string, data: string, cb: (err: string | null) => void): void {
466
- const id = wvFsWrite(this.handle, path, data) + 0;
459
+ const id = wvFsWrite(this.handle, path, data);
467
460
  if (id < 0) {
468
461
  this.defer(() => cb("EAGAIN: could not start a write of '" + path + "'"));
469
462
  return;
@@ -555,7 +548,7 @@ export class JanelaAppImpl<
555
548
  // Park the page's promise: the shim holds this call's id and
556
549
  // answers it when resolve/reject reaches wvResolve, whenever
557
550
  // that is. Meanwhile the loop is free to serve other calls.
558
- const id = wvDefer(h) + 0;
551
+ const id = wvDefer(h);
559
552
  if (id < 0) {
560
553
  wvReply(h, encode("cannot defer command: " + cmd));
561
554
  return 1;
@@ -579,7 +572,7 @@ export class JanelaAppImpl<
579
572
 
580
573
  wvBind(h, "__invoke");
581
574
  wvSetHtml(h, html);
582
- const rc = wvRun(h) + 0;
575
+ const rc = wvRun(h);
583
576
  return rc;
584
577
  }
585
578
  }
package/templates/main.ts CHANGED
@@ -3,10 +3,6 @@
3
3
  // Register commands here; the page calls them with `await janela.invoke(name, args)`.
4
4
  // Handlers take the arguments as a value and return a value — the runtime owns
5
5
  // JSON at the boundary, so there is no parsing or stringifying to do here.
6
- //
7
- // Gotcha inherited from scriptc: never use a bare FFI-backed call as a
8
- // complete variable initializer — wrap it in any expression (`+ 0`). Plain
9
- // TypeScript like everything in this file is unaffected.
10
6
 
11
7
  import type { JanelaApp } from "janela/host";
12
8
 
@@ -6,11 +6,8 @@
6
6
  // names, argument shapes, results and event payloads — with no code
7
7
  // generation and nothing to keep in sync.
8
8
  //
9
- // Two gotchas inherited from scriptc:
10
- // - never use a bare FFI-backed call as a complete variable initializer;
11
- // wrap it in any expression (`+ 0`);
12
- // - a command that returns nothing is declared `() => void` and its handler
13
- // returns `null`; every command answers the page's promise with a value.
9
+ // A command that returns nothing is declared `() => void` and its handler
10
+ // returns `null`; every command answers the page's promise with a value.
14
11
 
15
12
  import type { JanelaApp } from "janela/host";
16
13
 
@@ -6,11 +6,8 @@
6
6
  // names, argument shapes, results and event payloads — with no code
7
7
  // generation and nothing to keep in sync.
8
8
  //
9
- // Two gotchas inherited from scriptc:
10
- // - never use a bare FFI-backed call as a complete variable initializer;
11
- // wrap it in any expression (`+ 0`);
12
- // - a command that returns nothing is declared `() => void` and its handler
13
- // returns `null`; every command answers the page's promise with a value.
9
+ // A command that returns nothing is declared `() => void` and its handler
10
+ // returns `null`; every command answers the page's promise with a value.
14
11
 
15
12
  import type { JanelaApp } from "janela/host";
16
13
 
@@ -6,11 +6,8 @@
6
6
  // names, argument shapes, results and event payloads — with no code
7
7
  // generation and nothing to keep in sync.
8
8
  //
9
- // Two gotchas inherited from scriptc:
10
- // - never use a bare FFI-backed call as a complete variable initializer;
11
- // wrap it in any expression (`+ 0`);
12
- // - a command that returns nothing is declared `() => void` and its handler
13
- // returns `null`; every command answers the page's promise with a value.
9
+ // A command that returns nothing is declared `() => void` and its handler
10
+ // returns `null`; every command answers the page's promise with a value.
14
11
 
15
12
  import type { JanelaApp } from "janela/host";
16
13
 
@@ -6,11 +6,8 @@
6
6
  // names, argument shapes, results and event payloads — with no code
7
7
  // generation and nothing to keep in sync.
8
8
  //
9
- // Two gotchas inherited from scriptc:
10
- // - never use a bare FFI-backed call as a complete variable initializer;
11
- // wrap it in any expression (`+ 0`);
12
- // - a command that returns nothing is declared `() => void` and its handler
13
- // returns `null`; every command answers the page's promise with a value.
9
+ // A command that returns nothing is declared `() => void` and its handler
10
+ // returns `null`; every command answers the page's promise with a value.
14
11
 
15
12
  import type { JanelaApp } from "janela/host";
16
13