janela 0.10.0 → 0.10.1

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 (40) hide show
  1. package/bin/janela.mjs +6 -4
  2. package/package.json +1 -1
  3. package/shim/ios/app.cc +330 -0
  4. package/vendor-webview/core/include/webview/backends.hh +1 -0
  5. package/vendor-webview/core/include/webview/detail/backends/cocoa_webkit.hh +4 -1
  6. package/vendor-webview/core/include/webview/detail/backends/uikit_webkit.hh +460 -0
  7. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSBundle.hh +4 -2
  8. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSInvocation.hh +4 -2
  9. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSMethodSignature.hh +4 -2
  10. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSNotification.hh +4 -2
  11. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSNumber.hh +4 -2
  12. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSObject.hh +4 -2
  13. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSPoint.hh +4 -2
  14. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSRect.hh +4 -2
  15. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSSize.hh +4 -2
  16. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSString.hh +4 -2
  17. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSURL.hh +4 -2
  18. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSURLRequest.hh +4 -2
  19. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/NSValue.hh +4 -2
  20. package/vendor-webview/core/include/webview/detail/platform/darwin/cocoa/types.hh +4 -2
  21. package/vendor-webview/core/include/webview/detail/platform/darwin/objc/Class.hh +4 -1
  22. package/vendor-webview/core/include/webview/detail/platform/darwin/objc/autoreleasepool.hh +4 -1
  23. package/vendor-webview/core/include/webview/detail/platform/darwin/objc/invoke.hh +4 -1
  24. package/vendor-webview/core/include/webview/detail/platform/darwin/objc/memory.hh +4 -1
  25. package/vendor-webview/core/include/webview/detail/platform/darwin/objc/objc.hh +4 -1
  26. package/vendor-webview/core/include/webview/detail/platform/darwin/uikit/UIApplication.hh +80 -0
  27. package/vendor-webview/core/include/webview/detail/platform/darwin/uikit/UIScreen.hh +57 -0
  28. package/vendor-webview/core/include/webview/detail/platform/darwin/uikit/UIView.hh +79 -0
  29. package/vendor-webview/core/include/webview/detail/platform/darwin/uikit/UIViewController.hh +70 -0
  30. package/vendor-webview/core/include/webview/detail/platform/darwin/uikit/UIWindow.hh +67 -0
  31. package/vendor-webview/core/include/webview/detail/platform/darwin/uikit/uikit.hh +43 -0
  32. package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKOpenPanelParameters.hh +4 -2
  33. package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKScriptMessage.hh +4 -2
  34. package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKUserContentController.hh +4 -2
  35. package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKUserScript.hh +4 -2
  36. package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKWebView.hh +4 -2
  37. package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/WKWebViewConfiguration.hh +4 -2
  38. package/vendor-webview/core/include/webview/detail/platform/darwin/webkit/webkit.hh +4 -2
  39. package/vendor-webview/core/include/webview/macros.h +12 -2
  40. package/shim/ios/app.mm +0 -386
