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/README.md +149 -0
- package/api/index.d.ts +69 -4
- package/api/index.js +31 -2
- package/bin/janela.mjs +9 -1
- package/package.json +2 -2
- package/runtime/janela.ts +400 -265
- package/runtime/types.ts +58 -57
- package/templates/react/files/src/App.tsx +12 -6
- package/templates/react/files/src-host/main.ts +43 -18
- package/templates/solid/files/src/App.tsx +9 -4
- package/templates/solid/files/src-host/main.ts +43 -18
- package/templates/svelte/files/src/App.svelte +9 -4
- package/templates/svelte/files/src-host/main.ts +43 -18
- package/templates/vue/files/src/App.vue +13 -7
- package/templates/vue/files/src-host/main.ts +43 -18
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
|
-
*
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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 {
|
|
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
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
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
|
|
23
|
+
client.invoke("greet", { name: "__NAME__" }).then(setGreeting);
|
|
24
|
+
return off;
|
|
19
25
|
}, []);
|
|
20
26
|
|
|
21
27
|
const add = async () =>
|
|
22
|
-
setSum(await invoke
|
|
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
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
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
|
|
15
|
+
import { defineCommands, defineEvents, type JanelaApp } from "janela/host";
|
|
12
16
|
|
|
13
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
34
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
24
|
+
setGreeting(await client.invoke("greet", { name: "__NAME__" })),
|
|
20
25
|
);
|
|
21
26
|
|
|
22
27
|
const add = async () =>
|
|
23
|
-
setSum(await invoke
|
|
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
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
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
|
|
15
|
+
import { defineCommands, defineEvents, type JanelaApp } from "janela/host";
|
|
12
16
|
|
|
13
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
34
|
-
|
|
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 {
|
|
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
|
-
|
|
17
|
+
client.on("added", (value) => (events = [`host emitted: ${value}`, ...events]));
|
|
13
18
|
|
|
14
|
-
invoke
|
|
19
|
+
client.invoke("greet", { name: "__NAME__" }).then((g) => (greeting = g));
|
|
15
20
|
|
|
16
21
|
async function add() {
|
|
17
|
-
sum = await invoke
|
|
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
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
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
|
|
15
|
+
import { defineCommands, defineEvents, type JanelaApp } from "janela/host";
|
|
12
16
|
|
|
13
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
34
|
-
|
|
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 {
|
|
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
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
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
|
|
22
|
+
greeting.value = await client.invoke("greet", { name: "__NAME__" });
|
|
17
23
|
});
|
|
18
24
|
|
|
19
25
|
async function add() {
|
|
20
|
-
sum.value = await invoke
|
|
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
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
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
|
|
15
|
+
import { defineCommands, defineEvents, type JanelaApp } from "janela/host";
|
|
12
16
|
|
|
13
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
34
|
-
|
|
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
|
}
|