janela 0.3.1 → 0.5.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 (39) hide show
  1. package/README.md +178 -7
  2. package/api/global.d.ts +27 -0
  3. package/api/index.d.ts +102 -0
  4. package/api/index.js +64 -0
  5. package/bin/janela.mjs +15 -1
  6. package/package.json +15 -1
  7. package/runtime/janela.ts +38 -118
  8. package/runtime/types.ts +251 -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.tsx +55 -0
  14. package/templates/react/files/src/main.tsx +12 -0
  15. package/templates/react/files/src-host/main.ts +48 -22
  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 +55 -0
  20. package/templates/solid/files/src/main.tsx +7 -0
  21. package/templates/solid/files/src-host/main.ts +48 -22
  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 +15 -7
  26. package/templates/svelte/files/src/main.ts +7 -0
  27. package/templates/svelte/files/src-host/main.ts +48 -22
  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 +16 -8
  32. package/templates/vue/files/src-host/main.ts +48 -22
  33. package/templates/vue/files/tsconfig.json +23 -0
  34. package/templates/react/files/src/App.jsx +0 -38
  35. package/templates/react/files/src/main.jsx +0 -9
  36. package/templates/solid/files/src/App.jsx +0 -35
  37. package/templates/solid/files/src/main.jsx +0 -4
  38. package/templates/svelte/files/src/main.js +0 -4
  39. /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
@@ -101,6 +126,80 @@ export function setup(app: JanelaApp): void {
101
126
  }
