janela 0.1.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 (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +95 -0
  3. package/bin/janela.mjs +271 -0
  4. package/package.json +43 -0
  5. package/runtime/janela.ts +193 -0
  6. package/shim/wvshim.cc +201 -0
  7. package/templates/index.html +48 -0
  8. package/templates/janela.conf.json +10 -0
  9. package/templates/main.ts +37 -0
  10. package/vendor-webview/LICENSE +22 -0
  11. package/vendor-webview/core/include/webview/api.h +251 -0
  12. package/vendor-webview/core/include/webview/backends.hh +40 -0
  13. package/vendor-webview/core/include/webview/c_api_impl.hh +258 -0
  14. package/vendor-webview/core/include/webview/detail/backends/cocoa_webkit.hh +626 -0
  15. package/vendor-webview/core/include/webview/detail/backends/gtk_webkitgtk.hh +355 -0
  16. package/vendor-webview/core/include/webview/detail/backends/win32_edge.hh +909 -0
  17. package/vendor-webview/core/include/webview/detail/basic_result.hh +119 -0
  18. package/vendor-webview/core/include/webview/detail/engine_base.hh +382 -0
  19. package/vendor-webview/core/include/webview/detail/exceptions.hh +42 -0
  20. package/vendor-webview/core/include/webview/detail/json.hh +337 -0
  21. package/vendor-webview/core/include/webview/detail/native_library.hh +170 -0
  22. package/vendor-webview/core/include/webview/detail/optional.hh +118 -0
  23. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSApplication.hh +107 -0
  24. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSBundle.hh +56 -0
  25. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSEvent.hh +71 -0
  26. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSInvocation.hh +66 -0
  27. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSMethodSignature.hh +52 -0
  28. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSNotification.hh +51 -0
  29. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSNumber.hh +53 -0
  30. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSObject.hh +51 -0
  31. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSOpenPanel.hh +71 -0
  32. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSPoint.hh +53 -0
  33. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSRect.hh +53 -0
  34. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSSavePanel.hh +52 -0
  35. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSSize.hh +53 -0
  36. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSString.hh +87 -0
  37. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSURL.hh +63 -0
  38. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSURLRequest.hh +52 -0
  39. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSValue.hh +56 -0
  40. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSView.hh +91 -0
  41. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSWindow.hh +137 -0
  42. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/cocoa.hh +59 -0
  43. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/types.hh +49 -0
  44. package/vendor-webview/core/include/webview/detail/platform/darwin/objc/Class.hh +55 -0
  45. package/vendor-webview/core/include/webview/detail/platform/darwin/objc/autoreleasepool.hh +70 -0
  46. package/vendor-webview/core/include/webview/detail/platform/darwin/objc/invoke.hh +75 -0
  47. package/vendor-webview/core/include/webview/detail/platform/darwin/objc/memory.hh +57 -0
  48. package/vendor-webview/core/include/webview/detail/platform/darwin/objc/objc.hh +47 -0
  49. package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKOpenPanelParameters.hh +57 -0
  50. package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKScriptMessage.hh +51 -0
  51. package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKUserContentController.hh +61 -0
  52. package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKUserScript.hh +71 -0
  53. package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKWebView.hh +108 -0
  54. package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKWebViewConfiguration.hh +60 -0
  55. package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/webkit.hh +46 -0
  56. package/vendor-webview/core/include/webview/detail/platform/linux/gtk/compat.hh +137 -0
  57. package/vendor-webview/core/include/webview/detail/platform/linux/webkitgtk/compat.hh +142 -0
  58. package/vendor-webview/core/include/webview/detail/platform/linux/webkitgtk/dmabuf.hh +167 -0
  59. package/vendor-webview/core/include/webview/detail/platform/windows/com_init_wrapper.hh +126 -0
  60. package/vendor-webview/core/include/webview/detail/platform/windows/dpi.hh +161 -0
  61. package/vendor-webview/core/include/webview/detail/platform/windows/dwmapi.hh +67 -0
  62. package/vendor-webview/core/include/webview/detail/platform/windows/iid.hh +78 -0
  63. package/vendor-webview/core/include/webview/detail/platform/windows/ntdll.hh +58 -0
  64. package/vendor-webview/core/include/webview/detail/platform/windows/reg_key.hh +135 -0
  65. package/vendor-webview/core/include/webview/detail/platform/windows/shcore.hh +59 -0
  66. package/vendor-webview/core/include/webview/detail/platform/windows/theme.hh +76 -0
  67. package/vendor-webview/core/include/webview/detail/platform/windows/user32.hh +90 -0
  68. package/vendor-webview/core/include/webview/detail/platform/windows/version.hh +151 -0
  69. package/vendor-webview/core/include/webview/detail/platform/windows/webview2/loader.hh +391 -0
  70. package/vendor-webview/core/include/webview/detail/user_script.hh +76 -0
  71. package/vendor-webview/core/include/webview/detail/utility/string.hh +99 -0
  72. package/vendor-webview/core/include/webview/errors.h +67 -0
  73. package/vendor-webview/core/include/webview/errors.hh +85 -0
  74. package/vendor-webview/core/include/webview/json_deprecated.hh +61 -0
  75. package/vendor-webview/core/include/webview/macros.h +123 -0
  76. package/vendor-webview/core/include/webview/types.h +81 -0
  77. package/vendor-webview/core/include/webview/types.hh +48 -0
  78. package/vendor-webview/core/include/webview/version.h +67 -0
  79. package/vendor-webview/core/include/webview/webview.h +32 -0
  80. package/vendor-webview/core/include/webview.h +15 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mateus Lage
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # janela
2
+
3
+ > *janela* — Portuguese for **window**.
4
+
5
+ Desktop apps in pure TypeScript, compiled to native. No Rust, no Node, no
6
+ Electron. The backend is TypeScript compiled to a native binary by
7
+ [scriptc](https://scriptc.dev); the window is the OS webview via
8
+ [webview/webview](https://github.com/webview/webview) (WKWebView on macOS,
9
+ WebKitGTK on Linux). Binaries come out ~500 KB.
10
+
11
+ ## Quick start
12
+
13
+ ```bash
14
+ npm install -g janela # or: npx janela init my-app
15
+ janela init my-app # (or `jn init my-app`)
16
+ cd my-app
17
+ janela dev # build + run with logs in the terminal
18
+ janela build # .janela/out/my-app (+ my-app.app on macOS)
19
+ ```
20
+
21
+ Requirements: Node 18+, a C++ compiler (Xcode CLT on macOS; g++ +
22
+ `libwebkit2gtk-4.1-dev` on Linux). A worked example lives in
23
+ [`examples/demo`](examples/demo) — commands, events, and a file reader.
24
+
25
+ ## A project
26
+
27
+ ```
28
+ my-app/
29
+ ├── index.html frontend — any HTML/JS; calls janela.invoke() / janela.listen()
30
+ ├── src-host/main.ts backend — exports setup(app), registers commands
31
+ └── janela.conf.json name, bundle identifier, version, window
32
+ ```
33
+
34
+ Frontend API (injected before page load):
35
+
36
+ ```js
37
+ const sum = await janela.invoke("add", { a: 2, b: 40 }); // call a backend command
38
+ janela.listen("added", (payload) => { ... }); // backend-fired events
39
+ ```
40
+
41
+ Backend API (`src-host/main.ts`):
42
+
43
+ ```ts
44
+ import type { JanelaApp } from "./janela";
45
+
46
+ export function setup(app: JanelaApp): void {
47
+ app.command("add", (argsJson) => { // args in / result out as JSON text
48
+ const a = JSON.parse(argsJson) as { a: number; b: number };
49
+ app.emit("added", JSON.stringify(a.a + a.b)); // push an event to the page
50
+ return JSON.stringify(a.a + a.b);
51
+ });
52
+ // app.quit() closes the window and returns from run()
53
+ }
54
+ ```
55
+
56
+ The backend is ordinary TypeScript with scriptc's stdlib — including a
57
+ `node:fs` subset — so "read a file" or "call an API" is just code in a
58
+ command handler, no plugin layer needed.
59
+
60
+ ## What the CLI hides
61
+
62
+ `janela build` assembles `.janela/build/` (runtime + your `main.ts` +
63
+ generated `frontend.ts`/`config.ts`/`entry.ts` + a platform FFI manifest),
64
+ compiles the C shim over webview.h once into `.janela/cache/`, then runs
65
+ scriptc. On macOS the frameworks are linked as SDK `.tbd` stubs (scriptc has
66
+ no `-framework` support) and the binary is wrapped into an ad-hoc-signed
67
+ `.app` bundle.
68
+
69
+ ## Constraints inherited from scriptc 0.0.32
70
+
71
+ - Command args/results cross the boundary as **JSON text** — handlers
72
+ `JSON.parse` in and `JSON.stringify` out. The runtime keeps the byte channel
73
+ ASCII by `\uXXXX`-escaping non-ASCII (scriptc strings can't hold lone
74
+ surrogates, and `JSON.parse` is the reliable reassembly point).
75
+ - Never use a bare FFI call as a complete variable initializer or assignment
76
+ RHS — it is silently miscompiled. Wrap it in any expression (`+ 0`). Plain
77
+ TypeScript is unaffected; only the runtime does FFI, so app code rarely
78
+ meets this.
79
+ - One window per app for now; the run loop is single-threaded, so a slow
80
+ command blocks the UI (same as a blocking Tauri command handler).
81
+ - `console.log` from commands goes to stdout — visible under `janela dev`,
82
+ not when launched from Finder.
83
+
84
+ ## Status
85
+
86
+ Early proof of concept, macOS (arm64) and Linux (WebKitGTK). The design
87
+ notes and scriptc findings behind it are in
88
+ [docs/findings.md](docs/findings.md). Not yet: Windows, async commands
89
+ (the run loop is single-threaded), native dialogs/tray/menus, multi-window,
90
+ icons/installers/notarization.
91
+
92
+ ## License
93
+
94
+ MIT. Bundles [webview/webview](https://github.com/webview/webview) headers
95
+ (MIT, © Serge Zaitsev and contributors).
package/bin/janela.mjs ADDED
@@ -0,0 +1,271 @@
1
+ #!/usr/bin/env node
2
+ // janela — the CLI (the tauri-cli analogue).
3
+ //
4
+ // janela init <name> scaffold a new project
5
+ // janela build compile the project to a native binary (+ .app on macOS)
6
+ // janela dev build, then run the binary with logs in the terminal
7
+ //
8
+ // A project is: index.html (frontend), src-host/main.ts (commands),
9
+ // janela.conf.json (window + bundle config). Everything else — the C shim over
10
+ // webview.h, the vendored webview, the scriptc runtime library, the FFI
11
+ // manifest — lives in this package and is assembled into .janela/ at build time.
12
+
13
+ import { spawnSync } from "node:child_process";
14
+ import {
15
+ cpSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync,
16
+ } from "node:fs";
17
+ import { createRequire } from "node:module";
18
+ import { dirname, join, relative, resolve, sep } from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+
21
+ const KIT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
22
+ const require = createRequire(join(KIT, "package.json"));
23
+
24
+ function fail(msg) {
25
+ console.error(`janela: ${msg}`);
26
+ process.exit(1);
27
+ }
28
+
29
+ function run(argv, opts = {}) {
30
+ const r = spawnSync(argv[0], argv.slice(1), { stdio: "inherit", ...opts });
31
+ if (r.status !== 0) fail(`command failed (${r.status ?? r.error}): ${argv.join(" ")}`);
32
+ }
33
+
34
+ function capture(argv) {
35
+ const r = spawnSync(argv[0], argv.slice(1), { encoding: "utf8" });
36
+ if (r.status !== 0) fail(`command failed: ${argv.join(" ")}`);
37
+ return r.stdout.trim();
38
+ }
39
+
40
+ function scriptcBin() {
41
+ const pkg = require.resolve("scriptc/package.json");
42
+ const meta = JSON.parse(readFileSync(pkg, "utf8"));
43
+ const rel = typeof meta.bin === "string" ? meta.bin : meta.bin.scriptc;
44
+ return join(dirname(pkg), rel);
45
+ }
46
+
47
+ function loadConf(root) {
48
+ const p = join(root, "janela.conf.json");
49
+ if (!existsSync(p)) fail("no janela.conf.json here — run from a project root (or `janela init <name>`)");
50
+ const c = JSON.parse(readFileSync(p, "utf8"));
51
+ for (const k of ["name", "identifier", "window"]) {
52
+ if (!c[k]) fail(`janela.conf.json is missing '${k}'`);
53
+ }
54
+ return c;
55
+ }
56
+
57
+ // ---- shim ----------------------------------------------------------------
58
+
59
+ function buildShim(cacheDir) {
60
+ const src = join(KIT, "shim", "wvshim.cc");
61
+ const obj = join(cacheDir, "wvshim.o");
62
+ const lib = join(cacheDir, "libwvshim.a");
63
+ if (existsSync(lib) && statSync(lib).mtimeMs > statSync(src).mtimeMs) return lib;
64
+
65
+ console.log("janela: compiling webview shim");
66
+ const inc = `-I${join(KIT, "vendor-webview", "core", "include")}`;
67
+ if (process.platform === "darwin") {
68
+ run(["clang++", "-c", src, "-o", obj, "-std=c++17", "-O2", inc]);
69
+ } else {
70
+ const cflags = capture(["pkg-config", "--cflags", "gtk+-3.0", "webkit2gtk-4.1"]).split(/\s+/).filter(Boolean);
71
+ run(["g++", "-c", src, "-o", obj, "-std=c++17", "-O2", inc, ...cflags]);
72
+ }
73
+ run(["ar", "rcs", lib, obj]);
74
+ return lib;
75
+ }
76
+
77
+ // ---- FFI manifest ---------------------------------------------------------
78
+
79
+ const STR = (name, symbol) => ({ name, symbol, params: ["i32", "string"], returns: "i32" });
80
+
81
+ function ffiManifest(shimLib) {
82
+ const functions = [
83
+ { name: "wvCreate", symbol: "wv_create", params: ["i32"], returns: "i32" },
84
+ STR("wvSetTitle", "wv_set_title"),
85
+ { name: "wvSetSize", symbol: "wv_set_size", params: ["i32", "i32", "i32", "i32"], returns: "i32" },
86
+ STR("wvSetHtml", "wv_set_html"),
87
+ STR("wvInit", "wv_init"),
88
+ STR("wvEval", "wv_eval"),
89
+ STR("wvBind", "wv_bind"),
90
+ { name: "wvReqLen", symbol: "wv_req_len", params: ["i32"], returns: "i32" },
91
+ { name: "wvReqByte", symbol: "wv_req_byte", params: ["i32", "i32"], returns: "i32" },
92
+ { name: "wvReplyReset", symbol: "wv_reply_reset", params: ["i32"], returns: "i32" },
93
+ { name: "wvReplyPush", symbol: "wv_reply_push", params: ["i32", "i32"], returns: "i32" },
94
+ {
95
+ name: "wvRun", symbol: "wv_run",
96
+ params: [
97
+ "i32",
98
+ { callback: { id: "run", params: ["u32", "u32", { context: "run" }], returns: "i32", lifetime: "call" } },
99
+ { context: "run" },
100
+ ],
101
+ returns: "i32",
102
+ },
103
+ { name: "wvTerminate", symbol: "wv_terminate", params: ["i32"], returns: "i32" },
104
+ ];
105
+
106
+ if (process.platform === "darwin") {
107
+ // scriptc has no -framework support, but `libraries` entries are passed to
108
+ // the link as plain input files and ld64 accepts .tbd stubs.
109
+ const sdk = capture(["xcrun", "--sdk", "macosx", "--show-sdk-path"]);
110
+ return {
111
+ ffi_format: 2,
112
+ functions,
113
+ libraries: [
114
+ shimLib,
115
+ join(sdk, "System/Library/Frameworks/WebKit.framework/WebKit.tbd"),
116
+ join(sdk, "System/Library/Frameworks/Cocoa.framework/Cocoa.tbd"),
117
+ ],
118
+ system_libraries: ["c++"],
119
+ };
120
+ }
121
+ return {
122
+ ffi_format: 2,
123
+ functions,
124
+ libraries: [shimLib],
125
+ system_libraries: [
126
+ "stdc++", "webkit2gtk-4.1", "javascriptcoregtk-4.1", "gtk-3", "gdk-3",
127
+ "soup-3.0", "gio-2.0", "gobject-2.0", "glib-2.0", "gmodule-2.0",
128
+ "pango-1.0", "pangocairo-1.0", "harfbuzz", "atk-1.0", "cairo",
129
+ "cairo-gobject", "gdk_pixbuf-2.0", "z", "pthread",
130
+ ],
131
+ };
132
+ }
133
+
134
+ // ---- build ----------------------------------------------------------------
135
+
136
+ function build(root) {
137
+ const conf = loadConf(root);
138
+ const buildDir = join(root, ".janela", "build");
139
+ const cacheDir = join(root, ".janela", "cache");
140
+ const outDir = join(root, ".janela", "out");
141
+ for (const d of [buildDir, cacheDir, outDir]) mkdirSync(d, { recursive: true });
142
+
143
+ const shimLib = buildShim(cacheDir);
144
+
145
+ // Assemble the compile unit: runtime + user's commands + generated modules.
146
+ cpSync(join(KIT, "runtime", "janela.ts"), join(buildDir, "janela.ts"));
147
+ const mainSrc = join(root, "src-host", "main.ts");
148
+ if (!existsSync(mainSrc)) fail("missing src-host/main.ts");
149
+ cpSync(mainSrc, join(buildDir, "main.ts"));
150
+
151
+ const htmlSrc = join(root, "index.html");
152
+ if (!existsSync(htmlSrc)) fail("missing index.html");
153
+ const html = readFileSync(htmlSrc, "utf8");
154
+ writeFileSync(
155
+ join(buildDir, "frontend.ts"),
156
+ `// Generated by janela from index.html — do not edit.\nexport const INDEX_HTML: string = ${JSON.stringify(html)};\n`,
157
+ );
158
+
159
+ const w = conf.window;
160
+ writeFileSync(
161
+ join(buildDir, "config.ts"),
162
+ `// Generated by janela from janela.conf.json — do not edit.\n` +
163
+ `export const WINDOW = { title: ${JSON.stringify(String(w.title ?? conf.name))}, ` +
164
+ `width: ${Number(w.width ?? 800)}, height: ${Number(w.height ?? 600)} };\n`,
165
+ );
166
+
167
+ writeFileSync(
168
+ join(buildDir, "entry.ts"),
169
+ `// Generated by janela — do not edit.\n` +
170
+ `import { createApp } from "./janela";\n` +
171
+ `import { WINDOW } from "./config";\n` +
172
+ `import { INDEX_HTML } from "./frontend";\n` +
173
+ `import { setup } from "./main";\n\n` +
174
+ `const app = createApp(WINDOW);\n` +
175
+ `setup(app);\n` +
176
+ `const rc = app.run(INDEX_HTML) + 0;\n` +
177
+ `console.log("[janela] run returned", rc);\n`,
178
+ );
179
+
180
+ writeFileSync(join(buildDir, "janela.ffi.json"), JSON.stringify(ffiManifest(shimLib), null, 2) + "\n");
181
+
182
+ console.log("janela: compiling TypeScript to a native binary");
183
+ const bin = join(outDir, conf.name);
184
+ run(["node", scriptcBin(), "build", "entry.ts", "--ffi", "janela.ffi.json", "-o", bin], { cwd: buildDir });
185
+
186
+ if (process.platform === "darwin") {
187
+ const bundle = join(outDir, `${conf.name}.app`);
188
+ mkdirSync(join(bundle, "Contents", "MacOS"), { recursive: true });
189
+ cpSync(bin, join(bundle, "Contents", "MacOS", conf.name));
190
+ writeFileSync(
191
+ join(bundle, "Contents", "Info.plist"),
192
+ `<?xml version="1.0" encoding="UTF-8"?>
193
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
194
+ <plist version="1.0">
195
+ <dict>
196
+ <key>CFBundleName</key><string>${conf.name}</string>
197
+ <key>CFBundleDisplayName</key><string>${conf.name}</string>
198
+ <key>CFBundleIdentifier</key><string>${conf.identifier}</string>
199
+ <key>CFBundleExecutable</key><string>${conf.name}</string>
200
+ <key>CFBundlePackageType</key><string>APPL</string>
201
+ <key>CFBundleVersion</key><string>${conf.version ?? "0.1.0"}</string>
202
+ <key>CFBundleShortVersionString</key><string>${conf.version ?? "0.1.0"}</string>
203
+ <key>NSHighResolutionCapable</key><true/>
204
+ </dict>
205
+ </plist>
206
+ `,
207
+ );
208
+ spawnSync("codesign", ["--force", "--sign", "-", bundle]);
209
+ console.log(`janela: built ${relative(root, bin)} and ${relative(root, bundle)}`);
210
+ } else {
211
+ console.log(`janela: built ${relative(root, bin)}`);
212
+ }
213
+ return bin;
214
+ }
215
+
216
+ // ---- init -----------------------------------------------------------------
217
+
218
+ function init(name) {
219
+ if (!name || !/^[a-z][a-z0-9-]*$/.test(name)) fail("usage: janela init <name> (lowercase, digits, dashes)");
220
+ const dir = resolve(process.cwd(), name);
221
+ if (existsSync(dir)) fail(`${name}/ already exists`);
222
+ mkdirSync(join(dir, "src-host"), { recursive: true });
223
+
224
+ // Installed from the registry (this file lives under node_modules) →
225
+ // depend on the published version; a source checkout → a file: link.
226
+ const kitPkg = JSON.parse(readFileSync(join(KIT, "package.json"), "utf8"));
227
+ const janelaDep = KIT.includes(`${sep}node_modules${sep}`)
228
+ ? `^${kitPkg.version}`
229
+ : `file:${relative(dir, KIT) || "."}`;
230
+
231
+ const t = (f) => readFileSync(join(KIT, "templates", f), "utf8").replaceAll("__NAME__", name);
232
+ writeFileSync(join(dir, "index.html"), t("index.html"));
233
+ writeFileSync(join(dir, "src-host", "main.ts"), t("main.ts"));
234
+ writeFileSync(join(dir, "janela.conf.json"), t("janela.conf.json"));
235
+ writeFileSync(join(dir, ".gitignore"), ".janela/\nnode_modules/\n");
236
+ writeFileSync(
237
+ join(dir, "package.json"),
238
+ JSON.stringify(
239
+ {
240
+ name,
241
+ private: true,
242
+ scripts: { dev: "janela dev", build: "janela build" },
243
+ devDependencies: { janela: janelaDep },
244
+ },
245
+ null,
246
+ 2,
247
+ ) + "\n",
248
+ );
249
+ console.log(`janela: created ${name}/ — next: cd ${name} && janela dev`);
250
+ }
251
+
252
+ // ---- main -----------------------------------------------------------------
253
+
254
+ const [cmd, arg] = process.argv.slice(2);
255
+ switch (cmd) {
256
+ case "init":
257
+ init(arg);
258
+ break;
259
+ case "build":
260
+ build(process.cwd());
261
+ break;
262
+ case "dev": {
263
+ const bin = build(process.cwd());
264
+ console.log("janela: running (close the window or Ctrl-C to stop)");
265
+ run([bin]);
266
+ break;
267
+ }
268
+ default:
269
+ console.log("usage: janela init <name> | janela build | janela dev");
270
+ process.exit(cmd ? 1 : 0);
271
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "janela",
3
+ "version": "0.1.0",
4
+ "description": "Desktop apps in pure TypeScript, compiled to native. No Rust, no Node, no Electron.",
5
+ "type": "module",
6
+ "bin": {
7
+ "janela": "bin/janela.mjs",
8
+ "jn": "bin/janela.mjs"
9
+ },
10
+ "dependencies": {
11
+ "scriptc": "0.0.32"
12
+ },
13
+ "license": "MIT",
14
+ "author": "Mateus Lage",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/mmamedel/janela.git"
18
+ },
19
+ "homepage": "https://github.com/mmamedel/janela#readme",
20
+ "bugs": {
21
+ "url": "https://github.com/mmamedel/janela/issues"
22
+ },
23
+ "keywords": [
24
+ "desktop",
25
+ "webview",
26
+ "typescript",
27
+ "native",
28
+ "tauri",
29
+ "electron-alternative",
30
+ "scriptc"
31
+ ],
32
+ "engines": {
33
+ "node": ">=18"
34
+ },
35
+ "files": [
36
+ "bin/",
37
+ "runtime/",
38
+ "shim/",
39
+ "templates/",
40
+ "vendor-webview/core/include/",
41
+ "vendor-webview/LICENSE"
42
+ ]
43
+ }
@@ -0,0 +1,193 @@
1
+ // janela.ts — the janela runtime, compiled into the app by scriptc.
2
+ //
3
+ // The Tauri-shaped surface: one `__invoke` binding carries every command as a
4
+ // (name, argsJson) envelope, dispatched to handlers registered on the app
5
+ // object. Backend→frontend events ride wv_eval into the injected bootstrap.
6
+ //
7
+ // NOTE ON STYLE: every FFI call whose result initializes a variable is written
8
+ // `f(...) + 0`. scriptc 0.0.32 miscompiles a bare FFI call used as a complete
9
+ // initializer/assignment RHS (see FINDINGS.md); any enclosing expression is
10
+ // the workaround.
11
+
12
+ declare function wvCreate(debug: number): number;
13
+ declare function wvSetTitle(h: number, title: string): number;
14
+ declare function wvSetSize(h: number, w: number, ht: number, hint: number): number;
15
+ declare function wvSetHtml(h: number, html: string): number;
16
+ declare function wvInit(h: number, js: string): number;
17
+ declare function wvEval(h: number, js: string): number;
18
+ declare function wvBind(h: number, name: string): number;
19
+ declare function wvReqLen(h: number): number;
20
+ declare function wvReqByte(h: number, i: number): number;
21
+ declare function wvReplyReset(h: number): number;
22
+ declare function wvReplyPush(h: number, b: number): number;
23
+ declare function wvRun(h: number, cb: (bindIndex: number, seq: number) => number): number;
24
+ declare function wvTerminate(h: number): number;
25
+
26
+ // Injected into every page before it loads (webview_init).
27
+ const BOOTSTRAP =
28
+ "window.__wvListeners = {};" +
29
+ "window.janela = {" +
30
+ " invoke: function (cmd, args) {" +
31
+ " return window.__invoke(cmd, JSON.stringify(args === undefined ? null : args));" +
32
+ " }," +
33
+ " listen: function (event, cb) {" +
34
+ " if (!window.__wvListeners[event]) window.__wvListeners[event] = [];" +
35
+ " window.__wvListeners[event].push(cb);" +
36
+ " }," +
37
+ "};" +
38
+ "window.__wvEmit = function (event, payload) {" +
39
+ " var cbs = window.__wvListeners[event] || [];" +
40
+ " for (var i = 0; i < cbs.length; i++) cbs[i](payload);" +
41
+ "};";
42
+
43
+ // Handlers receive the invoke args as JSON text and must return JSON text
44
+ // (what the frontend promise resolves with). Throwing is not supported by
45
+ // scriptc across the FFI boundary — return an error envelope instead.
46
+ export type CommandHandler = (argsJson: string) => string;
47
+
48
+ export interface WindowConfig {
49
+ title: string;
50
+ width: number;
51
+ height: number;
52
+ }
53
+
54
+ export interface JanelaApp {
55
+ handle: number;
56
+ names: string[];
57
+ handlers: CommandHandler[];
58
+ /** Register a named command, callable from the page as janela.invoke(name, args). */
59
+ command: (name: string, h: CommandHandler) => void;
60
+ /** Fire an event into the page; payloadJson must be valid JSON text. */
61
+ emit: (event: string, payloadJson: string) => void;
62
+ /** Close the window and make run() return. */
63
+ quit: () => void;
64
+ /** Show the page and block until the window closes. Returns the run status. */
65
+ run: (html: string) => number;
66
+ }
67
+
68
+ const HEX = "0123456789abcdef";
69
+
70
+ function uEscape(unit: number): string {
71
+ return (
72
+ "\\u" +
73
+ HEX.charAt((unit >> 12) & 0xf) +
74
+ HEX.charAt((unit >> 8) & 0xf) +
75
+ HEX.charAt((unit >> 4) & 0xf) +
76
+ HEX.charAt(unit & 0xf)
77
+ );
78
+ }
79
+
80
+ // The request arrives as UTF-8 JSON bytes. scriptc strings cannot hold lone
81
+ // surrogates (String.fromCharCode(0xd83d) yields a replacement char), so we
82
+ // never build non-ASCII chars directly: every code point >= 0x80 is re-emitted
83
+ // as a JSON \uXXXX escape (a surrogate PAIR of escapes for astral planes),
84
+ // which JSON.parse reconstructs correctly. Legal only because the payload is
85
+ // always JSON, where non-ASCII can only occur inside strings.
86
+ function readRequest(h: number): string {
87
+ let out = "";
88
+ const n = wvReqLen(h) + 0;
89
+ let i = 0;
90
+ while (i < n) {
91
+ const b0 = wvReqByte(h, i) + 0;
92
+ i = i + 1;
93
+ let cp = b0;
94
+ if ((b0 & 0xe0) === 0xc0 && i < n) {
95
+ cp = ((b0 & 0x1f) << 6) | (wvReqByte(h, i) & 0x3f);
96
+ i = i + 1;
97
+ } else if ((b0 & 0xf0) === 0xe0 && i + 1 < n) {
98
+ cp = ((b0 & 0x0f) << 12) | ((wvReqByte(h, i) & 0x3f) << 6) | (wvReqByte(h, i + 1) & 0x3f);
99
+ i = i + 2;
100
+ } else if ((b0 & 0xf8) === 0xf0 && i + 2 < n) {
101
+ cp =
102
+ ((b0 & 0x07) << 18) |
103
+ ((wvReqByte(h, i) & 0x3f) << 12) |
104
+ ((wvReqByte(h, i + 1) & 0x3f) << 6) |
105
+ (wvReqByte(h, i + 2) & 0x3f);
106
+ i = i + 3;
107
+ }
108
+ if (cp < 0x80) {
109
+ out = out + String.fromCharCode(cp);
110
+ } else if (cp < 0x10000) {
111
+ out = out + uEscape(cp);
112
+ } else {
113
+ const v = cp - 0x10000;
114
+ out = out + uEscape(0xd800 + (v >> 10)) + uEscape(0xdc00 + (v & 0x3ff));
115
+ }
116
+ }
117
+ return out;
118
+ }
119
+
120
+ // The reply must be valid JSON when it reaches the page. Non-ASCII chars can
121
+ // only legally occur inside JSON strings, where a \uXXXX escape is always
122
+ // equivalent — so escaping every char >127 keeps the byte channel ASCII-clean
123
+ // (surrogate halves escape individually, which JSON also permits).
124
+ function writeReply(h: number, body: string): void {
125
+ wvReplyReset(h);
126
+ for (let i = 0; i < body.length; i++) {
127
+ const c = body.charCodeAt(i);
128
+ if (c < 0x80) {
129
+ wvReplyPush(h, c);
130
+ } else {
131
+ wvReplyPush(h, 92); // backslash
132
+ wvReplyPush(h, 117); // 'u'
133
+ wvReplyPush(h, HEX.charCodeAt((c >> 12) & 0xf));
134
+ wvReplyPush(h, HEX.charCodeAt((c >> 8) & 0xf));
135
+ wvReplyPush(h, HEX.charCodeAt((c >> 4) & 0xf));
136
+ wvReplyPush(h, HEX.charCodeAt(c & 0xf));
137
+ }
138
+ }
139
+ }
140
+
141
+ export function createApp(cfg: WindowConfig): JanelaApp {
142
+ const h = wvCreate(0) + 0;
143
+ wvSetTitle(h, cfg.title);
144
+ wvSetSize(h, cfg.width, cfg.height, 0);
145
+ wvInit(h, BOOTSTRAP);
146
+
147
+ const app: JanelaApp = {
148
+ handle: h,
149
+ names: [],
150
+ handlers: [],
151
+
152
+ command: (name, handler) => {
153
+ app.names.push(name);
154
+ app.handlers.push(handler);
155
+ },
156
+
157
+ emit: (event, payloadJson) => {
158
+ wvEval(
159
+ h,
160
+ "window.__wvEmit(" + JSON.stringify(event) + "," + payloadJson + ");",
161
+ );
162
+ },
163
+
164
+ quit: () => {
165
+ wvTerminate(h);
166
+ },
167
+
168
+ run: (html) => {
169
+ const INVOKE = wvBind(h, "__invoke") + 0;
170
+ wvSetHtml(h, html);
171
+
172
+ const rc = wvRun(h, (bindIndex, _seq) => {
173
+ if (bindIndex !== INVOKE) {
174
+ writeReply(h, '"unknown binding"');
175
+ return 1;
176
+ }
177
+ const env = JSON.parse(readRequest(h)) as string[];
178
+ const cmd = env[0];
179
+ const argsJson = env[1];
180
+ for (let i = 0; i < app.names.length; i++) {
181
+ if (app.names[i] === cmd) {
182
+ writeReply(h, app.handlers[i](argsJson));
183
+ return 0;
184
+ }
185
+ }
186
+ writeReply(h, JSON.stringify("unknown command: " + cmd));
187
+ return 1; // rejects the frontend promise
188
+ }) + 0;
189
+ return rc;
190
+ },
191
+ };
192
+ return app;
193
+ }