janela 0.14.1 → 0.14.2

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 (32) hide show
  1. package/README.md +10 -2
  2. package/api/index.d.ts +11 -1
  3. package/bin/janela.mjs +18 -2
  4. package/package.json +1 -1
  5. package/templates/index.html +224 -53
  6. package/templates/janela.conf.json +2 -2
  7. package/templates/react/files/index.html +1 -0
  8. package/templates/react/files/janela.conf.json +2 -2
  9. package/templates/react/files/src/App.tsx +154 -37
  10. package/templates/react/files/src/main.tsx +1 -0
  11. package/templates/react/files/src/styles.css +137 -0
  12. package/templates/react/files/src-host/main.ts +82 -0
  13. package/templates/solid/files/index.html +1 -0
  14. package/templates/solid/files/janela.conf.json +2 -2
  15. package/templates/solid/files/src/App.tsx +145 -35
  16. package/templates/solid/files/src/main.tsx +1 -0
  17. package/templates/solid/files/src/styles.css +137 -0
  18. package/templates/solid/files/src-host/main.ts +82 -0
  19. package/templates/svelte/files/index.html +1 -0
  20. package/templates/svelte/files/janela.conf.json +2 -2
  21. package/templates/svelte/files/src/App.svelte +116 -27
  22. package/templates/svelte/files/src/main.ts +1 -0
  23. package/templates/svelte/files/src/styles.css +137 -0
  24. package/templates/svelte/files/src-host/main.ts +82 -0
  25. package/templates/vue/files/index.html +1 -0
  26. package/templates/vue/files/janela.conf.json +2 -2
  27. package/templates/vue/files/src/App.vue +99 -20
  28. package/templates/vue/files/src/main.ts +1 -0
  29. package/templates/vue/files/src/styles.css +137 -0
  30. package/templates/vue/files/src-host/main.ts +82 -0
  31. package/templates/react/files/src/App.css +0 -3
  32. package/templates/solid/files/src/App.css +0 -3
package/README.md CHANGED
@@ -6,9 +6,11 @@ Desktop and mobile apps in pure TypeScript, compiled to native. No Rust, no
6
6
  Node, no Electron. The backend is TypeScript compiled to a native binary by
