janela 0.3.1 → 0.4.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 +69 -7
  2. package/api/global.d.ts +27 -0
  3. package/api/index.d.ts +37 -0
  4. package/api/index.js +35 -0
  5. package/bin/janela.mjs +15 -1
  6. package/package.json +15 -1
  7. package/runtime/janela.ts +24 -118
  8. package/runtime/types.ts +131 -0
  9. package/templates/index.html +4 -2
  10. package/templates/main.ts +1 -1
  11. package/templates/react/deps.json +14 -2
  12. package/templates/react/files/index.html +1 -1
  13. package/templates/react/files/src/{App.jsx → App.tsx} +19 -8
  14. package/templates/react/files/src/main.tsx +12 -0
  15. package/templates/react/files/src-host/main.ts +1 -1
  16. package/templates/react/files/tsconfig.json +25 -0
  17. package/templates/solid/deps.json +11 -2
  18. package/templates/solid/files/index.html +1 -1
  19. package/templates/solid/files/src/App.tsx +50 -0
  20. package/templates/solid/files/src/main.tsx +7 -0
  21. package/templates/solid/files/src-host/main.ts +1 -1
  22. package/templates/solid/files/tsconfig.json +26 -0
  23. package/templates/svelte/deps.json +6 -1
  24. package/templates/svelte/files/index.html +1 -1
  25. package/templates/svelte/files/src/App.svelte +10 -7
  26. package/templates/svelte/files/src/main.ts +7 -0
  27. package/templates/svelte/files/src-host/main.ts +1 -1
  28. package/templates/svelte/files/tsconfig.json +23 -0
  29. package/templates/vue/deps.json +12 -2
  30. package/templates/vue/files/index.html +1 -1
  31. package/templates/vue/files/src/App.vue +9 -7
  32. package/templates/vue/files/src-host/main.ts +1 -1
  33. package/templates/vue/files/tsconfig.json +23 -0
  34. package/templates/react/files/src/main.jsx +0 -9
  35. package/templates/solid/files/src/App.jsx +0 -35
  36. package/templates/solid/files/src/main.jsx +0 -4
  37. package/templates/svelte/files/src/main.js +0 -4
  38. /package/templates/vue/files/src/{main.js → main.ts} +0 -0
package/README.md CHANGED
@@ -70,7 +70,7 @@ MSI.
70
70
 
71
71
  ```
72
72
  my-app/
73
- ├── index.html frontend — any HTML/JS; calls janela.invoke() / janela.listen()
73
+ ├── index.html frontend — any HTML/JS/TS; calls invoke() / listen()
74
74
  ├── src-host/main.ts backend — exports setup(app), registers commands
75
75
  └── janela.conf.json name, bundle identifier, version, window
76
76
  ```
@@ -79,17 +79,42 @@ A Vite project adds a `vite.config.js` and a `src/` tree — that config is what
79
79
  makes janela build the frontend with Vite instead of inlining `index.html`
80
80
  directly.
81
81
 
82
- Frontend API (injected before page load):
82
+ Frontend API import it, and your editor and `tsc` know the shapes:
83
+
84
+ ```ts
85
+ import { invoke, listen } from "janela/api";
86
+
87
+ const sum = await invoke<number>("add", { a: 2, b: 40 }); // call a backend command
88
+ listen<number>("added", (payload) => { ... }); // backend-fired events
89
+ ```
90
+
91
+ `janela` is already a devDependency of a scaffolded project, so there is
92
+ nothing extra to install. The generic is what the host command returns —
93
+ values cross the boundary as values, so there is no JSON to parse.
94
+
95
+ <details>
96
+ <summary>No bundler? Use the injected global instead</summary>
97
+
98
+ janela injects the same two functions as `window.janela` before every document
99
+ loads, which is what the `vanilla` template uses — it needs no `npm install` at
100
+ all:
83
101
 
84
102
  ```js
85
- const sum = await janela.invoke("add", { a: 2, b: 40 }); // call a backend command
86
- janela.listen("added", (payload) => { ... }); // backend-fired events
103
+ const sum = await janela.invoke("add", { a: 2, b: 40 });
104
+ janela.listen("added", (payload) => { ... });
87
105
  ```
88
106
 
89
- Backend API (`src-host/main.ts`):
107
+ TypeScript users on this path can pull in the ambient types with
108
+ `/// <reference types="janela/global" />`, or by adding `"janela/global"` to
109
+ `compilerOptions.types`. With a bundler, prefer the import — it needs no
110
+ ambient declaration.
111
+
112
+ </details>
113
+
114
+ Backend API (`src-host/main.ts`) — also typed, from the same package:
90
115
 
