janela 0.7.0 → 0.8.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 CHANGED
@@ -141,9 +141,10 @@ framework templates' default.
141
141
  import type { JanelaApp } from "janela/host";
142
142
 
143
143
  export type AppCommands = {
144
- add: { args: { a: number; b: number }; result: number };
145
- greet: { args: { name: string }; result: string };
146
- wait: { args: { ms: number }; result: string };
144
+ add: (args: { a: number; b: number }) => number;
145
+ greet: (args: { name: string }) => string;
146
+ wait: (args: { ms: number }) => string;
147
+ quit: () => void; // a command that takes nothing
147
148
  };
148
149
  export type AppEvents = { added: number };
149
150
 
@@ -195,11 +196,23 @@ Two things worth knowing:
195
196
  own declarations rather than from an assertion you write by hand, which is
196
197
  only possible because both sides are TypeScript.
197
198
 
198
- Write `args: null` for a command that takes nothing `args: undefined`
199
- lowers to a zero-parameter function and fails to compile.
199
+ A command that takes nothing is declared `() => void`, and its handler
200
+ returns `null` every command answers the page's promise with a value, so
201
+ `void` is normalised to `null`. The page calls it as
202
+ `client.invoke("quit", null)`.
200
203
 
201
- The untyped `invoke` / `listen` still work unchanged; the contract is
202
- additive, and the `vanilla` template still uses the global.
204
+ An **event payload is a single value** of any JSON-shaped type. For an event
205
+ carrying several things, prefer an object (`{ done: number; total: number }`):
206
+ adding a field later does not break existing listeners, and the names read
207
+ better at the call site. A tuple works too, but note that only the payload
208
+ *value* may be a tuple — the varargs spelling `app.emit("progress", 3, 10)`
209
+ does not compile (`SC2011: values of type '[done: number, total: number]'
210
+ have no static representation`, which would require `--dynamic` and ~620 KB
211
+ of embedded engine).
212
+
213
+ The `{ args; result }` record form of 0.5.x–0.7.x is still accepted, and the
214
+ untyped `invoke` / `listen` still work unchanged; the contract is additive,
215
+ and the `vanilla` template still uses the global.
203
216
 
204
217
  ## Async commands
205
218
 
@@ -310,6 +323,30 @@ nested modal loop would otherwise re-enter the host loop underneath a live TS
310
323
  frame; [docs/native-shell.md](../../docs/native-shell.md) has the details, the
311
324
  per-platform table, and the Windows GUI-subsystem note.
312
325
 
326
+ ## Migrating from 0.7.x
327
+
328
+ Commands are declared as the functions they are. The old `{ args; result }`
329
+ record form still compiles, so this is optional — but the function form is
330
+ shorter, and a command that takes nothing is finally natural to write.
331
+
332
+ ```ts
333
+ // 0.7.x
334
+ export type AppCommands = {
335
+ add: { args: { a: number; b: number }; result: number };
336
+ quit: { args: null; result: null };
337
+ };
338
+
339
+ // 0.8.0
340
+ export type AppCommands = {
341
+ add: (args: { a: number; b: number }) => number;
342
+ quit: () => void;
343
+ };
344
+ ```
345
+
346
+ Nothing else changes: `export type App = JanelaApp<AppCommands, AppEvents>`,
347
+ `setup(app: App)`, `app.command(...)` and the page's `createClient<App>()` are
348
+ all as they were. A handler for a `() => void` command returns `null`.
349
+
313
350
  ## Migrating from 0.6.x
314
351
 
315
352
  The contract lives entirely in the types now, so the runtime tokens are gone.
package/api/index.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * parse.
7
7
  */
8
8
 
9
- import type { JanelaApp } from "../runtime/janela";
9
+ import type { JanelaAppImpl } from "../runtime/janela";
10
10
  import type { CommandShapes, Commands, Events } from "../runtime/types";
11
11
 
12
12
  /** Removes a subscription created by `listen` / `client.on`. */