102
127
  ```
103
128
 
129
+ ## The typed contract
130
+
131
+ The API above works, but `invoke<number>("add", …)` is an *assertion*: nothing
132
+ checks that the command exists, that the arguments match, or that the result
133
+ is really a number. Rename a command and the page still compiles.
134
+
135
+ Declare a contract instead, and both sides are checked against the same
136
+ declarations — no code generation, nothing to keep in sync. This is the
137
+ framework templates' default.
138
+
139
+ ```ts
140
+ // src-host/main.ts
141
+ import { defineCommands, defineEvents, emit, on, onAsync, type JanelaApp } from "janela/host";
142
+
143
+ export const commands = defineCommands<{
144
+ add: { args: { a: number; b: number }; result: number };
145
+ greet: { args: { name: string }; result: string };
146
+ wait: { args: { ms: number }; result: string };
147
+ }>();
148
+
149
+ export const events = defineEvents<{ added: number }>();
150
+
151
+ export type App = { commands: typeof commands; events: typeof events };
152
+
153
+ export function setup(app: JanelaApp): void {
154
+ on(app, commands, "add", (args) => { // args inferred: { a: number; b: number }
155
+ emit(app, events, "added", args.a + args.b);
156
+ return args.a + args.b; // return type checked against the contract
157
+ });
158
+ onAsync(app, commands, "wait", (args, resolve) => {
159
+ app.sleep(args.ms, () => resolve("waited " + args.ms + "ms"));
160
+ });
161
+ }
162
+ ```
163
+
164
+ ```ts
165
+ // src/App.tsx (or .vue, .svelte …)
166
+ import { createClient } from "janela/api";
167
+ import type { App } from "../src-host/main"; // type-only: erased at compile time
168
+
169
+ const client = createClient<App>();
170
+
171
+ const sum = await client.invoke("add", { a: 2, b: 40 }); // sum: number
172
+ const off = client.on("added", (v) => console.log(v)); // v: number
173
+ off(); // unsubscribe
174
+ ```
175
+
176
+ Now these are all compile errors:
177
+
178
+ ```ts
179
+ await client.invoke("addd", { a: 1, b: 2 }); // unknown command
180
+ await client.invoke("add", { a: 1, b: "2" }); // wrong argument type
181
+ const s: string = await client.invoke("add", { a: 1, b: 2 }); // wrong result
182
+ client.on("addedd", () => {}); // unknown event
183
+ client.on("added", (v: string) => {}); // wrong payload type
184
+ ```
185
+
186
+ Two things worth knowing:
187
+
188
+ - **`import type` is erased**, so no host code is bundled into the page — the
189
+ contract is a type edge and nothing more. (Verified: the built frontend
190
+ bundle contains none of the host's strings.)
191
+ - **Types erase at runtime.** Payloads still cross as JSON and nothing
192
+ validates a malformed one. This is compile-time safety, like Tauri's
193
+ `invoke<T>()` — the difference is that here the types come from the host's
194
+ own declarations rather than from an assertion you write by hand, which is
195
+ only possible because both sides are TypeScript.
196
+
197
+ Write `args: null` for a command that takes nothing — `args: undefined`
198
+ lowers to a zero-parameter function and fails to compile.
199
+
200
+ The untyped `invoke` / `listen` still work unchanged; the contract is
201
+ additive, and the `vanilla` template still uses the global.
202
+
104
203
  ## Async commands
105
204
 
106
205
  A command that has to wait — or to chew through real work — should not freeze
@@ -210,11 +309,83 @@ nested modal loop would otherwise re-enter the host loop underneath a live TS
210
309
  frame; [docs/native-shell.md](../../docs/native-shell.md) has the details, the
211
310
  per-platform table, and the Windows GUI-subsystem note.
212
311
 
312
+ ## Migrating from 0.4.x
313
+
314
+ Nothing breaks: `app.command`, `app.emit`, and the untyped `invoke` / `listen`
315
+ all work exactly as before, and the `vanilla` template is unchanged.
316
+
317
+ Two things are new:
318
+
319
+ - `listen()` (and the injected `janela.listen`) now **return a disposer**.
320
+ Previously they returned nothing, so existing code is unaffected.
321
+ - The **typed contract** — `defineCommands` / `defineEvents` on the host,
322
+ `createClient<App>()` on the page. The framework templates now scaffold with
323
+ it. See [The typed contract](#the-typed-contract).
324
+
325
+ To adopt it in an existing app, declare what the host already exposes and swap
326
+ the registrations:
327
+
328
+ ```ts
329
+ // before
330
+ app.command("add", (args) => {
331
+ const a = args as { a: number; b: number };
332
+ return a.a + a.b;
333
+ });
334
+
335
+ // after
336
+ export const commands = defineCommands<{
337
+ add: { args: { a: number; b: number }; result: number };
338
+ }>();
339
+ export type App = { commands: typeof commands; events: typeof events };
340
+
341
+ on(app, commands, "add", (args) => args.a + args.b); // args inferred, no cast
342
+ ```
343
+
344
+ then on the page, replace `invoke<number>("add", …)` with
345
+ `client.invoke("add", …)` built from `createClient<App>()`.
346
+
347
+ ## Migrating from 0.3.x
348
+
349
+ Nothing breaks: the injected `janela` global still works exactly as before.
350
+ What changed is the recommendation — the frontend now has a real module, so
351
+ editors and `tsc` can see it:
352
+
353
+ ```ts
354
+ // 0.3.x — an untyped global, invisible to tsc and ESLint
355
+ const sum = await janela.invoke("add", { a: 2, b: 40 });
356
+
357
+ // 0.4.x — typed, resolvable, and generic over what the command returns
358
+ import { invoke } from "janela/api";
359
+ const sum = await invoke<number>("add", { a: 2, b: 40 });
360
+ ```
361
+
362
+ The host side had the same problem and gets the same fix. `src-host/main.ts`
363
+ used to import `JanelaApp` from `"./janela"` — a path that only exists inside
364
+ `.janela/build/`, so an editor could never resolve it and the whole `app.*`
365
+ API was untyped:
366
+
367
+ ```ts
368
+ // 0.3.x — unresolved in the editor; JanelaApp was effectively `any`
369
+ import type { JanelaApp } from "./janela";
370
+
371
+ // 0.4.x — resolves against the installed package
372
+ import type { JanelaApp } from "janela/host";
373
+ ```
374
+
375
+ `janela build` rewrites that specifier to the local runtime copy while
376
+ assembling the compile unit, so the build stays fully static and a project
377
+ with no `node_modules` at all still compiles.
378
+
379
+ The framework templates (`vue`, `react`, `svelte`, `solid`) are TypeScript now
380
+ and scaffold with a `typecheck` script that covers `src/` and `src-host/`
381
+ alike. `vanilla` stays plain JavaScript on the global, so it still needs no
382
+ `npm install` before the first build.
383
+
213
384
  ## Migrating from 0.1.x
214
385
 
215
386
  Commands used to take and return **JSON text**; they now take and return
216
387
  **values**, with the runtime handling serialisation. The page-side API
217
- (`janela.invoke` / `janela.listen`) is unchanged.
388
+ (`invoke` / `listen`) is unchanged.
218
389
 
219
390
  ```ts
