janela 0.1.2 → 0.3.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.
Files changed (38) hide show
  1. package/README.md +135 -26
  2. package/bin/janela.mjs +382 -46
  3. package/package.json +3 -3
  4. package/runtime/janela.ts +289 -122
  5. package/shim/wvshim.cc +726 -67
  6. package/templates/index.html +31 -0
  7. package/templates/main.ts +62 -19
  8. package/templates/react/deps.json +4 -0
  9. package/templates/react/files/index.html +11 -0
  10. package/templates/react/files/janela.conf.json +10 -0
  11. package/templates/react/files/src/App.css +3 -0
  12. package/templates/react/files/src/App.jsx +38 -0
  13. package/templates/react/files/src/main.jsx +9 -0
  14. package/templates/react/files/src-host/main.ts +42 -0
  15. package/templates/react/files/vite.config.js +6 -0
  16. package/templates/solid/deps.json +4 -0
  17. package/templates/solid/files/index.html +11 -0
  18. package/templates/solid/files/janela.conf.json +10 -0
  19. package/templates/solid/files/src/App.css +3 -0
  20. package/templates/solid/files/src/App.jsx +35 -0
  21. package/templates/solid/files/src/main.jsx +4 -0
  22. package/templates/solid/files/src-host/main.ts +42 -0
  23. package/templates/solid/files/vite.config.js +6 -0
  24. package/templates/svelte/deps.json +7 -0
  25. package/templates/svelte/files/index.html +11 -0
  26. package/templates/svelte/files/janela.conf.json +10 -0
  27. package/templates/svelte/files/src/App.svelte +33 -0
  28. package/templates/svelte/files/src/main.js +4 -0
  29. package/templates/svelte/files/src-host/main.ts +42 -0
  30. package/templates/svelte/files/svelte.config.js +3 -0
  31. package/templates/svelte/files/vite.config.js +6 -0
  32. package/templates/vue/deps.json +4 -0
  33. package/templates/vue/files/index.html +11 -0
  34. package/templates/vue/files/janela.conf.json +10 -0
  35. package/templates/vue/files/src/App.vue +39 -0
  36. package/templates/vue/files/src/main.js +4 -0
  37. package/templates/vue/files/src-host/main.ts +42 -0
  38. package/templates/vue/files/vite.config.js +6 -0
package/README.md CHANGED
@@ -18,6 +18,19 @@ janela dev # build + run with logs in the terminal
18
18
  janela build # .janela/out/my-app (+ my-app.app on macOS)
19
19
  ```
20
20
 
21
+ Or start from a frontend framework — any Vite-based one:
22
+
23
+ ```bash
24
+ janela init my-app --template vue # or react | svelte | solid | vanilla
25
+ cd my-app && npm install
26
+ janela dev # Vite dev server + HMR, in a native window
27
+ ```
28
+
29
+ `vanilla` is the default and needs no frontend toolchain at all. With a
30
+ framework, `janela dev` runs your Vite dev server and points the window at it,
31
+ and `janela build` flattens the production bundle into the binary — see
32
+ [docs/frontend.md](../../docs/frontend.md).
33
+
21
34
  Requirements: Node 18+, a C++ compiler (Xcode CLT on macOS; g++ +
22
35
  `libwebkit2gtk-4.1-dev` on Linux; see [Windows](#windows) below). A worked
23
36
  example lives in [`examples/demo`](examples/demo) — commands, events, and a
@@ -41,9 +54,15 @@ containing `WebView2.h`. No `WebView2Loader.dll` is needed — webview.h has its
41
54
  own loader — and end users need only the **WebView2 runtime**, which is
42
55
  preinstalled on current Windows 10/11 (it ships with Edge).
43
56
 
44
- Two caveats today: the binary is a console-subsystem app, so a console window
45
- appears behind the UI (handy for `janela dev`, wrong for shipping), and there
46
- is no installer step you get a bare `.exe`, not an MSI.
57
+ `janela build` produces a **GUI-subsystem** `.exe`, so no console window
58
+ appears behind the UI which also means `console.log` from a command has
59
+ nowhere to go. `janela dev` keeps the console subsystem, so logs are there
60
+ while you work. (scriptc exposes no way to pass `-mwindows` to the linker, so
61
+ janela rewrites the PE `Subsystem` field after linking; see
62
+ [docs/native-shell.md](../../docs/native-shell.md).)
63
+
64
+ One caveat today: there is no installer step — you get a bare `.exe`, not an
65
+ MSI.
47
66
 
48
67
  [llvm-mingw]: https://github.com/mstorsjo/llvm-mingw/releases
49
68
 
@@ -56,6 +75,10 @@ my-app/
56
75
  └── janela.conf.json name, bundle identifier, version, window
57
76
  ```
58
77
 
78
+ A Vite project adds a `vite.config.js` and a `src/` tree — that config is what
79
+ makes janela build the frontend with Vite instead of inlining `index.html`
80
+ directly.
81
+
59
82
  Frontend API (injected before page load):