@@ -65,18 +65,22 @@ export interface Contract {
65
65
  /**
66
66
  * The command table declared by a contract.
67
67
  *
68
- * Reads the contract off a `JanelaApp<C, E>` — what a host's `App` type is
69
- * from 0.7.0 — and falls back to the 0.5.x/0.6.x `{ commands, events }`
70
- * wrapper, so a project written against either shape keeps checking.
68
+ * Reads the contract off the app type — what a host's `App` is from 0.7.0 —
69
+ * and falls back to the 0.5.x/0.6.x `{ commands, events }` wrapper, so a
70
+ * project written against either shape keeps checking.
71
+ *
72
+ * The inference targets the class rather than the `JanelaApp` alias: the
73
+ * alias applies `Norm<C>`, which cannot be inferred backwards, and the
74
+ * normalised table is what indexing wants in any case.
71
75
  */
72
- export type CommandsOf<A> = A extends JanelaApp<infer M, infer _E>
76
+ export type CommandsOf<A> = A extends JanelaAppImpl<infer M, infer _E>
73
77
  ? M
74
78
  : A extends { commands: Commands<infer M> }
75
79
  ? M
76
80
  : never;
77
81
 
78
82
  /** The event table declared by a contract; see CommandsOf for the two shapes. */
79
- export type EventsOf<A> = A extends JanelaApp<infer _M, infer E>
83
+ export type EventsOf<A> = A extends JanelaAppImpl<infer _M, infer E>
80
84
  ? E
81
85
  : A extends { events: Events<infer E> }
82
86
  ? E
package/bin/janela.mjs CHANGED
@@ -516,17 +516,19 @@ function build(root, { devUrl = null, gui = true } = {}) {
516
516
  join(buildDir, "entry.ts"),
517
517
  `// Generated by janela — do not edit.\n` +
518
518
  `import { createApp } from "./janela";\n` +
519
- `import type { CommandShapes, JanelaApp } from "./janela";\n` +
519
+ `import type { CommandShapes, JanelaAppImpl } from "./janela";\n` +
520
520
  `import { WINDOW } from "./config";\n` +
521
521
  `import { INDEX_HTML } from "./frontend";\n` +
522
522
  `import { setup } from "./main";\n\n` +
523
523
  `// The app's type parameters are read back off setup()'s own signature,\n` +
524
- `// so a contract-typed setup(app: JanelaApp<App>) and a plain\n` +
525
- `// setup(app: JanelaApp) each get an app instantiated to match. scriptc\n` +
526
- `// monomorphises generic classes, so the right instantiation must be\n` +
527
- `// CONSTRUCTED here - no cast can bridge JanelaApp<A> and JanelaApp<B>.\n` +
528
- `type CmdsOf<F> = F extends (app: JanelaApp<infer C, infer _E>) => void ? C : CommandShapes;\n` +
529
- `type EvtsOf<F> = F extends (app: JanelaApp<infer _C, infer E>) => void ? E : Record<string, unknown>;\n\n` +
524
+ `// so a contract-typed setup(app: App) and a plain setup(app: JanelaApp)\n` +
525
+ `// each get an app instantiated to match. scriptc monomorphises generic\n` +
526
+ `// classes, so the right instantiation must be CONSTRUCTED here - no cast\n` +
527
+ `// can bridge two of them. The inference reads the CLASS, not the\n` +
528
+ `// JanelaApp alias: the alias applies Norm<C>, which cannot be reversed,\n` +
529
+ `// and what is wanted here is the normalised table anyway.\n` +
530
+ `type CmdsOf<F> = F extends (app: JanelaAppImpl<infer C, infer _E>) => void ? C : CommandShapes;\n` +
531
+ `type EvtsOf<F> = F extends (app: JanelaAppImpl<infer _C, infer E>) => void ? E : Record<string, unknown>;\n\n` +
530
532
  `const app = createApp<CmdsOf<typeof setup>, EvtsOf<typeof setup>>(WINDOW);\n` +
531
533
  `setup(app);\n` +
532
534
  `const rc = app.run(INDEX_HTML) + 0;\n` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "janela",
3
- "version": "0.7.0",
3
+ "version": "0.8.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": {
package/runtime/janela.ts CHANGED
@@ -83,11 +83,16 @@ const BOOTSTRAP =
83
83
  // user's editor can see them). Re-exported here because the compiled build
84
84
  // resolves them through this module — see the specifier rewrite in the CLI.
85
85
  export type {
86
+ ArgsOf,
86
87
  AsyncCommandHandler,
87
88
  CommandHandler,
88
89
  CommandShape,
89
90
  CommandShapes,
91
+ CommandSpec,
92
+ CommandSpecs,
90
93
  Commands,
94
+ Norm,
95
+ ResultOf,
91
96
  DialogFilter,
92
97
  Events,
93
98
  FsCallback,
@@ -105,7 +110,9 @@ import type {
105
110
  AsyncCommandHandler,
106
111
  CommandHandler,
107
112
  CommandShapes,
113
+ CommandSpecs,
108
114
  Commands,
115
+ Norm,
109
116
  DialogFilter,
110
117
  Events,
111
118
  FsCallback,
@@ -156,7 +163,7 @@ const TICK_DRAIN_MS = 4;
156
163
  * interface (being signature-only) never is. A class receiver works even as a
157
164
  * plain function parameter, which is what `setup(app)` is.
158
165
  */
159
- export class JanelaApp<
166
+ export class JanelaAppImpl<
160
167
  C extends CommandShapes = CommandShapes,
161
168
  E = Record<string, unknown>,
162
169
  > {
@@ -571,11 +578,34 @@ export class JanelaApp<
571
578
  }
572
579
  }
573
580
 
581
+ /**
582
+ * A running janela app, typed by the contract it serves.
583
+ *
584
+ * This is an alias rather than the class itself so that a contract may be
585
+ * written as plain function types: `Norm` converts it to the record form the
586
+ * class indexes, at a point where the table is still concrete. Writing the
587
+ * record form directly keeps working — `Norm` is idempotent.
588
+ *
589
+ * ```ts
590
+ * export type AppCommands = { add: (args: { a: number; b: number }) => number };
591
+ * export type AppEvents = { added: number };
592
+ * export type App = JanelaApp<AppCommands, AppEvents>;
593
+ *
594
+ * export function setup(app: App): void {
595
+ * app.command("add", (args) => args.a + args.b); // args inferred, result checked
596
+ * }
597
+ * ```
598
+ */
599
+ export type JanelaApp<
600
+ C extends CommandSpecs = CommandShapes,
601
+ E = Record<string, unknown>,
602
+ > = JanelaAppImpl<Norm<C>, E>;
603
+
574
604
  export function createApp<
575
605
  C extends CommandShapes = CommandShapes,
576
606
  E = Record<string, unknown>,
577
- >(cfg: WindowConfig): JanelaApp<C, E> {
578
- return new JanelaApp<C, E>(cfg);
607
+ >(cfg: WindowConfig): JanelaAppImpl<C, E> {
608
+ return new JanelaAppImpl<C, E>(cfg);
579
609
  }
580
610
 
581
611
  // ---------------------------------------------------------------------------
@@ -586,7 +616,7 @@ export function createApp<
586
616
 
587
617
  /** @deprecated Use `app.command(name, handler)` on a contract-typed app. */
588
618
  export function on<M extends CommandShapes, K extends keyof M & string>(
589
- app: JanelaApp,
619
+ app: JanelaAppImpl,
590
620
  _commands: Commands<M>,
591
621
  name: K,
592
622
  handler: (args: M[K]["args"]) => M[K]["result"],
@@ -596,7 +626,7 @@ export function on<M extends CommandShapes, K extends keyof M & string>(
596
626
 
597
627
  /** @deprecated Use `app.commandAsync(name, handler)` on a contract-typed app. */
598
628
  export function onAsync<M extends CommandShapes, K extends keyof M & string>(
599
- app: JanelaApp,
629
+ app: JanelaAppImpl,
600
630
  _commands: Commands<M>,
601
631
  name: K,
602
632
  handler: (
@@ -615,7 +645,7 @@ export function onAsync<M extends CommandShapes, K extends keyof M & string>(
615
645
 
616
646
  /** @deprecated Use `app.emit(event, payload)` on a contract-typed app. */
617
647
  export function emit<E, K extends keyof E & string>(
618
- app: JanelaApp,
648
+ app: JanelaAppImpl,
619
649
  _events: Events<E>,
620
650
  name: K,
621
651
  payload: E[K],
package/runtime/types.ts CHANGED
@@ -90,15 +90,78 @@ export interface WindowConfig {
90
90
  // tokens and their `define*` constructors are the 0.5.x/0.6.x shape, kept so
91
91
  // projects written against it still compile.
92
92
 
93
- /** One command's argument and result types. */
93
+ /**
94
+ * One command's argument and result types, in normalised form.
95
+ *
96
+ * This is what the app class works with internally. A contract is *written*
97
+ * as plain function types — see CommandSpec — and normalised to this by
98
+ * `Norm` before it reaches the class.
99
+ */
94
100
  export interface CommandShape {
95
101
  args: unknown;
96
102
  result: unknown;
97
103
  }
98
104
 
99
- /** A contract's command table: name → shape. */
105
+ /** A normalised command table: name → shape. */
100
106
  export type CommandShapes = Record<string, CommandShape>;
101
107
 
108
+ /**
109
+ * How a command may be declared in a contract: as a plain function type
110
+ * (preferred), or as the `{ args; result }` record of 0.5.x–0.7.x.
111
+ *
112
+ * ```ts
113
+ * type AppCommands = {
114
+ * add: (args: { a: number; b: number }) => number;
115
+ * quit: () => void; // no arguments
116
+ * legacy: { args: { name: string }; result: string }; // still accepted
117
+ * };
118
+ * ```
119
+ */
120
+ export type CommandSpec = ((...args: never[]) => unknown) | CommandShape;
121
+
122
+ /** A contract's command table as written: name → spec. */
123
+ export type CommandSpecs = Record<string, CommandSpec>;
124
+
125
+ /**
126
+ * The argument type of a declared command. A function's single parameter, or
127
+ * a record's `args`. A command declared with no parameters takes `null` — the
128
+ * page's `invoke(name)` sends null, and nothing is lost.
129
+ */
130
+ export type ArgsOf<F> = F extends (...a: infer P) => unknown
131
+ ? P extends [infer A]
132
+ ? A
133
+ : null
134
+ : F extends { args: infer A }
135
+ ? A
136
+ : null;
137
+
138
+ /**
139
+ * The result type of a declared command. `void` is normalised to `null`:
140
+ * every command answers the page's promise with a value, and scriptc has no
141
+ * conversion from a void value to the `unknown` the handler table holds.
142
+ */
143
+ export type ResultOf<F> = F extends (...a: never[]) => infer R
144
+ ? [R] extends [void]
145
+ ? null
146
+ : R
147
+ : F extends { result: infer R }
148
+ ? R
149
+ : never;
150
+
151
+ /**
152
+ * Normalise a written contract to the record form the app class indexes.
153
+ *
154
+ * This runs where `C` is still concrete — in the `JanelaApp<C, E>` alias, one
155
+ * step before the class — on purpose. scriptc cannot compile a *value* whose
156
+ * type is an unresolved conditional or a mapped type indexed by a type
157
+ * parameter (`SC2001: values of type 'ArgsOf<C[K]>' cannot be compiled yet`),
158
+ * so the class body only ever sees plain indexed access on a record.
159
+ * Idempotent: normalising a record-form table returns it unchanged.
160
+ */
161
+ export type Norm<C> = {
162
+ [K in keyof C]: { args: ArgsOf<C[K]>; result: ResultOf<C[K]> };
163
+ };
164
+
102
165
  /**
103
166
  * A declared command contract. Carries `M` at the type level only — the value
104
167
  * is empty, and exists so that inference has something to read at a call site.
@@ -119,7 +182,7 @@ export interface Events<E> {
119
182
  * types and name the app itself:
120
183
  *
121
184
  * ```ts
122
- * export type AppCommands = { add: { args: { a: number; b: number }; result: number } };
185
+ * export type AppCommands = { add: (args: { a: number; b: number }) => number };
123
186
  * export type AppEvents = { added: number };
124
187
  * export type App = JanelaApp<AppCommands, AppEvents>;
125
188
  * export function setup(app: App): void { … }
@@ -9,19 +9,18 @@
9
9
  // Two gotchas inherited from scriptc:
10
10
  // - never use a bare FFI-backed call as a complete variable initializer;
11
11
  // wrap it in any expression (`+ 0`);
12
- // - write `args: null` for a command that takes nothing, not `undefined`
13
- // an `undefined` argument lowers to a zero-parameter function and fails
14
- // to compile.
12
+ // - a command that returns nothing is declared `() => void` and its handler
13
+ // returns `null`; every command answers the page's promise with a value.
15
14
 
16
15
  import type { JanelaApp } from "janela/host";
17
16
 
18
17
  /** Every command this app answers. Declared once; the page checks against it. */
19
18
  export type AppCommands = {
20
- add: { args: { a: number; b: number }; result: number };
21
- greet: { args: { name: string }; result: string };
22
- log: { args: string; result: null };
23
- wait: { args: { ms: number }; result: string };
24
- quit: { args: null; result: null };
19
+ add: (args: { a: number; b: number }) => number;
20
+ greet: (args: { name: string }) => string;
21
+ log: (args: string) => void;
22
+ wait: (args: { ms: number }) => string;
23
+ quit: () => void;
25
24
  };
26
25
 
27
26
  /** Every event this app emits, and what each one carries. */
@@ -9,19 +9,18 @@
9
9
  // Two gotchas inherited from scriptc:
10
10
  // - never use a bare FFI-backed call as a complete variable initializer;
11
11
  // wrap it in any expression (`+ 0`);
12
- // - write `args: null` for a command that takes nothing, not `undefined`
13
- // an `undefined` argument lowers to a zero-parameter function and fails
14
- // to compile.
12
+ // - a command that returns nothing is declared `() => void` and its handler
13
+ // returns `null`; every command answers the page's promise with a value.
15
14
 
16
15
  import type { JanelaApp } from "janela/host";
17
16
 
18
17
  /** Every command this app answers. Declared once; the page checks against it. */
19
18
  export type AppCommands = {
20
- add: { args: { a: number; b: number }; result: number };
21
- greet: { args: { name: string }; result: string };
22
- log: { args: string; result: null };
23
- wait: { args: { ms: number }; result: string };
24
- quit: { args: null; result: null };
19
+ add: (args: { a: number; b: number }) => number;
20
+ greet: (args: { name: string }) => string;
21
+ log: (args: string) => void;
22
+ wait: (args: { ms: number }) => string;
23
+ quit: () => void;
25
24
  };
26
25
 
27
26
  /** Every event this app emits, and what each one carries. */
@@ -9,19 +9,18 @@
9
9
  // Two gotchas inherited from scriptc:
10
10
  // - never use a bare FFI-backed call as a complete variable initializer;
11
11
  // wrap it in any expression (`+ 0`);
12
- // - write `args: null` for a command that takes nothing, not `undefined`
13
- // an `undefined` argument lowers to a zero-parameter function and fails
14
- // to compile.
12
+ // - a command that returns nothing is declared `() => void` and its handler
13
+ // returns `null`; every command answers the page's promise with a value.
15
14
 
16
15
  import type { JanelaApp } from "janela/host";
17
16
 
18
17
  /** Every command this app answers. Declared once; the page checks against it. */
19
18
  export type AppCommands = {
20
- add: { args: { a: number; b: number }; result: number };
21
- greet: { args: { name: string }; result: string };
22
- log: { args: string; result: null };
23
- wait: { args: { ms: number }; result: string };
24
- quit: { args: null; result: null };
19
+ add: (args: { a: number; b: number }) => number;
20
+ greet: (args: { name: string }) => string;
21
+ log: (args: string) => void;
22
+ wait: (args: { ms: number }) => string;
23
+ quit: () => void;
25
24
  };
26
25
 
27
26
  /** Every event this app emits, and what each one carries. */
@@ -9,19 +9,18 @@
9
9
  // Two gotchas inherited from scriptc:
10
10
  // - never use a bare FFI-backed call as a complete variable initializer;
11
11
  // wrap it in any expression (`+ 0`);
12
- // - write `args: null` for a command that takes nothing, not `undefined`
13
- // an `undefined` argument lowers to a zero-parameter function and fails
14
- // to compile.
12
+ // - a command that returns nothing is declared `() => void` and its handler
13
+ // returns `null`; every command answers the page's promise with a value.
15
14
 
16
15
  import type { JanelaApp } from "janela/host";
17
16
 
18
17
  /** Every command this app answers. Declared once; the page checks against it. */
19
18
  export type AppCommands = {
20
- add: { args: { a: number; b: number }; result: number };
21
- greet: { args: { name: string }; result: string };
22
- log: { args: string; result: null };
23
- wait: { args: { ms: number }; result: string };
24
- quit: { args: null; result: null };
19
+ add: (args: { a: number; b: number }) => number;
20
+ greet: (args: { name: string }) => string;
21
+ log: (args: string) => void;
22
+ wait: (args: { ms: number }) => string;
23
+ quit: () => void;
25
24
  };
26
25
 
27
26
  /** Every event this app emits, and what each one carries. */