janela 0.3.0 → 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.
Files changed (39) hide show
  1. package/README.md +77 -11
  2. package/api/global.d.ts +27 -0
  3. package/api/index.d.ts +37 -0
  4. package/api/index.js +35 -0
  5. package/bin/janela.mjs +21 -4
  6. package/package.json +15 -1
  7. package/runtime/janela.ts +138 -134
  8. package/runtime/types.ts +131 -0
  9. package/shim/wvshim.cc +55 -8
  10. package/templates/index.html +4 -2
  11. package/templates/main.ts +1 -1
  12. package/templates/react/deps.json +14 -2
  13. package/templates/react/files/index.html +1 -1
  14. package/templates/react/files/src/{App.jsx → App.tsx} +19 -8
  15. package/templates/react/files/src/main.tsx +12 -0
  16. package/templates/react/files/src-host/main.ts +1 -1
  17. package/templates/react/files/tsconfig.json +25 -0
  18. package/templates/solid/deps.json +11 -2
  19. package/templates/solid/files/index.html +1 -1
  20. package/templates/solid/files/src/App.tsx +50 -0
  21. package/templates/solid/files/src/main.tsx +7 -0
  22. package/templates/solid/files/src-host/main.ts +1 -1
  23. package/templates/solid/files/tsconfig.json +26 -0
  24. package/templates/svelte/deps.json +6 -1
  25. package/templates/svelte/files/index.html +1 -1
  26. package/templates/svelte/files/src/App.svelte +10 -7
  27. package/templates/svelte/files/src/main.ts +7 -0
  28. package/templates/svelte/files/src-host/main.ts +1 -1
  29. package/templates/svelte/files/tsconfig.json +23 -0
  30. package/templates/vue/deps.json +12 -2
  31. package/templates/vue/files/index.html +1 -1
  32. package/templates/vue/files/src/App.vue +9 -7
  33. package/templates/vue/files/src-host/main.ts +1 -1
  34. package/templates/vue/files/tsconfig.json +23 -0
  35. package/templates/react/files/src/main.jsx +0 -9
  36. package/templates/solid/files/src/App.jsx +0 -35
  37. package/templates/solid/files/src/main.jsx +0 -4
  38. package/templates/svelte/files/src/main.js +0 -4
  39. /package/templates/vue/files/src/{main.js → main.ts} +0 -0
@@ -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/shim/wvshim.cc CHANGED
@@ -820,18 +820,65 @@ int32_t wv_job_status(int32_t h, int32_t id) {
820
820
  return j->status.load(std::memory_order_acquire);
821
821
  }
822
822
 
823
- // Hand a finished job's payload to TS in one call. The callback is
824
- // lifetime:"call", so it runs synchronously here on the UI thread, the only
825
- // thread allowed to touch the scriptc runtime. The worker is already done by
826
- // then (the caller has observed a terminal status), so `data` is stable.
827
- int32_t wv_job_take(int32_t h, int32_t id,
828
- void (*sink)(const uint8_t *, size_t, void *), void *ctx) {
823
+ // Payload size in bytes, so TS knows when it has drained the whole thing.
824
+ // f64 rather than i32: a payload may exceed 2 GB, and every scriptc number is
825
+ // a double anyway (exact for byte counts far beyond any plausible file).
826
+ double wv_job_size(int32_t h, int32_t id) {
827
+ if (!app_at(h)) return -1;
828
+ Job *j = job_at(id);
829
+ if (!j) return -1;
830
+ if (j->status.load(std::memory_order_acquire) == JOB_PENDING) return -1;
831
+ return static_cast<double>(j->data.size());
832
+ }
833
+
834
+ // Hand ONE SLICE of a finished job's payload to TS, and answer how many bytes
835
+ // it covered. The callback is lifetime:"call", so it runs synchronously here —
836
+ // on the UI thread, the only thread allowed to touch the scriptc runtime. The
837
+ // worker is already done by then (the caller has observed a terminal status),
838
+ // so `data` is stable for the whole drain.
839
+ //
840
+ // Slicing exists so the UI thread can decode a large payload across several
841
+ // turns instead of stalling on all of it at once; the caller advances `offset`
842
+ // by the returned count until it reaches wv_job_size().
843
+ //
844
+ // The slice end is pulled back to a UTF-8 sequence boundary, because scriptc
845
+ // decodes a `string` param as UTF-8: cutting mid-sequence would turn one
846
+ // character into replacement characters on both sides of the seam. Bytes that
847
+ // are not valid UTF-8 have no boundary to find, so after four steps the cut
848
+ // stands as asked and the payload is passed through unchanged.
849
+ double wv_job_take_at(int32_t h, int32_t id, double offset, double max_bytes,
850
+ void (*sink)(const uint8_t *, size_t, void *),
851
+ void *ctx) {
829
852
  if (!app_at(h)) return -1;
830
853
  Job *j = job_at(id);
831
854
  if (!j || !sink) return -1;
832
855
  if (j->status.load(std::memory_order_acquire) == JOB_PENDING) return -1;
833
- sink(reinterpret_cast<const uint8_t *>(j->data.data()), j->data.size(), ctx);
834
- return 0;
856
+ if (offset < 0 || max_bytes < 0) return -1;
857
+
858
+ const size_t size = j->data.size();
859
+ const size_t off = static_cast<size_t>(offset);
860
+ if (off > size) return -1;
861
+ if (off == size) return 0;
862
+
863
+ size_t want = static_cast<size_t>(max_bytes);
864
+ if (want == 0) return 0;
865
+ size_t end = off + want;
866
+ if (end >= size) {
867
+ end = size;
868
+ } else {
869
+ const unsigned char *d =
870
+ reinterpret_cast<const unsigned char *>(j->data.data());
871
+ // d[end] is the first byte of the NEXT slice; while it is a continuation
872
+ // byte (10xxxxxx) the cut sits inside a character, so step back.
873
+ size_t e = end;
874
+ for (int guard = 0; guard < 4 && e > off && (d[e] & 0xc0) == 0x80; guard++) {
875
+ e--;
876
+ }
877
+ if (e > off) end = e;
878
+ }
879
+
880
+ sink(reinterpret_cast<const uint8_t *>(j->data.data()) + off, end - off, ctx);
881
+ return static_cast<double>(end - off);
835
882
  }
836
883
 
837
884
  // Release the slot for reuse. Refuses while the worker is still running, so a
@@ -30,8 +30,10 @@
30
30
  <ul id="events"></ul>
31
31
 
32
32
  <script>
33
- // `janela.invoke(name, args)` and `janela.listen(event, cb)` are injected by
34
- // janela before the page loads the Tauri-shaped frontend API.
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 "./janela";
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": { "react": "^19.0.0", "react-dom": "^19.0.0" },
3
- "devDependencies": { "@vitejs/plugin-react": "^4.3.4", "vite": "^6.0.7" }
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
  }