7
7
  [scriptc](https://scriptc.dev); the window is the OS webview via
8
8
  [webview/webview](https://github.com/webview/webview). A desktop binary comes
9
- out around 200400 KB — 191 KB for the smallest template — with no bundled
9
+ out around 225420 KB — 225 KB for the smallest template — with no bundled
10
10
  browser and no bundled runtime; iOS and Android bundles land around
11
- 211415 KB. Per-template figures are in
11
+ 228421 KB. Those are the *starter's* figures, and about 35 KB of each is the
12
+ native file dialog and worker-thread reader it demonstrates: delete the "Files
13
+ and the window" card and its two commands and the smallest binary is 212 KB. Per-template figures are in
12
14
  [docs/frontend.md](../../docs/frontend.md).
13
15
 
14
16
  Five targets, one runtime — the same `main.ts`, the same typed contract and the
@@ -30,6 +32,7 @@ desktop-only for now; on mobile they report clearly when called.
30
32
 
31
33
  ```bash
32
34
  npm install -g janela # or: npx janela init my-app
35
+ janela --version # which janela you actually have
33
36
  janela init my-app # (or `jn init my-app`)
34
37
  cd my-app
35
38
  janela dev # build + run with logs in the terminal
@@ -44,6 +47,11 @@ cd my-app && npm install
44
47
  janela dev # Vite dev server + HMR, in a native window
45
48
  ```
46
49
 
50
+ A `--template` a globally installed CLI has never heard of is now an error
51
+ rather than a silent fallback, and `janela --version` says which one you are
52
+ running — worth checking first if a scaffolded project comes out looking
53
+ nothing like the screenshots.
54
+
47
55
  `vanilla` is the default and needs no frontend toolchain at all. With a
48
56
  framework, `janela dev` runs your Vite dev server and points the window at it,
49
57
  and `janela build` flattens the production bundle into the binary.
package/api/index.d.ts CHANGED
@@ -94,10 +94,20 @@ export interface JanelaClient<A> {
94
94
  /**
95
95
  * Call a declared command. Unknown names and wrong argument shapes are
96
96
  * compile errors, and the result type comes from the contract.
97
+ *
98
+ * A command declared to take nothing — `quit: () => void` — normalises to
99
+ * `args: null`, and for those the argument is omitted rather than passed as
100
+ * an explicit `null`. That is what the rest-tuple is for: it makes the
101
+ * parameter optional for exactly those commands, and required with the
102
+ * declared shape for every other one. The bracketed `[X] extends [null]`
103
+ * form is deliberate — a bare `null extends X` would also match a command
104
+ * whose arguments are legitimately nullable.
97
105
  */
98
106
  invoke<K extends keyof CommandsOf<A> & string>(
99
107
  name: K,
100
- args: CommandsOf<A>[K]["args"],
108
+ ...args: [CommandsOf<A>[K]["args"]] extends [null]
109
+ ? [args?: null]
110
+ : [args: CommandsOf<A>[K]["args"]]
101
111
  ): Promise<CommandsOf<A>[K]["result"]>;
102
112
  /**
103
113
  * Subscribe to a declared event; the payload type is inferred. Returns a
package/bin/janela.mjs CHANGED
@@ -4,6 +4,7 @@
4
4
  // janela init <name> scaffold a new project
5
5
  // janela build compile the project to a native binary (+ .app on macOS)
6
6
  // janela dev build, then run the binary with logs in the terminal
7
+ // janela --version which janela this is
7
8
  //
8
9
  // A project is: index.html (frontend), src-host/main.ts (commands),
9
10
  // janela.conf.json (window + bundle config). Everything else — the C shim over
@@ -26,6 +27,9 @@ import {
26
27
 
27
28
  const KIT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
28
29
  const require = createRequire(join(KIT, "package.json"));
30
+ // createRequire is rooted AT the package.json, so "./package.json" is this
31
+ // package's own — not the bin/ directory's and not the workspace's.
32
+ const VERSION = require("./package.json").version;
29
33
 
30
34
  function fail(msg) {
31
35
  console.error(`janela: ${msg}`);
@@ -1277,6 +1281,16 @@ async function dev(root) {
1277
1281
  const argv = process.argv.slice(2);
1278
1282
  const cmd = argv[0];
1279
1283
 
1284
+ // Answered before anything else, because the question it settles is "is the
1285
+ // janela running this command the one I think it is?". A globally installed
1286
+ // CLI several minor versions behind accepts `init` and quietly ignores flags it
1287
+ // has never heard of — `--template` did not exist before 0.3.0 — so the only
1288
+ // symptom is a project that came out looking wrong.
1289
+ if (cmd === "--version" || cmd === "-v") {
1290
+ console.log(VERSION);
1291
+ process.exit(0);
1292
+ }
1293
+
1280
1294
  function flag(name, fallback) {
1281
1295
  const eq = argv.find((a) => a.startsWith(`--${name}=`));
1282
1296
  if (eq) return eq.slice(name.length + 3);
@@ -1356,9 +1370,11 @@ switch (cmd) {
1356
1370
  break;
1357
1371
  default:
1358
1372
  console.log(
1359
- "usage: janela init <name> [--template vanilla|vue|react|svelte|solid]\n" +
1373
+ `janela ${VERSION}\n\n` +
1374
+ "usage: janela init <name> [--template vanilla|vue|react|svelte|solid]\n" +
1360
1375
  " janela build [--target desktop|ios|android]\n" +
1361
- " janela dev [--target desktop|ios|android]",
1376
+ " janela dev [--target desktop|ios|android]\n" +
1377
+ " janela --version",
1362
1378
  );
1363
1379
  process.exit(cmd ? 1 : 0);
1364
1380
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "janela",
3
- "version": "0.14.1",
3
+ "version": "0.14.2",
4
4
  "description": "Desktop, iOS and Android apps in pure TypeScript, compiled to native. No Rust, no Node, no Electron.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,33 +1,204 @@
1
1
  <!doctype html>
2
- <html>
2
+ <html lang="en">
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>__NAME__</title>
5
7
  <style>
6
- body { font: 16px -apple-system, sans-serif; padding: 2rem; }
7
- input[type="number"] { width: 5em; }
8
- #events { color: #666; font-size: 13px; }
8
+ /* Everything here is yours to replace. It is deliberately one file with
9
+ no dependencies: this template has no bundler, so what you see is what
10
+ runs. The mark below is drawn in CSS rather than shipped as an image —
11
+ a janela frontend is embedded in the binary, so every asset has to be
12
+ inlined anyway, and a window needs no artwork. */
13
+ :root {
14
+ color-scheme: light dark;
15
+ --bg: #f6f6f7;
16
+ --card: #ffffff;
17
+ --text: #18181b;
18
+ --muted: #71717a;
19
+ --line: #e4e4e7;
20
+ --accent: #2563eb;
21
+ --accent-soft: #eff4ff;
22
+ --shadow: 0 1px 2px rgb(0 0 0 / 0.04), 0 8px 24px -12px rgb(0 0 0 / 0.12);
23
+ }
24
+ @media (prefers-color-scheme: dark) {
25
+ :root {
26
+ --bg: #17171a;
27
+ --card: #202024;
28
+ --text: #fafafa;
29
+ --muted: #a1a1aa;
30
+ --line: #2e2e34;
31
+ --accent: #60a5fa;
32
+ --accent-soft: #1d2433;
33
+ --shadow: 0 1px 2px rgb(0 0 0 / 0.3), 0 8px 24px -12px rgb(0 0 0 / 0.5);
34
+ }
35
+ }
36
+
37
+ * { box-sizing: border-box; }
38
+ body {
39
+ margin: 0;
40
+ min-height: 100vh;
41
+ display: flex;
42
+ justify-content: center;
43
+ background: var(--bg);
44
+ color: var(--text);
45
+ font: 15px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
46
+ -webkit-font-smoothing: antialiased;
47
+ }
48
+ main { width: 100%; max-width: 760px; padding: 26px 22px 32px; }
49
+
50
+ /* --- header ------------------------------------------------------- */
51
+ header { text-align: center; margin-bottom: 20px; }
52
+ .mark {
53
+ width: 56px; height: 44px; margin: 0 auto 14px;
54
+ border: 2px solid var(--text); border-radius: 8px;
55
+ display: flex; flex-direction: column; overflow: hidden;
56
+ }
57
+ .mark .bar {
58
+ height: 13px; flex: none;
59
+ border-bottom: 2px solid var(--text);
60
+ display: flex; align-items: center; gap: 3px; padding: 0 4px;
61
+ }
62
+ .mark .bar i { width: 3px; height: 3px; border-radius: 50%; background: var(--text); }
63
+ .mark .pane { flex: 1; background: linear-gradient(135deg, var(--accent), transparent 70%); opacity: 0.85; }
64
+ h1 { margin: 0; font-size: 26px; letter-spacing: -0.02em; font-weight: 650; }
65
+ .sub { margin: 5px 0 0; color: var(--muted); font-size: 13.5px; }
66
+
67
+ /* The one line that proves the whole stack: this string was produced by
68
+ TypeScript compiled to machine code, and handed to the page. */
69
+ .greeting {
70
+ margin: 0 0 20px;
71
+ padding: 11px 16px;
72
+ border: 1px solid var(--line);
73
+ border-radius: 10px;
74
+ background: var(--accent-soft);
75
+ font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
76
+ text-align: center;
77
+ overflow-wrap: anywhere;
78
+ }
79
+
80
+ /* --- cards -------------------------------------------------------- */
81
+ /* auto-fit rather than a fixed column count: the same page is the iOS and
82
+ Android frontend, where it must read as a single column. */
83
+ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 13px; }
84
+ .card {
85
+ background: var(--card);
86
+ border: 1px solid var(--line);
87
+ border-radius: 12px;
88
+ padding: 15px 17px;
89
+ box-shadow: var(--shadow);
90
+ }
91
+ .card h2 {
92
+ margin: 0 0 2px;
93
+ font-size: 13px; font-weight: 600;
94
+ letter-spacing: 0.04em; text-transform: uppercase;
95
+ color: var(--muted);
96
+ }
97
+ .card.wide { grid-column: 1 / -1; }
98
+ .card p.why { margin: 0 0 11px; color: var(--muted); font-size: 12.5px; line-height: 1.45; }
99
+ .card h3 {
100
+ margin: 16px 0 6px; font-size: 11px; font-weight: 600;
101
+ letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted);
102
+ }
103
+ .row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
104
+
105
+ input, button {
106
+ font: inherit;
107
+ border-radius: 8px;
108
+ border: 1px solid var(--line);
109
+ padding: 8px 12px;
110
+ background: var(--card);
111
+ color: var(--text);
112
+ transition: border-color 0.15s, background 0.15s;
113
+ }
114
+ input { min-width: 0; }
115
+ input[type="number"] { width: 5.5em; text-align: center; }
116
+ input.grow { flex: 1 1 12em; }
117
+ input:focus-visible, button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
118
+ button { cursor: pointer; font-weight: 500; }
119
+ button:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
120
+ button:disabled { opacity: 0.5; cursor: default; }
121
+ button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
122
+ button.primary:hover:not(:disabled) { filter: brightness(1.08); color: #fff; }
123
+
124
+ .result { margin: 12px 0 0; font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--muted); }
125
+ .result.filled { color: var(--text); }
126
+ pre.result { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 11em; overflow: auto; margin-top: 12px; }
127
+
128
+ /* A dot that keeps moving while a 2s command is pending. If it stalls,
129
+ the host has blocked the UI thread — which is the bug this proves is
130
+ absent. */
131
+ .pulse { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); animation: pulse 1s ease-in-out infinite; }
132
+ @keyframes pulse { 0%, 100% { opacity: 0.25; transform: scale(0.8); } 50% { opacity: 1; transform: scale(1.15); } }
133
+ @media (prefers-reduced-motion: reduce) { .pulse { animation: none; opacity: 1; } }
134
+
135
+ ul.events { list-style: none; margin: 0; padding: 0; font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }
136
+ ul.events li { padding: 3px 0; border-bottom: 1px dashed var(--line); }
137
+ ul.events li:last-child { border-bottom: 0; }
138
+ ul.events:empty::after { content: "nothing yet — press add"; color: var(--muted); font-family: inherit; }
139
+
140
+ footer { margin-top: 18px; display: flex; flex-wrap: wrap; gap: 10px; align-items: center; justify-content: center; color: var(--muted); font-size: 13px; }
141
+ footer code { background: var(--card); border: 1px solid var(--line); border-radius: 5px; padding: 1px 5px; font-size: 12px; }
142
+
9
143
  </style>
