janela 0.4.0 → 0.6.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/runtime/types.ts CHANGED
@@ -8,7 +8,8 @@
8
8
  * - re-exported by runtime/janela.ts, which is what the compiled build
9
9
  * actually links against (the CLI copies both files into .janela/build/).
10
10
  *
11
- * Keep it declaration-only: no runtime code lives here.
11
+ * Mostly declarations; the typed-contract helpers at the bottom are the only
12
+ * runtime code, and they are deliberately trivial.
12
13
  */
13
14
 
14
15
  // Handlers take the invoked arguments as a value and return a value; the
@@ -72,60 +73,60 @@ export interface WindowConfig {
72
73
  height: number;
73
74
  }
74
75
 
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;
76
+ // ---------------------------------------------------------------------------
77
+ // Typed IPC contract
78
+ // ---------------------------------------------------------------------------
79
+ //
80
+ // The contract is a TYPE the host declares and the frontend imports with
81
+ // `import type`. Because both sides are TypeScript, no code generation is
82
+ // involved and nothing can drift: the frontend checks against the host's own
83
+ // declarations, and the import is erased, so no host code reaches the bundle.
84
+ //
85
+ // Payloads still cross as JSON, so these types are compile-time only. Nothing
86
+ // validates a malformed payload at runtime.
87
+ //
88
+ // SHAPE NOTE: the registrars below are standalone generic FUNCTIONS taking the
89
+ // contract as a value, rather than methods on a returned registrar object.
90
+ // That is not a style choice — scriptc cannot dispatch a generic method
91
+ // through an interface-typed receiver (SC1090), so `commands.on(...)` does not
92
+ // compile in a host build, while `on(app, commands, ...)` does.
93
+
94
+ /** One command's argument and result types. */
95
+ export interface CommandShape {
96
+ args: unknown;
97
+ result: unknown;
98
+ }
99
+
100
+ /** A contract's command table: name → shape. */
101
+ export type CommandShapes = Record<string, CommandShape>;
102
+
103
+ /**
104
+ * A declared command contract. Carries `M` at the type level only — the value
105
+ * is empty, and exists so that inference has something to read at a call site.
106
+ */
107
+ export interface Commands<M extends CommandShapes> {
108
+ __commands?: M;
109
+ }
110
+
111
+ /** A declared event contract: event name → payload type. */
112
+ export interface Events<E> {
113
+ __events?: E;
114
+ }
115
+
116
+ /**
117
+ * Declare the commands a host exposes.
118
+ *
119
+ * ```ts
120
+ * export const commands = defineCommands<{
121
+ * add: { args: { a: number; b: number }; result: number };
122
+ * }>();
123
+ * ```
124
+ */
125
+ export function defineCommands<M extends CommandShapes>(): Commands<M> {
126
+ return {};
127
+ }
128
+
129
+ /** Declare the events a host emits: `defineEvents<{ added: number }>()`. */
130
+ export function defineEvents<E>(): Events<E> {
131
+ return {};
131
132
  }
@@ -1,7 +1,12 @@
1
1
  import { useEffect, useState } from "react";
2
- import { invoke, listen } from "janela/api";
2
+ import { createClient } from "janela/api";
3
+ // Type-only import of the host contract: erased at compile time, so no host
4
+ // code is bundled into the page.
5
+ import type { App as Contract } from "../src-host/main";
3
6
  import "./App.css";
4
7
 
