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.
Files changed (39) hide show
  1. package/README.md +178 -7
  2. package/api/global.d.ts +27 -0
  3. package/api/index.d.ts +102 -0
  4. package/api/index.js +64 -0
  5. package/bin/janela.mjs +15 -1
  6. package/package.json +15 -1
  7. package/runtime/janela.ts +38 -118
  8. package/runtime/types.ts +251 -0
  9. package/templates/index.html +4 -2
  10. package/templates/main.ts +1 -1
  11. package/templates/react/deps.json +14 -2
  12. package/templates/react/files/index.html +1 -1
  13. package/templates/react/files/src/App.tsx +55 -0
  14. package/templates/react/files/src/main.tsx +12 -0
  15. package/templates/react/files/src-host/main.ts +48 -22
  16. package/templates/react/files/tsconfig.json +25 -0
  17. package/templates/solid/deps.json +11 -2
  18. package/templates/solid/files/index.html +1 -1
  19. package/templates/solid/files/src/App.tsx +55 -0
  20. package/templates/solid/files/src/main.tsx +7 -0
  21. package/templates/solid/files/src-host/main.ts +48 -22
  22. package/templates/solid/files/tsconfig.json +26 -0
  23. package/templates/svelte/deps.json +6 -1
  24. package/templates/svelte/files/index.html +1 -1
  25. package/templates/svelte/files/src/App.svelte +15 -7
  26. package/templates/svelte/files/src/main.ts +7 -0
  27. package/templates/svelte/files/src-host/main.ts +48 -22
  28. package/templates/svelte/files/tsconfig.json +23 -0
  29. package/templates/vue/deps.json +12 -2
  30. package/templates/vue/files/index.html +1 -1
  31. package/templates/vue/files/src/App.vue +16 -8
  32. package/templates/vue/files/src-host/main.ts +48 -22
  33. package/templates/vue/files/tsconfig.json +23 -0
  34. package/templates/react/files/src/App.jsx +0 -38
  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
@@ -1,42 +1,68 @@
1
1
  // src-host/main.ts — your app's backend, compiled to native code by scriptc.
2
2
  //
3
- // Register commands here; the page calls them with `await janela.invoke(name, args)`.
4
- // Handlers take the arguments as a value and return a value — the runtime owns
5
- // JSON at the boundary, so there is no parsing or stringifying to do here.
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
- // Gotcha inherited from scriptc: never use a bare FFI-backed call as a
8
- // complete variable initializer — wrap it in any expression (`+ 0`). Plain
9
- // TypeScript like everything in this file is unaffected.
8
+ // Two gotchas inherited from scriptc:
9
+ // - never use a bare FFI-backed call as a complete variable initializer;
10
+ // wrap it in any expression (`+ 0`);
11
+ // - write `args: null` for a command that takes nothing, not `undefined` —
12
+ // an `undefined` argument lowers to a zero-parameter function and fails
13
+ // to compile.
10
14
 
