janela 0.4.0 → 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.
- package/README.md +109 -0
- package/api/index.d.ts +69 -4
- package/api/index.js +31 -2
- package/package.json +1 -1
- package/runtime/janela.ts +14 -0
- package/runtime/types.ts +121 -1
- package/templates/react/files/src/App.tsx +12 -6
- package/templates/react/files/src-host/main.ts +48 -22
- package/templates/solid/files/src/App.tsx +9 -4
- package/templates/solid/files/src-host/main.ts +48 -22
- package/templates/svelte/files/src/App.svelte +9 -4
- package/templates/svelte/files/src-host/main.ts +48 -22
- package/templates/vue/files/src/App.vue +13 -7
- package/templates/vue/files/src-host/main.ts +48 -22
package/README.md
CHANGED
|
@@ -126,6 +126,80 @@ export function setup(app: JanelaApp): void {
|
|
|
126
126
|
}
|
|
127
127
|
```
|
|
128
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
|
+
|
|
129
203
|
## Async commands
|
|
130
204
|
|
|
131
205
|
A command that has to wait — or to chew through real work — should not freeze
|
|
@@ -235,6 +309,41 @@ nested modal loop would otherwise re-enter the host loop underneath a live TS
|
|
|
235
309
|
frame; [docs/native-shell.md](../../docs/native-shell.md) has the details, the
|
|
236
310
|
per-platform table, and the Windows GUI-subsystem note.
|
|
237
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
|
+
|
|
238
347
|
## Migrating from 0.3.x
|
|
239
348
|
|
|
240
349
|
Nothing breaks: the injected `janela` global still works exactly as before.
|
package/api/index.d.ts
CHANGED
|
@@ -6,10 +6,15 @@
|
|
|
6
6
|
* parse.
|
|
7
7
|
*/
|
|
8
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
|
+
|
|
9
14
|
/** The bridge janela injects as `window.janela` before each document loads. */
|
|
10
15
|
export interface JanelaBridge {
|
|
11
16
|
invoke<T = unknown>(cmd: string, args?: unknown): Promise<T>;
|
|
12
|
-
listen<T = unknown>(event: string, cb: (payload: T) => void):
|
|
17
|
+
listen<T = unknown>(event: string, cb: (payload: T) => void): Unlisten;
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
/**
|
|
@@ -19,19 +24,79 @@ export interface JanelaBridge {
|
|
|
19
24
|
* const sum = await invoke<number>("add", { a: 2, b: 40 });
|
|
20
25
|
* ```
|
|
21
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
|
+
*
|
|
22
31
|
* Rejects if the command is unknown, if the handler rejected, or if the page
|
|
23
32
|
* is not running inside a janela window.
|
|
24
33
|
*/
|
|
25
34
|
export declare function invoke<T = unknown>(cmd: string, args?: unknown): Promise<T>;
|
|
26
35
|
|
|
27
36
|
/**
|
|
28
|
-
* Subscribe to an event the host sends with `app.emit`.
|
|
37
|
+
* Subscribe to an event the host sends with `app.emit`. Returns a disposer.
|
|
29
38
|
*
|
|
30
39
|
* ```ts
|
|
31
|
-
* listen<number>("added", (sum) => console.log(sum));
|
|
40
|
+
* const off = listen<number>("added", (sum) => console.log(sum));
|
|
41
|
+
* off();
|
|
32
42
|
* ```
|
|
33
43
|
*/
|
|
34
44
|
export declare function listen<T = unknown>(
|
|
35
45
|
event: string,
|
|
36
46
|
cb: (payload: T) => void,
|
|
37
|
-
):
|
|
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
CHANGED
|
@@ -29,7 +29,36 @@ export async function invoke(cmd, args) {
|
|
|
29
29
|
return bridge().invoke(cmd, args);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Subscribe to an event the host sends with `app.emit`. Returns a function
|
|
34
|
+
* that removes the subscription.
|
|
35
|
+
*/
|
|
33
36
|
export function listen(event, cb) {
|
|
34
|
-
bridge().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
|
+
};
|
|
35
64
|
}
|
package/package.json
CHANGED
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) {" +
|
|
@@ -80,7 +85,11 @@ const BOOTSTRAP =
|
|
|
80
85
|
export type {
|
|
81
86
|
AsyncCommandHandler,
|
|
82
87
|
CommandHandler,
|
|
88
|
+
CommandShape,
|
|
89
|
+
CommandShapes,
|
|
90
|
+
Commands,
|
|
83
91
|
DialogFilter,
|
|
92
|
+
Events,
|
|
84
93
|
FsCallback,
|
|
85
94
|
JanelaApp,
|
|
86
95
|
OpenDialogOptions,
|
|
@@ -88,6 +97,11 @@ export type {
|
|
|
88
97
|
WindowConfig,
|
|
89
98
|
} from "./types";
|
|
90
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
|
+
|
|
91
105
|
import type {
|
|
92
106
|
AsyncCommandHandler,
|
|
93
107
|
CommandHandler,
|
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
|
|
@@ -129,3 +130,122 @@ export interface JanelaApp {
|
|
|
129
130
|
/** Show the page and block until the window closes. Returns the run status. */
|
|
130
131
|
run: (html: string) => number;
|
|
131
132
|
}
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Typed IPC contract
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
//
|
|
138
|
+
// The contract is a TYPE the host declares and the frontend imports with
|
|
139
|
+
// `import type`. Because both sides are TypeScript, no code generation is
|
|
140
|
+
// involved and nothing can drift: the frontend checks against the host's own
|
|
141
|
+
// declarations, and the import is erased, so no host code reaches the bundle.
|
|
142
|
+
//
|
|
143
|
+
// Payloads still cross as JSON, so these types are compile-time only. Nothing
|
|
144
|
+
// validates a malformed payload at runtime.
|
|
145
|
+
//
|
|
146
|
+
// SHAPE NOTE: the registrars below are standalone generic FUNCTIONS taking the
|
|
147
|
+
// contract as a value, rather than methods on a returned registrar object.
|
|
148
|
+
// That is not a style choice — scriptc cannot dispatch a generic method
|
|
149
|
+
// through an interface-typed receiver (SC1090), so `commands.on(...)` does not
|
|
150
|
+
// compile in a host build, while `on(app, commands, ...)` does.
|
|
151
|
+
|
|
152
|
+
/** One command's argument and result types. */
|
|
153
|
+
export interface CommandShape {
|
|
154
|
+
args: unknown;
|
|
155
|
+
result: unknown;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** A contract's command table: name → shape. */
|
|
159
|
+
export type CommandShapes = Record<string, CommandShape>;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* A declared command contract. Carries `M` at the type level only — the value
|
|
163
|
+
* is empty, and exists so that inference has something to read at a call site.
|
|
164
|
+
*/
|
|
165
|
+
export interface Commands<M extends CommandShapes> {
|
|
166
|
+
__commands?: M;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** A declared event contract: event name → payload type. */
|
|
170
|
+
export interface Events<E> {
|
|
171
|
+
__events?: E;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Declare the commands a host exposes.
|
|
176
|
+
*
|
|
177
|
+
* ```ts
|
|
178
|
+
* export const commands = defineCommands<{
|
|
179
|
+
* add: { args: { a: number; b: number }; result: number };
|
|
180
|
+
* }>();
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
183
|
+
export function defineCommands<M extends CommandShapes>(): Commands<M> {
|
|
184
|
+
return {};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Declare the events a host emits: `defineEvents<{ added: number }>()`. */
|
|
188
|
+
export function defineEvents<E>(): Events<E> {
|
|
189
|
+
return {};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Register a command against the contract. `args` is inferred from the
|
|
194
|
+
* contract, and the return type is checked against it, so the handler is
|
|
195
|
+
* written once with no casts.
|
|
196
|
+
*
|
|
197
|
+
* ```ts
|
|
198
|
+
* on(app, commands, "add", (args) => args.a + args.b);
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
export function on<M extends CommandShapes, K extends keyof M & string>(
|
|
202
|
+
app: JanelaApp,
|
|
203
|
+
_commands: Commands<M>,
|
|
204
|
+
name: K,
|
|
205
|
+
handler: (args: M[K]["args"]) => M[K]["result"],
|
|
206
|
+
): void {
|
|
207
|
+
app.command(name, (args: unknown) => handler(args as M[K]["args"]));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Register a command that answers on a later turn. `resolve` takes the
|
|
212
|
+
* contract's result type; see AsyncCommandHandler for the timing rules.
|
|
213
|
+
*/
|
|
214
|
+
export function onAsync<M extends CommandShapes, K extends keyof M & string>(
|
|
215
|
+
app: JanelaApp,
|
|
216
|
+
_commands: Commands<M>,
|
|
217
|
+
name: K,
|
|
218
|
+
handler: (
|
|
219
|
+
args: M[K]["args"],
|
|
220
|
+
resolve: (value: M[K]["result"]) => void,
|
|
221
|
+
reject: (reason: unknown) => void,
|
|
222
|
+
) => void,
|
|
223
|
+
): void {
|
|
224
|
+
app.commandAsync(
|
|
225
|
+
name,
|
|
226
|
+
(args: unknown, resolve: (value: unknown) => void, reject: (reason: unknown) => void) => {
|
|
227
|
+
handler(
|
|
228
|
+
args as M[K]["args"],
|
|
229
|
+
(value: M[K]["result"]) => resolve(value),
|
|
230
|
+
reject,
|
|
231
|
+
);
|
|
232
|
+
},
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Emit a declared event. The name must exist in the contract and the payload
|
|
238
|
+
* must match its type.
|
|
239
|
+
*
|
|
240
|
+
* ```ts
|
|
241
|
+
* emit(app, events, "added", 42);
|
|
242
|
+
* ```
|
|
243
|
+
*/
|
|
244
|
+
export function emit<E, K extends keyof E & string>(
|
|
245
|
+
app: JanelaApp,
|
|
246
|
+
_events: Events<E>,
|
|
247
|
+
name: K,
|
|
248
|
+
payload: E[K],
|
|
249
|
+
): void {
|
|
250
|
+
app.emit(name, payload);
|
|
251
|
+
}
|
|
@@ -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,68 @@
|
|
|
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
|
|
15
|
+
import {
|
|
16
|
+
defineCommands,
|
|
17
|
+
defineEvents,
|
|
18
|
+
emit,
|
|
19
|
+
on,
|
|
20
|
+
onAsync,
|
|
21
|
+
type JanelaApp,
|
|
22
|
+
} from "janela/host";
|
|
23
|
+
|
|
24
|
+
export const commands = defineCommands<{
|
|
25
|
+
add: { args: { a: number; b: number }; result: number };
|
|
26
|
+
greet: { args: { name: string }; result: string };
|
|
27
|
+
log: { args: string; result: null };
|
|
28
|
+
wait: { args: { ms: number }; result: string };
|
|
29
|
+
quit: { args: null; result: null };
|
|
30
|
+
}>();
|
|
31
|
+
|
|
32
|
+
export const events = defineEvents<{
|
|
33
|
+
added: number;
|
|
34
|
+
}>();
|
|
35
|
+
|
|
36
|
+
/** The contract the page imports with `import type { App } from "../src-host/main"`. */
|
|
37
|
+
export type App = { commands: typeof commands; events: typeof events };
|
|
12
38
|
|
|
13
39
|
export function setup(app: JanelaApp): void {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
40
|
+
// `args` is inferred from the contract, and the return type is checked
|
|
41
|
+
// against it — no casts, and no way to drift from what the page expects.
|
|
42
|
+
on(app, commands, "add", (args) => {
|
|
43
|
+
const sum = args.a + args.b;
|
|
44
|
+
emit(app, events, "added", sum);
|
|
19
45
|
return sum;
|
|
20
46
|
});
|
|
21
47
|
|
|
22
|
-
app
|
|
23
|
-
|
|
24
|
-
return "Hello, " + a.name + " — from the native TS binary";
|
|
48
|
+
on(app, commands, "greet", (args) => {
|
|
49
|
+
return "Hello, " + args.name + " — from the native TS binary";
|
|
25
50
|
});
|
|
26
51
|
|
|
27
|
-
app
|
|
28
|
-
console.log("[host] page says:", args
|
|
52
|
+
on(app, commands, "log", (args) => {
|
|
53
|
+
console.log("[host] page says:", args);
|
|
54
|
+
return null;
|
|
29
55
|
});
|
|
30
56
|
|
|
31
57
|
// An async command: answers later, without freezing the window.
|
|
32
|
-
app
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
resolve("waited " + a.ms + "ms without blocking the UI");
|
|
58
|
+
onAsync(app, commands, "wait", (args, resolve) => {
|
|
59
|
+
app.sleep(args.ms, () => {
|
|
60
|
+
resolve("waited " + args.ms + "ms without blocking the UI");
|
|
36
61
|
});
|
|
37
62
|
});
|
|
38
63
|
|
|
39
|
-
app
|
|
64
|
+
on(app, commands, "quit", (_args) => {
|
|
40
65
|
app.quit();
|
|
66
|
+
return null;
|
|
41
67
|
});
|
|
42
68
|
}
|
|
@@ -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,68 @@
|
|
|
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
|
|
15
|
+
import {
|
|
16
|
+
defineCommands,
|
|
17
|
+
defineEvents,
|
|
18
|
+
emit,
|
|
19
|
+
on,
|
|
20
|
+
onAsync,
|
|
21
|
+
type JanelaApp,
|
|
22
|
+
} from "janela/host";
|
|
23
|
+
|
|
24
|
+
export const commands = defineCommands<{
|
|
25
|
+
add: { args: { a: number; b: number }; result: number };
|
|
26
|
+
greet: { args: { name: string }; result: string };
|
|
27
|
+
log: { args: string; result: null };
|
|
28
|
+
wait: { args: { ms: number }; result: string };
|
|
29
|
+
quit: { args: null; result: null };
|
|
30
|
+
}>();
|
|
31
|
+
|
|
32
|
+
export const events = defineEvents<{
|
|
33
|
+
added: number;
|
|
34
|
+
}>();
|
|
35
|
+
|
|
36
|
+
/** The contract the page imports with `import type { App } from "../src-host/main"`. */
|
|
37
|
+
export type App = { commands: typeof commands; events: typeof events };
|
|
12
38
|
|
|
13
39
|
export function setup(app: JanelaApp): void {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
40
|
+
// `args` is inferred from the contract, and the return type is checked
|
|
41
|
+
// against it — no casts, and no way to drift from what the page expects.
|
|
42
|
+
on(app, commands, "add", (args) => {
|
|
43
|
+
const sum = args.a + args.b;
|
|
44
|
+
emit(app, events, "added", sum);
|
|
19
45
|
return sum;
|
|
20
46
|
});
|
|
21
47
|
|
|
22
|
-
app
|
|
23
|
-
|
|
24
|
-
return "Hello, " + a.name + " — from the native TS binary";
|
|
48
|
+
on(app, commands, "greet", (args) => {
|
|
49
|
+
return "Hello, " + args.name + " — from the native TS binary";
|
|
25
50
|
});
|
|
26
51
|
|
|
27
|
-
app
|
|
28
|
-
console.log("[host] page says:", args
|
|
52
|
+
on(app, commands, "log", (args) => {
|
|
53
|
+
console.log("[host] page says:", args);
|
|
54
|
+
return null;
|
|
29
55
|
});
|
|
30
56
|
|
|
31
57
|
// An async command: answers later, without freezing the window.
|
|
32
|
-
app
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
resolve("waited " + a.ms + "ms without blocking the UI");
|
|
58
|
+
onAsync(app, commands, "wait", (args, resolve) => {
|
|
59
|
+
app.sleep(args.ms, () => {
|
|
60
|
+
resolve("waited " + args.ms + "ms without blocking the UI");
|
|
36
61
|
});
|
|
37
62
|
});
|
|
38
63
|
|
|
39
|
-
app
|
|
64
|
+
on(app, commands, "quit", (_args) => {
|
|
40
65
|
app.quit();
|
|
66
|
+
return null;
|
|
41
67
|
});
|
|
42
68
|
}
|
|
@@ -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,68 @@
|
|
|
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
|
|
15
|
+
import {
|
|
16
|
+
defineCommands,
|
|
17
|
+
defineEvents,
|
|
18
|
+
emit,
|
|
19
|
+
on,
|
|
20
|
+
onAsync,
|
|
21
|
+
type JanelaApp,
|
|
22
|
+
} from "janela/host";
|
|
23
|
+
|
|
24
|
+
export const commands = defineCommands<{
|
|
25
|
+
add: { args: { a: number; b: number }; result: number };
|
|
26
|
+
greet: { args: { name: string }; result: string };
|
|
27
|
+
log: { args: string; result: null };
|
|
28
|
+
wait: { args: { ms: number }; result: string };
|
|
29
|
+
quit: { args: null; result: null };
|
|
30
|
+
}>();
|
|
31
|
+
|
|
32
|
+
export const events = defineEvents<{
|
|
33
|
+
added: number;
|
|
34
|
+
}>();
|
|
35
|
+
|
|
36
|
+
/** The contract the page imports with `import type { App } from "../src-host/main"`. */
|
|
37
|
+
export type App = { commands: typeof commands; events: typeof events };
|
|
12
38
|
|
|
13
39
|
export function setup(app: JanelaApp): void {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
40
|
+
// `args` is inferred from the contract, and the return type is checked
|
|
41
|
+
// against it — no casts, and no way to drift from what the page expects.
|
|
42
|
+
on(app, commands, "add", (args) => {
|
|
43
|
+
const sum = args.a + args.b;
|
|
44
|
+
emit(app, events, "added", sum);
|
|
19
45
|
return sum;
|
|
20
46
|
});
|
|
21
47
|
|
|
22
|
-
app
|
|
23
|
-
|
|
24
|
-
return "Hello, " + a.name + " — from the native TS binary";
|
|
48
|
+
on(app, commands, "greet", (args) => {
|
|
49
|
+
return "Hello, " + args.name + " — from the native TS binary";
|
|
25
50
|
});
|
|
26
51
|
|
|
27
|
-
app
|
|
28
|
-
console.log("[host] page says:", args
|
|
52
|
+
on(app, commands, "log", (args) => {
|
|
53
|
+
console.log("[host] page says:", args);
|
|
54
|
+
return null;
|
|
29
55
|
});
|
|
30
56
|
|
|
31
57
|
// An async command: answers later, without freezing the window.
|
|
32
|
-
app
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
resolve("waited " + a.ms + "ms without blocking the UI");
|
|
58
|
+
onAsync(app, commands, "wait", (args, resolve) => {
|
|
59
|
+
app.sleep(args.ms, () => {
|
|
60
|
+
resolve("waited " + args.ms + "ms without blocking the UI");
|
|
36
61
|
});
|
|
37
62
|
});
|
|
38
63
|
|
|
39
|
-
app
|
|
64
|
+
on(app, commands, "quit", (_args) => {
|
|
40
65
|
app.quit();
|
|
66
|
+
return null;
|
|
41
67
|
});
|
|
42
68
|
}
|
|
@@ -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,68 @@
|
|
|
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
|
|
15
|
+
import {
|
|
16
|
+
defineCommands,
|
|
17
|
+
defineEvents,
|
|
18
|
+
emit,
|
|
19
|
+
on,
|
|
20
|
+
onAsync,
|
|
21
|
+
type JanelaApp,
|
|
22
|
+
} from "janela/host";
|
|
23
|
+
|
|
24
|
+
export const commands = defineCommands<{
|
|
25
|
+
add: { args: { a: number; b: number }; result: number };
|
|
26
|
+
greet: { args: { name: string }; result: string };
|
|
27
|
+
log: { args: string; result: null };
|
|
28
|
+
wait: { args: { ms: number }; result: string };
|
|
29
|
+
quit: { args: null; result: null };
|
|
30
|
+
}>();
|
|
31
|
+
|
|
32
|
+
export const events = defineEvents<{
|
|
33
|
+
added: number;
|
|
34
|
+
}>();
|
|
35
|
+
|
|
36
|
+
/** The contract the page imports with `import type { App } from "../src-host/main"`. */
|
|
37
|
+
export type App = { commands: typeof commands; events: typeof events };
|
|
12
38
|
|
|
13
39
|
export function setup(app: JanelaApp): void {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
40
|
+
// `args` is inferred from the contract, and the return type is checked
|
|
41
|
+
// against it — no casts, and no way to drift from what the page expects.
|
|
42
|
+
on(app, commands, "add", (args) => {
|
|
43
|
+
const sum = args.a + args.b;
|
|
44
|
+
emit(app, events, "added", sum);
|
|
19
45
|
return sum;
|
|
20
46
|
});
|
|
21
47
|
|
|
22
|
-
app
|
|
23
|
-
|
|
24
|
-
return "Hello, " + a.name + " — from the native TS binary";
|
|
48
|
+
on(app, commands, "greet", (args) => {
|
|
49
|
+
return "Hello, " + args.name + " — from the native TS binary";
|
|
25
50
|
});
|
|
26
51
|
|
|
27
|
-
app
|
|
28
|
-
console.log("[host] page says:", args
|
|
52
|
+
on(app, commands, "log", (args) => {
|
|
53
|
+
console.log("[host] page says:", args);
|
|
54
|
+
return null;
|
|
29
55
|
});
|
|
30
56
|
|
|
31
57
|
// An async command: answers later, without freezing the window.
|
|
32
|
-
app
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
resolve("waited " + a.ms + "ms without blocking the UI");
|
|
58
|
+
onAsync(app, commands, "wait", (args, resolve) => {
|
|
59
|
+
app.sleep(args.ms, () => {
|
|
60
|
+
resolve("waited " + args.ms + "ms without blocking the UI");
|
|
36
61
|
});
|
|
37
62
|
});
|
|
38
63
|
|
|
39
|
-
app
|
|
64
|
+
on(app, commands, "quit", (_args) => {
|
|
40
65
|
app.quit();
|
|
66
|
+
return null;
|
|
41
67
|
});
|
|
42
68
|
}
|