8
+ const client = createClient<Contract>();
9
+
5
10
  export default function App() {
6
11
  const [greeting, setGreeting] = useState("…");
7
12
  const [a, setA] = useState(2);
@@ -10,16 +15,17 @@ export default function App() {
10
15
  const [events, setEvents] = useState<string[]>([]);
11
16
 
12
17
  useEffect(() => {
13
- // Backend→frontend events. The payload arrives as a value, and the
14
- // generic says which value.
15
- listen<number>("added", (value) =>
18
+ // The event name is checked against the contract, `value` is inferred,
19
+ // and `on` returns a disposer.
20
+ const off = client.on("added", (value) =>
16
21
  setEvents((prev) => [`host emitted: ${value}`, ...prev]),
17
22
  );
18
- invoke<string>("greet", { name: "__NAME__" }).then(setGreeting);
23
+ client.invoke("greet", { name: "__NAME__" }).then(setGreeting);
24
+ return off;
19
25
  }, []);
20
26
 
21
27
  const add = async () =>
22
- setSum(await invoke<number>("add", { a, b }));
28
+ setSum(await client.invoke("add", { a, b }));
23
29
 
24
30
  return (
25
31
  <>
@@ -1,42 +1,67 @@
1
1
  // src-host/main.ts — your app's backend, compiled to native code by scriptc.
2
2
  //
3
- // Register commands here; the page calls them with `await janela.invoke(name, args)`.
4
- // Handlers take the arguments as a value and return a value — the runtime owns
5
- // JSON at the boundary, so there is no parsing or stringifying to do here.
3
+ // The contract below is the single declaration of what this app exposes. The
4
+ // frontend imports `App` with `import type`, so the page is checked against
5
+ // these exact types — command names, argument shapes, results and event
6
+ // payloads — with no code generation and nothing to keep in sync.
6
7
  //
7
- // Gotcha inherited from scriptc: never use a bare FFI-backed call as a
8
- // complete variable initializer — wrap it in any expression (`+ 0`). Plain
9
- // TypeScript like everything in this file is unaffected.
8
+ // Two gotchas inherited from scriptc:
9
+ // - never use a bare FFI-backed call as a complete variable initializer;
10
+ // wrap it in any expression (`+ 0`);
11
+ // - write `args: null` for a command that takes nothing, not `undefined` —
12
+ // an `undefined` argument lowers to a zero-parameter function and fails
13
+ // to compile.
10
14
 
11
- import type { JanelaApp } from "janela/host";
15
+ import { defineCommands, defineEvents, type JanelaApp } from "janela/host";
12
16
 
13
- export function setup(app: JanelaApp): void {
17
+ /** Every command this app answers. Declared once; the page checks against it. */
18
+ export type AppCommands = {
19
+ add: { args: { a: number; b: number }; result: number };
20
+ greet: { args: { name: string }; result: string };
21
+ log: { args: string; result: null };
22
+ wait: { args: { ms: number }; result: string };
23
+ quit: { args: null; result: null };
24
+ };
25
+
26
+ /** Every event this app emits, and what each one carries. */
27
+ export type AppEvents = {
28
+ added: number;
29
+ };
30
+
31
+ export const commands = defineCommands<AppCommands>();
32
+ export const events = defineEvents<AppEvents>();
33
+
34
+ /** The contract the page imports with `import type { App } from "../src-host/main"`. */
35
+ export type App = { commands: typeof commands; events: typeof events };
36
+
37
+ // Typing the app with the contract is what makes `app.command` checked: the
38
+ // name must be one of the declared ones, `args` is inferred from it, and the
39
+ // return value has to match. Same for `app.emit`.
40
+ export function setup(app: JanelaApp<AppCommands, AppEvents>): void {
14
41
  app.command("add", (args) => {
15
- const a = args as { a: number; b: number };
16
- const sum = a.a + a.b;
17
- // Backend→frontend event: the page listens with janela.listen("added", …).
42
+ const sum = args.a + args.b;
18
43
  app.emit("added", sum);
19
44
  return sum;
20
45
  });
21
46
 
22
47
  app.command("greet", (args) => {
23
- const a = args as { name: string };
24
- return "Hello, " + a.name + " — from the native TS binary";
48
+ return "Hello, " + args.name + " — from the native TS binary";
25
49
  });
26
50
 
27
51
  app.command("log", (args) => {
28
- console.log("[host] page says:", args as string);
52
+ console.log("[host] page says:", args);
53
+ return null;
29
54
  });
30
55
 
31
56
  // An async command: answers later, without freezing the window.
32
57
  app.commandAsync("wait", (args, resolve) => {
33
- const a = args as { ms: number };
34
- app.sleep(a.ms, () => {
35
- resolve("waited " + a.ms + "ms without blocking the UI");
58
+ app.sleep(args.ms, () => {
59
+ resolve("waited " + args.ms + "ms without blocking the UI");
36
60
  });
37
61
  });
38
62
 
39
- app.command("quit", () => {
63
+ app.command("quit", (_args) => {
40
64
  app.quit();
65
+ return null;
41
66
  });
42
67
  }
@@ -1,7 +1,12 @@
1
1
  import { createSignal, onMount, For, Show } from "solid-js";
2
- import { invoke, listen } from "janela/api";
2
+ import { createClient } from "janela/api";
3
+ // Type-only import of the host contract: erased at compile time, so no host
4
+ // code is bundled into the page.
5
+ import type { App as Contract } from "../src-host/main";
3
6
  import "./App.css";
4
7
 
8
+ const client = createClient<Contract>();
9
+
5
10
  export default function App() {
6
11
  const [greeting, setGreeting] = createSignal("…");
7
12
  const [a, setA] = createSignal(2);
@@ -11,16 +16,16 @@ export default function App() {
11
16
 
12
17
  // Backend→frontend events. The payload arrives as a value, and the generic
13
18
  // says which value.
14
- listen<number>("added", (value) =>
19
+ client.on("added", (value) =>
15
20
  setEvents((prev) => [`host emitted: ${value}`, ...prev]),
16
21
  );
17
22
 
18
23
  onMount(async () =>
19
- setGreeting(await invoke<string>("greet", { name: "__NAME__" })),
24
+ setGreeting(await client.invoke("greet", { name: "__NAME__" })),
20
25
  );
21
26
 
22
27
  const add = async () =>
23
- setSum(await invoke<number>("add", { a: a(), b: b() }));
28
+ setSum(await client.invoke("add", { a: a(), b: b() }));
24
29
 
25
30
  return (
26
31
  <>
@@ -1,42 +1,67 @@
1
1
  // src-host/main.ts — your app's backend, compiled to native code by scriptc.
2
2
  //
3
- // Register commands here; the page calls them with `await janela.invoke(name, args)`.
4
- // Handlers take the arguments as a value and return a value — the runtime owns
5
- // JSON at the boundary, so there is no parsing or stringifying to do here.
3
+ // The contract below is the single declaration of what this app exposes. The
4
+ // frontend imports `App` with `import type`, so the page is checked against
5
+ // these exact types — command names, argument shapes, results and event
6
+ // payloads — with no code generation and nothing to keep in sync.
6
7
  //
7
- // Gotcha inherited from scriptc: never use a bare FFI-backed call as a
8
- // complete variable initializer — wrap it in any expression (`+ 0`). Plain
9
- // TypeScript like everything in this file is unaffected.
8
+ // Two gotchas inherited from scriptc:
9
+ // - never use a bare FFI-backed call as a complete variable initializer;
10
+ // wrap it in any expression (`+ 0`);
11
+ // - write `args: null` for a command that takes nothing, not `undefined` —
12
+ // an `undefined` argument lowers to a zero-parameter function and fails
13
+ // to compile.
10
14
 
11
- import type { JanelaApp } from "janela/host";
15
+ import { defineCommands, defineEvents, type JanelaApp } from "janela/host";
12
16
 
13
- export function setup(app: JanelaApp): void {
17
+ /** Every command this app answers. Declared once; the page checks against it. */
18
+ export type AppCommands = {
19
+ add: { args: { a: number; b: number }; result: number };
20
+ greet: { args: { name: string }; result: string };
21
+ log: { args: string; result: null };
22
+ wait: { args: { ms: number }; result: string };
23
+ quit: { args: null; result: null };
24
+ };
25
+
26
+ /** Every event this app emits, and what each one carries. */
27
+ export type AppEvents = {
28
+ added: number;
29
+ };
30
+
31
+ export const commands = defineCommands<AppCommands>();
32
+ export const events = defineEvents<AppEvents>();
33
+
34
+ /** The contract the page imports with `import type { App } from "../src-host/main"`. */
35
+ export type App = { commands: typeof commands; events: typeof events };
36
+
37
+ // Typing the app with the contract is what makes `app.command` checked: the
38
+ // name must be one of the declared ones, `args` is inferred from it, and the
39
+ // return value has to match. Same for `app.emit`.
40
+ export function setup(app: JanelaApp<AppCommands, AppEvents>): void {
14
41
  app.command("add", (args) => {
15
- const a = args as { a: number; b: number };
16
- const sum = a.a + a.b;
17
- // Backend→frontend event: the page listens with janela.listen("added", …).
42
+ const sum = args.a + args.b;
18
43
  app.emit("added", sum);
19
44
  return sum;
20
45
  });
21
46
 
22
47
  app.command("greet", (args) => {
23
- const a = args as { name: string };
24
- return "Hello, " + a.name + " — from the native TS binary";
48
+ return "Hello, " + args.name + " — from the native TS binary";
25
49
  });
26
50
 
27
51
  app.command("log", (args) => {
28
- console.log("[host] page says:", args as string);
52
+ console.log("[host] page says:", args);
53
+ return null;
29
54
  });
30
55
 
31
56
  // An async command: answers later, without freezing the window.
32
57
  app.commandAsync("wait", (args, resolve) => {
33
- const a = args as { ms: number };
34
- app.sleep(a.ms, () => {
35
- resolve("waited " + a.ms + "ms without blocking the UI");
58
+ app.sleep(args.ms, () => {
59
+ resolve("waited " + args.ms + "ms without blocking the UI");
36
60
  });
37
61
  });
38
62
 
39
- app.command("quit", () => {
63
+ app.command("quit", (_args) => {
40
64
  app.quit();
65
+ return null;
41
66
  });
42
67
  }
@@ -1,5 +1,10 @@
1
1
  <script lang="ts">
2
- import { invoke, listen } from "janela/api";
2
+ import { createClient } from "janela/api";
3
+ // Type-only import of the host contract: erased at compile time, so no
4
+ // host code is bundled into the page.
5
+ import type { App as Contract } from "../src-host/main";
6
+
7
+ const client = createClient<Contract>();
3
8
 
4
9
  let greeting = $state("…");
5
10
  let a = $state(2);
@@ -9,12 +14,12 @@
9
14
 
10
15
  // Backend→frontend events. The payload arrives as a value, and the generic
11
16
  // says which value.
12
- listen<number>("added", (value) => (events = [`host emitted: ${value}`, ...events]));
17
+ client.on("added", (value) => (events = [`host emitted: ${value}`, ...events]));
13
18
 
14
- invoke<string>("greet", { name: "__NAME__" }).then((g) => (greeting = g));
19
+ client.invoke("greet", { name: "__NAME__" }).then((g) => (greeting = g));
15
20
 
16
21
  async function add() {
17
- sum = await invoke<number>("add", { a: Number(a), b: Number(b) });
22
+ sum = await client.invoke("add", { a: Number(a), b: Number(b) });
18
23
  }
19
24
  </script>
20
25
 
@@ -1,42 +1,67 @@
1
1
  // src-host/main.ts — your app's backend, compiled to native code by scriptc.
2
2
  //
3
- // Register commands here; the page calls them with `await janela.invoke(name, args)`.
4
- // Handlers take the arguments as a value and return a value — the runtime owns
5
- // JSON at the boundary, so there is no parsing or stringifying to do here.
3
+ // The contract below is the single declaration of what this app exposes. The
4
+ // frontend imports `App` with `import type`, so the page is checked against
5
+ // these exact types — command names, argument shapes, results and event
6
+ // payloads — with no code generation and nothing to keep in sync.
6
7
  //
7
- // Gotcha inherited from scriptc: never use a bare FFI-backed call as a
8
- // complete variable initializer — wrap it in any expression (`+ 0`). Plain
9
- // TypeScript like everything in this file is unaffected.
8
+ // Two gotchas inherited from scriptc:
9
+ // - never use a bare FFI-backed call as a complete variable initializer;
10
+ // wrap it in any expression (`+ 0`);
11
+ // - write `args: null` for a command that takes nothing, not `undefined` —
12
+ // an `undefined` argument lowers to a zero-parameter function and fails
13
+ // to compile.
10
14
 
11
- import type { JanelaApp } from "janela/host";
15
+ import { defineCommands, defineEvents, type JanelaApp } from "janela/host";
12
16
 
13
- export function setup(app: JanelaApp): void {
17
+ /** Every command this app answers. Declared once; the page checks against it. */
18
+ export type AppCommands = {
19
+ add: { args: { a: number; b: number }; result: number };
20
+ greet: { args: { name: string }; result: string };
21
+ log: { args: string; result: null };
22
+ wait: { args: { ms: number }; result: string };
23
+ quit: { args: null; result: null };
24
+ };
25
+
26
+ /** Every event this app emits, and what each one carries. */
27
+ export type AppEvents = {
28
+ added: number;
29
+ };
30
+
31
+ export const commands = defineCommands<AppCommands>();
32
+ export const events = defineEvents<AppEvents>();
33
+
34
+ /** The contract the page imports with `import type { App } from "../src-host/main"`. */
35
+ export type App = { commands: typeof commands; events: typeof events };
36
+
37
+ // Typing the app with the contract is what makes `app.command` checked: the
38
+ // name must be one of the declared ones, `args` is inferred from it, and the
39
+ // return value has to match. Same for `app.emit`.
40
+ export function setup(app: JanelaApp<AppCommands, AppEvents>): void {
14
41
  app.command("add", (args) => {
15
- const a = args as { a: number; b: number };
16
- const sum = a.a + a.b;
17
- // Backend→frontend event: the page listens with janela.listen("added", …).
42
+ const sum = args.a + args.b;
18
43
  app.emit("added", sum);
19
44
  return sum;
20
45
  });
21
46
 
22
47
  app.command("greet", (args) => {
23
- const a = args as { name: string };
24
- return "Hello, " + a.name + " — from the native TS binary";
48
+ return "Hello, " + args.name + " — from the native TS binary";
25
49
  });
26
50
 
27
51
  app.command("log", (args) => {
28
- console.log("[host] page says:", args as string);
52
+ console.log("[host] page says:", args);
53
+ return null;
29
54
  });
30
55
 
31
56
  // An async command: answers later, without freezing the window.
32
57
  app.commandAsync("wait", (args, resolve) => {
33
- const a = args as { ms: number };
34
- app.sleep(a.ms, () => {
35
- resolve("waited " + a.ms + "ms without blocking the UI");
58
+ app.sleep(args.ms, () => {
59
+ resolve("waited " + args.ms + "ms without blocking the UI");
36
60
  });
37
61
  });
38
62
 
39
- app.command("quit", () => {
63
+ app.command("quit", (_args) => {
40
64
  app.quit();
65
+ return null;
41
66
  });
42
67
  }
@@ -1,6 +1,11 @@
1
1
  <script setup lang="ts">
2
- import { onMounted, ref } from "vue";
3
- import { invoke, listen } from "janela/api";
2
+ import { onMounted, onUnmounted, ref } from "vue";
3
+ import { createClient } from "janela/api";
4
+ // A type-only import of the host's contract. It is erased at compile time, so
5
+ // no host code is bundled into the page — this is purely a type edge.
6
+ import type { App } from "../src-host/main";
7
+
8
+ const client = createClient<App>();
4
9
 
5
10
  const greeting = ref("…");
6
11
  const a = ref(2);
@@ -8,16 +13,17 @@ const b = ref(40);
8
13
  const sum = ref<number | null>(null);
9
14
  const events = ref<string[]>([]);
10
15
 
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}`));
16
+ // The event name is checked against the contract and `value` is inferred.
17
+ // `on` returns a disposer.
18
+ const off = client.on("added", (value) => events.value.unshift(`host emitted: ${value}`));
19
+ onUnmounted(off);
14
20
 
15
21
  onMounted(async () => {
16
- greeting.value = await invoke<string>("greet", { name: "__NAME__" });
22
+ greeting.value = await client.invoke("greet", { name: "__NAME__" });
17
23
  });
18
24
 
19
25
  async function add() {
20
- sum.value = await invoke<number>("add", { a: Number(a.value), b: Number(b.value) });
26
+ sum.value = await client.invoke("add", { a: Number(a.value), b: Number(b.value) });
21
27
  }
22
28
  </script>
23
29
 
@@ -1,42 +1,67 @@
1
1
  // src-host/main.ts — your app's backend, compiled to native code by scriptc.
2
2
  //
3
- // Register commands here; the page calls them with `await janela.invoke(name, args)`.
4
- // Handlers take the arguments as a value and return a value — the runtime owns
5
- // JSON at the boundary, so there is no parsing or stringifying to do here.
3
+ // The contract below is the single declaration of what this app exposes. The
4
+ // frontend imports `App` with `import type`, so the page is checked against
5
+ // these exact types — command names, argument shapes, results and event
6
+ // payloads — with no code generation and nothing to keep in sync.
6
7
  //
7
- // Gotcha inherited from scriptc: never use a bare FFI-backed call as a
8
- // complete variable initializer — wrap it in any expression (`+ 0`). Plain
9
- // TypeScript like everything in this file is unaffected.
8
+ // Two gotchas inherited from scriptc:
9
+ // - never use a bare FFI-backed call as a complete variable initializer;
10
+ // wrap it in any expression (`+ 0`);
11
+ // - write `args: null` for a command that takes nothing, not `undefined` —
12
+ // an `undefined` argument lowers to a zero-parameter function and fails
13
+ // to compile.
10
14
 
11
- import type { JanelaApp } from "janela/host";
15
+ import { defineCommands, defineEvents, type JanelaApp } from "janela/host";
12
16
 
13
- export function setup(app: JanelaApp): void {
17
+ /** Every command this app answers. Declared once; the page checks against it. */
18
+ export type AppCommands = {
19
+ add: { args: { a: number; b: number }; result: number };
20
+ greet: { args: { name: string }; result: string };
21
+ log: { args: string; result: null };
22
+ wait: { args: { ms: number }; result: string };
23
+ quit: { args: null; result: null };
24
+ };
25
+
26
+ /** Every event this app emits, and what each one carries. */
27
+ export type AppEvents = {
28
+ added: number;
29
+ };
30
+
31
+ export const commands = defineCommands<AppCommands>();
32
+ export const events = defineEvents<AppEvents>();
33
+
34
+ /** The contract the page imports with `import type { App } from "../src-host/main"`. */
35
+ export type App = { commands: typeof commands; events: typeof events };
36
+
37
+ // Typing the app with the contract is what makes `app.command` checked: the
38
+ // name must be one of the declared ones, `args` is inferred from it, and the
39
+ // return value has to match. Same for `app.emit`.
40
+ export function setup(app: JanelaApp<AppCommands, AppEvents>): void {
14
41
  app.command("add", (args) => {
15
- const a = args as { a: number; b: number };
16
- const sum = a.a + a.b;
17
- // Backend→frontend event: the page listens with janela.listen("added", …).
42
+ const sum = args.a + args.b;
18
43
  app.emit("added", sum);
19
44
  return sum;
20
45
  });
21
46
 
22
47
  app.command("greet", (args) => {
23
- const a = args as { name: string };
24
- return "Hello, " + a.name + " — from the native TS binary";
48
+ return "Hello, " + args.name + " — from the native TS binary";
25
49
  });
26
50
 
27
51
  app.command("log", (args) => {
28
- console.log("[host] page says:", args as string);
52
+ console.log("[host] page says:", args);
53
+ return null;
29
54
  });
30
55
 
31
56
  // An async command: answers later, without freezing the window.
32
57
  app.commandAsync("wait", (args, resolve) => {
33
- const a = args as { ms: number };
34
- app.sleep(a.ms, () => {
35
- resolve("waited " + a.ms + "ms without blocking the UI");
58
+ app.sleep(args.ms, () => {
59
+ resolve("waited " + args.ms + "ms without blocking the UI");
36
60
  });
37
61
  });
38
62
 
39
- app.command("quit", () => {
63
+ app.command("quit", (_args) => {
40
64
  app.quit();
65
+ return null;
41
66
  });
42
67
  }