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.
- package/LICENSE +21 -0
- package/README.md +95 -0
- package/bin/janela.mjs +271 -0
- package/package.json +43 -0
- package/runtime/janela.ts +193 -0
- package/shim/wvshim.cc +201 -0
- package/templates/index.html +48 -0
- package/templates/janela.conf.json +10 -0
- package/templates/main.ts +37 -0
- package/vendor-webview/LICENSE +22 -0
- package/vendor-webview/core/include/webview/api.h +251 -0
- package/vendor-webview/core/include/webview/backends.hh +40 -0
- package/vendor-webview/core/include/webview/c_api_impl.hh +258 -0
- package/vendor-webview/core/include/webview/detail/backends/cocoa_webkit.hh +626 -0
- package/vendor-webview/core/include/webview/detail/backends/gtk_webkitgtk.hh +355 -0
- package/vendor-webview/core/include/webview/detail/backends/win32_edge.hh +909 -0
- package/vendor-webview/core/include/webview/detail/basic_result.hh +119 -0
- package/vendor-webview/core/include/webview/detail/engine_base.hh +382 -0
- package/vendor-webview/core/include/webview/detail/exceptions.hh +42 -0
- package/vendor-webview/core/include/webview/detail/json.hh +337 -0
- package/vendor-webview/core/include/webview/detail/native_library.hh +170 -0
- package/vendor-webview/core/include/webview/detail/optional.hh +118 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSApplication.hh +107 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSBundle.hh +56 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSEvent.hh +71 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSInvocation.hh +66 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSMethodSignature.hh +52 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSNotification.hh +51 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSNumber.hh +53 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSObject.hh +51 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSOpenPanel.hh +71 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSPoint.hh +53 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSRect.hh +53 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSSavePanel.hh +52 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSSize.hh +53 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSString.hh +87 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSURL.hh +63 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSURLRequest.hh +52 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSValue.hh +56 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSView.hh +91 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSWindow.hh +137 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/cocoa.hh +59 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/types.hh +49 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/objc/Class.hh +55 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/objc/autoreleasepool.hh +70 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/objc/invoke.hh +75 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/objc/memory.hh +57 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/objc/objc.hh +47 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKOpenPanelParameters.hh +57 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKScriptMessage.hh +51 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKUserContentController.hh +61 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKUserScript.hh +71 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKWebView.hh +108 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKWebViewConfiguration.hh +60 -0
- package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/webkit.hh +46 -0
- package/vendor-webview/core/include/webview/detail/platform/linux/gtk/compat.hh +137 -0
- package/vendor-webview/core/include/webview/detail/platform/linux/webkitgtk/compat.hh +142 -0
- package/vendor-webview/core/include/webview/detail/platform/linux/webkitgtk/dmabuf.hh +167 -0
- package/vendor-webview/core/include/webview/detail/platform/windows/com_init_wrapper.hh +126 -0
- package/vendor-webview/core/include/webview/detail/platform/windows/dpi.hh +161 -0
- package/vendor-webview/core/include/webview/detail/platform/windows/dwmapi.hh +67 -0
- package/vendor-webview/core/include/webview/detail/platform/windows/iid.hh +78 -0
- package/vendor-webview/core/include/webview/detail/platform/windows/ntdll.hh +58 -0
- package/vendor-webview/core/include/webview/detail/platform/windows/reg_key.hh +135 -0
- package/vendor-webview/core/include/webview/detail/platform/windows/shcore.hh +59 -0
- package/vendor-webview/core/include/webview/detail/platform/windows/theme.hh +76 -0
- package/vendor-webview/core/include/webview/detail/platform/windows/user32.hh +90 -0
- package/vendor-webview/core/include/webview/detail/platform/windows/version.hh +151 -0
- package/vendor-webview/core/include/webview/detail/platform/windows/webview2/loader.hh +391 -0
- package/vendor-webview/core/include/webview/detail/user_script.hh +76 -0
- package/vendor-webview/core/include/webview/detail/utility/string.hh +99 -0
- package/vendor-webview/core/include/webview/errors.h +67 -0
- package/vendor-webview/core/include/webview/errors.hh +85 -0
- package/vendor-webview/core/include/webview/json_deprecated.hh +61 -0
- package/vendor-webview/core/include/webview/macros.h +123 -0
- package/vendor-webview/core/include/webview/types.h +81 -0
- package/vendor-webview/core/include/webview/types.hh +48 -0
- package/vendor-webview/core/include/webview/version.h +67 -0
- package/vendor-webview/core/include/webview/webview.h +32 -0
- 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
|
+
}
|