10
144
  </head>
11
145
  <body>
12
- <h1>__NAME__</h1>
13
- <p>
14
- <input id="a" type="number" value="2" /> +
15
- <input id="b" type="number" value="40" />
16
- <button id="add">add</button>
17
- <button id="wait">wait 2s (async)</button>
18
- <button id="quit">Quit</button>
19
- </p>
20
- <p>
21
- <input id="path" type="text" value="janela.conf.json" size="32" />
22
- <button id="read">read file (async)</button>
23
- <button id="open">pick a file…</button>
24
- </p>
25
- <p>
26
- <input id="title" type="text" value="__NAME__ — renamed" size="24" />
27
- <button id="retitle">set window title</button>
28
- </p>
29
- <pre id="out">booting…</pre>
30
- <ul id="events"></ul>
146
+ <main>
147
+ <header>
148
+ <div class="mark" aria-hidden="true">
149
+ <div class="bar"><i></i><i></i><i></i></div>
150
+ <div class="pane"></div>
151
+ </div>
152
+ <h1>__NAME__</h1>
153
+ <p class="sub">TypeScript, compiled to a native binary. No Rust, no Node, no Electron.</p>
154
+ </header>
155
+
156
+ <p class="greeting" id="out">booting…</p>
157
+
158
+ <div class="grid">
159
+ <section class="card">
160
+ <h2>Typed commands</h2>
161
+ <p class="why">The name, the arguments and the result are all checked at compile time — no codegen, because both sides are TypeScript.</p>
162
+ <div class="row">
163
+ <input id="a" type="number" value="2" aria-label="first number" />
164
+ <span aria-hidden="true">+</span>
165
+ <input id="b" type="number" value="40" aria-label="second number" />
166
+ <button id="add" class="primary">add</button>
167
+ </div>
168
+ <p class="result" id="sum">—</p>
169
+ <h3>Events from the host</h3>
170
+ <ul class="events" id="events"></ul>
171
+ </section>
172
+
173
+ <section class="card">
174
+ <h2>Async without blocking</h2>
175
+ <p class="why">The window keeps answering while this is pending: the dot keeps moving and <strong>add</strong> still works.</p>
176
+ <div class="row">
177
+ <button id="wait">wait 2s</button>
178
+ <span id="spinner" hidden><span class="pulse"></span></span>
179
+ </div>
180
+ <p class="result" id="waited">—</p>
181
+ </section>
182
+
183
+ <section class="card wide">
184
+ <h2>Files and the window</h2>
185
+ <p class="why">Reads run on a worker thread, so a large file never freezes the window. The dialog is the real native one.</p>
186
+ <div class="row">
187
+ <input id="path" class="grow" type="text" value="janela.conf.json" aria-label="file to read" />
188
+ <button id="read">read</button>
189
+ <button id="open">pick a file…</button>
190
+ <input id="title" class="grow" type="text" value="__NAME__ — renamed" aria-label="window title" />
191
+ <button id="retitle">set title</button>
192
+ </div>
193
+ <pre class="result" id="file">—</pre>
194
+ </section>
195
+ </div>
196
+
197
+ <footer>
198
+ <span>Edit <code>index.html</code> for the page and <code>src-host/main.ts</code> for the commands.</span>
199
+ <button id="quit">Quit</button>
200
+ </footer>
201
+ </main>
31
202
 
