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 +22 -14
- package/bin/janela.mjs +1 -1
- package/package.json +2 -2
- package/runtime/janela.ts +11 -18
- package/templates/main.ts +0 -4
- package/templates/react/files/src-host/main.ts +2 -5
- package/templates/solid/files/src-host/main.ts +2 -5
- package/templates/svelte/files/src-host/main.ts +2 -5
- package/templates/vue/files/src-host/main.ts +2 -5
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).
|
|
9
|
-
around 400
|
|
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.
|
|
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.
|
|
587
|
-
|
|
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:
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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)
|
|
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.
|
|
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.
|
|
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)
|
|
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
|
-
|
|
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()
|
|
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
|
-
})
|
|
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])
|
|
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])
|
|
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
|
-
)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
-
//
|
|
10
|
-
//
|
|
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
|
-
//
|
|
10
|
-
//
|
|
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
|
-
//
|
|
10
|
-
//
|
|
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
|
-
//
|
|
10
|
-
//
|
|
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
|
|