11
- import type { JanelaApp } from "./janela";
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
- app.command("add", (args) => {
15
- const a = args as { a: number; b: number };
16
- const sum = a.a + a.b;
17
- // Backend→frontend event: the page listens with janela.listen("added", …).
18
- app.emit("added", sum);
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.command("greet", (args) => {
23
- const a = args as { name: string };
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.command("log", (args) => {
28
- console.log("[host] page says:", args as string);
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.commandAsync("wait", (args, resolve) => {
33
- const a = args as { ms: number };
34
- app.sleep(a.ms, () => {
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.command("quit", () => {
64
+ on(app, commands, "quit", (_args) => {
40
65
  app.quit();
66
+ return null;
41
67
  });
42
68
  }
@@ -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,25 @@
1
- <script>
1
+ <script lang="ts">
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>();
8
+
2
9
  let greeting = $state("…");
3
10
  let a = $state(2);
4
11
  let b = $state(40);
5
- let sum = $state(null);
6
- let events = $state([]);
12
+ let sum = $state<number | null>(null);
13
+ let events = $state<string[]>([]);
7
14
 
8
- // Backend→frontend events. The payload arrives as a value.
9
- janela.listen("added", (value) => (events = [`host emitted: ${value}`, ...events]));
15
+ // Backend→frontend events. The payload arrives as a value, and the generic
16
+ // says which value.
17
+ client.on("added", (value) => (events = [`host emitted: ${value}`, ...events]));
10
18
 
11
- janela.invoke("greet", { name: "__NAME__" }).then((g) => (greeting = g));
19
+ client.invoke("greet", { name: "__NAME__" }).then((g) => (greeting = g));
12
20
 
13
21
  async function add() {
14
- sum = await janela.invoke("add", { a: Number(a), b: Number(b) });
22
+ sum = await client.invoke("add", { a: Number(a), b: Number(b) });
15
23
  }
16
24
  </script>
17
25
 
@@ -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 });
@@ -1,42 +1,68 @@
1
1
  // src-host/main.ts — your app's backend, compiled to native code by scriptc.
2
2
  //
3
- // Register commands here; the page calls them with `await janela.invoke(name, args)`.
4
- // Handlers take the arguments as a value and return a value — the runtime owns
5
- // JSON at the boundary, so there is no parsing or stringifying to do here.
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
- // Gotcha inherited from scriptc: never use a bare FFI-backed call as a
8
- // complete variable initializer — wrap it in any expression (`+ 0`). Plain
9
- // TypeScript like everything in this file is unaffected.
8
+ // Two gotchas inherited from scriptc:
9
+ // - never use a bare FFI-backed call as a complete variable initializer;
10
+ // wrap it in any expression (`+ 0`);
11
+ // - write `args: null` for a command that takes nothing, not `undefined` —
12
+ // an `undefined` argument lowers to a zero-parameter function and fails
13
+ // to compile.
10
14
 
11
- import type { JanelaApp } from "./janela";
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
- app.command("add", (args) => {
15
- const a = args as { a: number; b: number };
16
- const sum = a.a + a.b;
17
- // Backend→frontend event: the page listens with janela.listen("added", …).
18
- app.emit("added", sum);
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.command("greet", (args) => {
23
- const a = args as { name: string };
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.command("log", (args) => {
28
- console.log("[host] page says:", args as string);
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.commandAsync("wait", (args, resolve) => {
33
- const a = args as { ms: number };
34
- app.sleep(a.ms, () => {
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.command("quit", () => {
64
+ on(app, commands, "quit", (_args) => {
40
65
  app.quit();
66
+ return null;
41
67
  });
42
68
  }
@@ -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,29 @@
1
- <script setup>
2
- import { onMounted, ref } from "vue";
1
+ <script setup lang="ts">
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>();
3
9
 
4
10
  const greeting = ref("…");
5
11
  const a = ref(2);
6
12
  const b = ref(40);
7
- const sum = ref(null);
8
- const events = ref([]);
13
+ const sum = ref<number | null>(null);
14
+ const events = ref<string[]>([]);
9
15
 
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}`));
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);
12
20
 
13
21
  onMounted(async () => {
14
- greeting.value = await janela.invoke("greet", { name: "__NAME__" });
22
+ greeting.value = await client.invoke("greet", { name: "__NAME__" });
15
23
  });
16
24
 
17
25
  async function add() {
18
- sum.value = await janela.invoke("add", { a: Number(a.value), b: Number(b.value) });
26
+ sum.value = await client.invoke("add", { a: Number(a.value), b: Number(b.value) });
19
27
  }
20
28
  </script>
21
29
 
@@ -1,42 +1,68 @@
1
1
  // src-host/main.ts — your app's backend, compiled to native code by scriptc.
2
2
  //
3
- // Register commands here; the page calls them with `await janela.invoke(name, args)`.
4
- // Handlers take the arguments as a value and return a value — the runtime owns
5
- // JSON at the boundary, so there is no parsing or stringifying to do here.
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
- // Gotcha inherited from scriptc: never use a bare FFI-backed call as a
8
- // complete variable initializer — wrap it in any expression (`+ 0`). Plain
9
- // TypeScript like everything in this file is unaffected.
8
+ // Two gotchas inherited from scriptc:
9
+ // - never use a bare FFI-backed call as a complete variable initializer;
10
+ // wrap it in any expression (`+ 0`);
11
+ // - write `args: null` for a command that takes nothing, not `undefined` —
12
+ // an `undefined` argument lowers to a zero-parameter function and fails
13
+ // to compile.
10
14
 
11
- import type { JanelaApp } from "./janela";
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
- app.command("add", (args) => {
15
- const a = args as { a: number; b: number };
16
- const sum = a.a + a.b;
17
- // Backend→frontend event: the page listens with janela.listen("added", …).
18
- app.emit("added", sum);
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.command("greet", (args) => {
23
- const a = args as { name: string };
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.command("log", (args) => {
28
- console.log("[host] page says:", args as string);
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.commandAsync("wait", (args, resolve) => {
33
- const a = args as { ms: number };
34
- app.sleep(a.ms, () => {
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.command("quit", () => {
64
+ on(app, commands, "quit", (_args) => {
40
65
  app.quit();
66
+ return null;
41
67
  });
42
68
  }
@@ -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,38 +0,0 @@
1
- import { useEffect, useState } from "react";
2
- import "./App.css";
3
-
4
- export default function App() {
5
- const [greeting, setGreeting] = useState("…");
6
- const [a, setA] = useState(2);
7
- const [b, setB] = useState(40);
8
- const [sum, setSum] = useState(null);
9
- const [events, setEvents] = useState([]);
10
-
11
- useEffect(() => {
12
- // Backend→frontend events. The payload arrives as a value.
13
- janela.listen("added", (value) =>
14
- setEvents((prev) => [`host emitted: ${value}`, ...prev]),
15
- );
16
- janela.invoke("greet", { name: "__NAME__" }).then(setGreeting);
17
- }, []);
18
-
19
- const add = async () =>
20
- setSum(await janela.invoke("add", { a: Number(a), b: Number(b) }));
21
-
22
- return (
23
- <>
24
- <h1>{greeting}</h1>
25
- <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
- <button onClick={add}>add</button>
29
- {sum !== null && <span> = {sum}</span>}
30
- </p>
31
- <ul>
32
- {events.map((e, i) => (
33
- <li key={i}>{e}</li>
34
- ))}
35
- </ul>
36
- </>
37
- );
38
- }
@@ -1,9 +0,0 @@
1
- import { StrictMode } from "react";
2
- import { createRoot } from "react-dom/client";
3
- import App from "./App.jsx";
4
-
5
- createRoot(document.getElementById("root")).render(
6
- <StrictMode>
7
- <App />
8
- </StrictMode>,
9
- );
@@ -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
- }
@@ -1,4 +0,0 @@
1
- import { render } from "solid-js/web";
2
- import App from "./App.jsx";
3
-
4
- render(() => <App />, document.getElementById("root"));
@@ -1,4 +0,0 @@
1
- import { mount } from "svelte";
2
- import App from "./App.svelte";
3
-
4
- mount(App, { target: document.getElementById("app") });
File without changes