janela 0.3.1 → 0.4.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 +69 -7
- package/api/global.d.ts +27 -0
- package/api/index.d.ts +37 -0
- package/api/index.js +35 -0
- package/bin/janela.mjs +15 -1
- package/package.json +15 -1
- package/runtime/janela.ts +24 -118
- package/runtime/types.ts +131 -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.jsx → App.tsx} +19 -8
- package/templates/react/files/src/main.tsx +12 -0
- package/templates/react/files/src-host/main.ts +1 -1
- 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 +50 -0
- package/templates/solid/files/src/main.tsx +7 -0
- package/templates/solid/files/src-host/main.ts +1 -1
- 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 +10 -7
- package/templates/svelte/files/src/main.ts +7 -0
- package/templates/svelte/files/src-host/main.ts +1 -1
- 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 +9 -7
- package/templates/vue/files/src-host/main.ts +1 -1
- package/templates/vue/files/tsconfig.json +23 -0
- 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/README.md
CHANGED
|
@@ -70,7 +70,7 @@ MSI.
|
|
|
70
70
|
|
|
71
71
|
```
|
|
72
72
|
my-app/
|
|
73
|
-
├── index.html frontend — any HTML/JS; calls
|
|
73
|
+
├── index.html frontend — any HTML/JS/TS; calls invoke() / listen()
|
|
74
74
|
├── src-host/main.ts backend — exports setup(app), registers commands
|
|
75
75
|
└── janela.conf.json name, bundle identifier, version, window
|
|
76
76
|
```
|
|
@@ -79,17 +79,42 @@ A Vite project adds a `vite.config.js` and a `src/` tree — that config is what
|
|
|
79
79
|
makes janela build the frontend with Vite instead of inlining `index.html`
|
|
80
80
|
directly.
|
|
81
81
|
|
|
82
|
-
Frontend API
|
|
82
|
+
Frontend API — import it, and your editor and `tsc` know the shapes:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { invoke, listen } from "janela/api";
|
|
86
|
+
|
|
87
|
+
const sum = await invoke<number>("add", { a: 2, b: 40 }); // call a backend command
|
|
88
|
+
listen<number>("added", (payload) => { ... }); // backend-fired events
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`janela` is already a devDependency of a scaffolded project, so there is
|
|
92
|
+
nothing extra to install. The generic is what the host command returns —
|
|
93
|
+
values cross the boundary as values, so there is no JSON to parse.
|
|
94
|
+
|
|
95
|
+
<details>
|
|
96
|
+
<summary>No bundler? Use the injected global instead</summary>
|
|
97
|
+
|
|
98
|
+
janela injects the same two functions as `window.janela` before every document
|
|
99
|
+
loads, which is what the `vanilla` template uses — it needs no `npm install` at
|
|
100
|
+
all:
|
|
83
101
|
|
|
84
102
|
```js
|
|
85
|
-
const sum = await janela.invoke("add", { a: 2, b: 40 });
|
|
86
|
-
janela.listen("added", (payload) => { ... });
|
|
103
|
+
const sum = await janela.invoke("add", { a: 2, b: 40 });
|
|
104
|
+
janela.listen("added", (payload) => { ... });
|
|
87
105
|
```
|
|
88
106
|
|
|
89
|
-
|
|
107
|
+
TypeScript users on this path can pull in the ambient types with
|
|
108
|
+
`/// <reference types="janela/global" />`, or by adding `"janela/global"` to
|
|
109
|
+
`compilerOptions.types`. With a bundler, prefer the import — it needs no
|
|
110
|
+
ambient declaration.
|
|
111
|
+
|
|
112
|
+
</details>
|
|
113
|
+
|
|
114
|
+
Backend API (`src-host/main.ts`) — also typed, from the same package:
|
|
90
115
|
|
|
91
116
|
```ts
|
|
92
|
-
import type { JanelaApp } from "
|
|
117
|
+
import type { JanelaApp } from "janela/host";
|
|
93
118
|
|
|
94
119
|
export function setup(app: JanelaApp): void {
|
|
95
120
|
app.command("add", (args) => { // values in, values out
|
|
@@ -210,11 +235,48 @@ nested modal loop would otherwise re-enter the host loop underneath a live TS
|
|
|
210
235
|
frame; [docs/native-shell.md](../../docs/native-shell.md) has the details, the
|
|
211
236
|
per-platform table, and the Windows GUI-subsystem note.
|
|
212
237
|
|
|
238
|
+
## Migrating from 0.3.x
|
|
239
|
+
|
|
240
|
+
Nothing breaks: the injected `janela` global still works exactly as before.
|
|
241
|
+
What changed is the recommendation — the frontend now has a real module, so
|
|
242
|
+
editors and `tsc` can see it:
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
// 0.3.x — an untyped global, invisible to tsc and ESLint
|
|
246
|
+
const sum = await janela.invoke("add", { a: 2, b: 40 });
|
|
247
|
+
|
|
248
|
+
// 0.4.x — typed, resolvable, and generic over what the command returns
|
|
249
|
+
import { invoke } from "janela/api";
|
|
250
|
+
const sum = await invoke<number>("add", { a: 2, b: 40 });
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
The host side had the same problem and gets the same fix. `src-host/main.ts`
|
|
254
|
+
used to import `JanelaApp` from `"./janela"` — a path that only exists inside
|
|
255
|
+
`.janela/build/`, so an editor could never resolve it and the whole `app.*`
|
|
256
|
+
API was untyped:
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
// 0.3.x — unresolved in the editor; JanelaApp was effectively `any`
|
|
260
|
+
import type { JanelaApp } from "./janela";
|
|
261
|
+
|
|
262
|
+
// 0.4.x — resolves against the installed package
|
|
263
|
+
import type { JanelaApp } from "janela/host";
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
`janela build` rewrites that specifier to the local runtime copy while
|
|
267
|
+
assembling the compile unit, so the build stays fully static and a project
|
|
268
|
+
with no `node_modules` at all still compiles.
|
|
269
|
+
|
|
270
|
+
The framework templates (`vue`, `react`, `svelte`, `solid`) are TypeScript now
|
|
271
|
+
and scaffold with a `typecheck` script that covers `src/` and `src-host/`
|
|
272
|
+
alike. `vanilla` stays plain JavaScript on the global, so it still needs no
|
|
273
|
+
`npm install` before the first build.
|
|
274
|
+
|
|
213
275
|
## Migrating from 0.1.x
|
|
214
276
|
|
|
215
277
|
Commands used to take and return **JSON text**; they now take and return
|
|
216
278
|
**values**, with the runtime handling serialisation. The page-side API
|
|
217
|
-
(`
|
|
279
|
+
(`invoke` / `listen`) is unchanged.
|
|
218
280
|
|
|
219
281
|
```ts
|
|
220
282
|
// 0.1.x
|
package/api/global.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the injected `janela` global, for pages that use it directly
|
|
3
|
+
* rather than importing `janela/api` — a plain `<script>` with no bundler,
|
|
4
|
+
* typically. Pull them in from a TypeScript project with:
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* /// <reference types="janela/global" />
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* or by adding `"janela/global"` to `compilerOptions.types` in tsconfig.json.
|
|
11
|
+
*
|
|
12
|
+
* If you have a bundler, prefer `import { invoke, listen } from "janela/api"` —
|
|
13
|
+
* it needs no ambient declaration and is what the templates use.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { JanelaBridge } from "./index.js";
|
|
17
|
+
|
|
18
|
+
declare global {
|
|
19
|
+
/** The host bridge janela injects before the document loads. */
|
|
20
|
+
const janela: JanelaBridge;
|
|
21
|
+
|
|
22
|
+
interface Window {
|
|
23
|
+
janela: JanelaBridge;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export {};
|
package/api/index.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The janela frontend API.
|
|
3
|
+
*
|
|
4
|
+
* Values cross the boundary as values — janela owns the JSON at the edge — so
|
|
5
|
+
* the generic parameter is what the host command returns, not a string to
|
|
6
|
+
* parse.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** The bridge janela injects as `window.janela` before each document loads. */
|
|
10
|
+
export interface JanelaBridge {
|
|
11
|
+
invoke<T = unknown>(cmd: string, args?: unknown): Promise<T>;
|
|
12
|
+
listen<T = unknown>(event: string, cb: (payload: T) => void): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Call a command the host registered with `app.command` / `app.commandAsync`.
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* const sum = await invoke<number>("add", { a: 2, b: 40 });
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* Rejects if the command is unknown, if the handler rejected, or if the page
|
|
23
|
+
* is not running inside a janela window.
|
|
24
|
+
*/
|
|
25
|
+
export declare function invoke<T = unknown>(cmd: string, args?: unknown): Promise<T>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Subscribe to an event the host sends with `app.emit`.
|
|
29
|
+
*
|
|
30
|
+
* ```ts
|
|
31
|
+
* listen<number>("added", (sum) => console.log(sum));
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare function listen<T = unknown>(
|
|
35
|
+
event: string,
|
|
36
|
+
cb: (payload: T) => void,
|
|
37
|
+
): void;
|
package/api/index.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// The janela frontend API — `import { invoke, listen } from "janela/api"`.
|
|
2
|
+
//
|
|
3
|
+
// janela injects a bridge as `window.janela` into every document before it
|
|
4
|
+
// loads, so this module is a thin wrapper over that global rather than a
|
|
5
|
+
// transport of its own. Importing it is the recommended style: bundlers
|
|
6
|
+
// resolve it, editors complete it, and `tsc` checks it. The global stays
|
|
7
|
+
// available unchanged for pages with no build step.
|
|
8
|
+
|
|
9
|
+
function bridge() {
|
|
10
|
+
const found = typeof globalThis === "undefined" ? undefined : globalThis.janela;
|
|
11
|
+
if (!found || typeof found.invoke !== "function") {
|
|
12
|
+
throw new Error(
|
|
13
|
+
"janela: no host bridge on this page (window.janela is undefined). " +
|
|
14
|
+
"The page is not running inside a janela window — run the app with " +
|
|
15
|
+
"`janela dev`, or `janela build` and launch the binary. Opening the " +
|
|
16
|
+
"page in a browser, or serving it with plain `vite`, leaves no host " +
|
|
17
|
+
"to talk to.",
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
return found;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Call a command the host registered with `app.command` / `app.commandAsync`.
|
|
25
|
+
* Arguments and the resolved value are ordinary values; janela owns the
|
|
26
|
+
* serialisation at the boundary.
|
|
27
|
+
*/
|
|
28
|
+
export async function invoke(cmd, args) {
|
|
29
|
+
return bridge().invoke(cmd, args);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Subscribe to an event the host sends with `app.emit`. */
|
|
33
|
+
export function listen(event, cb) {
|
|
34
|
+
bridge().listen(event, cb);
|
|
35
|
+
}
|
package/bin/janela.mjs
CHANGED
|
@@ -481,9 +481,22 @@ function build(root, { devUrl = null, gui = true } = {}) {
|
|
|
481
481
|
|
|
482
482
|
// Assemble the compile unit: runtime + user's commands + generated modules.
|
|
483
483
|
cpSync(join(KIT, "runtime", "janela.ts"), join(buildDir, "janela.ts"));
|
|
484
|
+
cpSync(join(KIT, "runtime", "types.ts"), join(buildDir, "types.ts"));
|
|
484
485
|
const mainSrc = join(root, "src-host", "main.ts");
|
|
485
486
|
if (!existsSync(mainSrc)) fail("missing src-host/main.ts");
|
|
486
|
-
|
|
487
|
+
// A project's main.ts imports from "janela/host" so that it resolves in the
|
|
488
|
+
// editor against the installed package. Here it is compiled next to the
|
|
489
|
+
// runtime instead, so the specifier is rewritten to that local copy: the
|
|
490
|
+
// build never resolves through node_modules, which keeps it static and will
|
|
491
|
+
// keep working when "janela/host" starts exporting values (not just types)
|
|
492
|
+
// as well.
|
|
493
|
+
writeFileSync(
|
|
494
|
+
join(buildDir, "main.ts"),
|
|
495
|
+
readFileSync(mainSrc, "utf8").replace(
|
|
496
|
+
/(\bfrom\s*)(['"])janela\/host\2/g,
|
|
497
|
+
"$1$2./janela$2",
|
|
498
|
+
),
|
|
499
|
+
);
|
|
487
500
|
|
|
488
501
|
const html = frontendHtml(root, conf, devUrl);
|
|
489
502
|
writeFileSync(
|
|
@@ -616,6 +629,7 @@ function init(name, template) {
|
|
|
616
629
|
const extra = JSON.parse(readFileSync(join(tdir, "deps.json"), "utf8"));
|
|
617
630
|
pkg.type = "module";
|
|
618
631
|
Object.assign(pkg.devDependencies, extra.devDependencies ?? {});
|
|
632
|
+
Object.assign(pkg.scripts, extra.scripts ?? {});
|
|
619
633
|
if (extra.dependencies) pkg.dependencies = extra.dependencies;
|
|
620
634
|
}
|
|
621
635
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "janela",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Desktop apps in pure TypeScript, compiled to native. No Rust, no Node, no Electron.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"janela": "bin/janela.mjs",
|
|
8
8
|
"jn": "bin/janela.mjs"
|
|
9
9
|
},
|
|
10
|
+
"exports": {
|
|
11
|
+
"./api": {
|
|
12
|
+
"types": "./api/index.d.ts",
|
|
13
|
+
"default": "./api/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./host": {
|
|
16
|
+
"types": "./runtime/types.ts"
|
|
17
|
+
},
|
|
18
|
+
"./global": {
|
|
19
|
+
"types": "./api/global.d.ts"
|
|
20
|
+
},
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
10
23
|
"dependencies": {
|
|
11
24
|
"scriptc": "0.0.35"
|
|
12
25
|
},
|
|
@@ -33,6 +46,7 @@
|
|
|
33
46
|
"node": ">=24"
|
|
34
47
|
},
|
|
35
48
|
"files": [
|
|
49
|
+
"api/",
|
|
36
50
|
"bin/",
|
|
37
51
|
"runtime/",
|
|
38
52
|
"shim/",
|
package/runtime/janela.ts
CHANGED
|
@@ -74,124 +74,30 @@ const BOOTSTRAP =
|
|
|
74
74
|
" for (var i = 0; i < cbs.length; i++) cbs[i](payload);" +
|
|
75
75
|
"};";
|
|
76
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
|
-
* '/x'") and `text` is empty. Errors arrive as values, never as throws —
|
|
102
|
-
* scriptc cannot propagate an exception across the FFI boundary.
|
|
103
|
-
*/
|
|
104
|
-
export type FsCallback = (err: string | null, text: string) => void;
|
|
105
|
-
|
|
106
|
-
/** A named group of extensions offered in a dialog's file-type popup. */
|
|
107
|
-
export interface DialogFilter {
|
|
108
|
-
name: string;
|
|
109
|
-
/** Bare extensions, no dot and no glob: ["png", "jpg"]. */
|
|
110
|
-
extensions: string[];
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export interface OpenDialogOptions {
|
|
114
|
-
title?: string;
|
|
115
|
-
/** Directory the dialog opens in. */
|
|
116
|
-
defaultPath?: string;
|
|
117
|
-
/** Allow picking more than one entry. */
|
|
118
|
-
multiple?: boolean;
|
|
119
|
-
/** Pick directories instead of files. Not supported on Windows. */
|
|
120
|
-
directory?: boolean;
|
|
121
|
-
filters?: DialogFilter[];
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export interface SaveDialogOptions {
|
|
125
|
-
title?: string;
|
|
126
|
-
defaultPath?: string;
|
|
127
|
-
/** Filename pre-filled in the name field. */
|
|
128
|
-
defaultName?: string;
|
|
129
|
-
filters?: DialogFilter[];
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export interface WindowConfig {
|
|
133
|
-
title: string;
|
|
134
|
-
width: number;
|
|
135
|
-
height: number;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export interface JanelaApp {
|
|
139
|
-
handle: number;
|
|
140
|
-
names: string[];
|
|
141
|
-
handlers: CommandHandler[];
|
|
142
|
-
/** Register a named command, callable from the page as janela.invoke(name, args). */
|
|
143
|
-
command: (name: string, h: CommandHandler) => void;
|
|
144
|
-
/** Register a command that answers later; see AsyncCommandHandler. */
|
|
145
|
-
commandAsync: (name: string, h: AsyncCommandHandler) => void;
|
|
146
|
-
/** Run fn on the next turn of the host loop — the way to slice long work. */
|
|
147
|
-
defer: (fn: () => void) => void;
|
|
148
|
-
/** Run fn after at least ms. The host loop's timer; scriptc's setTimeout
|
|
149
|
-
* cannot fire while the window is open (its loop is parked inside run()). */
|
|
150
|
-
sleep: (ms: number, fn: () => void) => void;
|
|
151
|
-
/**
|
|
152
|
-
* Read a file without blocking the window. The syscall runs on a shim
|
|
153
|
-
* worker thread; the callback lands on the UI thread on a later turn.
|
|
154
|
-
* Prefer this over node:fs readFileSync inside a command — that one blocks
|
|
155
|
-
* the loop, and with it the whole window.
|
|
156
|
-
*/
|
|
157
|
-
readFileAsync: (path: string, cb: FsCallback) => void;
|
|
158
|
-
/** Write a file without blocking the window; cb(null) on success. */
|
|
159
|
-
writeFileAsync: (
|
|
160
|
-
path: string,
|
|
161
|
-
data: string,
|
|
162
|
-
cb: (err: string | null) => void,
|
|
163
|
-
) => void;
|
|
164
|
-
/**
|
|
165
|
-
* Show the native "open" dialog. `cb` gets the chosen paths, or null if the
|
|
166
|
-
* user cancelled. The modal runs on a later turn of the UI thread, so
|
|
167
|
-
* calling this from inside a command does not block that command's reply —
|
|
168
|
-
* pair it with commandAsync when the page is waiting for the result.
|
|
169
|
-
*/
|
|
170
|
-
openFileDialog: (
|
|
171
|
-
options: OpenDialogOptions,
|
|
172
|
-
cb: (paths: string[] | null, err?: string) => void,
|
|
173
|
-
) => void;
|
|
174
|
-
/** Show the native "save" dialog; cb gets the path, or null on cancel. */
|
|
175
|
-
saveFileDialog: (
|
|
176
|
-
options: SaveDialogOptions,
|
|
177
|
-
cb: (path: string | null, err?: string) => void,
|
|
178
|
-
) => void;
|
|
179
|
-
/** Change the window title at any time, not just at startup. */
|
|
180
|
-
setTitle: (title: string) => void;
|
|
181
|
-
/**
|
|
182
|
-
* Resize the window. `hint` is webview's sizing hint: 0 none, 1 minimum,
|
|
183
|
-
* 2 maximum, 3 fixed.
|
|
184
|
-
*/
|
|
185
|
-
setSize: (width: number, height: number, hint?: number) => void;
|
|
186
|
-
/** Enter or leave fullscreen. */
|
|
187
|
-
setFullscreen: (on: boolean) => void;
|
|
188
|
-
/** Fire an event into the page; the payload is delivered as a value. */
|
|
189
|
-
emit: (event: string, payload: unknown) => void;
|
|
190
|
-
/** Close the window and make run() return. */
|
|
191
|
-
quit: () => void;
|
|
192
|
-
/** Show the page and block until the window closes. Returns the run status. */
|
|
193
|
-
run: (html: string) => number;
|
|
194
|
-
}
|
|
77
|
+
// The public host types live in ./types (shipped as `janela/host` too, so a
|
|
78
|
+
// user's editor can see them). Re-exported here because the compiled build
|
|
79
|
+
// resolves them through this module — see the specifier rewrite in the CLI.
|
|
80
|
+
export type {
|
|
81
|
+
AsyncCommandHandler,
|
|
82
|
+
CommandHandler,
|
|
83
|
+
DialogFilter,
|
|
84
|
+
FsCallback,
|
|
85
|
+
JanelaApp,
|
|
86
|
+
OpenDialogOptions,
|
|
87
|
+
SaveDialogOptions,
|
|
88
|
+
WindowConfig,
|
|
89
|
+
} from "./types";
|
|
90
|
+
|
|
91
|
+
import type {
|
|
92
|
+
AsyncCommandHandler,
|
|
93
|
+
CommandHandler,
|
|
94
|
+
DialogFilter,
|
|
95
|
+
FsCallback,
|
|
96
|
+
JanelaApp,
|
|
97
|
+
OpenDialogOptions,
|
|
98
|
+
SaveDialogOptions,
|
|
99
|
+
WindowConfig,
|
|
100
|
+
} from "./types";
|
|
195
101
|
|
|
196
102
|
// JSON.stringify yields undefined for undefined; the wire always needs a
|
|
197
103
|
// value, and a command that returns nothing should read as null in the page.
|
package/runtime/types.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
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
|
+
* Keep it declaration-only: no runtime code lives here.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// Handlers take the invoked arguments as a value and return a value; the
|
|
15
|
+
// runtime owns JSON at the boundary. `args` is whatever the page passed to
|
|
16
|
+
// janela.invoke(name, args) — cast it to the shape you expect. The return
|
|
17
|
+
// value is what the page's promise resolves with.
|
|
18
|
+
//
|
|
19
|
+
// Throwing is not supported by scriptc across the FFI boundary. Use
|
|
20
|
+
// commandAsync's `reject` to fail a call, or return an error value.
|
|
21
|
+
export type CommandHandler = (args: unknown) => unknown;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* An async command: return immediately, answer later. `resolve`/`reject` take
|
|
25
|
+
* a value and settle the page's `await janela.invoke(...)` promise whenever
|
|
26
|
+
* they are called — from a later defer()/sleep() turn, or from another
|
|
27
|
+
* command. The window stays responsive for as long as the call is pending.
|
|
28
|
+
*/
|
|
29
|
+
export type AsyncCommandHandler = (
|
|
30
|
+
args: unknown,
|
|
31
|
+
resolve: (value: unknown) => void,
|
|
32
|
+
reject: (reason: unknown) => void,
|
|
33
|
+
) => void;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Completion of an async file operation. `err` is null on success; on failure
|
|
37
|
+
* it carries a Node-shaped message ("ENOENT: no such file or directory, open
|
|
38
|
+
* '/x'") and `text` is empty. Errors arrive as values, never as throws —
|
|
39
|
+
* scriptc cannot propagate an exception across the FFI boundary.
|
|
40
|
+
*/
|
|
41
|
+
export type FsCallback = (err: string | null, text: string) => void;
|
|
42
|
+
|
|
43
|
+
/** A named group of extensions offered in a dialog's file-type popup. */
|
|
44
|
+
export interface DialogFilter {
|
|
45
|
+
name: string;
|
|
46
|
+
/** Bare extensions, no dot and no glob: ["png", "jpg"]. */
|
|
47
|
+
extensions: string[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface OpenDialogOptions {
|
|
51
|
+
title?: string;
|
|
52
|
+
/** Directory the dialog opens in. */
|
|
53
|
+
defaultPath?: string;
|
|
54
|
+
/** Allow picking more than one entry. */
|
|
55
|
+
multiple?: boolean;
|
|
56
|
+
/** Pick directories instead of files. Not supported on Windows. */
|
|
57
|
+
directory?: boolean;
|
|
58
|
+
filters?: DialogFilter[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface SaveDialogOptions {
|
|
62
|
+
title?: string;
|
|
63
|
+
defaultPath?: string;
|
|
64
|
+
/** Filename pre-filled in the name field. */
|
|
65
|
+
defaultName?: string;
|
|
66
|
+
filters?: DialogFilter[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface WindowConfig {
|
|
70
|
+
title: string;
|
|
71
|
+
width: number;
|
|
72
|
+
height: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
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;
|
|
131
|
+
}
|
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
|
}
|
|
@@ -1,30 +1,41 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
|
+
import { invoke, listen } from "janela/api";
|
|
2
3
|
import "./App.css";
|
|
3
4
|
|
|
4
5
|
export default function App() {
|
|
5
6
|
const [greeting, setGreeting] = useState("…");
|
|
6
7
|
const [a, setA] = useState(2);
|
|
7
8
|
const [b, setB] = useState(40);
|
|
8
|
-
const [sum, setSum] = useState(null);
|
|
9
|
-
const [events, setEvents] = useState([]);
|
|
9
|
+
const [sum, setSum] = useState<number | null>(null);
|
|
10
|
+
const [events, setEvents] = useState<string[]>([]);
|
|
10
11
|
|
|
11
12
|
useEffect(() => {
|
|
12
|
-
// Backend→frontend events. The payload arrives as a value
|
|
13
|
-
|
|
13
|
+
// Backend→frontend events. The payload arrives as a value, and the
|
|
14
|
+
// generic says which value.
|
|
15
|
+
listen<number>("added", (value) =>
|
|
14
16
|
setEvents((prev) => [`host emitted: ${value}`, ...prev]),
|
|
15
17
|
);
|
|
16
|
-
|
|
18
|
+
invoke<string>("greet", { name: "__NAME__" }).then(setGreeting);
|
|
17
19
|
}, []);
|
|
18
20
|
|
|
19
21
|
const add = async () =>
|
|
20
|
-
setSum(await
|
|
22
|
+
setSum(await invoke<number>("add", { a, b }));
|
|
21
23
|
|
|
22
24
|
return (
|
|
23
25
|
<>
|
|
24
26
|
<h1>{greeting}</h1>
|
|
25
27
|
<p>
|
|
26
|
-
<input
|
|
27
|
-
|
|
28
|
+
<input
|
|
29
|
+
type="number"
|
|
30
|
+
value={a}
|
|
31
|
+
onChange={(e) => setA(Number(e.target.value))}
|
|
32
|
+
/>{" "}
|
|
33
|
+
+
|
|
34
|
+
<input
|
|
35
|
+
type="number"
|
|
36
|
+
value={b}
|
|
37
|
+
onChange={(e) => setB(Number(e.target.value))}
|
|
38
|
+
/>
|
|
28
39
|
<button onClick={add}>add</button>
|
|
29
40
|
{sum !== null && <span> = {sum}</span>}
|
|
30
41
|
</p>
|
|
@@ -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
|
+
);
|
|
@@ -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) => {
|
|
@@ -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,50 @@
|
|
|
1
|
+
import { createSignal, onMount, For, Show } from "solid-js";
|
|
2
|
+
import { invoke, listen } from "janela/api";
|
|
3
|
+
import "./App.css";
|
|
4
|
+
|
|
5
|
+
export default function App() {
|
|
6
|
+
const [greeting, setGreeting] = createSignal("…");
|
|
7
|
+
const [a, setA] = createSignal(2);
|
|
8
|
+
const [b, setB] = createSignal(40);
|
|
9
|
+
const [sum, setSum] = createSignal<number | null>(null);
|
|
10
|
+
const [events, setEvents] = createSignal<string[]>([]);
|
|
11
|
+
|
|
12
|
+
// Backend→frontend events. The payload arrives as a value, and the generic
|
|
13
|
+
// says which value.
|
|
14
|
+
listen<number>("added", (value) =>
|
|
15
|
+
setEvents((prev) => [`host emitted: ${value}`, ...prev]),
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
onMount(async () =>
|
|
19
|
+
setGreeting(await invoke<string>("greet", { name: "__NAME__" })),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const add = async () =>
|
|
23
|
+
setSum(await invoke<number>("add", { a: a(), b: b() }));
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<>
|
|
27
|
+
<h1>{greeting()}</h1>
|
|
28
|
+
<p>
|
|
29
|
+
<input
|
|
30
|
+
type="number"
|
|
31
|
+
value={a()}
|
|
32
|
+
onInput={(e) => setA(Number(e.currentTarget.value))}
|
|
33
|
+
/>{" "}
|
|
34
|
+
+
|
|
35
|
+
<input
|
|
36
|
+
type="number"
|
|
37
|
+
value={b()}
|
|
38
|
+
onInput={(e) => setB(Number(e.currentTarget.value))}
|
|
39
|
+
/>
|
|
40
|
+
<button onClick={add}>add</button>
|
|
41
|
+
<Show when={sum() !== null}>
|
|
42
|
+
<span> = {sum()}</span>
|
|
43
|
+
</Show>
|
|
44
|
+
</p>
|
|
45
|
+
<ul>
|
|
46
|
+
<For each={events()}>{(e) => <li>{e}</li>}</For>
|
|
47
|
+
</ul>
|
|
48
|
+
</>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -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) => {
|
|
@@ -0,0 +1,26 @@
|
|
|
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": "preserve",
|
|
13
|
+
"jsxImportSource": "solid-js",
|
|
14
|
+
"strict": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
"allowImportingTsExtensions": true,
|
|
17
|
+
"skipLibCheck": true,
|
|
18
|
+
"isolatedModules": true,
|
|
19
|
+
"verbatimModuleSyntax": true
|
|
20
|
+
},
|
|
21
|
+
"include": [
|
|
22
|
+
"src/**/*.ts",
|
|
23
|
+
"src/**/*.tsx",
|
|
24
|
+
"src-host/**/*.ts"
|
|
25
|
+
]
|
|
26
|
+
}
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
"devDependencies": {
|
|
3
3
|
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
|
4
4
|
"svelte": "^5.16.0",
|
|
5
|
-
"vite": "^6.0.7"
|
|
5
|
+
"vite": "^6.0.7",
|
|
6
|
+
"svelte-check": "^4.1.4",
|
|
7
|
+
"typescript": "^5.7.3"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"typecheck": "svelte-check --tsconfig ./tsconfig.json"
|
|
6
11
|
}
|
|
7
12
|
}
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
<script>
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { invoke, listen } from "janela/api";
|
|
3
|
+
|
|
2
4
|
let greeting = $state("…");
|
|
3
5
|
let a = $state(2);
|
|
4
6
|
let b = $state(40);
|
|
5
|
-
let sum = $state(null);
|
|
6
|
-
let events = $state([]);
|
|
7
|
+
let sum = $state<number | null>(null);
|
|
8
|
+
let events = $state<string[]>([]);
|
|
7
9
|
|
|
8
|
-
// Backend→frontend events. The payload arrives as a value
|
|
9
|
-
|
|
10
|
+
// Backend→frontend events. The payload arrives as a value, and the generic
|
|
11
|
+
// says which value.
|
|
12
|
+
listen<number>("added", (value) => (events = [`host emitted: ${value}`, ...events]));
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
invoke<string>("greet", { name: "__NAME__" }).then((g) => (greeting = g));
|
|
12
15
|
|
|
13
16
|
async function add() {
|
|
14
|
-
sum = await
|
|
17
|
+
sum = await invoke<number>("add", { a: Number(a), b: Number(b) });
|
|
15
18
|
}
|
|
16
19
|
</script>
|
|
17
20
|
|
|
@@ -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) => {
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
"strict": true,
|
|
13
|
+
"noEmit": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"isolatedModules": true,
|
|
16
|
+
"verbatimModuleSyntax": true
|
|
17
|
+
},
|
|
18
|
+
"include": [
|
|
19
|
+
"src/**/*.ts",
|
|
20
|
+
"src/**/*.svelte",
|
|
21
|
+
"src-host/**/*.ts"
|
|
22
|
+
]
|
|
23
|
+
}
|
package/templates/vue/deps.json
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
{
|
|
2
|
-
"dependencies": {
|
|
3
|
-
|
|
2
|
+
"dependencies": {
|
|
3
|
+
"vue": "^3.5.13"
|
|
4
|
+
},
|
|
5
|
+
"devDependencies": {
|
|
6
|
+
"@vitejs/plugin-vue": "^5.2.1",
|
|
7
|
+
"vite": "^6.0.7",
|
|
8
|
+
"typescript": "^5.7.3",
|
|
9
|
+
"vue-tsc": "^2.2.0"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"typecheck": "vue-tsc --noEmit"
|
|
13
|
+
}
|
|
4
14
|
}
|
|
@@ -1,21 +1,23 @@
|
|
|
1
|
-
<script setup>
|
|
1
|
+
<script setup lang="ts">
|
|
2
2
|
import { onMounted, ref } from "vue";
|
|
3
|
+
import { invoke, listen } from "janela/api";
|
|
3
4
|
|
|
4
5
|
const greeting = ref("…");
|
|
5
6
|
const a = ref(2);
|
|
6
7
|
const b = ref(40);
|
|
7
|
-
const sum = ref(null);
|
|
8
|
-
const events = ref([]);
|
|
8
|
+
const sum = ref<number | null>(null);
|
|
9
|
+
const events = ref<string[]>([]);
|
|
9
10
|
|
|
10
|
-
// Backend→frontend events. The payload arrives as a value, not a JSON string
|
|
11
|
-
|
|
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}`));
|
|
12
14
|
|
|
13
15
|
onMounted(async () => {
|
|
14
|
-
greeting.value = await
|
|
16
|
+
greeting.value = await invoke<string>("greet", { name: "__NAME__" });
|
|
15
17
|
});
|
|
16
18
|
|
|
17
19
|
async function add() {
|
|
18
|
-
sum.value = await
|
|
20
|
+
sum.value = await invoke<number>("add", { a: Number(a.value), b: Number(b.value) });
|
|
19
21
|
}
|
|
20
22
|
</script>
|
|
21
23
|
|
|
@@ -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) => {
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
"strict": true,
|
|
13
|
+
"noEmit": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"isolatedModules": true,
|
|
16
|
+
"verbatimModuleSyntax": true
|
|
17
|
+
},
|
|
18
|
+
"include": [
|
|
19
|
+
"src/**/*.ts",
|
|
20
|
+
"src/**/*.vue",
|
|
21
|
+
"src-host/**/*.ts"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { createSignal, onMount, For, Show } from "solid-js";
|
|
2
|
-
import "./App.css";
|
|
3
|
-
|
|
4
|
-
export default function App() {
|
|
5
|
-
const [greeting, setGreeting] = createSignal("…");
|
|
6
|
-
const [a, setA] = createSignal(2);
|
|
7
|
-
const [b, setB] = createSignal(40);
|
|
8
|
-
const [sum, setSum] = createSignal(null);
|
|
9
|
-
const [events, setEvents] = createSignal([]);
|
|
10
|
-
|
|
11
|
-
// Backend→frontend events. The payload arrives as a value.
|
|
12
|
-
janela.listen("added", (value) =>
|
|
13
|
-
setEvents((prev) => [`host emitted: ${value}`, ...prev]),
|
|
14
|
-
);
|
|
15
|
-
|
|
16
|
-
onMount(async () => setGreeting(await janela.invoke("greet", { name: "__NAME__" })));
|
|
17
|
-
|
|
18
|
-
const add = async () =>
|
|
19
|
-
setSum(await janela.invoke("add", { a: Number(a()), b: Number(b()) }));
|
|
20
|
-
|
|
21
|
-
return (
|
|
22
|
-
<>
|
|
23
|
-
<h1>{greeting()}</h1>
|
|
24
|
-
<p>
|
|
25
|
-
<input type="number" value={a()} onInput={(e) => setA(e.currentTarget.value)} /> +
|
|
26
|
-
<input type="number" value={b()} onInput={(e) => setB(e.currentTarget.value)} />
|
|
27
|
-
<button onClick={add}>add</button>
|
|
28
|
-
<Show when={sum() !== null}><span> = {sum()}</span></Show>
|
|
29
|
-
</p>
|
|
30
|
-
<ul>
|
|
31
|
-
<For each={events()}>{(e) => <li>{e}</li>}</For>
|
|
32
|
-
</ul>
|
|
33
|
-
</>
|
|
34
|
-
);
|
|
35
|
-
}
|
|
File without changes
|