91
116
  ```ts
92
- import type { JanelaApp } from "./janela";
117
+ import type { JanelaApp } from "janela/host";
93
118
 
94
119
  export function setup(app: JanelaApp): void {
95
120
  app.command("add", (args) => { // values in, values out
@@ -210,11 +235,48 @@ nested modal loop would otherwise re-enter the host loop underneath a live TS
210
235
  frame; [docs/native-shell.md](../../docs/native-shell.md) has the details, the
211
236
  per-platform table, and the Windows GUI-subsystem note.
212
237
 
238
+ ## Migrating from 0.3.x
239
+
240
+ Nothing breaks: the injected `janela` global still works exactly as before.
241
+ What changed is the recommendation — the frontend now has a real module, so
242
+ editors and `tsc` can see it:
243
+
244
+ ```ts
245
+ // 0.3.x — an untyped global, invisible to tsc and ESLint
246
+ const sum = await janela.invoke("add", { a: 2, b: 40 });
247
+
248
+ // 0.4.x — typed, resolvable, and generic over what the command returns
249
+ import { invoke } from "janela/api";
250
+ const sum = await invoke<number>("add", { a: 2, b: 40 });
251
+ ```
252
+
253
+ The host side had the same problem and gets the same fix. `src-host/main.ts`
254
+ used to import `JanelaApp` from `"./janela"` — a path that only exists inside
255
+ `.janela/build/`, so an editor could never resolve it and the whole `app.*`
256
+ API was untyped:
257
+
258
+ ```ts
259
+ // 0.3.x — unresolved in the editor; JanelaApp was effectively `any`
260
+ import type { JanelaApp } from "./janela";
261
+
262
+ // 0.4.x — resolves against the installed package
263
+ import type { JanelaApp } from "janela/host";
264
+ ```
265
+
266
+ `janela build` rewrites that specifier to the local runtime copy while
267
+ assembling the compile unit, so the build stays fully static and a project
268
+ with no `node_modules` at all still compiles.
269
+
270
+ The framework templates (`vue`, `react`, `svelte`, `solid`) are TypeScript now
271
+ and scaffold with a `typecheck` script that covers `src/` and `src-host/`
272
+ alike. `vanilla` stays plain JavaScript on the global, so it still needs no
273
+ `npm install` before the first build.
274
+
213
275
  ## Migrating from 0.1.x
214
276
 
215
277
  Commands used to take and return **JSON text**; they now take and return
216
278
  **values**, with the runtime handling serialisation. The page-side API
217
- (`janela.invoke` / `janela.listen`) is unchanged.
279
+ (`invoke` / `listen`) is unchanged.
218
280
 
219
281
  ```ts