220
391
  // 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,102 @@
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
+ import type { CommandShapes, Commands, Events } from "../runtime/types";
10
+
11
+ /** Removes a subscription created by `listen` / `client.on`. */
12
+ export type Unlisten = () => void;
13
+
14
+ /** The bridge janela injects as `window.janela` before each document loads. */
15
+ export interface JanelaBridge {
16
+ invoke<T = unknown>(cmd: string, args?: unknown): Promise<T>;
17
+ listen<T = unknown>(event: string, cb: (payload: T) => void): Unlisten;
18
+ }
19
+
20
+ /**
21
+ * Call a command the host registered with `app.command` / `app.commandAsync`.
22
+ *
23
+ * ```ts
24
+ * const sum = await invoke<number>("add", { a: 2, b: 40 });
25
+ * ```
26
+ *
27
+ * The generic is an assertion, not a check — prefer `createClient` when the
28
+ * host declares a contract, which verifies the name, the arguments and the
29
+ * result against the host's own types.
30
+ *
31
+ * Rejects if the command is unknown, if the handler rejected, or if the page
32
+ * is not running inside a janela window.
33
+ */
34
+ export declare function invoke<T = unknown>(cmd: string, args?: unknown): Promise<T>;
35
+
36
+ /**
37
+ * Subscribe to an event the host sends with `app.emit`. Returns a disposer.
38
+ *
39
+ * ```ts
40
+ * const off = listen<number>("added", (sum) => console.log(sum));
41
+ * off();
42
+ * ```
43
+ */
44
+ export declare function listen<T = unknown>(
45
+ event: string,
46
+ cb: (payload: T) => void,
47
+ ): Unlisten;
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Typed contract
51
+ // ---------------------------------------------------------------------------
52
+
53
+ /** Anything shaped like a host contract module's exported `App` type. */
54
+ export interface Contract {
55
+ commands: Commands<CommandShapes>;
56
+ events: Events<unknown>;
57
+ }
58
+
59
+ /** The command table declared by a contract. */
60
+ export type CommandsOf<A> = A extends { commands: Commands<infer M> } ? M : never;
61
+
62
+ /** The event table declared by a contract. */
63
+ export type EventsOf<A> = A extends { events: Events<infer E> } ? E : never;
64
+
65
+ /**
66
+ * A client bound to a host contract: command names, argument shapes, result
67
+ * types and event payloads are all checked against the host's declarations.
68
+ */
69
+ export interface JanelaClient<A> {
70
+ /**
71
+ * Call a declared command. Unknown names and wrong argument shapes are
72
+ * compile errors, and the result type comes from the contract.
73
+ */
74
+ invoke<K extends keyof CommandsOf<A> & string>(
75
+ name: K,
76
+ args: CommandsOf<A>[K]["args"],
77
+ ): Promise<CommandsOf<A>[K]["result"]>;
78
+ /**
79
+ * Subscribe to a declared event; the payload type is inferred. Returns a
80
+ * disposer that removes the subscription.
81
+ */
82
+ on<K extends keyof EventsOf<A> & string>(
83
+ event: K,
84
+ cb: (payload: EventsOf<A>[K]) => void,
85
+ ): Unlisten;
86
+ }
87
+
88
+ /**
89
+ * Build a client checked against a host's contract.
90
+ *
91
+ * ```ts
92
+ * import type { App } from "../src-host/main";
93
+ * const client = createClient<App>();
94
+ * const sum = await client.invoke("add", { a: 2, b: 40 }); // number
95
+ * const off = client.on("added", (v) => console.log(v)); // v: number
96
+ * ```
97
+ *
98
+ * `import type` is erased, so no host code is bundled into the page — the
99
+ * contract is a type edge and nothing else. Payloads still cross as JSON, so
100
+ * this is compile-time safety; nothing is validated at runtime.
101
+ */
102
+ export declare function createClient<A>(): JanelaClient<A>;
package/api/index.js ADDED
@@ -0,0 +1,64 @@
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
+ /**
33
+ * Subscribe to an event the host sends with `app.emit`. Returns a function
34
+ * that removes the subscription.
35
+ */
36
+ export function listen(event, cb) {
37
+ const off = bridge().listen(event, cb);
38
+ // Hosts before 0.5.0 returned nothing from listen; keep a working disposer
39
+ // either way rather than handing back undefined.
40
+ if (typeof off === "function") return off;
41
+ return function unlisten() {
42
+ const all = globalThis.__wvListeners ? globalThis.__wvListeners[event] : undefined;
43
+ if (!all) return;
44
+ const i = all.indexOf(cb);
45
+ if (i >= 0) all.splice(i, 1);
46
+ };
47
+ }
48
+
49
+ /**
50
+ * A client bound to a host's declared contract. The type parameter carries the
51
+ * command and event tables; at runtime this is the same bridge as `invoke` and
52
+ * `listen`, so the contract costs nothing and validates nothing — it is
53
+ * checked entirely by the compiler.
54
+ */
55
+ export function createClient() {
56
+ return {
57
+ invoke(name, args) {
58
+ return invoke(name, args);
59
+ },
60
+ on(event, cb) {
61
+ return listen(event, cb);
62
+ },
63
+ };
64
+ }
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.5.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
@@ -67,6 +67,11 @@ const BOOTSTRAP =
67
67
  " listen: function (event, cb) {" +
