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/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
|
|
@@ -101,6 +126,80 @@ export function setup(app: JanelaApp): void {
|
|
|
101
126
|
}
|
|
102
127
|
```
|
|
103
128
|
|
|
129
|
+
## The typed contract
|
|
130
|
+
|
|
131
|
+
The API above works, but `invoke<number>("add", …)` is an *assertion*: nothing
|
|
132
|
+
checks that the command exists, that the arguments match, or that the result
|
|
133
|
+
is really a number. Rename a command and the page still compiles.
|
|
134
|
+
|
|
135
|
+
Declare a contract instead, and both sides are checked against the same
|
|
136
|
+
declarations — no code generation, nothing to keep in sync. This is the
|
|
137
|
+
framework templates' default.
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
// src-host/main.ts
|
|
141
|
+
import { defineCommands, defineEvents, emit, on, onAsync, type JanelaApp } from "janela/host";
|
|
142
|
+
|
|
143
|
+
export const commands = defineCommands<{
|
|
144
|
+
add: { args: { a: number; b: number }; result: number };
|
|
145
|
+
greet: { args: { name: string }; result: string };
|
|
146
|
+
wait: { args: { ms: number }; result: string };
|
|
147
|
+
}>();
|
|
148
|
+
|
|
149
|
+
export const events = defineEvents<{ added: number }>();
|
|
150
|
+
|
|
151
|
+
export type App = { commands: typeof commands; events: typeof events };
|
|
152
|
+
|
|
153
|
+
export function setup(app: JanelaApp): void {
|
|
154
|
+
on(app, commands, "add", (args) => { // args inferred: { a: number; b: number }
|
|
155
|
+
emit(app, events, "added", args.a + args.b);
|
|
156
|
+
return args.a + args.b; // return type checked against the contract
|
|
157
|
+
});
|
|
158
|
+
onAsync(app, commands, "wait", (args, resolve) => {
|
|
159
|
+
app.sleep(args.ms, () => resolve("waited " + args.ms + "ms"));
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
// src/App.tsx (or .vue, .svelte …)
|
|
166
|
+
import { createClient } from "janela/api";
|
|
167
|
+
import type { App } from "../src-host/main"; // type-only: erased at compile time
|
|
168
|
+
|
|
169
|
+
const client = createClient<App>();
|
|
170
|
+
|
|
171
|
+
const sum = await client.invoke("add", { a: 2, b: 40 }); // sum: number
|
|
172
|
+
const off = client.on("added", (v) => console.log(v)); // v: number
|
|
173
|
+
off(); // unsubscribe
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Now these are all compile errors:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
await client.invoke("addd", { a: 1, b: 2 }); // unknown command
|
|
180
|
+
await client.invoke("add", { a: 1, b: "2" }); // wrong argument type
|
|
181
|
+
const s: string = await client.invoke("add", { a: 1, b: 2 }); // wrong result
|
|
182
|
+
client.on("addedd", () => {}); // unknown event
|
|
183
|
+
client.on("added", (v: string) => {}); // wrong payload type
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Two things worth knowing:
|
|
187
|
+
|
|
188
|
+
- **`import type` is erased**, so no host code is bundled into the page — the
|
|
189
|
+
contract is a type edge and nothing more. (Verified: the built frontend
|
|
190
|
+
bundle contains none of the host's strings.)
|
|
191
|
+
- **Types erase at runtime.** Payloads still cross as JSON and nothing
|
|
192
|
+
validates a malformed one. This is compile-time safety, like Tauri's
|
|
193
|
+
`invoke<T>()` — the difference is that here the types come from the host's
|
|
194
|
+
own declarations rather than from an assertion you write by hand, which is
|
|
195
|
+
only possible because both sides are TypeScript.
|
|
196
|
+
|
|
197
|
+
Write `args: null` for a command that takes nothing — `args: undefined`
|
|
198
|
+
lowers to a zero-parameter function and fails to compile.
|
|
199
|
+
|
|
200
|
+
The untyped `invoke` / `listen` still work unchanged; the contract is
|
|
201
|
+
additive, and the `vanilla` template still uses the global.
|
|
202
|
+
|
|
104
203
|
## Async commands
|
|
105
204
|
|
|
106
205
|
A command that has to wait — or to chew through real work — should not freeze
|
|
@@ -210,11 +309,83 @@ nested modal loop would otherwise re-enter the host loop underneath a live TS
|
|
|
210
309
|
frame; [docs/native-shell.md](../../docs/native-shell.md) has the details, the
|
|
211
310
|
per-platform table, and the Windows GUI-subsystem note.
|
|
212
311
|
|
|
312
|
+
## Migrating from 0.4.x
|
|
313
|
+
|
|
314
|
+
Nothing breaks: `app.command`, `app.emit`, and the untyped `invoke` / `listen`
|
|
315
|
+
all work exactly as before, and the `vanilla` template is unchanged.
|
|
316
|
+
|
|
317
|
+
Two things are new:
|
|
318
|
+
|
|
319
|
+
- `listen()` (and the injected `janela.listen`) now **return a disposer**.
|
|
320
|
+
Previously they returned nothing, so existing code is unaffected.
|
|
321
|
+
- The **typed contract** — `defineCommands` / `defineEvents` on the host,
|
|
322
|
+
`createClient<App>()` on the page. The framework templates now scaffold with
|
|
323
|
+
it. See [The typed contract](#the-typed-contract).
|
|
324
|
+
|
|
325
|
+
To adopt it in an existing app, declare what the host already exposes and swap
|
|
326
|
+
the registrations:
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
// before
|
|
330
|
+
app.command("add", (args) => {
|
|
331
|
+
const a = args as { a: number; b: number };
|
|
332
|
+
return a.a + a.b;
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// after
|
|
336
|
+
export const commands = defineCommands<{
|
|
337
|
+
add: { args: { a: number; b: number }; result: number };
|
|
338
|
+
}>();
|
|
339
|
+
export type App = { commands: typeof commands; events: typeof events };
|
|
340
|
+
|
|
341
|
+
on(app, commands, "add", (args) => args.a + args.b); // args inferred, no cast
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
then on the page, replace `invoke<number>("add", …)` with
|
|
345
|
+
`client.invoke("add", …)` built from `createClient<App>()`.
|
|
346
|
+
|
|
347
|
+
## Migrating from 0.3.x
|
|
348
|
+
|
|
349
|
+
Nothing breaks: the injected `janela` global still works exactly as before.
|
|
350
|
+
What changed is the recommendation — the frontend now has a real module, so
|
|
351
|
+
editors and `tsc` can see it:
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
// 0.3.x — an untyped global, invisible to tsc and ESLint
|
|
355
|
+
const sum = await janela.invoke("add", { a: 2, b: 40 });
|
|
356
|
+
|
|
357
|
+
// 0.4.x — typed, resolvable, and generic over what the command returns
|
|
358
|
+
import { invoke } from "janela/api";
|
|
359
|
+
const sum = await invoke<number>("add", { a: 2, b: 40 });
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
The host side had the same problem and gets the same fix. `src-host/main.ts`
|
|
363
|
+
used to import `JanelaApp` from `"./janela"` — a path that only exists inside
|
|
364
|
+
`.janela/build/`, so an editor could never resolve it and the whole `app.*`
|
|
365
|
+
API was untyped:
|
|
366
|
+
|
|
367
|
+
```ts
|
|
368
|
+
// 0.3.x — unresolved in the editor; JanelaApp was effectively `any`
|
|
369
|
+
import type { JanelaApp } from "./janela";
|
|
370
|
+
|
|
371
|
+
// 0.4.x — resolves against the installed package
|
|
372
|
+
import type { JanelaApp } from "janela/host";
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
`janela build` rewrites that specifier to the local runtime copy while
|
|
376
|
+
assembling the compile unit, so the build stays fully static and a project
|
|
377
|
+
with no `node_modules` at all still compiles.
|
|
378
|
+
|
|
379
|
+
The framework templates (`vue`, `react`, `svelte`, `solid`) are TypeScript now
|
|
380
|
+
and scaffold with a `typecheck` script that covers `src/` and `src-host/`
|
|
381
|
+
alike. `vanilla` stays plain JavaScript on the global, so it still needs no
|
|
382
|
+
`npm install` before the first build.
|
|
383
|
+
|
|
213
384
|
## Migrating from 0.1.x
|
|
214
385
|
|
|
215
386
|
Commands used to take and return **JSON text**; they now take and return
|
|
216
387
|
**values**, with the runtime handling serialisation. The page-side API
|
|
217
|
-
(`
|
|
388
|
+
(`invoke` / `listen`) is unchanged.
|
|
218
389
|
|
|
219
390
|
```ts
|
|
220
391
|
// 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,102 @@
|
|
|
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
|
+
import type { CommandShapes, Commands, Events } from "../runtime/types";
|
|
10
|
+
|
|
11
|
+
/** Removes a subscription created by `listen` / `client.on`. */
|
|
12
|
+
export type Unlisten = () => void;
|
|
13
|
+
|
|
14
|
+
/** The bridge janela injects as `window.janela` before each document loads. */
|
|
15
|
+
export interface JanelaBridge {
|
|
16
|
+
invoke<T = unknown>(cmd: string, args?: unknown): Promise<T>;
|
|
17
|
+
listen<T = unknown>(event: string, cb: (payload: T) => void): Unlisten;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Call a command the host registered with `app.command` / `app.commandAsync`.
|
|
22
|
+
*
|
|
23
|
+
* ```ts
|
|
24
|
+
* const sum = await invoke<number>("add", { a: 2, b: 40 });
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* The generic is an assertion, not a check — prefer `createClient` when the
|
|
28
|
+
* host declares a contract, which verifies the name, the arguments and the
|
|
29
|
+
* result against the host's own types.
|
|
30
|
+
*
|
|
31
|
+
* Rejects if the command is unknown, if the handler rejected, or if the page
|
|
32
|
+
* is not running inside a janela window.
|
|
33
|
+
*/
|
|
34
|
+
export declare function invoke<T = unknown>(cmd: string, args?: unknown): Promise<T>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Subscribe to an event the host sends with `app.emit`. Returns a disposer.
|
|
38
|
+
*
|
|
39
|
+
* ```ts
|
|
40
|
+
* const off = listen<number>("added", (sum) => console.log(sum));
|
|
41
|
+
* off();
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare function listen<T = unknown>(
|
|
45
|
+
event: string,
|
|
46
|
+
cb: (payload: T) => void,
|
|
47
|
+
): Unlisten;
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Typed contract
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
/** Anything shaped like a host contract module's exported `App` type. */
|
|
54
|
+
export interface Contract {
|
|
55
|
+
commands: Commands<CommandShapes>;
|
|
56
|
+
events: Events<unknown>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The command table declared by a contract. */
|
|
60
|
+
export type CommandsOf<A> = A extends { commands: Commands<infer M> } ? M : never;
|
|
61
|
+
|
|
62
|
+
/** The event table declared by a contract. */
|
|
63
|
+
export type EventsOf<A> = A extends { events: Events<infer E> } ? E : never;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A client bound to a host contract: command names, argument shapes, result
|
|
67
|
+
* types and event payloads are all checked against the host's declarations.
|
|
68
|
+
*/
|
|
69
|
+
export interface JanelaClient<A> {
|
|
70
|
+
/**
|
|
71
|
+
* Call a declared command. Unknown names and wrong argument shapes are
|
|
72
|
+
* compile errors, and the result type comes from the contract.
|
|
73
|
+
*/
|
|
74
|
+
invoke<K extends keyof CommandsOf<A> & string>(
|
|
75
|
+
name: K,
|
|
76
|
+
args: CommandsOf<A>[K]["args"],
|
|
77
|
+
): Promise<CommandsOf<A>[K]["result"]>;
|
|
78
|
+
/**
|
|
79
|
+
* Subscribe to a declared event; the payload type is inferred. Returns a
|
|
80
|
+
* disposer that removes the subscription.
|
|
81
|
+
*/
|
|
82
|
+
on<K extends keyof EventsOf<A> & string>(
|
|
83
|
+
event: K,
|
|
84
|
+
cb: (payload: EventsOf<A>[K]) => void,
|
|
85
|
+
): Unlisten;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Build a client checked against a host's contract.
|
|
90
|
+
*
|
|
91
|
+
* ```ts
|
|
92
|
+
* import type { App } from "../src-host/main";
|
|
93
|
+
* const client = createClient<App>();
|
|
94
|
+
* const sum = await client.invoke("add", { a: 2, b: 40 }); // number
|
|
95
|
+
* const off = client.on("added", (v) => console.log(v)); // v: number
|
|
96
|
+
* ```
|
|
97
|
+
*
|
|
98
|
+
* `import type` is erased, so no host code is bundled into the page — the
|
|
99
|
+
* contract is a type edge and nothing else. Payloads still cross as JSON, so
|
|
100
|
+
* this is compile-time safety; nothing is validated at runtime.
|
|
101
|
+
*/
|
|
102
|
+
export declare function createClient<A>(): JanelaClient<A>;
|
package/api/index.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
/**
|
|
33
|
+
* Subscribe to an event the host sends with `app.emit`. Returns a function
|
|
34
|
+
* that removes the subscription.
|
|
35
|
+
*/
|
|
36
|
+
export function listen(event, cb) {
|
|
37
|
+
const off = bridge().listen(event, cb);
|
|
38
|
+
// Hosts before 0.5.0 returned nothing from listen; keep a working disposer
|
|
39
|
+
// either way rather than handing back undefined.
|
|
40
|
+
if (typeof off === "function") return off;
|
|
41
|
+
return function unlisten() {
|
|
42
|
+
const all = globalThis.__wvListeners ? globalThis.__wvListeners[event] : undefined;
|
|
43
|
+
if (!all) return;
|
|
44
|
+
const i = all.indexOf(cb);
|
|
45
|
+
if (i >= 0) all.splice(i, 1);
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A client bound to a host's declared contract. The type parameter carries the
|
|
51
|
+
* command and event tables; at runtime this is the same bridge as `invoke` and
|
|
52
|
+
* `listen`, so the contract costs nothing and validates nothing — it is
|
|
53
|
+
* checked entirely by the compiler.
|
|
54
|
+
*/
|
|
55
|
+
export function createClient() {
|
|
56
|
+
return {
|
|
57
|
+
invoke(name, args) {
|
|
58
|
+
return invoke(name, args);
|
|
59
|
+
},
|
|
60
|
+
on(event, cb) {
|
|
61
|
+
return listen(event, cb);
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
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.5.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
|
@@ -67,6 +67,11 @@ const BOOTSTRAP =
|
|
|
67
67
|
" listen: function (event, cb) {" +
|
|
68
68
|
" if (!window.__wvListeners[event]) window.__wvListeners[event] = [];" +
|
|
69
69
|
" window.__wvListeners[event].push(cb);" +
|
|
70
|
+
" return function () {" +
|
|
71
|
+
" var a = window.__wvListeners[event] || [];" +
|
|
72
|
+
" var i = a.indexOf(cb);" +
|
|
73
|
+
" if (i >= 0) a.splice(i, 1);" +
|
|
74
|
+
" };" +
|
|
70
75
|
" }," +
|
|
71
76
|
"};" +
|
|
72
77
|
"window.__wvEmit = function (event, payload) {" +
|
|
@@ -74,124 +79,39 @@ const BOOTSTRAP =
|
|
|
74
79
|
" for (var i = 0; i < cbs.length; i++) cbs[i](payload);" +
|
|
75
80
|
"};";
|
|
76
81
|
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
}
|
|
82
|
+
// The public host types live in ./types (shipped as `janela/host` too, so a
|
|
83
|
+
// user's editor can see them). Re-exported here because the compiled build
|
|
84
|
+
// resolves them through this module — see the specifier rewrite in the CLI.
|
|
85
|
+
export type {
|
|
86
|
+
AsyncCommandHandler,
|
|
87
|
+
CommandHandler,
|
|
88
|
+
CommandShape,
|
|
89
|
+
CommandShapes,
|
|
90
|
+
Commands,
|
|
91
|
+
DialogFilter,
|
|
92
|
+
Events,
|
|
93
|
+
FsCallback,
|
|
94
|
+
JanelaApp,
|
|
95
|
+
OpenDialogOptions,
|
|
96
|
+
SaveDialogOptions,
|
|
97
|
+
WindowConfig,
|
|
98
|
+
} from "./types";
|
|
99
|
+
|
|
100
|
+
// The typed-contract helpers are values, so they are re-exported as values.
|
|
101
|
+
// A project's `import { defineCommands } from "janela/host"` is rewritten to
|
|
102
|
+
// this module by the CLI before scriptc sees it.
|
|
103
|
+
export { defineCommands, defineEvents, emit, on, onAsync } from "./types";
|
|
104
|
+
|
|
105
|
+
import type {
|
|
106
|
+
AsyncCommandHandler,
|
|
107
|
+
CommandHandler,
|
|
108
|
+
DialogFilter,
|
|
109
|
+
FsCallback,
|
|
110
|
+
JanelaApp,
|
|
111
|
+
OpenDialogOptions,
|
|
112
|
+
SaveDialogOptions,
|
|
113
|
+
WindowConfig,
|
|
114
|
+
} from "./types";
|
|
195
115
|
|
|
196
116
|
// JSON.stringify yields undefined for undefined; the wire always needs a
|
|
197
117
|
// value, and a command that returns nothing should read as null in the page.
|