220
282
  // 0.1.x
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Types for the injected `janela` global, for pages that use it directly
3
+ * rather than importing `janela/api` — a plain `<script>` with no bundler,
4
+ * typically. Pull them in from a TypeScript project with:
5
+ *
6
+ * ```ts
7
+ * /// <reference types="janela/global" />
8
+ * ```
9
+ *
10
+ * or by adding `"janela/global"` to `compilerOptions.types` in tsconfig.json.
11
+ *
12
+ * If you have a bundler, prefer `import { invoke, listen } from "janela/api"` —
13
+ * it needs no ambient declaration and is what the templates use.
14
+ */
15
+
16
+ import type { JanelaBridge } from "./index.js";
17
+
18
+ declare global {
19
+ /** The host bridge janela injects before the document loads. */
20
+ const janela: JanelaBridge;
21
+
22
+ interface Window {
23
+ janela: JanelaBridge;
24
+ }
25
+ }
26
+
27
+ export {};
package/api/index.d.ts ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The janela frontend API.
3
+ *
4
+ * Values cross the boundary as values — janela owns the JSON at the edge — so
5
+ * the generic parameter is what the host command returns, not a string to
6
+ * parse.
7
+ */
8
+
9
+ /** The bridge janela injects as `window.janela` before each document loads. */
10
+ export interface JanelaBridge {
11
+ invoke<T = unknown>(cmd: string, args?: unknown): Promise<T>;
12
+ listen<T = unknown>(event: string, cb: (payload: T) => void): void;
13
+ }
14
+
15
+ /**
16
+ * Call a command the host registered with `app.command` / `app.commandAsync`.
17
+ *
18
+ * ```ts
19
+ * const sum = await invoke<number>("add", { a: 2, b: 40 });
20
+ * ```
21
+ *
22
+ * Rejects if the command is unknown, if the handler rejected, or if the page
23
+ * is not running inside a janela window.
24
+ */
25
+ export declare function invoke<T = unknown>(cmd: string, args?: unknown): Promise<T>;
26
+
27
+ /**
28
+ * Subscribe to an event the host sends with `app.emit`.
29
+ *
30
+ * ```ts
31
+ * listen<number>("added", (sum) => console.log(sum));
32
+ * ```
33
+ */
34
+ export declare function listen<T = unknown>(
35
+ event: string,
36
+ cb: (payload: T) => void,
37
+ ): void;
package/api/index.js ADDED
@@ -0,0 +1,35 @@
1
+ // The janela frontend API — `import { invoke, listen } from "janela/api"`.
2
+ //
3
+ // janela injects a bridge as `window.janela` into every document before it
4
+ // loads, so this module is a thin wrapper over that global rather than a
5
+ // transport of its own. Importing it is the recommended style: bundlers
6
+ // resolve it, editors complete it, and `tsc` checks it. The global stays
7
+ // available unchanged for pages with no build step.
8
+
9
+ function bridge() {
10
+ const found = typeof globalThis === "undefined" ? undefined : globalThis.janela;
11
+ if (!found || typeof found.invoke !== "function") {
12
+ throw new Error(
13
+ "janela: no host bridge on this page (window.janela is undefined). " +
14
+ "The page is not running inside a janela window — run the app with " +
15
+ "`janela dev`, or `janela build` and launch the binary. Opening the " +
16
+ "page in a browser, or serving it with plain `vite`, leaves no host " +
17
+ "to talk to.",
18
+ );
19
+ }
20
+ return found;
21
+ }
22
+
23
+ /**
24
+ * Call a command the host registered with `app.command` / `app.commandAsync`.
25
+ * Arguments and the resolved value are ordinary values; janela owns the
26
+ * serialisation at the boundary.
27
+ */
28
+ export async function invoke(cmd, args) {
29
+ return bridge().invoke(cmd, args);
30
+ }
31
+
32
+ /** Subscribe to an event the host sends with `app.emit`. */
33
+ export function listen(event, cb) {
34
+ bridge().listen(event, cb);
35
+ }
package/bin/janela.mjs CHANGED
@@ -481,9 +481,22 @@ function build(root, { devUrl = null, gui = true } = {}) {
481
481
 
482
482
  // Assemble the compile unit: runtime + user's commands + generated modules.
483
483
  cpSync(join(KIT, "runtime", "janela.ts"), join(buildDir, "janela.ts"));
484
+ cpSync(join(KIT, "runtime", "types.ts"), join(buildDir, "types.ts"));
484
485
  const mainSrc = join(root, "src-host", "main.ts");
485
486
  if (!existsSync(mainSrc)) fail("missing src-host/main.ts");
486
- cpSync(mainSrc, join(buildDir, "main.ts"));
487
+ // A project's main.ts imports from "janela/host" so that it resolves in the
488
+ // editor against the installed package. Here it is compiled next to the
489
+ // runtime instead, so the specifier is rewritten to that local copy: the
490
+ // build never resolves through node_modules, which keeps it static and will
491
+ // keep working when "janela/host" starts exporting values (not just types)
492
+ // as well.
493
+ writeFileSync(
494
+ join(buildDir, "main.ts"),
495
+ readFileSync(mainSrc, "utf8").replace(
496
+ /(\bfrom\s*)(['"])janela\/host\2/g,
497
+ "$1$2./janela$2",
498
+ ),
499
+ );
487
500
 
488
501
  const html = frontendHtml(root, conf, devUrl);
489
502
  writeFileSync(
@@ -616,6 +629,7 @@ function init(name, template) {
616
629
  const extra = JSON.parse(readFileSync(join(tdir, "deps.json"), "utf8"));
617
630
  pkg.type = "module";
618
631
  Object.assign(pkg.devDependencies, extra.devDependencies ?? {});
632
+ Object.assign(pkg.scripts, extra.scripts ?? {});
619
633
  if (extra.dependencies) pkg.dependencies = extra.dependencies;
620
634
  }
621
635
 
package/package.json CHANGED
@@ -1,12 +1,25 @@
1
1
  {
2
2
  "name": "janela",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Desktop apps in pure TypeScript, compiled to native. No Rust, no Node, no Electron.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "janela": "bin/janela.mjs",
8
8
  "jn": "bin/janela.mjs"
9
9
  },
10
+ "exports": {
11
+ "./api": {
12
+ "types": "./api/index.d.ts",
13
+ "default": "./api/index.js"
14
+ },
15
+ "./host": {
16
+ "types": "./runtime/types.ts"
17
+ },
18
+ "./global": {
19
+ "types": "./api/global.d.ts"
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
10
23
  "dependencies": {
11
24
  "scriptc": "0.0.35"
12
25
  },
@@ -33,6 +46,7 @@
33
46
  "node": ">=24"
34
47
  },
35
48
  "files": [
49
+ "api/",
36
50
  "bin/",
37
51
  "runtime/",
38
52
  "shim/",
package/runtime/janela.ts CHANGED
@@ -74,124 +74,30 @@ const BOOTSTRAP =
74
74
  " for (var i = 0; i < cbs.length; i++) cbs[i](payload);" +
75
75
  "};";
76
76
 
77
- // Handlers take the invoked arguments as a value and return a value; the
78
- // runtime owns JSON at the boundary. `args` is whatever the page passed to
79
- // janela.invoke(name, args) cast it to the shape you expect. The return
80
- // value is what the page's promise resolves with.
81
- //
82
- // Throwing is not supported by scriptc across the FFI boundary. Use
83
- // commandAsync's `reject` to fail a call, or return an error value.
84
- export type CommandHandler = (args: unknown) => unknown;
85
-
86
- /**
87
- * An async command: return immediately, answer later. `resolve`/`reject` take
88
- * a value and settle the page's `await janela.invoke(...)` promise whenever
89
- * they are called — from a later defer()/sleep() turn, or from another
90
- * command. The window stays responsive for as long as the call is pending.
91
- */
92
- export type AsyncCommandHandler = (
93
- args: unknown,
94
- resolve: (value: unknown) => void,
95
- reject: (reason: unknown) => void,
96
- ) => void;
97
-
98
- /**
99
- * Completion of an async file operation. `err` is null on success; on failure
100
- * it carries a Node-shaped message ("ENOENT: no such file or directory, open
101
- * '/x'") and `text` is empty. Errors arrive as values, never as throws —
102
- * scriptc cannot propagate an exception across the FFI boundary.
103
- */
104
- export type FsCallback = (err: string | null, text: string) => void;
105
-
106
- /** A named group of extensions offered in a dialog's file-type popup. */
107
- export interface DialogFilter {
108
- name: string;
109
- /** Bare extensions, no dot and no glob: ["png", "jpg"]. */
110
- extensions: string[];
111
- }
112
-
113
- export interface OpenDialogOptions {
114
- title?: string;
115
- /** Directory the dialog opens in. */
116
- defaultPath?: string;
117
- /** Allow picking more than one entry. */
118
- multiple?: boolean;
119
- /** Pick directories instead of files. Not supported on Windows. */
120
- directory?: boolean;
121
- filters?: DialogFilter[];
122
- }
123
-
124
- export interface SaveDialogOptions {
125
- title?: string;
126
- defaultPath?: string;
127
- /** Filename pre-filled in the name field. */
128
- defaultName?: string;
129
- filters?: DialogFilter[];
130
- }
131
-
132
- export interface WindowConfig {
133
- title: string;
134
- width: number;
135
- height: number;
136
- }
137
-
138
- export interface JanelaApp {
139
- handle: number;
140
- names: string[];
141
- handlers: CommandHandler[];
142
- /** Register a named command, callable from the page as janela.invoke(name, args). */
143
- command: (name: string, h: CommandHandler) => void;
144
- /** Register a command that answers later; see AsyncCommandHandler. */
145
- commandAsync: (name: string, h: AsyncCommandHandler) => void;
146
- /** Run fn on the next turn of the host loop — the way to slice long work. */
147
- defer: (fn: () => void) => void;
148
- /** Run fn after at least ms. The host loop's timer; scriptc's setTimeout
149
- * cannot fire while the window is open (its loop is parked inside run()). */
150
- sleep: (ms: number, fn: () => void) => void;
151
- /**
152
- * Read a file without blocking the window. The syscall runs on a shim
153
- * worker thread; the callback lands on the UI thread on a later turn.
154
- * Prefer this over node:fs readFileSync inside a command — that one blocks
155
- * the loop, and with it the whole window.
156
- */
157
- readFileAsync: (path: string, cb: FsCallback) => void;
158
- /** Write a file without blocking the window; cb(null) on success. */
159
- writeFileAsync: (
160
- path: string,
161
- data: string,
162
- cb: (err: string | null) => void,
163
- ) => void;
164
- /**
165
- * Show the native "open" dialog. `cb` gets the chosen paths, or null if the
166
- * user cancelled. The modal runs on a later turn of the UI thread, so
167
- * calling this from inside a command does not block that command's reply —
168
- * pair it with commandAsync when the page is waiting for the result.
169
- */
170
- openFileDialog: (
171
- options: OpenDialogOptions,
172
- cb: (paths: string[] | null, err?: string) => void,
173
- ) => void;
174
- /** Show the native "save" dialog; cb gets the path, or null on cancel. */
175
- saveFileDialog: (
176
- options: SaveDialogOptions,
177
- cb: (path: string | null, err?: string) => void,
178
- ) => void;
179
- /** Change the window title at any time, not just at startup. */
180
- setTitle: (title: string) => void;
181
- /**
182
- * Resize the window. `hint` is webview's sizing hint: 0 none, 1 minimum,
183
- * 2 maximum, 3 fixed.
184
- */
185
- setSize: (width: number, height: number, hint?: number) => void;
186
- /** Enter or leave fullscreen. */
187
- setFullscreen: (on: boolean) => void;
188
- /** Fire an event into the page; the payload is delivered as a value. */
189
- emit: (event: string, payload: unknown) => void;
190
- /** Close the window and make run() return. */
191
- quit: () => void;
192
- /** Show the page and block until the window closes. Returns the run status. */
193
- run: (html: string) => number;
194
- }
77
+ // The public host types live in ./types (shipped as `janela/host` too, so a
78
+ // user's editor can see them). Re-exported here because the compiled build
79
+ // resolves them through this module see the specifier rewrite in the CLI.
80
+ export type {
81
+ AsyncCommandHandler,
82
+ CommandHandler,
83
+ DialogFilter,
84
+ FsCallback,
85
+ JanelaApp,
86
+ OpenDialogOptions,
87
+ SaveDialogOptions,
88
+ WindowConfig,
89
+ } from "./types";
90
+
91
+ import type {
92
+ AsyncCommandHandler,
93
+ CommandHandler,
94
+ DialogFilter,
95
+ FsCallback,
96
+ JanelaApp,
97
+ OpenDialogOptions,
98
+ SaveDialogOptions,
99
+ WindowConfig,
100
+ } from "./types";
195
101
 
196
102
  // JSON.stringify yields undefined for undefined; the wire always needs a
197
103
  // value, and a command that returns nothing should read as null in the page.
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Public host-side types for a janela app — the shapes `src-host/main.ts`
3
+ * works with.
4
+ *
5
+ * This file is the single definition of those types. It is both:
6
+ * - what `import type { JanelaApp } from "janela/host"` resolves to in an
7
+ * editor, via the package's exports map; and
8
+ * - re-exported by runtime/janela.ts, which is what the compiled build
9
+ * actually links against (the CLI copies both files into .janela/build/).
10
+ *
11
+ * Keep it declaration-only: no runtime code lives here.
12
+ */
13
+
14
+ // Handlers take the invoked arguments as a value and return a value; the
15
+ // runtime owns JSON at the boundary. `args` is whatever the page passed to
16
+ // janela.invoke(name, args) — cast it to the shape you expect. The return
17
+ // value is what the page's promise resolves with.
18
+ //
19
+ // Throwing is not supported by scriptc across the FFI boundary. Use
20
+ // commandAsync's `reject` to fail a call, or return an error value.
21
+ export type CommandHandler = (args: unknown) => unknown;
22
+
23
+ /**
24
+ * An async command: return immediately, answer later. `resolve`/`reject` take
25
+ * a value and settle the page's `await janela.invoke(...)` promise whenever
26
+ * they are called — from a later defer()/sleep() turn, or from another
27
+ * command. The window stays responsive for as long as the call is pending.
28
+ */
29
+ export type AsyncCommandHandler = (
30
+ args: unknown,
31
+ resolve: (value: unknown) => void,
32
+ reject: (reason: unknown) => void,
33
+ ) => void;
34
+
35
+ /**
36
+ * Completion of an async file operation. `err` is null on success; on failure
37
+ * it carries a Node-shaped message ("ENOENT: no such file or directory, open
38
+ * '/x'") and `text` is empty. Errors arrive as values, never as throws —
39
+ * scriptc cannot propagate an exception across the FFI boundary.
40
+ */
41
+ export type FsCallback = (err: string | null, text: string) => void;
42
+
43
+ /** A named group of extensions offered in a dialog's file-type popup. */
44
+ export interface DialogFilter {
45
+ name: string;
46
+ /** Bare extensions, no dot and no glob: ["png", "jpg"]. */
47
+ extensions: string[];
48
+ }
49
+
50
+ export interface OpenDialogOptions {
51
+ title?: string;
52
+ /** Directory the dialog opens in. */
53
+ defaultPath?: string;
54
+ /** Allow picking more than one entry. */
55
+ multiple?: boolean;
56
+ /** Pick directories instead of files. Not supported on Windows. */
57
+ directory?: boolean;
58
+ filters?: DialogFilter[];
59
+ }
60
+
61
+ export interface SaveDialogOptions {
62
+ title?: string;
63
+ defaultPath?: string;
64
+ /** Filename pre-filled in the name field. */
65
+ defaultName?: string;
66
+ filters?: DialogFilter[];
67
+ }
68
+
69
+ export interface WindowConfig {
70
+ title: string;
71
+ width: number;
72
+ height: number;
73
+ }
74
+
75
+ export interface JanelaApp {
76
+ handle: number;
77
+ names: string[];
78
+ handlers: CommandHandler[];
79
+ /** Register a named command, callable from the page as janela.invoke(name, args). */
80
+ command: (name: string, h: CommandHandler) => void;
81
+ /** Register a command that answers later; see AsyncCommandHandler. */
82
+ commandAsync: (name: string, h: AsyncCommandHandler) => void;
83
+ /** Run fn on the next turn of the host loop — the way to slice long work. */
84
+ defer: (fn: () => void) => void;
85
+ /** Run fn after at least ms. The host loop's timer; scriptc's setTimeout
86
+ * cannot fire while the window is open (its loop is parked inside run()). */
87
+ sleep: (ms: number, fn: () => void) => void;
88
+ /**
89
+ * Read a file without blocking the window. The syscall runs on a shim
90
+ * worker thread; the callback lands on the UI thread on a later turn.
91
+ * Prefer this over node:fs readFileSync inside a command — that one blocks
92
+ * the loop, and with it the whole window.
93
+ */
94
+ readFileAsync: (path: string, cb: FsCallback) => void;
95
+ /** Write a file without blocking the window; cb(null) on success. */
96
+ writeFileAsync: (
97
+ path: string,
98
+ data: string,
99
+ cb: (err: string | null) => void,
100
+ ) => void;
101
+ /**
102
+ * Show the native "open" dialog. `cb` gets the chosen paths, or null if the
103
+ * user cancelled. The modal runs on a later turn of the UI thread, so
104
+ * calling this from inside a command does not block that command's reply —
105
+ * pair it with commandAsync when the page is waiting for the result.
106
+ */
107
+ openFileDialog: (
108
+ options: OpenDialogOptions,
109
+ cb: (paths: string[] | null, err?: string) => void,
110
+ ) => void;
111
+ /** Show the native "save" dialog; cb gets the path, or null on cancel. */
112
+ saveFileDialog: (
113
+ options: SaveDialogOptions,
114
+ cb: (path: string | null, err?: string) => void,
115
+ ) => void;
116
+ /** Change the window title at any time, not just at startup. */
117
+ setTitle: (title: string) => void;
118
+ /**
119
+ * Resize the window. `hint` is webview's sizing hint: 0 none, 1 minimum,
120
+ * 2 maximum, 3 fixed.
121
+ */
122
+ setSize: (width: number, height: number, hint?: number) => void;
123
+ /** Enter or leave fullscreen. */
124
+ setFullscreen: (on: boolean) => void;
125
+ /** Fire an event into the page; the payload is delivered as a value. */
126
+ emit: (event: string, payload: unknown) => void;
127
+ /** Close the window and make run() return. */
128
+ quit: () => void;
129
+ /** Show the page and block until the window closes. Returns the run status. */
130
+ run: (html: string) => number;
131
+ }
@@ -30,8 +30,10 @@
30
30
  <ul id="events"></ul>
31
31
 
32
32
  <script>
33
- // `janela.invoke(name, args)` and `janela.listen(event, cb)` are injected by
34
- // janela before the page loads the Tauri-shaped frontend API.
33
+ // This template has no bundler, so it uses the `janela` global that the
34
+ // host injects before the page loads. Projects with a build step should
35
+ // `import { invoke, listen } from "janela/api"` instead — same functions,
36
+ // but typed and resolvable by the bundler.
35
37
  window.onload = async () => {
36
38
  const out = document.getElementById("out");
37
39
  const events = document.getElementById("events");
package/templates/main.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  // complete variable initializer — wrap it in any expression (`+ 0`). Plain
9
9
  // TypeScript like everything in this file is unaffected.
10
10
 
11
- import type { JanelaApp } from "./janela";
11
+ import type { JanelaApp } from "janela/host";
12
12
 
13
13
  export function setup(app: JanelaApp): void {
14
14
  app.command("add", (args) => {
@@ -1,4 +1,16 @@
1
1
  {
2
- "dependencies": { "react": "^19.0.0", "react-dom": "^19.0.0" },
3
- "devDependencies": { "@vitejs/plugin-react": "^4.3.4", "vite": "^6.0.7" }
2
+ "dependencies": {
3
+ "react": "^19.0.0",
4
+ "react-dom": "^19.0.0"
5
+ },
6
+ "devDependencies": {
7
+ "@vitejs/plugin-react": "^4.3.4",
8
+ "vite": "^6.0.7",
9
+ "@types/react": "^19.0.7",
10
+ "@types/react-dom": "^19.0.3",
11
+ "typescript": "^5.7.3"
12
+ },
13
+ "scripts": {
14
+ "typecheck": "tsc --noEmit"
15
+ }
4
16
  }
@@ -6,6 +6,6 @@
6
6
  </head>
7
7
  <body>
8
8
  <div id="root"></div>
9
- <script type="module" src="/src/main.jsx"></script>
9
+ <script type="module" src="/src/main.tsx"></script>
10
10
  </body>
11
11
  </html>
@@ -1,30 +1,41 @@
1
1
  import { useEffect, useState } from "react";
2
+ import { invoke, listen } from "janela/api";
2
3
  import "./App.css";
3
4
 
4
5
  export default function App() {
5
6
  const [greeting, setGreeting] = useState("…");
6
7
  const [a, setA] = useState(2);
7
8
  const [b, setB] = useState(40);
8
- const [sum, setSum] = useState(null);
9
- const [events, setEvents] = useState([]);
9
+ const [sum, setSum] = useState<number | null>(null);
10
+ const [events, setEvents] = useState<string[]>([]);
10
11
 
11
12
  useEffect(() => {
12
- // Backend→frontend events. The payload arrives as a value.
13
- janela.listen("added", (value) =>
13
+ // Backend→frontend events. The payload arrives as a value, and the
14
+ // generic says which value.
15
+ listen<number>("added", (value) =>
14
16
  setEvents((prev) => [`host emitted: ${value}`, ...prev]),
15
17
  );
16
- janela.invoke("greet", { name: "__NAME__" }).then(setGreeting);
18
+ invoke<string>("greet", { name: "__NAME__" }).then(setGreeting);
17
19
  }, []);
18
20
 
19
21
  const add = async () =>
20
- setSum(await janela.invoke("add", { a: Number(a), b: Number(b) }));
22
+ setSum(await invoke<number>("add", { a, b }));
21
23
 
22
24
  return (
23
25
  <>
24
26
  <h1>{greeting}</h1>
25
27
  <p>
26
- <input type="number" value={a} onChange={(e) => setA(e.target.value)} /> +
27
- <input type="number" value={b} onChange={(e) => setB(e.target.value)} />
28
+ <input
29
+ type="number"
30
+ value={a}
31
+ onChange={(e) => setA(Number(e.target.value))}
32
+ />{" "}
33
+ +
34
+ <input
35
+ type="number"
36
+ value={b}
37
+ onChange={(e) => setB(Number(e.target.value))}
38
+ />
28
39
  <button onClick={add}>add</button>
29
40
  {sum !== null && <span> = {sum}</span>}
30
41
  </p>
@@ -0,0 +1,12 @@
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import App from "./App.tsx";
4
+
5
+ const root = document.getElementById("root");
6
+ if (!root) throw new Error("index.html is missing #root");
7
+
8
+ createRoot(root).render(
9
+ <StrictMode>
10
+ <App />
11
+ </StrictMode>,
12
+ );
@@ -8,7 +8,7 @@
8
8
  // complete variable initializer — wrap it in any expression (`+ 0`). Plain
9
9
  // TypeScript like everything in this file is unaffected.
10
10
 
11
- import type { JanelaApp } from "./janela";
11
+ import type { JanelaApp } from "janela/host";
12
12
 
13
13
  export function setup(app: JanelaApp): void {
14
14
  app.command("add", (args) => {
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": [
7
+ "ES2022",
8
+ "DOM",
9
+ "DOM.Iterable"
10
+ ],
11
+ "types": [],
12
+ "jsx": "react-jsx",
13
+ "strict": true,
14
+ "noEmit": true,
15
+ "allowImportingTsExtensions": true,
16
+ "skipLibCheck": true,
17
+ "isolatedModules": true,
18
+ "verbatimModuleSyntax": true
19
+ },
20
+ "include": [
21
+ "src/**/*.ts",
22
+ "src/**/*.tsx",
23
+ "src-host/**/*.ts"
24
+ ]
25
+ }
@@ -1,4 +1,13 @@
1
1
  {
2
- "dependencies": { "solid-js": "^1.9.4" },
3
- "devDependencies": { "vite": "^6.0.7", "vite-plugin-solid": "^2.11.0" }
2
+ "dependencies": {
3
+ "solid-js": "^1.9.4"
4
+ },
5
+ "devDependencies": {
6
+ "vite": "^6.0.7",
7
+ "vite-plugin-solid": "^2.11.0",
8
+ "typescript": "^5.7.3"
9
+ },
10
+ "scripts": {
11
+ "typecheck": "tsc --noEmit"
12
+ }
4
13
  }
@@ -6,6 +6,6 @@
6
6
  </head>
7
7
  <body>
8
8
  <div id="root"></div>
9
- <script type="module" src="/src/main.jsx"></script>
9
+ <script type="module" src="/src/main.tsx"></script>
10
10
  </body>
11
11
  </html>
@@ -0,0 +1,50 @@
1
+ import { createSignal, onMount, For, Show } from "solid-js";
2
+ import { invoke, listen } from "janela/api";
3
+ import "./App.css";
4
+
5
+ export default function App() {
6
+ const [greeting, setGreeting] = createSignal("…");
7
+ const [a, setA] = createSignal(2);
8
+ const [b, setB] = createSignal(40);
9
+ const [sum, setSum] = createSignal<number | null>(null);
10
+ const [events, setEvents] = createSignal<string[]>([]);
11
+
12
+ // Backend→frontend events. The payload arrives as a value, and the generic
13
+ // says which value.
14
+ listen<number>("added", (value) =>
15
+ setEvents((prev) => [`host emitted: ${value}`, ...prev]),
16
+ );
17
+
18
+ onMount(async () =>
19
+ setGreeting(await invoke<string>("greet", { name: "__NAME__" })),
20
+ );
21
+
22
+ const add = async () =>
23
+ setSum(await invoke<number>("add", { a: a(), b: b() }));
24
+
25
+ return (
26
+ <>
27
+ <h1>{greeting()}</h1>
28
+ <p>
29
+ <input
30
+ type="number"
31
+ value={a()}
32
+ onInput={(e) => setA(Number(e.currentTarget.value))}
33
+ />{" "}
34
+ +
35
+ <input
36
+ type="number"
37
+ value={b()}
38
+ onInput={(e) => setB(Number(e.currentTarget.value))}
39
+ />
40
+ <button onClick={add}>add</button>
41
+ <Show when={sum() !== null}>
42
+ <span> = {sum()}</span>
43
+ </Show>
44
+ </p>
45
+ <ul>
46
+ <For each={events()}>{(e) => <li>{e}</li>}</For>
47
+ </ul>
48
+ </>
49
+ );
50
+ }
@@ -0,0 +1,7 @@
1
+ import { render } from "solid-js/web";
2
+ import App from "./App.tsx";
3
+
4
+ const root = document.getElementById("root");
5
+ if (!root) throw new Error("index.html is missing #root");
6
+
7
+ render(() => <App />, root);
@@ -8,7 +8,7 @@
8
8
  // complete variable initializer — wrap it in any expression (`+ 0`). Plain
9
9
  // TypeScript like everything in this file is unaffected.
10
10
 
11
- import type { JanelaApp } from "./janela";
11
+ import type { JanelaApp } from "janela/host";
12
12
 
13
13
  export function setup(app: JanelaApp): void {
14
14
  app.command("add", (args) => {
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": [
7
+ "ES2022",
8
+ "DOM",
9
+ "DOM.Iterable"
10
+ ],
11
+ "types": [],
12
+ "jsx": "preserve",
13
+ "jsxImportSource": "solid-js",
14
+ "strict": true,
15
+ "noEmit": true,
16
+ "allowImportingTsExtensions": true,
17
+ "skipLibCheck": true,
18
+ "isolatedModules": true,
19
+ "verbatimModuleSyntax": true
20
+ },
21
+ "include": [
22
+ "src/**/*.ts",
23
+ "src/**/*.tsx",
24
+ "src-host/**/*.ts"
25
+ ]
26
+ }
@@ -2,6 +2,11 @@
2
2
  "devDependencies": {
3
3
  "@sveltejs/vite-plugin-svelte": "^5.0.3",
4
4
  "svelte": "^5.16.0",
5
- "vite": "^6.0.7"
5
+ "vite": "^6.0.7",
6
+ "svelte-check": "^4.1.4",
7
+ "typescript": "^5.7.3"
8
+ },
9
+ "scripts": {
10
+ "typecheck": "svelte-check --tsconfig ./tsconfig.json"
6
11
  }
7
12
  }
@@ -6,6 +6,6 @@
6
6
  </head>
7
7
  <body>
8
8
  <div id="app"></div>
9
- <script type="module" src="/src/main.js"></script>
9
+ <script type="module" src="/src/main.ts"></script>
10
10
  </body>
11
11
  </html>
@@ -1,17 +1,20 @@
1
- <script>
1
+ <script lang="ts">
2
+ import { invoke, listen } from "janela/api";
3
+
2
4
  let greeting = $state("…");
3
5
  let a = $state(2);
4
6
  let b = $state(40);
5
- let sum = $state(null);
6
- let events = $state([]);
7
+ let sum = $state<number | null>(null);
8
+ let events = $state<string[]>([]);
7
9
 
8
- // Backend→frontend events. The payload arrives as a value.
9
- janela.listen("added", (value) => (events = [`host emitted: ${value}`, ...events]));
10
+ // Backend→frontend events. The payload arrives as a value, and the generic
11
+ // says which value.
12
+ listen<number>("added", (value) => (events = [`host emitted: ${value}`, ...events]));
10
13
 
11
- janela.invoke("greet", { name: "__NAME__" }).then((g) => (greeting = g));
14
+ invoke<string>("greet", { name: "__NAME__" }).then((g) => (greeting = g));
12
15
 
13
16
  async function add() {
14
- sum = await janela.invoke("add", { a: Number(a), b: Number(b) });
17
+ sum = await invoke<number>("add", { a: Number(a), b: Number(b) });
15
18
  }
16
19
  </script>
17
20
 
@@ -0,0 +1,7 @@
1
+ import { mount } from "svelte";
2
+ import App from "./App.svelte";
3
+
4
+ const target = document.getElementById("app");
5
+ if (!target) throw new Error("index.html is missing #app");
6
+
7
+ mount(App, { target });
@@ -8,7 +8,7 @@
8
8
  // complete variable initializer — wrap it in any expression (`+ 0`). Plain
9
9
  // TypeScript like everything in this file is unaffected.
10
10
 
11
- import type { JanelaApp } from "./janela";
11
+ import type { JanelaApp } from "janela/host";
12
12
 
13
13
  export function setup(app: JanelaApp): void {
14
14
  app.command("add", (args) => {
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": [
7
+ "ES2022",
8
+ "DOM",
9
+ "DOM.Iterable"
10
+ ],
11
+ "types": [],
12
+ "strict": true,
13
+ "noEmit": true,
14
+ "skipLibCheck": true,
15
+ "isolatedModules": true,
16
+ "verbatimModuleSyntax": true
17
+ },
18
+ "include": [
19
+ "src/**/*.ts",
20
+ "src/**/*.svelte",
21
+ "src-host/**/*.ts"
22
+ ]
23
+ }
@@ -1,4 +1,14 @@
1
1
  {
2
- "dependencies": { "vue": "^3.5.13" },
3
- "devDependencies": { "@vitejs/plugin-vue": "^5.2.1", "vite": "^6.0.7" }
2
+ "dependencies": {
3
+ "vue": "^3.5.13"
4
+ },
5
+ "devDependencies": {
6
+ "@vitejs/plugin-vue": "^5.2.1",
7
+ "vite": "^6.0.7",
8
+ "typescript": "^5.7.3",
9
+ "vue-tsc": "^2.2.0"
10
+ },
11
+ "scripts": {
12
+ "typecheck": "vue-tsc --noEmit"
13
+ }
4
14
  }
@@ -6,6 +6,6 @@
6
6
  </head>
7
7
  <body>
8
8
  <div id="app"></div>
9
- <script type="module" src="/src/main.js"></script>
9
+ <script type="module" src="/src/main.ts"></script>
10
10
  </body>
11
11
  </html>
@@ -1,21 +1,23 @@
1
- <script setup>
1
+ <script setup lang="ts">
2
2
  import { onMounted, ref } from "vue";
3
+ import { invoke, listen } from "janela/api";
3
4
 
4
5
  const greeting = ref("…");
5
6
  const a = ref(2);
6
7
  const b = ref(40);
7
- const sum = ref(null);
8
- const events = ref([]);
8
+ const sum = ref<number | null>(null);
9
+ const events = ref<string[]>([]);
9
10
 
10
- // Backend→frontend events. The payload arrives as a value, not a JSON string.
11
- janela.listen("added", (value) => events.value.unshift(`host emitted: ${value}`));
11
+ // Backend→frontend events. The payload arrives as a value, not a JSON string,
12
+ // and the generic says which value.
13
+ listen<number>("added", (value) => events.value.unshift(`host emitted: ${value}`));
12
14
 
13
15
  onMounted(async () => {
14
- greeting.value = await janela.invoke("greet", { name: "__NAME__" });
16
+ greeting.value = await invoke<string>("greet", { name: "__NAME__" });
15
17
  });
16
18
 
17
19
  async function add() {
18
- sum.value = await janela.invoke("add", { a: Number(a.value), b: Number(b.value) });
20
+ sum.value = await invoke<number>("add", { a: Number(a.value), b: Number(b.value) });
19
21
  }
20
22
  </script>
21
23
 
@@ -8,7 +8,7 @@
8
8
  // complete variable initializer — wrap it in any expression (`+ 0`). Plain
9
9
  // TypeScript like everything in this file is unaffected.
10
10
 
11
- import type { JanelaApp } from "./janela";
11
+ import type { JanelaApp } from "janela/host";
12
12
 
13
13
  export function setup(app: JanelaApp): void {
14
14
  app.command("add", (args) => {
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": [
7
+ "ES2022",
8
+ "DOM",
9
+ "DOM.Iterable"
10
+ ],
11
+ "types": [],
12
+ "strict": true,
13
+ "noEmit": true,
14
+ "skipLibCheck": true,
15
+ "isolatedModules": true,
16
+ "verbatimModuleSyntax": true
17
+ },
18
+ "include": [
19
+ "src/**/*.ts",
20
+ "src/**/*.vue",
21
+ "src-host/**/*.ts"
22
+ ]
23
+ }
@@ -1,9 +0,0 @@
1
- import { StrictMode } from "react";
2
- import { createRoot } from "react-dom/client";
3
- import App from "./App.jsx";
4
-
5
- createRoot(document.getElementById("root")).render(
6
- <StrictMode>
7
- <App />
8
- </StrictMode>,
9
- );
@@ -1,35 +0,0 @@
1
- import { createSignal, onMount, For, Show } from "solid-js";
2
- import "./App.css";
3
-
4
- export default function App() {
5
- const [greeting, setGreeting] = createSignal("…");
6
- const [a, setA] = createSignal(2);
7
- const [b, setB] = createSignal(40);
8
- const [sum, setSum] = createSignal(null);
9
- const [events, setEvents] = createSignal([]);
10
-
11
- // Backend→frontend events. The payload arrives as a value.
12
- janela.listen("added", (value) =>
13
- setEvents((prev) => [`host emitted: ${value}`, ...prev]),
14
- );
15
-
16
- onMount(async () => setGreeting(await janela.invoke("greet", { name: "__NAME__" })));
17
-
18
- const add = async () =>
19
- setSum(await janela.invoke("add", { a: Number(a()), b: Number(b()) }));
20
-
21
- return (
22
- <>
23
- <h1>{greeting()}</h1>
24
- <p>
25
- <input type="number" value={a()} onInput={(e) => setA(e.currentTarget.value)} /> +
26
- <input type="number" value={b()} onInput={(e) => setB(e.currentTarget.value)} />
27
- <button onClick={add}>add</button>
28
- <Show when={sum() !== null}><span> = {sum()}</span></Show>
29
- </p>
30
- <ul>
31
- <For each={events()}>{(e) => <li>{e}</li>}</For>
32
- </ul>
33
- </>
34
- );
35
- }
@@ -1,4 +0,0 @@
1
- import { render } from "solid-js/web";
2
- import App from "./App.jsx";
3
-
4
- render(() => <App />, document.getElementById("root"));
@@ -1,4 +0,0 @@
1
- import { mount } from "svelte";
2
- import App from "./App.svelte";
3
-
4
- mount(App, { target: document.getElementById("app") });
File without changes