janela 0.13.1 → 0.14.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.
- package/README.md +22 -14
- package/bin/janela.mjs +16 -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
|
+
211–415 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
|
@@ -372,6 +372,13 @@ function buildIos(root, conf, buildDir, outDir) {
|
|
|
372
372
|
`-I${join(KIT, "vendor-webview", "core", "include")}`,
|
|
373
373
|
"-framework", "UIKit", "-framework", "WebKit", "-framework", "Foundation",
|
|
374
374
|
lib,
|
|
375
|
+
// scriptc runs its section elimination only when it performs the final
|
|
376
|
+
// executable link itself; `--lib` deliberately keeps the archive intact
|
|
377
|
+
// (see executableSectionEliminationFlags in its native-toolchain.js), so
|
|
378
|
+
// on the mobile lanes the last link is ours and so is the stripping.
|
|
379
|
+
// ld64 dead-strips per symbol subsection, so the archive needs no
|
|
380
|
+
// -ffunction-sections to benefit: 409 KB -> 232 KB stripped.
|
|
381
|
+
"-Wl,-dead_strip",
|
|
375
382
|
"-o", join(bundle, conf.name),
|
|
376
383
|
]);
|
|
377
384
|
run(["strip", join(bundle, conf.name)]);
|
|
@@ -551,6 +558,14 @@ function buildAndroid(root, conf, buildDir, outDir) {
|
|
|
551
558
|
// libc++_shared.so beside ours; static keeps the APK to one library.
|
|
552
559
|
"-static-libstdc++",
|
|
553
560
|
`-I${join(KIT, "vendor-webview", "core", "include")}`,
|
|
561
|
+
// As on iOS, the final link is ours, so the section GC is too. A shared
|
|
562
|
+
// library exports every default-visibility symbol, which roots the whole
|
|
563
|
+
// scriptc archive and leaves --gc-sections nearly nothing to collect
|
|
564
|
+
// (1.41 MB -> 1.35 MB on its own). --exclude-libs drops the archive out
|
|
565
|
+
// of the dynamic symbol table first, so GC can then discard what the
|
|
566
|
+
// shell never reaches: 1.41 MB -> 853 KB stripped. The JNI entry points
|
|
567
|
+
// come from app.cc rather than the archive, so they stay exported.
|
|
568
|
+
"-Wl,--gc-sections", "-Wl,--exclude-libs,ALL",
|
|
554
569
|
lib, "-llog", "-o", so,
|
|
555
570
|
]);
|
|
556
571
|
run([join(sdk.toolchain, "llvm-strip"), so]);
|
|
@@ -994,7 +1009,7 @@ function build(root, { devUrl = null, gui = true, target = "desktop" } = {}) {
|
|
|
994
1009
|
`}\n`
|
|
995
1010
|
: `const app = createApp<CmdsOf<typeof setup>, EvtsOf<typeof setup>>(WINDOW);\n` +
|
|
996
1011
|
`setup(app);\n` +
|
|
997
|
-
`const rc = app.run(INDEX_HTML)
|
|
1012
|
+
`const rc = app.run(INDEX_HTML);\n` +
|
|
998
1013
|
`console.log("[janela] run returned", rc);\n`;
|
|
999
1014
|
|
|
1000
1015
|
writeFileSync(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "janela",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
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
|
|