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.
- package/README.md +135 -26
- package/bin/janela.mjs +382 -46
- package/package.json +3 -3
- package/runtime/janela.ts +289 -122
- package/shim/wvshim.cc +726 -67
- package/templates/index.html +31 -0
- package/templates/main.ts +62 -19
- package/templates/react/deps.json +4 -0
- package/templates/react/files/index.html +11 -0
- package/templates/react/files/janela.conf.json +10 -0
- package/templates/react/files/src/App.css +3 -0
- package/templates/react/files/src/App.jsx +38 -0
- package/templates/react/files/src/main.jsx +9 -0
- package/templates/react/files/src-host/main.ts +42 -0
- package/templates/react/files/vite.config.js +6 -0
- package/templates/solid/deps.json +4 -0
- package/templates/solid/files/index.html +11 -0
- package/templates/solid/files/janela.conf.json +10 -0
- package/templates/solid/files/src/App.css +3 -0
- package/templates/solid/files/src/App.jsx +35 -0
- package/templates/solid/files/src/main.jsx +4 -0
- package/templates/solid/files/src-host/main.ts +42 -0
- package/templates/solid/files/vite.config.js +6 -0
- package/templates/svelte/deps.json +7 -0
- package/templates/svelte/files/index.html +11 -0
- package/templates/svelte/files/janela.conf.json +10 -0
- package/templates/svelte/files/src/App.svelte +33 -0
- package/templates/svelte/files/src/main.js +4 -0
- package/templates/svelte/files/src-host/main.ts +42 -0
- package/templates/svelte/files/svelte.config.js +3 -0
- package/templates/svelte/files/vite.config.js +6 -0
- package/templates/vue/deps.json +4 -0
- package/templates/vue/files/index.html +11 -0
- package/templates/vue/files/janela.conf.json +10 -0
- package/templates/vue/files/src/App.vue +39 -0
- package/templates/vue/files/src/main.js +4 -0
- package/templates/vue/files/src-host/main.ts +42 -0
- 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
|
-
|
|
45
|
-
appears behind the UI
|
|
46
|
-
|
|
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", (
|
|
73
|
-
const a =
|
|
74
|
-
app.emit("added",
|
|
75
|
-
return
|
|
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", (
|
|
89
|
-
const a =
|
|
90
|
-
app.sleep(a.ms, () => resolve(
|
|
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", (
|
|
95
|
-
const a =
|
|
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(
|
|
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(
|
|
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
|
|
245
|
+
## Constraints inherited from scriptc
|
|
137
246
|
|
|
138
|
-
- Command args
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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)
|
|
155
|
-
notes and scriptc findings behind it are in
|
|
156
|
-
[docs/findings.md](docs/findings.md). Not yet:
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|