60
83
 
61
84
  ```js
@@ -69,10 +92,10 @@ Backend API (`src-host/main.ts`):
69
92
  import type { JanelaApp } from "./janela";
70
93
 
71
94
  export function setup(app: JanelaApp): void {
72
- app.command("add", (argsJson) => { // args in / result out as JSON text
73
- const a = JSON.parse(argsJson) as { a: number; b: number };
74
- app.emit("added", JSON.stringify(a.a + a.b)); // push an event to the page
75
- return JSON.stringify(a.a + a.b);
95
+ app.command("add", (args) => { // values in, values out
96
+ const a = args as { a: number; b: number };
97
+ app.emit("added", a.a + a.b); // push an event to the page
98
+ return a.a + a.b;
76
99
  });
77
100
  // app.quit() closes the window and returns from run()
78
101
  }
@@ -85,19 +108,19 @@ the window. Register it with `commandAsync` and answer whenever you are ready;
85
108
  the page keeps using the same `await janela.invoke(...)`.
86
109
 
87
110
  ```ts
88
- app.commandAsync("wait", (argsJson, resolve, reject) => {
89
- const a = JSON.parse(argsJson) as { ms: number };
90
- app.sleep(a.ms, () => resolve(JSON.stringify("done"))); // resolve later
111
+ app.commandAsync("wait", (args, resolve, reject) => {
112
+ const a = args as { ms: number };
113
+ app.sleep(a.ms, () => resolve("done")); // resolve later
91
114
  });
92
115
 
93
116
  // Work that cannot just wait: slice it, yielding to the UI between slices.
94
- app.commandAsync("countTo", (argsJson, resolve) => {
95
- const a = JSON.parse(argsJson) as { n: number };
117
+ app.commandAsync("countTo", (args, resolve) => {
118
+ const a = args as { n: number };
96
119
  let i = 0;
97
120
  const step = (): void => {
98
121
  const end = Math.min(i + 2_000_000, a.n);
99
122
  for (; i < end; i++) { /* ... */ }
100
- if (i < a.n) app.defer(step); else resolve(JSON.stringify(i));
123
+ if (i < a.n) app.defer(step); else resolve(i);
101
124
  };
102
125
  app.defer(step);
103
126
  });
@@ -105,8 +128,36 @@ app.commandAsync("countTo", (argsJson, resolve) => {
105
128
 
106
129
  - `app.defer(fn)` — run `fn` on the next turn of the host loop.
107
130
  - `app.sleep(ms, fn)` — run `fn` after at least `ms`.
108
- - `resolve(json)` / `reject(json)` settle the page's promise; `reject` makes
109
- `await janela.invoke(...)` throw. Settling twice is ignored.
131
+ - `resolve(value)` / `reject(reason)` settle the page's promise; `reject`
132
+ makes `await janela.invoke(...)` throw. Settling twice is ignored.
133
+
134
+ ## File I/O
135
+
136
+ `node:fs` works in a handler, but `readFileSync` **blocks the window** for as
137
+ long as the syscall runs — parking a promise does not change that. Use the
138
+ async pair instead: the syscall runs on a worker thread inside the shim, and
139
+ only the result crosses back to your (single-threaded) TypeScript.
140
+
141
+ ```ts
142
+ app.commandAsync("readFile", (args, resolve) => {
143
+ const a = args as { path: string };
144
+ app.readFileAsync(a.path, (err, text) => {
145
+ if (err !== null) { resolve({ ok: false, error: err }); return; }
146
+ resolve({ ok: true, text });
147
+ });
148
+ });
149
+
150
+ app.writeFileAsync("out.txt", "contents", (err) => { /* err is null on success */ });
151
+ ```
152
+
153
+ Errors arrive as values, never throws — `err` carries a Node-shaped message
154
+ (`ENOENT: no such file or directory, open '/x'`). UTF-8 round-trips exactly,
155
+ astral characters included.
156
+
157
+ The payload crosses in a single call (format 3), and the drain that follows it
158
+ costs about **10 ms per MB on the UI thread** — fine for config files and
159
+ documents, still worth chunking for very large media. See
160
+ [docs/async.md](../../docs/async.md) for the measurements.
110
161
 
111
162
  **Use `app.sleep`, not `setTimeout`.** scriptc's own event loop is parked for
112
163
  as long as the program sits inside the `run()` FFI call, so `setTimeout`,
@@ -124,6 +175,64 @@ The backend is ordinary TypeScript with scriptc's stdlib — including a
124
175
  `node:fs` subset — so "read a file" or "call an API" is just code in a
125
176
  command handler, no plugin layer needed.
126
177
 
178
+ ## Native dialogs and window control
179
+
180
+ ```ts
181
+ app.commandAsync("openFile", (_args, resolve) => {
182
+ app.openFileDialog(
183
+ { title: "Pick a file", filters: [{ name: "Text", extensions: ["txt", "md"] }] },
184
+ (paths, err) => {
185
+ if (paths === null) { resolve({ cancelled: true }); return; } // cancel
186
+ app.readFileAsync(paths[0], (rerr, text) => resolve({ path: paths[0], text }));
187
+ },
188
+ );
189
+ });
190
+
191
+ app.saveFileDialog({ defaultName: "untitled.txt" }, (path) => { /* … */ });
192
+
193
+ app.setTitle("new title");
194
+ app.setSize(720, 480, 0);
195
+ app.setFullscreen(true);
196
+ ```
197
+
198
+ A cancel is `null`, not an error. Options: `title`, `defaultPath`, `filters`,
199
+ plus `multiple` and `directory` for open, and `defaultName` for save.
200
+ `directory: true` is not supported on Windows and reports `ENOTSUP`.
201
+
202
+ Use `commandAsync` for dialogs — the user may take as long as they like, and
203
+ the window keeps serving other calls meanwhile. The modal itself runs on a
204
+ later UI-thread turn rather than inside the call that requests it, because a
205
+ nested modal loop would otherwise re-enter the host loop underneath a live TS
206
+ frame; [docs/native-shell.md](../../docs/native-shell.md) has the details, the
207
+ per-platform table, and the Windows GUI-subsystem note.
208
+
209
+ ## Migrating from 0.1.x
210
+
211
+ Commands used to take and return **JSON text**; they now take and return
212
+ **values**, with the runtime handling serialisation. The page-side API
213
+ (`janela.invoke` / `janela.listen`) is unchanged.
214
+
215
+ ```ts
216
+ // 0.1.x
217
+ app.command("add", (argsJson) => {
218
+ const a = JSON.parse(argsJson) as { a: number; b: number };
219
+ app.emit("added", JSON.stringify(a.a + a.b));
220
+ return JSON.stringify(a.a + a.b);
221
+ });
222
+
223
+ // 0.2.x
224
+ app.command("add", (args) => {
225
+ const a = args as { a: number; b: number };
226
+ app.emit("added", a.a + a.b);
227
+ return a.a + a.b;
228
+ });
229
+ ```
230
+
231
+ Mechanically: drop the `JSON.parse(argsJson)` (cast `args` instead), drop
232
+ every `JSON.stringify` around a result, `resolve`/`reject`/`emit` payload, and
233
+ return nothing at all where you used to return `"null"`. Requires Node 24 to
234
+ build (scriptc 0.0.35's floor).
235
+
127
236
  ## What the CLI hides
128
237
 
129
238
  `janela build` assembles `.janela/build/` (runtime + your `main.ts` +
@@ -133,12 +242,12 @@ scriptc. On macOS the frameworks are linked as SDK `.tbd` stubs (scriptc has
133
242
  no `-framework` support) and the binary is wrapped into an ad-hoc-signed
134
243
  `.app` bundle.
135
244
 
136
- ## Constraints inherited from scriptc 0.0.32
245
+ ## Constraints inherited from scriptc
137
246
 
138
- - Command args/results cross the boundary as **JSON text** handlers
139
- `JSON.parse` in and `JSON.stringify` out. The runtime keeps the byte channel
140
- ASCII by `\uXXXX`-escaping non-ASCII (scriptc strings can't hold lone
141
- surrogates, and `JSON.parse` is the reliable reassembly point).
247
+ - Command args and results are ordinary values; the runtime serialises them at
248
+ the boundary, so anything that survives `JSON.stringify`/`JSON.parse` round
249
+ trips (including full Unicode). `args` is typed `unknown` — cast it to the
250
+ shape you expect.
142
251
  - Never use a bare FFI call as a complete variable initializer or assignment
143
252
  RHS — it is silently miscompiled. Wrap it in any expression (`+ 0`). Plain
144
253
  TypeScript is unaffected; only the runtime does FFI, so app code rarely
@@ -151,12 +260,12 @@ no `-framework` support) and the binary is wrapped into an ad-hoc-signed
151
260
 
152
261
  ## Status
153
262
 
154
- Early proof of concept, macOS (arm64) and Linux (WebKitGTK). The design
155
- notes and scriptc findings behind it are in
156
- [docs/findings.md](docs/findings.md). Not yet: Windows, async commands
157
- that run in parallel (host code is single-threaded; `commandAsync` interleaves
158
- instead), native dialogs/tray/menus, multi-window,
159
- icons/installers/notarization.
263
+ Early proof of concept, on macOS (arm64), Linux (WebKitGTK) and Windows
264
+ (WebView2). The design notes and scriptc findings behind it are in
265
+ [docs/findings.md](../../docs/findings.md). Not yet: async commands that run in
266
+ parallel (host code is single-threaded; `commandAsync` interleaves instead),
267
+ tray icons and menus, multi-window, directory picking on Windows,
268
+ `app.center()`, and icons/installers/notarization.
160
269
 
161
270
  ## Releasing
162
271