@@ -6,6 +6,6 @@
6
6
  </head>
7
7
  <body>
8
8
  <div id="root"></div>
9
- <script type="module" src="/src/main.jsx"></script>
9
+ <script type="module" src="/src/main.tsx"></script>
10
10
  </body>
11
11
  </html>
@@ -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
- janela.listen("added", (value) =>
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
- janela.invoke("greet", { name: "__NAME__" }).then(setGreeting);
18
+ invoke<string>("greet", { name: "__NAME__" }).then(setGreeting);
17
19
  }, []);
18
20
 
19
21
  const add = async () =>
20
- setSum(await janela.invoke("add", { a: Number(a), b: Number(b) }));
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 type="number" value={a} onChange={(e) => setA(e.target.value)} /> +
27
- <input type="number" value={b} onChange={(e) => setB(e.target.value)} />
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 "./janela";
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": { "solid-js": "^1.9.4" },
3
- "devDependencies": { "vite": "^6.0.7", "vite-plugin-solid": "^2.11.0" }
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
  }
@@ -6,6 +6,6 @@
6
6
  </head>
7
7
  <body>
8
8
  <div id="root"></div>
9
- <script type="module" src="/src/main.jsx"></script>
9
+ <script type="module" src="/src/main.tsx"></script>
10
10
  </body>
11
11
  </html>
@@ -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
+ }
@@ -0,0 +1,7 @@
1
+ import { render } from "solid-js/web";
2
+ import App from "./App.tsx";
3
+
4
+ const root = document.getElementById("root");
5
+ if (!root) throw new Error("index.html is missing #root");
6
+
7
+ render(() => <App />, root);
@@ -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 "./janela";
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
  }
@@ -6,6 +6,6 @@
6
6
  </head>
7
7
  <body>
8
8
  <div id="app"></div>
9
- <script type="module" src="/src/main.js"></script>
9
+ <script type="module" src="/src/main.ts"></script>
10
10
  </body>
11
11
  </html>
@@ -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
- janela.listen("added", (value) => (events = [`host emitted: ${value}`, ...events]));
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
- janela.invoke("greet", { name: "__NAME__" }).then((g) => (greeting = g));
14
+ invoke<string>("greet", { name: "__NAME__" }).then((g) => (greeting = g));
12
15
 
13
16
  async function add() {
14
- sum = await janela.invoke("add", { a: Number(a), b: Number(b) });
17
+ sum = await invoke<number>("add", { a: Number(a), b: Number(b) });
15
18
  }
16
19
  </script>
17
20
 
@@ -0,0 +1,7 @@
1
+ import { mount } from "svelte";
2
+ import App from "./App.svelte";
3
+
4
+ const target = document.getElementById("app");
5
+ if (!target) throw new Error("index.html is missing #app");
6
+
7
+ mount(App, { target });
@@ -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 "./janela";
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
+ }
@@ -1,4 +1,14 @@
1
1
  {
2
- "dependencies": { "vue": "^3.5.13" },
3
- "devDependencies": { "@vitejs/plugin-vue": "^5.2.1", "vite": "^6.0.7" }
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
  }
@@ -6,6 +6,6 @@
6
6
  </head>
7
7
  <body>
8
8
  <div id="app"></div>
9
- <script type="module" src="/src/main.js"></script>
9
+ <script type="module" src="/src/main.ts"></script>
10
10
  </body>
11
11
  </html>
@@ -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
- janela.listen("added", (value) => events.value.unshift(`host emitted: ${value}`));
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 janela.invoke("greet", { name: "__NAME__" });
16
+ greeting.value = await invoke<string>("greet", { name: "__NAME__" });
15
17
  });
16
18
 
17
19
  async function add() {
18
- sum.value = await janela.invoke("add", { a: Number(a.value), b: Number(b.value) });
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 "./janela";
11
+ import type { JanelaApp } from "janela/host";
12
12
 
13
13
  export function setup(app: JanelaApp): void {
14
14
  app.command("add", (args) => {