68
68
  " if (!window.__wvListeners[event]) window.__wvListeners[event] = [];" +
69
69
  " window.__wvListeners[event].push(cb);" +
70
+ " return function () {" +
71
+ " var a = window.__wvListeners[event] || [];" +
72
+ " var i = a.indexOf(cb);" +
73
+ " if (i >= 0) a.splice(i, 1);" +
74
+ " };" +
70
75
  " }," +
71
76
  "};" +
72
77
  "window.__wvEmit = function (event, payload) {" +
@@ -74,124 +79,39 @@ const BOOTSTRAP =
74
79
  " for (var i = 0; i < cbs.length; i++) cbs[i](payload);" +
75
80
  "};";
76
81
 
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
- }
82
+ // The public host types live in ./types (shipped as `janela/host` too, so a
83
+ // user's editor can see them). Re-exported here because the compiled build
84
+ // resolves them through this module see the specifier rewrite in the CLI.
85
+ export type {
86
+ AsyncCommandHandler,
87
+ CommandHandler,
88
+ CommandShape,
89
+ CommandShapes,
90
+ Commands,
91
+ DialogFilter,
92
+ Events,
93
+ FsCallback,
94
+ JanelaApp,
95
+ OpenDialogOptions,
96
+ SaveDialogOptions,
97
+ WindowConfig,
98
+ } from "./types";
99
+
100
+ // The typed-contract helpers are values, so they are re-exported as values.
101
+ // A project's `import { defineCommands } from "janela/host"` is rewritten to
102
+ // this module by the CLI before scriptc sees it.
103
+ export { defineCommands, defineEvents, emit, on, onAsync } from "./types";
104
+
105
+ import type {
106
+ AsyncCommandHandler,
107
+ CommandHandler,
108
+ DialogFilter,
109
+ FsCallback,
110
+ JanelaApp,
111
+ OpenDialogOptions,
112
+ SaveDialogOptions,
113
+ WindowConfig,
114
+ } from "./types";
195
115
 
196
116
  // JSON.stringify yields undefined for undefined; the wire always needs a
197
117
  // value, and a command that returns nothing should read as null in the page.