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.
- package/README.md +178 -7
- package/api/global.d.ts +27 -0
- package/api/index.d.ts +102 -0
- package/api/index.js +64 -0
- package/bin/janela.mjs +15 -1
- package/package.json +15 -1
- package/runtime/janela.ts +38 -118
- package/runtime/types.ts +251 -0
- package/templates/index.html +4 -2
- package/templates/main.ts +1 -1
- package/templates/react/deps.json +14 -2
- package/templates/react/files/index.html +1 -1
- package/templates/react/files/src/App.tsx +55 -0
- package/templates/react/files/src/main.tsx +12 -0
- package/templates/react/files/src-host/main.ts +48 -22
- package/templates/react/files/tsconfig.json +25 -0
- package/templates/solid/deps.json +11 -2
- package/templates/solid/files/index.html +1 -1
- package/templates/solid/files/src/App.tsx +55 -0
- package/templates/solid/files/src/main.tsx +7 -0
- package/templates/solid/files/src-host/main.ts +48 -22
- package/templates/solid/files/tsconfig.json +26 -0
- package/templates/svelte/deps.json +6 -1
- package/templates/svelte/files/index.html +1 -1
- package/templates/svelte/files/src/App.svelte +15 -7
- package/templates/svelte/files/src/main.ts +7 -0
- package/templates/svelte/files/src-host/main.ts +48 -22
- package/templates/svelte/files/tsconfig.json +23 -0
- package/templates/vue/deps.json +12 -2
- package/templates/vue/files/index.html +1 -1
- package/templates/vue/files/src/App.vue +16 -8
- package/templates/vue/files/src-host/main.ts +48 -22
- package/templates/vue/files/tsconfig.json +23 -0
- package/templates/react/files/src/App.jsx +0 -38
- package/templates/react/files/src/main.jsx +0 -9
- package/templates/solid/files/src/App.jsx +0 -35
- package/templates/solid/files/src/main.jsx +0 -4
- package/templates/svelte/files/src/main.js +0 -4
- /package/templates/vue/files/src/{main.js → main.ts} +0 -0
package/runtime/types.ts
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public host-side types for a janela app — the shapes `src-host/main.ts`
|
|
3
|
+
* works with.
|
|
4
|
+
*
|
|
5
|
+
* This file is the single definition of those types. It is both:
|
|
6
|
+
* - what `import type { JanelaApp } from "janela/host"` resolves to in an
|
|
7
|
+
* editor, via the package's exports map; and
|
|
8
|
+
* - re-exported by runtime/janela.ts, which is what the compiled build
|
|
9
|
+
* actually links against (the CLI copies both files into .janela/build/).
|
|
10
|
+
*
|
|
11
|
+
* Mostly declarations; the typed-contract helpers at the bottom are the only
|
|
12
|
+
* runtime code, and they are deliberately trivial.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// Handlers take the invoked arguments as a value and return a value; the
|
|
16
|
+
// runtime owns JSON at the boundary. `args` is whatever the page passed to
|
|
17
|
+
// janela.invoke(name, args) — cast it to the shape you expect. The return
|
|
18
|
+
// value is what the page's promise resolves with.
|
|
19
|
+
//
|
|
20
|
+
// Throwing is not supported by scriptc across the FFI boundary. Use
|
|
21
|
+
// commandAsync's `reject` to fail a call, or return an error value.
|
|
22
|
+
export type CommandHandler = (args: unknown) => unknown;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* An async command: return immediately, answer later. `resolve`/`reject` take
|
|
26
|
+
* a value and settle the page's `await janela.invoke(...)` promise whenever
|
|
27
|
+
* they are called — from a later defer()/sleep() turn, or from another
|
|
28
|
+
* command. The window stays responsive for as long as the call is pending.
|
|
29
|
+
*/
|
|
30
|
+
export type AsyncCommandHandler = (
|
|
31
|
+
args: unknown,
|
|
32
|
+
resolve: (value: unknown) => void,
|
|
33
|
+
reject: (reason: unknown) => void,
|
|
34
|
+
) => void;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Completion of an async file operation. `err` is null on success; on failure
|
|
38
|
+
* it carries a Node-shaped message ("ENOENT: no such file or directory, open
|
|
39
|
+
* '/x'") and `text` is empty. Errors arrive as values, never as throws —
|
|
40
|
+
* scriptc cannot propagate an exception across the FFI boundary.
|
|
41
|
+
*/
|
|
42
|
+
export type FsCallback = (err: string | null, text: string) => void;
|
|
43
|
+
|
|
44
|
+
/** A named group of extensions offered in a dialog's file-type popup. */
|
|
45
|
+
export interface DialogFilter {
|
|
46
|
+
name: string;
|
|
47
|
+
/** Bare extensions, no dot and no glob: ["png", "jpg"]. */
|
|
48
|
+
extensions: string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface OpenDialogOptions {
|
|
52
|
+
title?: string;
|
|
53
|
+
/** Directory the dialog opens in. */
|
|
54
|
+
defaultPath?: string;
|
|
55
|
+
/** Allow picking more than one entry. */
|
|
56
|
+
multiple?: boolean;
|
|
57
|
+
/** Pick directories instead of files. Not supported on Windows. */
|
|
58
|
+
directory?: boolean;
|
|
59
|
+
filters?: DialogFilter[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface SaveDialogOptions {
|
|
63
|
+
title?: string;
|
|
64
|
+
defaultPath?: string;
|
|
65
|
+
/** Filename pre-filled in the name field. */
|
|
66
|
+
defaultName?: string;
|
|
67
|
+
filters?: DialogFilter[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface WindowConfig {
|
|
71
|
+
title: string;
|
|
72
|
+
width: number;
|
|
73
|
+
height: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface JanelaApp {
|
|
77
|
+
handle: number;
|
|
78
|
+
names: string[];
|
|
79
|
+
handlers: CommandHandler[];
|
|
80
|
+
/** Register a named command, callable from the page as janela.invoke(name, args). */
|
|
81
|
+
command: (name: string, h: CommandHandler) => void;
|
|
82
|
+
/** Register a command that answers later; see AsyncCommandHandler. */
|
|
83
|
+
commandAsync: (name: string, h: AsyncCommandHandler) => void;
|
|
84
|
+
/** Run fn on the next turn of the host loop — the way to slice long work. */
|
|
85
|
+
defer: (fn: () => void) => void;
|
|
86
|
+
/** Run fn after at least ms. The host loop's timer; scriptc's setTimeout
|
|
87
|
+
* cannot fire while the window is open (its loop is parked inside run()). */
|
|
88
|
+
sleep: (ms: number, fn: () => void) => void;
|
|
89
|
+
/**
|
|
90
|
+
* Read a file without blocking the window. The syscall runs on a shim
|
|
91
|
+
* worker thread; the callback lands on the UI thread on a later turn.
|
|
92
|
+
* Prefer this over node:fs readFileSync inside a command — that one blocks
|
|
93
|
+
* the loop, and with it the whole window.
|
|
94
|
+
*/
|
|
95
|
+
readFileAsync: (path: string, cb: FsCallback) => void;
|
|
96
|
+
/** Write a file without blocking the window; cb(null) on success. */
|
|
97
|
+
writeFileAsync: (
|
|
98
|
+
path: string,
|
|
99
|
+
data: string,
|
|
100
|
+
cb: (err: string | null) => void,
|
|
101
|
+
) => void;
|
|
102
|
+
/**
|
|
103
|
+
* Show the native "open" dialog. `cb` gets the chosen paths, or null if the
|
|
104
|
+
* user cancelled. The modal runs on a later turn of the UI thread, so
|
|
105
|
+
* calling this from inside a command does not block that command's reply —
|
|
106
|
+
* pair it with commandAsync when the page is waiting for the result.
|
|
107
|
+
*/
|
|
108
|
+
openFileDialog: (
|
|
109
|
+
options: OpenDialogOptions,
|
|
110
|
+
cb: (paths: string[] | null, err?: string) => void,
|
|
111
|
+
) => void;
|
|
112
|
+
/** Show the native "save" dialog; cb gets the path, or null on cancel. */
|
|
113
|
+
saveFileDialog: (
|
|
114
|
+
options: SaveDialogOptions,
|
|
115
|
+
cb: (path: string | null, err?: string) => void,
|
|
116
|
+
) => void;
|
|
117
|
+
/** Change the window title at any time, not just at startup. */
|
|
118
|
+
setTitle: (title: string) => void;
|
|
119
|
+
/**
|
|
120
|
+
* Resize the window. `hint` is webview's sizing hint: 0 none, 1 minimum,
|
|
121
|
+
* 2 maximum, 3 fixed.
|
|
122
|
+
*/
|
|
123
|
+
setSize: (width: number, height: number, hint?: number) => void;
|
|
124
|
+
/** Enter or leave fullscreen. */
|
|
125
|
+
setFullscreen: (on: boolean) => void;
|
|
126
|
+
/** Fire an event into the page; the payload is delivered as a value. */
|
|
127
|
+
emit: (event: string, payload: unknown) => void;
|
|
128
|
+
/** Close the window and make run() return. */
|
|
129
|
+
quit: () => void;
|
|
130
|
+
/** Show the page and block until the window closes. Returns the run status. */
|
|
131
|
+
run: (html: string) => number;
|
|
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
|
+
}
|
package/templates/index.html
CHANGED
|
@@ -30,8 +30,10 @@
|
|
|
30
30
|
<ul id="events"></ul>
|
|
31
31
|
|
|
32
32
|
<script>
|
|
33
|
-
//
|
|
34
|
-
//
|
|
33
|
+
// This template has no bundler, so it uses the `janela` global that the
|
|
34
|
+
// host injects before the page loads. Projects with a build step should
|
|
35
|
+
// `import { invoke, listen } from "janela/api"` instead — same functions,
|
|
36
|
+
// but typed and resolvable by the bundler.
|
|
35
37
|
window.onload = async () => {
|
|
36
38
|
const out = document.getElementById("out");
|
|
37
39
|
const events = document.getElementById("events");
|
package/templates/main.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// complete variable initializer — wrap it in any expression (`+ 0`). Plain
|
|
9
9
|
// TypeScript like everything in this file is unaffected.
|
|
10
10
|
|
|
11
|
-
import type { JanelaApp } from "
|
|
11
|
+
import type { JanelaApp } from "janela/host";
|
|
12
12
|
|
|
13
13
|
export function setup(app: JanelaApp): void {
|
|
14
14
|
app.command("add", (args) => {
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
{
|
|
2
|
-
"dependencies": {
|
|
3
|
-
|
|
2
|
+
"dependencies": {
|
|
3
|
+
"react": "^19.0.0",
|
|
4
|
+
"react-dom": "^19.0.0"
|
|
5
|
+
},
|
|
6
|
+
"devDependencies": {
|
|
7
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
8
|
+
"vite": "^6.0.7",
|
|
9
|
+
"@types/react": "^19.0.7",
|
|
10
|
+
"@types/react-dom": "^19.0.3",
|
|
11
|
+
"typescript": "^5.7.3"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"typecheck": "tsc --noEmit"
|
|
15
|
+
}
|
|
4
16
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
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";
|
|
6
|
+
import "./App.css";
|
|
7
|
+
|
|
8
|
+
const client = createClient<Contract>();
|
|
9
|
+
|
|
10
|
+
export default function App() {
|
|
11
|
+
const [greeting, setGreeting] = useState("…");
|
|
12
|
+
const [a, setA] = useState(2);
|
|
13
|
+
const [b, setB] = useState(40);
|
|
14
|
+
const [sum, setSum] = useState<number | null>(null);
|
|
15
|
+
const [events, setEvents] = useState<string[]>([]);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
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) =>
|
|
21
|
+
setEvents((prev) => [`host emitted: ${value}`, ...prev]),
|
|
22
|
+
);
|
|
23
|
+
client.invoke("greet", { name: "__NAME__" }).then(setGreeting);
|
|
24
|
+
return off;
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
const add = async () =>
|
|
28
|
+
setSum(await client.invoke("add", { a, b }));
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<>
|
|
32
|
+
<h1>{greeting}</h1>
|
|
33
|
+
<p>
|
|
34
|
+
<input
|
|
35
|
+
type="number"
|
|
36
|
+
value={a}
|
|
37
|
+
onChange={(e) => setA(Number(e.target.value))}
|
|
38
|
+
/>{" "}
|
|
39
|
+
+
|
|
40
|
+
<input
|
|
41
|
+
type="number"
|
|
42
|
+
value={b}
|
|
43
|
+
onChange={(e) => setB(Number(e.target.value))}
|
|
44
|
+
/>
|
|
45
|
+
<button onClick={add}>add</button>
|
|
46
|
+
{sum !== null && <span> = {sum}</span>}
|
|
47
|
+
</p>
|
|
48
|
+
<ul>
|
|
49
|
+
{events.map((e, i) => (
|
|
50
|
+
<li key={i}>{e}</li>
|
|
51
|
+
))}
|
|
52
|
+
</ul>
|
|
53
|
+
</>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { StrictMode } from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import App from "./App.tsx";
|
|
4
|
+
|
|
5
|
+
const root = document.getElementById("root");
|
|
6
|
+
if (!root) throw new Error("index.html is missing #root");
|
|
7
|
+
|
|
8
|
+
createRoot(root).render(
|
|
9
|
+
<StrictMode>
|
|
10
|
+
<App />
|
|
11
|
+
</StrictMode>,
|
|
12
|
+
);
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": [
|
|
7
|
+
"ES2022",
|
|
8
|
+
"DOM",
|
|
9
|
+
"DOM.Iterable"
|
|
10
|
+
],
|
|
11
|
+
"types": [],
|
|
12
|
+
"jsx": "react-jsx",
|
|
13
|
+
"strict": true,
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
"allowImportingTsExtensions": true,
|
|
16
|
+
"skipLibCheck": true,
|
|
17
|
+
"isolatedModules": true,
|
|
18
|
+
"verbatimModuleSyntax": true
|
|
19
|
+
},
|
|
20
|
+
"include": [
|
|
21
|
+
"src/**/*.ts",
|
|
22
|
+
"src/**/*.tsx",
|
|
23
|
+
"src-host/**/*.ts"
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"dependencies": {
|
|
3
|
-
|
|
2
|
+
"dependencies": {
|
|
3
|
+
"solid-js": "^1.9.4"
|
|
4
|
+
},
|
|
5
|
+
"devDependencies": {
|
|
6
|
+
"vite": "^6.0.7",
|
|
7
|
+
"vite-plugin-solid": "^2.11.0",
|
|
8
|
+
"typescript": "^5.7.3"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"typecheck": "tsc --noEmit"
|
|
12
|
+
}
|
|
4
13
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createSignal, onMount, For, Show } from "solid-js";
|
|
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";
|
|
6
|
+
import "./App.css";
|
|
7
|
+
|
|
8
|
+
const client = createClient<Contract>();
|
|
9
|
+
|
|
10
|
+
export default function App() {
|
|
11
|
+
const [greeting, setGreeting] = createSignal("…");
|
|
12
|
+
const [a, setA] = createSignal(2);
|
|
13
|
+
const [b, setB] = createSignal(40);
|
|
14
|
+
const [sum, setSum] = createSignal<number | null>(null);
|
|
15
|
+
const [events, setEvents] = createSignal<string[]>([]);
|
|
16
|
+
|
|
17
|
+
// Backend→frontend events. The payload arrives as a value, and the generic
|
|
18
|
+
// says which value.
|
|
19
|
+
client.on("added", (value) =>
|
|
20
|
+
setEvents((prev) => [`host emitted: ${value}`, ...prev]),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
onMount(async () =>
|
|
24
|
+
setGreeting(await client.invoke("greet", { name: "__NAME__" })),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const add = async () =>
|
|
28
|
+
setSum(await client.invoke("add", { a: a(), b: b() }));
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<>
|
|
32
|
+
<h1>{greeting()}</h1>
|
|
33
|
+
<p>
|
|
34
|
+
<input
|
|
35
|
+
type="number"
|
|
36
|
+
value={a()}
|
|
37
|
+
onInput={(e) => setA(Number(e.currentTarget.value))}
|
|
38
|
+
/>{" "}
|
|
39
|
+
+
|
|
40
|
+
<input
|
|
41
|
+
type="number"
|
|
42
|
+
value={b()}
|
|
43
|
+
onInput={(e) => setB(Number(e.currentTarget.value))}
|
|
44
|
+
/>
|
|
45
|
+
<button onClick={add}>add</button>
|
|
46
|
+
<Show when={sum() !== null}>
|
|
47
|
+
<span> = {sum()}</span>
|
|
48
|
+
</Show>
|
|
49
|
+
</p>
|
|
50
|
+
<ul>
|
|
51
|
+
<For each={events()}>{(e) => <li>{e}</li>}</For>
|
|
52
|
+
</ul>
|
|
53
|
+
</>
|
|
54
|
+
);
|
|
55
|
+
}
|