32
203
  <script>
33
204
  // This template has no bundler, so it uses the `janela` global that the
@@ -35,56 +206,56 @@
35
206
  // `import { invoke, listen } from "janela/api"` instead — same functions,
36
207
  // but typed and resolvable by the bundler.
37
208
  window.onload = async () => {
38
- const out = document.getElementById("out");
39
- const events = document.getElementById("events");
209
+ const $ = (id) => document.getElementById(id);
210
+ const show = (el, text) => {
211
+ el.textContent = text;
212
+ el.classList.add("filled");
213
+ };
40
214
 
41
215
  janela.listen("added", (sum) => {
42
216
  const li = document.createElement("li");
43
- li.textContent = "event 'added' from host: " + sum;
44
- events.prepend(li);
217
+ li.textContent = "host emitted 'added': " + sum;
218
+ $("events").prepend(li);
45
219
  });
46
220
 
47
- out.textContent = await janela.invoke("greet", { name: "__NAME__" });
221
+ // The greeting is a round trip: the string is built in compiled
222
+ // TypeScript and returned to the page.
223
+ $("out").textContent = await janela.invoke("greet", { name: "__NAME__" });
48
224
  await janela.invoke("log", "page loaded");
49
225
 
50
- document.getElementById("add").onclick = async () => {
51
- const a = Number(document.getElementById("a").value);
52
- const b = Number(document.getElementById("b").value);
53
- const sum = await janela.invoke("add", { a, b });
54
- out.textContent = `add(${a}, ${b}) -> ${sum}`;
226
+ $("add").onclick = async () => {
227
+ const a = Number($("a").value);
228
+ const b = Number($("b").value);
229
+ show($("sum"), `add(${a}, ${b}) → ${await janela.invoke("add", { a, b })}`);
55
230
  };
56
- // The window stays responsive while this is pending: the spinner keeps
57
- // animating and "add" still works.
58
- document.getElementById("wait").onclick = async (e) => {
231
+
232
+ $("wait").onclick = async (e) => {
59
233
  e.target.disabled = true;
60
- out.textContent = "waiting… (try 'add' — it still answers)";
61
- out.textContent = await janela.invoke("wait", { ms: 2000 });
234
+ $("spinner").hidden = false;
235
+ show($("waited"), "waiting… try add, it still answers");
236
+ const answer = await janela.invoke("wait", { ms: 2000 });
237
+ show($("waited"), answer);
238
+ $("spinner").hidden = true;
62
239
  e.target.disabled = false;
63
240
  };
64
241
 
65
- // Reads run on a worker thread in the shim, so even a large file
66
- // does not freeze the window while it loads.
67
- document.getElementById("read").onclick = async () => {
68
- const path = document.getElementById("path").value;
242
+ $("read").onclick = async () => {
243
+ const path = $("path").value;
69
244
  const r = await janela.invoke("readFile", { path });
70
- out.textContent = r.ok
71
- ? `${path}: ${r.length} chars\n\n` + r.text.slice(0, 400)
72
- : "error: " + r.error;
245
+ show($("file"), r.ok ? `${path} — ${r.length} chars\n\n${r.text.slice(0, 400)}` : "error: " + r.error);
73
246
  };
74
247
 
75
248
  // The native open dialog. The window keeps serving other commands
76
249
  // while the user is deciding.
77
- document.getElementById("open").onclick = async () => {
250
+ $("open").onclick = async () => {
78
251
  const r = await janela.invoke("openFile", {});
79
- if (!r.ok) out.textContent = "error: " + r.error;
80
- else if (r.cancelled) out.textContent = "cancelled";
81
- else out.textContent = `${r.path}: ${r.length} chars\n\n` + r.text.slice(0, 400);
252
+ if (!r.ok) show($("file"), "error: " + r.error);
253
+ else if (r.cancelled) show($("file"), "cancelled");
254
+ else show($("file"), `${r.path} ${r.length} chars\n\n${r.text.slice(0, 400)}`);
82
255
  };
83
256
 
84
- document.getElementById("retitle").onclick = () =>
85
- janela.invoke("setTitle", { title: document.getElementById("title").value });
86
-
87
- document.getElementById("quit").onclick = () => janela.invoke("quit");
257
+ $("retitle").onclick = () => janela.invoke("setTitle", { title: $("title").value });
258
+ $("quit").onclick = () => janela.invoke("quit");
88
259
  };
89
260
  </script>
90
261
  </body>
@@ -4,8 +4,8 @@
4
4
  "version": "0.1.0",
5
5
  "window": {
6
6
  "title": "__NAME__",
7
- "width": 640,
8
- "height": 420
7
+ "width": 820,
8
+ "height": 700
9
9
  },
10
10
  "ios": {
11
11
  "identifier": "dev.janela.__NAME__",
@@ -2,6 +2,7 @@
2
2
  <html>
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
5
6
  <title>__NAME__</title>
6
7
  </head>
7
8
  <body>
@@ -4,8 +4,8 @@
4
4
  "version": "0.1.0",
5
5
  "window": {
6
6
  "title": "__NAME__",
7
- "width": 640,
8
- "height": 420
7
+ "width": 820,
8
+ "height": 700
9
9
  },
10
10
  "ios": {
11
11
  "identifier": "dev.janela.__NAME__",