package/bin/janela.mjs CHANGED
@@ -418,14 +418,16 @@ function buildIos(root, conf, buildDir, outDir) {
418
418
  const bundle = join(outDir, `${conf.name}.app`);
419
419
  mkdirSync(bundle, { recursive: true });
420
420
  const sdk = capture(["xcrun", "--sdk", "iphonesimulator", "--show-sdk-path"]);
421
- // The .mm extension picks Objective-C++; an explicit `-x objective-c++`
422
- // would leak onto the archive that follows and be compiled as source.
421
+ // Plain C++: the shell drives the webview through webview.h's UIKit
422
+ // backend, which uses the Objective-C runtime rather than Objective-C, so
423
+ // no .mm and no -fobjc-arc. Blocks still need -fblocks for libdispatch.
423
424
  run([
424
425
  "xcrun", "clang++",
425
- join(KIT, "shim", "ios", "app.mm"),
426
+ join(KIT, "shim", "ios", "app.cc"),
426
427
  "-target", `arm64-apple-ios${ios.minimumVersion}-simulator`,
427
428
  "-isysroot", sdk,
428
- "-fobjc-arc", "-std=c++17", "-O2",
429
+ "-std=c++17", "-O2", "-fblocks",
430
+ `-I${join(KIT, "vendor-webview", "core", "include")}`,
429
431
  "-framework", "UIKit", "-framework", "WebKit", "-framework", "Foundation",
430
432
  lib,
431
433
  "-o", join(bundle, conf.name),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "janela",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "Desktop and iOS apps in pure TypeScript, compiled to native. No Rust, no Node, no Electron.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,330 @@
1
+ // janela's iOS shell.
2
+ //
3
+ // UIKit owns the run loop and the app's TypeScript is a linked scriptc
4
+ // library we call into — the mirror image of the desktop shim, where
5
+ // TypeScript owns main() and drives a C library over FFI.
6
+ //
7
+ // Everything webview-shaped lives behind webview.h's UIKit backend: creating
8
+ // the web view, injecting the page bootstrap, binding the invoke channel,
9
+ // evaluating JavaScript, and settling a page promise that was answered later.
10
+ // What is left here is janela's own: the due-ordered timer queue, the table of
11
+ // page replies we are holding, file I/O, and the scriptc library's C ABI.
12
+ //
13
+ // scriptc builds iOS as a library only, and library mode links no event loop
14
+ // (SC4005) and creates no threads, so the shell owns the clock and every
15
+ // blocking call.
16
+
17
+ #include "webview.h"
18
+
19
+ #include <dispatch/dispatch.h>
20
+
21
+ #include <cstdlib>
22
+ #include <cstring>
23
+ #include <fstream>
24
+ #include <map>
25
+ #include <sstream>
26
+ #include <string>
27
+
28
+ // ---- the scriptc library's C ABI (see the generated profile) ---------------
29
+ extern "C" {
30
+ void jl_init(void);
31
+ void jl_reset(void);
32
+ void jl_set_panic_sink(void (*fn)(void *, const char *, size_t, const char *,
33
+ size_t),
34
+ void *ctx);
35
+ int32_t jl_set_callback(const char *name, void (*fn)(void), void *ctx);
36
+ void jl_handle_invoke(const char *cmd, size_t cmd_len, const char *args,
37
+ size_t args_len, char **out, size_t *out_len);
38
+ void jl_index_html(char **out, size_t *out_len);
39
+ void jl_on_timer(double id);
40
+ void jl_on_fs_done(double id, bool ok, const char *payload, size_t payload_len);
41
+ }
42
+
43
+ namespace {
44
+
45
+ webview::webview *g_webview = nullptr;
46
+
47
+ /// Copy a library-owned result out of its arena and release it. Results live
48
+ /// until the next jl_reset(), so nothing may hold the pointer past this call.
49
+ std::string take_result(char *out, size_t out_len) {
50
+ std::string s = out ? std::string(out, out_len) : std::string{"null"};
51
+ jl_reset();
52
+ return s;
53
+ }
54
+
55
+ void panic_sink(void *, const char *sym, size_t sym_len, const char *msg,
56
+ size_t msg_len) {
57
+ std::fprintf(stderr, "[janela] host panic in %.*s: %.*s\n", (int)sym_len, sym,
58
+ (int)msg_len, msg);
59
+ }
60
+
61
+ // ---- what the shell owns for the library ----------------------------------
62
+ //
63
+ // THE RULE (upstream scriptc #263): a channel handler must never re-enter the
64
+ // library. Every handler below only records what it was told and returns; the
65
+ // re-entry happens from a dispatched block, i.e. at the top of a later turn
66
+ // with no library frame beneath it. A breach silently appears to work, so this
67
+ // has to hold by construction rather than by testing.
68
+
69
+ /// Page replies whose answer was not ready when handle_invoke returned:
70
+ /// janela's pending id -> the webview binding id we have not resolved yet.
71
+ std::map<long long, std::string> g_held;
72
+ /// Envelopes that arrived before we knew the binding id — a command that
73
+ /// resolves synchronously settles during its own dispatch, before the
74
+ /// {"pending":id} envelope has made it back to us.
75
+ std::map<long long, std::string> g_early;
76
+
77
+ void settle(const std::string &binding_id, const std::string &envelope) {
78
+ if (g_webview) {
79
+ g_webview->resolve(binding_id, 0, envelope);
80
+ }
81
+ }
82
+
83
+ /// Answer the page for a pending id, whichever half arrived first.
84
+ void resolve_pending(long long pending_id, const std::string &envelope) {
85
+ auto it = g_held.find(pending_id);
86
+ if (it != g_held.end()) {
87
+ std::string binding_id = it->second;
88
+ g_held.erase(it);
89
+ settle(binding_id, envelope);
90
+ } else {
91
+ g_early[pending_id] = envelope; // handle_invoke has not returned yet
92
+ }
93
+ }
94
+
95
+ /// Register the binding id a pending answer belongs to.
96
+ void hold_pending(long long pending_id, const std::string &binding_id) {
97
+ auto it = g_early.find(pending_id);
98
+ if (it != g_early.end()) {
99
+ std::string envelope = it->second;
100
+ g_early.erase(it);
101
+ settle(binding_id, envelope); // it resolved synchronously; answer now
102
+ } else {
103
+ g_held[pending_id] = binding_id;
104
+ }
105
+ }
106
+
107
+ // ---- channels the library calls (TS -> shell) ------------------------------
108
+
109
+ /// Park a continuation with us and call back when it comes due. A zero delay
110
+ /// still goes through dispatch_after(0), which posts to the next turn — that
111
+ /// is app.defer(), and it costs no timer.
112
+ void host_schedule(void *, double id, double ms) {
113
+ int64_t delay = (int64_t)(ms > 0 ? ms : 0);
114
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay * NSEC_PER_MSEC),
115
+ dispatch_get_main_queue(), ^{
116
+ jl_on_timer(id); // fresh turn: no library frame beneath us
117
+ });
118
+ }
119
+
120
+ /// The library has an answer for a held page reply.
121
+ void host_settle(void *, double pending_id, const char *env, size_t env_len) {
122
+ auto key = (long long)pending_id;
123
+ std::string envelope(env ? env : "null", env ? env_len : 4);
124
+ dispatch_async(dispatch_get_main_queue(), ^{
125
+ resolve_pending(key, envelope);
126
+ });
127
+ }
128
+
129
+ // ---- the file queue --------------------------------------------------------
130
+ //
131
+ // Blocking I/O happens here, never on the main queue and never inside the
132
+ // library, which links no threads of its own.
133
+ //
134
+ // An iOS app has no useful working directory and cannot see host paths, so a
135
+ // relative path is taken as relative to the app's Documents directory — the
136
+ // one place a sandboxed app can freely read and write. HOME is the app's
137
+ // container, so this needs no Foundation call.
138
+ std::string resolve_path(const std::string &path) {
139
+ if (!path.empty() && path[0] == '/') {
140
+ return path;
141
+ }
142
+ const char *home = std::getenv("HOME");
143
+ std::string base = home ? std::string(home) + "/Documents" : std::string(".");
144
+ return base + "/" + path;
145
+ }
146
+
147
+ dispatch_queue_t fs_queue() {
148
+ static dispatch_queue_t q =
149
+ dispatch_queue_create("dev.janela.fs", DISPATCH_QUEUE_SERIAL);
150
+ return q;
151
+ }
152
+
153
+ // Takes the payload BY VALUE: a block captures a reference parameter as a
154
+ // reference, so a temporary passed by the caller would already be destroyed by
155
+ // the time the block runs on the main queue, and the library would decode
156
+ // freed memory.
157
+ void finish_fs(double id, bool ok, std::string payload) {
158
+ dispatch_async(dispatch_get_main_queue(), ^{
159
+ jl_on_fs_done(id, ok, payload.c_str(), payload.size());
160
+ });
161
+ }
162
+
163
+ void host_read_file(void *, double id, const char *path, size_t path_len) {
164
+ std::string p = resolve_path(std::string(path, path_len));
165
+ dispatch_async(fs_queue(), ^{
166
+ std::ifstream in(p, std::ios::binary);
167
+ if (!in) {
168
+ finish_fs(id, false, "ENOENT: no such file or directory, open '" + p + "'");
169
+ return;
170
+ }
171
+ std::ostringstream ss;
172
+ ss << in.rdbuf();
173
+ finish_fs(id, true, ss.str());
174
+ });
175
+ }
176
+
177
+ void host_write_file(void *, double id, const char *path, size_t path_len,
178
+ const char *data, size_t data_len) {
179
+ std::string p = resolve_path(std::string(path, path_len));
180
+ std::string d(data, data_len);
181
+ dispatch_async(fs_queue(), ^{
182
+ std::ofstream out(p, std::ios::binary | std::ios::trunc);
183
+ if (!out) {
184
+ finish_fs(id, false, "EACCES: could not open '" + p + "' for writing");
185
+ return;
186
+ }
187
+ out.write(d.data(), (std::streamsize)d.size());
188
+ out.close();
189
+ if (!out) {
190
+ finish_fs(id, false, "EIO: write failed for '" + p + "'");
191
+ return;
192
+ }
193
+ finish_fs(id, true, "");
194
+ });
195
+ }
196
+
197
+ // ---- host -> page: the event channel ---------------------------------------
198
+
199
+ /// Minimal JSON string quoting, for splicing an event name into a JS call.
200
+ std::string js_quote(const std::string &raw) {
201
+ std::string out = "\"";
202
+ for (char c : raw) {
203
+ switch (c) {
204
+ case '"':
205
+ out += "\\\"";
206
+ break;
207
+ case '\\':
208
+ out += "\\\\";
209
+ break;
210
+ case '\n':
211
+ out += "\\n";
212
+ break;
213
+ case '\r':
214
+ out += "\\r";
215
+ break;
216
+ case '\t':
217
+ out += "\\t";
218
+ break;
219
+ default:
220
+ if ((unsigned char)c < 0x20) {
221
+ char buf[7];
222
+ std::snprintf(buf, sizeof(buf), "\\u%04x", c);
223
+ out += buf;
224
+ } else {
225
+ out += c;
226
+ }
227
+ }
228
+ }
229
+ out += "\"";
230
+ return out;
231
+ }
232
+
233
+ void emit_event(void *, const char *name, size_t name_len, const char *payload,
234
+ size_t payload_len) {
235
+ std::string js = "window.__wvEmit(" +
236
+ js_quote(std::string(name, name_len)) + "," +
237
+ std::string(payload, payload_len) + ");";
238
+ dispatch_async(dispatch_get_main_queue(), ^{
239
+ if (g_webview) {
240
+ g_webview->eval(js);
241
+ }
242
+ });
243
+ }
244
+
245
+ // ---- the page-side bridge --------------------------------------------------
246
+ //
247
+ // webview.h binds __janelaInvoke and injects the plumbing that turns it into a
248
+ // promise; this adds the janela surface on top, so `janela/api` and a
249
+ // project's frontend work exactly as they do on desktop.
250
+
251
+ const char *const kBootstrap = R"JS(
252
+ window.__wvListeners = {};
253
+ window.janela = {
254
+ invoke: function (cmd, args) {
255
+ return window.__janelaInvoke(cmd, JSON.stringify(args === undefined ? null : args))
256
+ .then(function (env) {
257
+ if (env && env.ok) return env.value;
258
+ throw new Error((env && env.error) || 'janela: invoke failed');
259
+ });
260
+ },
261
+ listen: function (event, cb) {
262
+ if (!window.__wvListeners[event]) window.__wvListeners[event] = [];
263
+ window.__wvListeners[event].push(cb);
264
+ return function () {
265
+ var a = window.__wvListeners[event] || [];
266
+ var i = a.indexOf(cb);
267
+ if (i >= 0) a.splice(i, 1);
268
+ };
269
+ },
270
+ };
271
+ window.__wvEmit = function (event, payload) {
272
+ var cbs = window.__wvListeners[event] || [];
273
+ for (var i = 0; i < cbs.length; i++) cbs[i](payload);
274
+ };
275
+ )JS";
276
+
277
+ /// The page's invoke channel. `req` is a JSON array of the two arguments the
278
+ /// bootstrap passed: [cmd, argsJson].
279
+ void on_invoke(const std::string &binding_id, const std::string &req, void *) {
280
+ std::string cmd = webview::detail::json_parse(req, "", 0);
281
+ std::string args = webview::detail::json_parse(req, "", 1);
282
+
283
+ char *out = nullptr;
284
+ size_t out_len = 0;
285
+ jl_handle_invoke(cmd.c_str(), cmd.size(), args.c_str(), args.size(), &out,
286
+ &out_len);
287
+ std::string reply = take_result(out, out_len);
288
+
289
+ // An async command answers later: the library returns {"pending":<id>} and
290
+ // we hold the page's promise until host_settle() tells us what it is.
291
+ std::string pending = webview::detail::json_parse(reply, "pending", 0);
292
+ if (!pending.empty()) {
293
+ hold_pending(std::strtoll(pending.c_str(), nullptr, 10), binding_id);
294
+ return;
295
+ }
296
+
297
+ // Otherwise the reply is already the envelope the bootstrap expects.
298
+ settle(binding_id, reply);
299
+ }
300
+
301
+ } // namespace
302
+
303
+ int main() {
304
+ // Registration is a pure store and is legal before init; the panic sink and
305
+ // every channel must be in place before any TypeScript runs, since setup()
306
+ // executes during jl_init().
307
+ jl_set_panic_sink(panic_sink, nullptr);
308
+ if (jl_set_callback("hostSchedule", (void (*)(void))host_schedule, nullptr) ||
309
+ jl_set_callback("hostSettle", (void (*)(void))host_settle, nullptr) ||
310
+ jl_set_callback("hostReadFile", (void (*)(void))host_read_file, nullptr) ||
311
+ jl_set_callback("hostWriteFile", (void (*)(void))host_write_file,
312
+ nullptr) ||
313
+ jl_set_callback("janelaEmit", (void (*)(void))emit_event, nullptr)) {
314
+ std::fprintf(stderr, "[janela] could not register a host channel\n");
315
+ }
316
+ jl_init();
317
+
318
+ webview::webview w(false, nullptr);
319
+ g_webview = &w;
320
+ w.init(kBootstrap);
321
+ w.bind("__janelaInvoke", on_invoke, nullptr);
322
+
323
+ char *out = nullptr;
324
+ size_t out_len = 0;
325
+ jl_index_html(&out, &out_len);
326
+ w.set_html(take_result(out, out_len));
327
+
328
+ w.run(); // enters UIApplicationMain; does not return
329
+ return 0;
330
+ }
@@ -30,6 +30,7 @@
30
30
 
31
31
  #include "detail/backends/cocoa_webkit.hh"
32
32
  #include "detail/backends/gtk_webkitgtk.hh"
33
+ #include "detail/backends/uikit_webkit.hh"
33
34
  #include "detail/backends/win32_edge.hh"
34
35
 
35
36
  namespace webview {
@@ -55,7 +55,10 @@
55
55
  #include <memory>
56
56
  #include <string>
57
57
 
58
- #include <objc/objc-runtime.h>
58
+ // <objc/objc-runtime.h> is an umbrella that only exists in the macOS SDK;
59
+ // these two are present on every Apple platform.
60
+ #include <objc/message.h>
61
+ #include <objc/runtime.h>
59
62
 
60
63
  namespace webview {
61
64
  namespace detail {