bunite-core 0.0.1 → 0.0.4

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/package.json +3 -2
  2. package/src/bun/core/App.ts +155 -15
  3. package/src/bun/core/BrowserView.ts +124 -44
  4. package/src/bun/core/BrowserWindow.ts +94 -47
  5. package/src/bun/core/Socket.ts +2 -1
  6. package/src/bun/core/SurfaceBrowserIPC.ts +65 -0
  7. package/src/bun/core/SurfaceManager.ts +201 -0
  8. package/src/bun/core/SurfaceRegistry.ts +60 -0
  9. package/src/bun/core/Utils.ts +275 -46
  10. package/src/bun/events/appEvents.ts +2 -1
  11. package/src/bun/events/webviewEvents.ts +1 -3
  12. package/src/bun/events/windowEvents.ts +2 -0
  13. package/src/bun/index.ts +4 -3
  14. package/src/bun/preload/inline.ts +19 -25
  15. package/src/bun/proc/native.ts +158 -122
  16. package/src/native/shared/callbacks.h +6 -6
  17. package/src/native/shared/ffi_exports.h +123 -119
  18. package/src/native/shared/log.h +24 -0
  19. package/src/native/shared/webview_storage.h +5 -5
  20. package/src/native/win/native_host_appres.cpp +258 -0
  21. package/src/native/win/native_host_cef.cpp +834 -0
  22. package/src/native/win/native_host_ffi.cpp +935 -0
  23. package/src/native/win/native_host_internal.h +285 -0
  24. package/src/native/win/native_host_runtime.cpp +286 -0
  25. package/src/native/win/native_host_utils.cpp +314 -0
  26. package/src/native/win/process_helper_win.cpp +126 -26
  27. package/src/preload/runtime.built.js +1 -1
  28. package/src/preload/runtime.ts +65 -42
  29. package/src/preload/tsconfig.json +2 -1
  30. package/src/preload/tsconfig.tsbuildinfo +1 -0
  31. package/src/preload/webviewElement.ts +307 -0
  32. package/src/shared/cefVersion.ts +2 -0
  33. package/src/shared/log.ts +40 -0
  34. package/src/shared/paths.ts +122 -52
  35. package/src/shared/rpc.ts +7 -1
  36. package/src/shared/webviewPolyfill.ts +80 -0
  37. package/src/view/index.ts +8 -5
  38. package/src/native/shared/cef_response_filter.h +0 -116
  39. package/src/native/win/native_host.cpp +0 -2453
  40. package/src/types/config.ts +0 -29
@@ -0,0 +1,314 @@
1
+ #include "native_host_internal.h"
2
+
3
+ namespace bunite_win {
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // String / encoding utilities
7
+ // ---------------------------------------------------------------------------
8
+
9
+ std::wstring utf8ToWide(const std::string& value) {
10
+ if (value.empty()) {
11
+ return {};
12
+ }
13
+
14
+ const int required = MultiByteToWideChar(CP_UTF8, 0, value.c_str(), -1, nullptr, 0);
15
+ if (required <= 0) {
16
+ return {};
17
+ }
18
+
19
+ std::wstring converted(required - 1, L'\0');
20
+ MultiByteToWideChar(CP_UTF8, 0, value.c_str(), -1, converted.data(), required);
21
+ return converted;
22
+ }
23
+
24
+ std::string escapeJsonString(const std::string& value) {
25
+ std::string escaped;
26
+ escaped.reserve(value.size());
27
+
28
+ for (const unsigned char ch : value) {
29
+ switch (ch) {
30
+ case '\\':
31
+ escaped += "\\\\";
32
+ break;
33
+ case '"':
34
+ escaped += "\\\"";
35
+ break;
36
+ case '\n':
37
+ escaped += "\\n";
38
+ break;
39
+ case '\r':
40
+ escaped += "\\r";
41
+ break;
42
+ case '\t':
43
+ escaped += "\\t";
44
+ break;
45
+ default:
46
+ if (ch < 0x20) {
47
+ char buffer[7];
48
+ std::snprintf(buffer, sizeof(buffer), "\\u%04x", ch);
49
+ escaped += buffer;
50
+ } else {
51
+ escaped.push_back(static_cast<char>(ch));
52
+ }
53
+ break;
54
+ }
55
+ }
56
+
57
+ return escaped;
58
+ }
59
+
60
+ std::vector<std::string> splitButtonLabels(const std::string& buttons_csv) {
61
+ std::vector<std::string> labels;
62
+ if (buttons_csv.empty()) {
63
+ return labels;
64
+ }
65
+
66
+ std::stringstream stream(buttons_csv);
67
+ std::string label;
68
+ while (std::getline(stream, label, '\x1f')) {
69
+ const size_t first = label.find_first_not_of(" \t");
70
+ if (first == std::string::npos) {
71
+ continue;
72
+ }
73
+ const size_t last = label.find_last_not_of(" \t");
74
+ std::string normalized = label.substr(first, last - first + 1);
75
+ std::transform(
76
+ normalized.begin(),
77
+ normalized.end(),
78
+ normalized.begin(),
79
+ [](unsigned char ch) { return static_cast<char>(std::tolower(ch)); }
80
+ );
81
+ if (!normalized.empty()) {
82
+ labels.push_back(normalized);
83
+ }
84
+ }
85
+
86
+ return labels;
87
+ }
88
+
89
+ std::string trimAsciiWhitespace(const std::string& value) {
90
+ const size_t first = value.find_first_not_of(" \t\r\n");
91
+ if (first == std::string::npos) {
92
+ return {};
93
+ }
94
+ const size_t last = value.find_last_not_of(" \t\r\n");
95
+ return value.substr(first, last - first + 1);
96
+ }
97
+
98
+ std::string toLowerAscii(std::string value) {
99
+ std::transform(
100
+ value.begin(),
101
+ value.end(),
102
+ value.begin(),
103
+ [](unsigned char ch) { return static_cast<char>(std::tolower(ch)); }
104
+ );
105
+ return value;
106
+ }
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // Chromium flags parsing
110
+ // ---------------------------------------------------------------------------
111
+
112
+
113
+ // Simple flat JSON object parser for chromiumFlags.
114
+ // Input is always a JSON-serialized Record<string, string | boolean> from TS.
115
+ // Does not depend on CEF, so it can run before CefInitialize.
116
+ std::map<std::string, std::string> parseChromiumFlagsJson(const std::string& json) {
117
+ std::map<std::string, std::string> flags;
118
+ if (json.empty()) {
119
+ return flags;
120
+ }
121
+
122
+ size_t pos = json.find('{');
123
+ if (pos == std::string::npos) {
124
+ return flags;
125
+ }
126
+ ++pos;
127
+
128
+ auto skipWhitespace = [&]() {
129
+ while (pos < json.size() && (json[pos] == ' ' || json[pos] == '\t' || json[pos] == '\n' || json[pos] == '\r')) {
130
+ ++pos;
131
+ }
132
+ };
133
+
134
+ auto parseString = [&]() -> std::string {
135
+ if (pos >= json.size() || json[pos] != '"') {
136
+ return {};
137
+ }
138
+ ++pos;
139
+ std::string result;
140
+ while (pos < json.size() && json[pos] != '"') {
141
+ if (json[pos] == '\\' && pos + 1 < json.size()) {
142
+ ++pos;
143
+ }
144
+ result += json[pos++];
145
+ }
146
+ if (pos < json.size()) {
147
+ ++pos; // closing quote
148
+ }
149
+ return result;
150
+ };
151
+
152
+ while (pos < json.size()) {
153
+ skipWhitespace();
154
+ if (pos >= json.size() || json[pos] == '}') {
155
+ break;
156
+ }
157
+ if (json[pos] == ',') {
158
+ ++pos;
159
+ continue;
160
+ }
161
+
162
+ std::string key = parseString();
163
+ if (key.empty()) {
164
+ break;
165
+ }
166
+
167
+ skipWhitespace();
168
+ if (pos >= json.size() || json[pos] != ':') {
169
+ break;
170
+ }
171
+ ++pos;
172
+ skipWhitespace();
173
+
174
+ if (pos >= json.size()) {
175
+ break;
176
+ }
177
+
178
+ if (json[pos] == '"') {
179
+ flags[key] = parseString();
180
+ } else if (json.compare(pos, 4, "true") == 0) {
181
+ flags[key] = "true";
182
+ pos += 4;
183
+ } else if (json.compare(pos, 5, "false") == 0) {
184
+ flags[key] = "false";
185
+ pos += 5;
186
+ } else {
187
+ while (pos < json.size() && json[pos] != ',' && json[pos] != '}') {
188
+ ++pos;
189
+ }
190
+ }
191
+ }
192
+
193
+ return flags;
194
+ }
195
+
196
+ // ---------------------------------------------------------------------------
197
+ // Navigation rules
198
+ // ---------------------------------------------------------------------------
199
+
200
+ bool globMatchCaseInsensitive(const std::string& pattern, const std::string& value) {
201
+ size_t pattern_index = 0;
202
+ size_t value_index = 0;
203
+ size_t star_pattern_index = std::string::npos;
204
+ size_t star_value_index = 0;
205
+
206
+ while (value_index < value.size()) {
207
+ if (
208
+ pattern_index < pattern.size() &&
209
+ std::tolower(static_cast<unsigned char>(pattern[pattern_index])) ==
210
+ std::tolower(static_cast<unsigned char>(value[value_index]))
211
+ ) {
212
+ pattern_index += 1;
213
+ value_index += 1;
214
+ } else if (pattern_index < pattern.size() && pattern[pattern_index] == '*') {
215
+ star_pattern_index = pattern_index++;
216
+ star_value_index = value_index;
217
+ } else if (star_pattern_index != std::string::npos) {
218
+ pattern_index = star_pattern_index + 1;
219
+ value_index = ++star_value_index;
220
+ } else {
221
+ return false;
222
+ }
223
+ }
224
+
225
+ while (pattern_index < pattern.size() && pattern[pattern_index] == '*') {
226
+ pattern_index += 1;
227
+ }
228
+
229
+ return pattern_index == pattern.size();
230
+ }
231
+
232
+ std::vector<std::string> parseNavigationRulesJson(const std::string& rules_json) {
233
+ std::vector<std::string> rules;
234
+ if (rules_json.empty()) {
235
+ return rules;
236
+ }
237
+
238
+ CefRefPtr<CefValue> parsed = CefParseJSON(rules_json, JSON_PARSER_RFC);
239
+ if (!parsed || parsed->GetType() != VTYPE_LIST) {
240
+ return rules;
241
+ }
242
+
243
+ CefRefPtr<CefListValue> list = parsed->GetList();
244
+ if (!list) {
245
+ return rules;
246
+ }
247
+
248
+ rules.reserve(list->GetSize());
249
+ for (size_t index = 0; index < list->GetSize(); index += 1) {
250
+ if (list->GetType(index) != VTYPE_STRING) {
251
+ continue;
252
+ }
253
+
254
+ const std::string rule = list->GetString(index).ToString();
255
+ if (!rule.empty()) {
256
+ rules.push_back(rule);
257
+ }
258
+ }
259
+
260
+ return rules;
261
+ }
262
+
263
+ bool shouldAlwaysAllowNavigationUrl(const std::string& url) {
264
+ return url == "about:blank" || url.rfind("appres://app.internal/internal/", 0) == 0;
265
+ }
266
+
267
+ bool shouldAllowNavigation(const ViewHost* view, const std::string& url) {
268
+ if (!view || shouldAlwaysAllowNavigationUrl(url) || view->navigation_rules.empty()) {
269
+ return true;
270
+ }
271
+
272
+ bool allowed = true; // Match electrobun's last-match-wins, default-allow semantics.
273
+ for (const std::string& raw_rule : view->navigation_rules) {
274
+ const bool is_block_rule = !raw_rule.empty() && raw_rule.front() == '^';
275
+ const std::string pattern = is_block_rule ? raw_rule.substr(1) : raw_rule;
276
+
277
+ if (pattern.empty()) {
278
+ continue;
279
+ }
280
+ if (globMatchCaseInsensitive(pattern, url)) {
281
+ allowed = !is_block_rule;
282
+ }
283
+ }
284
+
285
+ return allowed;
286
+ }
287
+
288
+ // ---------------------------------------------------------------------------
289
+ // Event emit
290
+ // ---------------------------------------------------------------------------
291
+
292
+ void emitWindowEvent(uint32_t window_id, const char* event_name, const std::string& payload) {
293
+ BuniteWindowEventHandler handler = nullptr;
294
+ {
295
+ std::lock_guard<std::mutex> lock(g_runtime.lifecycle_mutex);
296
+ handler = g_runtime.window_event_handler;
297
+ }
298
+ if (handler) {
299
+ handler(window_id, _strdup(event_name ? event_name : ""), _strdup(payload.c_str()));
300
+ }
301
+ }
302
+
303
+ void emitWebviewEvent(uint32_t view_id, const char* event_name, const std::string& payload) {
304
+ BuniteWebviewEventHandler handler = nullptr;
305
+ {
306
+ std::lock_guard<std::mutex> lock(g_runtime.lifecycle_mutex);
307
+ handler = g_runtime.webview_event_handler;
308
+ }
309
+ if (handler) {
310
+ handler(view_id, _strdup(event_name ? event_name : ""), _strdup(payload.c_str()));
311
+ }
312
+ }
313
+
314
+ } // namespace bunite_win
@@ -1,26 +1,126 @@
1
- #include "include/cef_app.h"
2
-
3
- #include <windows.h>
4
-
5
- class BuniteHelperApp : public CefApp {
6
- public:
7
- void OnRegisterCustomSchemes(CefRawPtr<CefSchemeRegistrar> registrar) override {
8
- registrar->AddCustomScheme(
9
- "views",
10
- CEF_SCHEME_OPTION_STANDARD |
11
- CEF_SCHEME_OPTION_CORS_ENABLED |
12
- CEF_SCHEME_OPTION_SECURE |
13
- CEF_SCHEME_OPTION_CSP_BYPASSING |
14
- CEF_SCHEME_OPTION_FETCH_ENABLED
15
- );
16
- }
17
-
18
- private:
19
- IMPLEMENT_REFCOUNTING(BuniteHelperApp);
20
- };
21
-
22
- int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int) {
23
- CefMainArgs main_args(hInstance);
24
- CefRefPtr<CefApp> app = new BuniteHelperApp();
25
- return CefExecuteProcess(main_args, app, nullptr);
26
- }
1
+ #include "include/cef_app.h"
2
+ #include "include/cef_parser.h"
3
+ #include "include/cef_v8.h"
4
+
5
+ #include <windows.h>
6
+
7
+ #include <map>
8
+ #include <string>
9
+
10
+ namespace {
11
+
12
+ struct PreloadScriptInfo {
13
+ std::string script;
14
+ std::vector<std::string> allowed_origins; // e.g. "http://localhost:3000"
15
+ };
16
+
17
+ std::string getUrlOrigin(const std::string& url) {
18
+ CefURLParts parts;
19
+ if (!CefParseURL(url, parts)) {
20
+ return "";
21
+ }
22
+ const std::string scheme = CefString(&parts.scheme).ToString();
23
+ const std::string host = CefString(&parts.host).ToString();
24
+ const std::string port = CefString(&parts.port).ToString();
25
+ if (scheme.empty() || host.empty()) return "";
26
+ if (port.empty()) return scheme + "://" + host;
27
+ return scheme + "://" + host + ":" + port;
28
+ }
29
+
30
+ } // namespace
31
+
32
+ class BuniteHelperApp : public CefApp, public CefRenderProcessHandler {
33
+ public:
34
+ CefRefPtr<CefRenderProcessHandler> GetRenderProcessHandler() override {
35
+ return this;
36
+ }
37
+
38
+ void OnRegisterCustomSchemes(CefRawPtr<CefSchemeRegistrar> registrar) override {
39
+ registrar->AddCustomScheme(
40
+ "appres",
41
+ CEF_SCHEME_OPTION_STANDARD |
42
+ CEF_SCHEME_OPTION_CORS_ENABLED |
43
+ CEF_SCHEME_OPTION_SECURE |
44
+ CEF_SCHEME_OPTION_CSP_BYPASSING |
45
+ CEF_SCHEME_OPTION_FETCH_ENABLED
46
+ );
47
+ }
48
+
49
+ void OnBrowserCreated(
50
+ CefRefPtr<CefBrowser> browser,
51
+ CefRefPtr<CefDictionaryValue> extra_info
52
+ ) override {
53
+ if (extra_info && (extra_info->HasKey("preloadScript") || extra_info->HasKey("preloadOrigins"))) {
54
+ PreloadScriptInfo info;
55
+ if (extra_info->HasKey("preloadScript")) {
56
+ info.script = extra_info->GetString("preloadScript").ToString();
57
+ }
58
+ if (extra_info->HasKey("preloadOrigins")) {
59
+ auto list = extra_info->GetList("preloadOrigins");
60
+ for (size_t i = 0; i < list->GetSize(); ++i) {
61
+ info.allowed_origins.push_back(list->GetString(i).ToString());
62
+ }
63
+ }
64
+ preload_scripts_[browser->GetIdentifier()] = std::move(info);
65
+ }
66
+ }
67
+
68
+ void OnBrowserDestroyed(CefRefPtr<CefBrowser> browser) override {
69
+ preload_scripts_.erase(browser->GetIdentifier());
70
+ }
71
+
72
+ void OnContextCreated(
73
+ CefRefPtr<CefBrowser> browser,
74
+ CefRefPtr<CefFrame> frame,
75
+ CefRefPtr<CefV8Context> context
76
+ ) override {
77
+ if (!frame->IsMain()) return;
78
+
79
+ const std::string url = frame->GetURL().ToString();
80
+ if (url.empty() || url == "about:blank") return;
81
+
82
+ const auto it = preload_scripts_.find(browser->GetIdentifier());
83
+ if (it == preload_scripts_.end() || it->second.script.empty()) return;
84
+
85
+ const bool is_appres = url.rfind("appres://app.internal/", 0) == 0;
86
+ bool is_allowed_origin = false;
87
+ if (!it->second.allowed_origins.empty()) {
88
+ const std::string origin = getUrlOrigin(url);
89
+ for (const auto& allowed : it->second.allowed_origins) {
90
+ if (origin == allowed) { is_allowed_origin = true; break; }
91
+ }
92
+ }
93
+ if (!is_appres && !is_allowed_origin) return;
94
+
95
+ // Skip isolated-world contexts (DevTools overlay, extensions, etc.) that
96
+ // lack full Web APIs. The page's main-world context has customElements;
97
+ // DevTools-injected contexts do not.
98
+ context->Enter();
99
+ CefRefPtr<CefV8Value> ce = context->GetGlobal()->GetValue("customElements");
100
+ bool is_main_world = ce && !ce->IsNull() && !ce->IsUndefined();
101
+ context->Exit();
102
+ if (!is_main_world) return;
103
+
104
+ CefRefPtr<CefV8Value> retval;
105
+ CefRefPtr<CefV8Exception> exception;
106
+ bool ok = context->Eval(it->second.script, "bunite://preload", 0, retval, exception);
107
+ if (!ok && exception) {
108
+ std::string msg = exception->GetMessage().ToString();
109
+ int line = exception->GetLineNumber();
110
+ std::string src_line = exception->GetSourceLine().ToString();
111
+ LOG(ERROR) << "bunite preload eval failed at line " << line
112
+ << ": " << msg << "\n " << src_line;
113
+ }
114
+ }
115
+
116
+ private:
117
+ std::map<int, PreloadScriptInfo> preload_scripts_;
118
+
119
+ IMPLEMENT_REFCOUNTING(BuniteHelperApp);
120
+ };
121
+
122
+ int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int) {
123
+ CefMainArgs main_args(hInstance);
124
+ CefRefPtr<CefApp> app = new BuniteHelperApp();
125
+ return CefExecuteProcess(main_args, app, nullptr);
126
+ }
@@ -1 +1 @@
1
- var W=(()=>{let F;return()=>{if(!F){let q=Uint8Array.from(atob(__buniteSecretKeyBase64),(H)=>H.charCodeAt(0));F=crypto.subtle.importKey("raw",q,"AES-GCM",!1,["encrypt","decrypt"])}return F}})();async function X(F){let q=await W(),H=crypto.getRandomValues(new Uint8Array(12)),A=new Uint8Array(await crypto.subtle.encrypt({name:"AES-GCM",iv:H},q,F)),z=new Uint8Array(1+H.length+A.length);return z[0]=1,z.set(H,1),z.set(A,1+H.length),z}async function Y(F){if(F.length<29)throw Error("Invalid bunite RPC frame.");if(F[0]!==1)throw Error("Unsupported bunite RPC frame version.");let q=await W(),H=F.slice(1,13),A=F.slice(13);return new Uint8Array(await crypto.subtle.decrypt({name:"AES-GCM",iv:H},q,A))}function B(F){let q=[];function H(A){if(A===null||A===void 0)q.push(192);else if(A===!0)q.push(195);else if(A===!1)q.push(194);else if(typeof A==="number")if(Number.isInteger(A)&&A>=0&&A<128)q.push(A);else if(Number.isInteger(A)&&A>=-32&&A<0)q.push(A&255);else if(Number.isInteger(A)&&A>=0&&A<=65535)q.push(205,A>>8&255,A&255);else if(Number.isInteger(A)&&A>=0&&A<=4294967295)q.push(206,A>>24&255,A>>16&255,A>>8&255,A&255);else{let z=new ArrayBuffer(9),G=new DataView(z);G.setUint8(0,203),G.setFloat64(1,A);for(let J=0;J<9;J++)q.push(G.getUint8(J))}else if(typeof A==="string"){let z=new TextEncoder().encode(A);if(z.length<32)q.push(160|z.length);else if(z.length<256)q.push(217,z.length);else if(z.length<65536)q.push(218,z.length>>8&255,z.length&255);else q.push(219,z.length>>24&255,z.length>>16&255,z.length>>8&255,z.length&255);for(let G=0;G<z.length;G++)q.push(z[G])}else if(Array.isArray(A)){if(A.length<16)q.push(144|A.length);else if(A.length<65536)q.push(220,A.length>>8&255,A.length&255);else q.push(221,A.length>>24&255,A.length>>16&255,A.length>>8&255,A.length&255);A.forEach(H)}else if(typeof A==="object"){let z=Object.keys(A);if(z.length<16)q.push(128|z.length);else if(z.length<65536)q.push(222,z.length>>8&255,z.length&255);else q.push(223,z.length>>24&255,z.length>>16&255,z.length>>8&255,z.length&255);for(let G of z)H(G),H(A[G])}}return H(F),new Uint8Array(q)}function I(F){let q=0;function H(){let A=F[q++];if(A<=127)return A;if(A>=224)return A-256;if(A===192)return null;if(A===194)return!1;if(A===195)return!0;if(A===204)return F[q++];if(A===205){let z=F[q]<<8|F[q+1];return q+=2,z}if(A===206){let z=(F[q]<<24|F[q+1]<<16|F[q+2]<<8|F[q+3])>>>0;return q+=4,z}if(A===203){let z=new DataView(F.buffer,F.byteOffset+q,8);return q+=8,z.getFloat64(0)}if(A===208){let z=F[q++];return z>127?z-256:z}if(A===209){let z=F[q]<<8|F[q+1];return q+=2,z>32767?z-65536:z}if(A===210){let z=F[q]<<24|F[q+1]<<16|F[q+2]<<8|F[q+3];return q+=4,z}if((A&224)===160){let z=A&31,G=new TextDecoder().decode(F.subarray(q,q+z));return q+=z,G}if(A===217){let z=F[q++],G=new TextDecoder().decode(F.subarray(q,q+z));return q+=z,G}if(A===218){let z=F[q]<<8|F[q+1];q+=2;let G=new TextDecoder().decode(F.subarray(q,q+z));return q+=z,G}if(A===219){let z=(F[q]<<24|F[q+1]<<16|F[q+2]<<8|F[q+3])>>>0;q+=4;let G=new TextDecoder().decode(F.subarray(q,q+z));return q+=z,G}if((A&240)===144){let z=A&15,G=[];for(let J=0;J<z;J++)G.push(H());return G}if(A===220){let z=F[q]<<8|F[q+1];q+=2;let G=[];for(let J=0;J<z;J++)G.push(H());return G}if(A===221){let z=(F[q]<<24|F[q+1]<<16|F[q+2]<<8|F[q+3])>>>0;q+=4;let G=[];for(let J=0;J<z;J++)G.push(H());return G}if((A&240)===128){let z=A&15,G={};for(let J=0;J<z;J++)G[H()]=H();return G}if(A===222){let z=F[q]<<8|F[q+1];q+=2;let G={};for(let J=0;J<z;J++)G[H()]=H();return G}if(A===223){let z=(F[q]<<24|F[q+1]<<16|F[q+2]<<8|F[q+3])>>>0;q+=4;let G={};for(let J=0;J<z;J++)G[H()]=H();return G}if(A===196){let z=F[q++],G=F.slice(q,q+z);return q+=z,G}if(A===197){let z=F[q]<<8|F[q+1];q+=2;let G=F.slice(q,q+z);return q+=z,G}return}return H()}var N=window;N.__bunite??={};N.__buniteWebviewId=__buniteWebviewId;N.__buniteRpcSocketPort=__buniteRpcSocketPort;N.__bunite_encrypt=X;N.__bunite_decrypt=Y;N.bunite=N.__bunite;N.bunite.invoke=(()=>{let F=null,q=1,H=new Map;function A(){let G=N.__bunite?._socket;if(G&&G.readyState<=WebSocket.OPEN&&G!==F)return F=G,z(G),G;if(F&&F.readyState<=WebSocket.OPEN)return F;return F=new WebSocket(`ws://localhost:${__buniteRpcSocketPort}/socket?webviewId=${__buniteWebviewId}`),F.binaryType="arraybuffer",N.__bunite._socket=F,z(F),F}function z(G){G.addEventListener("message",async(J)=>{try{let Q=await Y(new Uint8Array(J.data)),M=I(Q);if(M?.type==="response"&&M.scope==="global"){let O=H.get(M.id);if(O)H.delete(M.id),clearTimeout(O.timeout),M.success?O.resolve(M.payload):O.reject(Error(M.error||"Unknown error"))}}catch{}})}return(G,J)=>new Promise((Q,M)=>{let O=A(),T=q++,Z=setTimeout(()=>{H.delete(T),M(Error(`bunite.invoke timed out: ${G}`))},15000);H.set(T,{resolve:Q,reject:M,timeout:Z});let _={type:"request",id:T,method:G,params:J??null,scope:"global"},U=async()=>{let $=await X(B(_));O.send($.buffer)};if(O.readyState===WebSocket.OPEN)U();else O.addEventListener("open",()=>U(),{once:!0})})})();
1
+ class _{element;onBoundsChange;observer=null;rafId=0;lastRect={x:0,y:0,width:0,height:0};dirty=!1;stopped=!1;constructor(z,q){this.element=z,this.onBoundsChange=q}start(){this.observer=new ResizeObserver(()=>this.markDirty()),this.observer.observe(this.element),this.scheduleFrame()}stop(){if(this.stopped=!0,this.observer?.disconnect(),this.observer=null,this.rafId)cancelAnimationFrame(this.rafId),this.rafId=0}markDirty(){this.dirty=!0}scheduleFrame(){if(this.stopped)return;this.rafId=requestAnimationFrame(()=>{this.flush(),this.scheduleFrame()})}flush(){let z=window.devicePixelRatio||1,q=this.element.getBoundingClientRect(),J={x:Math.round(q.x*z),y:Math.round(q.y*z),width:Math.round(q.width*z),height:Math.round(q.height*z)};if(!this.dirty&&J.x===this.lastRect.x&&J.y===this.lastRect.y&&J.width===this.lastRect.width&&J.height===this.lastRect.height)return;this.dirty=!1,this.lastRect=J,this.onBoundsChange(J)}}class $ extends HTMLElement{static observedAttributes=["src"];_surfaceId=null;_syncCtrl=null;_initPromise=null;_aborted=!1;_pendingSrc=null;_syncHidden=!1;_userHidden=!1;_layoutObserver=null;_unsubNavigate=null;constructor(){super()}connectedCallback(){this._aborted=!1,this._syncHidden=!1,this._userHidden=!1,this._unsubNavigate=bunite.on("__bunite:webview.didNavigate",(z)=>{if(z?.surfaceId===this._surfaceId)this.dispatchEvent(new CustomEvent("did-navigate",{detail:{url:z.url}}))}),this._waitForLayout()}_waitForLayout(){if(this._layoutObserver)return;let z=()=>{if(!this.isConnected||this._aborted)return!0;let q=this.getBoundingClientRect();if(q.width>0&&q.height>0){if(this.getAttribute("src")||this._pendingSrc||"")this.initSurface();return!0}return!1};requestAnimationFrame(()=>{if(z())return;this._layoutObserver=new ResizeObserver(()=>{if(z())this._layoutObserver?.disconnect(),this._layoutObserver=null}),this._layoutObserver.observe(this)})}disconnectedCallback(){if(this._aborted=!0,this._unsubNavigate?.(),this._unsubNavigate=null,this._layoutObserver?.disconnect(),this._layoutObserver=null,this._syncCtrl?.stop(),this._syncCtrl=null,this._surfaceId!=null){let z=this._surfaceId;this._surfaceId=null,bunite.invoke("__bunite:surface.remove",{surfaceId:z}).catch(()=>{})}else if(this._initPromise)this._initPromise.then((z)=>{bunite.invoke("__bunite:surface.remove",{surfaceId:z.surfaceId}).catch(()=>{})}).catch(()=>{});this._initPromise=null}attributeChangedCallback(z,q,J){if(z!=="src")return;if(this._surfaceId!=null)bunite.invoke("__bunite:webview.navigate",{surfaceId:this._surfaceId,url:J||""}).catch(()=>{});else if(this._initPromise)this._pendingSrc=J||"";else if(this.isConnected&&!this._aborted&&J)this._waitForLayout()}setHidden(z){this._userHidden=z,this._applySurfaceHidden()}goBack(){if(this._surfaceId!=null)bunite.invoke("__bunite:webview.goBack",{surfaceId:this._surfaceId}).catch(()=>{})}reload(){if(this._surfaceId!=null)bunite.invoke("__bunite:webview.reload",{surfaceId:this._surfaceId}).catch(()=>{})}navigate(z){this.setAttribute("src",z)}_applySurfaceHidden(){if(this._surfaceId==null)return;bunite.invoke("__bunite:surface.setHidden",{surfaceId:this._surfaceId,hidden:this._userHidden||this._syncHidden}).catch(()=>{})}initSurface(){if(this._surfaceId!=null||this._initPromise!=null)return;let z=window.devicePixelRatio||1,q=this.getBoundingClientRect(),J=this._pendingSrc||this.getAttribute("src")||"";this._pendingSrc=null;let F=bunite.invoke("__bunite:surface.init",{src:J,x:Math.round(q.x*z),y:Math.round(q.y*z),width:Math.round(q.width*z),height:Math.round(q.height*z),hidden:this._userHidden});this._initPromise=F,F.then((G)=>{if(this._initPromise!==F)return;if(this._aborted){bunite.invoke("__bunite:surface.remove",{surfaceId:G.surfaceId}).catch(()=>{});return}if(this._surfaceId=G.surfaceId,this._userHidden)this._applySurfaceHidden();if(this._pendingSrc!=null){let H=this._pendingSrc;this._pendingSrc=null,bunite.invoke("__bunite:webview.navigate",{surfaceId:this._surfaceId,url:H}).catch(()=>{})}this._syncCtrl=new _(this,(H)=>{if(this._surfaceId==null)return;if(H.width===0&&H.height===0){if(!this._syncHidden)this._syncHidden=!0,this._applySurfaceHidden();return}if(this._syncHidden)this._syncHidden=!1,this._applySurfaceHidden();bunite.invoke("__bunite:surface.resize",{surfaceId:this._surfaceId,x:H.x,y:H.y,w:H.width,h:H.height}).catch(()=>{})}),this._syncCtrl.start()}).catch(()=>{}).finally(()=>{if(this._initPromise===F)this._initPromise=null})}}if(typeof customElements<"u"){customElements.define("bunite-webview",$);let z=()=>bunite.invoke("__bunite:surface.bringAllVisiblesToFront").catch(()=>{});document.addEventListener("pointerdown",z,!0),document.addEventListener("dragstart",()=>{bunite.invoke("__bunite:surface.setAllPassthrough",{passthrough:!0}).catch(()=>{})},!0),document.addEventListener("dragend",()=>{bunite.invoke("__bunite:surface.setAllPassthrough",{passthrough:!1}).catch(()=>{}),z()},!0)}var Z=(()=>{let z;return()=>{if(!z){let q=Uint8Array.from(atob(__buniteSecretKeyBase64),(J)=>J.charCodeAt(0));z=crypto.subtle.importKey("raw",q,"AES-GCM",!1,["encrypt","decrypt"])}return z}})();async function K(z){let q=await Z(),J=crypto.getRandomValues(new Uint8Array(12)),F=new Uint8Array(await crypto.subtle.encrypt({name:"AES-GCM",iv:J},q,z)),G=new Uint8Array(1+J.length+F.length);return G[0]=1,G.set(J,1),G.set(F,1+J.length),G}async function W(z){if(z.length<29)throw Error("Invalid bunite RPC frame.");if(z[0]!==1)throw Error("Unsupported bunite RPC frame version.");let q=await Z(),J=z.slice(1,13),F=z.slice(13);return new Uint8Array(await crypto.subtle.decrypt({name:"AES-GCM",iv:J},q,F))}function I(z){let q=[];function J(F){if(F===null||F===void 0)q.push(192);else if(F===!0)q.push(195);else if(F===!1)q.push(194);else if(typeof F==="number")if(Number.isInteger(F)&&F>=0&&F<128)q.push(F);else if(Number.isInteger(F)&&F>=-32&&F<0)q.push(F&255);else if(Number.isInteger(F)&&F>=0&&F<=65535)q.push(205,F>>8&255,F&255);else if(Number.isInteger(F)&&F>=0&&F<=4294967295)q.push(206,F>>24&255,F>>16&255,F>>8&255,F&255);else{let G=new ArrayBuffer(9),H=new DataView(G);H.setUint8(0,203),H.setFloat64(1,F);for(let M=0;M<9;M++)q.push(H.getUint8(M))}else if(typeof F==="string"){let G=new TextEncoder().encode(F);if(G.length<32)q.push(160|G.length);else if(G.length<256)q.push(217,G.length);else if(G.length<65536)q.push(218,G.length>>8&255,G.length&255);else q.push(219,G.length>>24&255,G.length>>16&255,G.length>>8&255,G.length&255);for(let H=0;H<G.length;H++)q.push(G[H])}else if(Array.isArray(F)){if(F.length<16)q.push(144|F.length);else if(F.length<65536)q.push(220,F.length>>8&255,F.length&255);else q.push(221,F.length>>24&255,F.length>>16&255,F.length>>8&255,F.length&255);F.forEach(J)}else if(typeof F==="object"){let G=Object.keys(F);if(G.length<16)q.push(128|G.length);else if(G.length<65536)q.push(222,G.length>>8&255,G.length&255);else q.push(223,G.length>>24&255,G.length>>16&255,G.length>>8&255,G.length&255);for(let H of G)J(H),J(F[H])}}return J(z),new Uint8Array(q)}function D(z){let q=0;function J(){let F=z[q++];if(F<=127)return F;if(F>=224)return F-256;if(F===192)return null;if(F===194)return!1;if(F===195)return!0;if(F===204)return z[q++];if(F===205){let G=z[q]<<8|z[q+1];return q+=2,G}if(F===206){let G=(z[q]<<24|z[q+1]<<16|z[q+2]<<8|z[q+3])>>>0;return q+=4,G}if(F===203){let G=new DataView(z.buffer,z.byteOffset+q,8);return q+=8,G.getFloat64(0)}if(F===208){let G=z[q++];return G>127?G-256:G}if(F===209){let G=z[q]<<8|z[q+1];return q+=2,G>32767?G-65536:G}if(F===210){let G=z[q]<<24|z[q+1]<<16|z[q+2]<<8|z[q+3];return q+=4,G}if((F&224)===160){let G=F&31,H=new TextDecoder().decode(z.subarray(q,q+G));return q+=G,H}if(F===217){let G=z[q++],H=new TextDecoder().decode(z.subarray(q,q+G));return q+=G,H}if(F===218){let G=z[q]<<8|z[q+1];q+=2;let H=new TextDecoder().decode(z.subarray(q,q+G));return q+=G,H}if(F===219){let G=(z[q]<<24|z[q+1]<<16|z[q+2]<<8|z[q+3])>>>0;q+=4;let H=new TextDecoder().decode(z.subarray(q,q+G));return q+=G,H}if((F&240)===144){let G=F&15,H=[];for(let M=0;M<G;M++)H.push(J());return H}if(F===220){let G=z[q]<<8|z[q+1];q+=2;let H=[];for(let M=0;M<G;M++)H.push(J());return H}if(F===221){let G=(z[q]<<24|z[q+1]<<16|z[q+2]<<8|z[q+3])>>>0;q+=4;let H=[];for(let M=0;M<G;M++)H.push(J());return H}if((F&240)===128){let G=F&15,H={};for(let M=0;M<G;M++)H[J()]=J();return H}if(F===222){let G=z[q]<<8|z[q+1];q+=2;let H={};for(let M=0;M<G;M++)H[J()]=J();return H}if(F===223){let G=(z[q]<<24|z[q+1]<<16|z[q+2]<<8|z[q+3])>>>0;q+=4;let H={};for(let M=0;M<G;M++)H[J()]=J();return H}if(F===196){let G=z[q++],H=z.slice(q,q+G);return q+=G,H}if(F===197){let G=z[q]<<8|z[q+1];q+=2;let H=z.slice(q,q+G);return q+=G,H}return}return J()}var N=window;N.__bunite??={};N.__buniteWebviewId=__buniteWebviewId;N.__buniteRpcSocketPort=__buniteRpcSocketPort;N.__bunite_encrypt=K;N.__bunite_decrypt=W;var R=new Map,U=new Map,Q=null,X=!1;function T(z){if(X)return;X=!0,z.addEventListener("message",async(q)=>{try{let J=await W(new Uint8Array(q.data)),F=D(J);if(F?.type==="response"&&F.scope==="global"){let G=U.get(F.id);if(G)U.delete(F.id),clearTimeout(G.timeout),F.success?G.resolve(F.payload):G.reject(Error(F.error||"Unknown error"))}else if(F?.type==="event"){let G=R.get(F.channel);if(G)for(let H of G)H(F.data)}}catch{}}),z.addEventListener("close",()=>{X=!1,Q=null})}function O(){let z=N.__bunite?._socket;if(z&&z.readyState<=WebSocket.OPEN&&z!==Q)return Q=z,T(z),z;if(Q&&Q.readyState<=WebSocket.OPEN)return Q;return Q=new WebSocket(`ws://localhost:${__buniteRpcSocketPort}/socket?webviewId=${__buniteWebviewId}`),Q.binaryType="arraybuffer",N.__bunite._socket=Q,T(Q),Q}N.bunite=N.__bunite;N.bunite.on=(z,q)=>{O();let J=R.get(z);if(!J)J=new Set,R.set(z,J);return J.add(q),()=>{J.delete(q)}};N.bunite.off=(z,q)=>{R.get(z)?.delete(q)};var S=1;N.bunite.invoke=(z,q)=>new Promise((J,F)=>{let G=O(),H=S++,M=setTimeout(()=>{U.delete(H),F(Error(`bunite.invoke timed out: ${z}`))},15000);U.set(H,{resolve:J,reject:F,timeout:M});let B={type:"request",id:H,method:z,params:q??null,scope:"global"},Y=async()=>{let A=await K(I(B));G.send(A.buffer)};if(G.readyState===WebSocket.OPEN)Y();else G.addEventListener("open",()=>Y(),{once:!0})});
@@ -1,4 +1,4 @@
1
- // Preload runtime — built once at package build time, injected into every views:// page.
1
+ // Preload runtime — built once at package build time, injected into every appres:// page.
2
2
  // The config variables (__buniteWebviewId, __buniteRpcSocketPort, __buniteSecretKeyBase64)
3
3
  // are injected by inline.ts as a small preamble before this script.
4
4
 
@@ -147,50 +147,72 @@ w.__buniteRpcSocketPort = __buniteRpcSocketPort;
147
147
  w.__bunite_encrypt = buniteEncrypt;
148
148
  w.__bunite_decrypt = buniteDecrypt;
149
149
 
150
- // --- bunite.invoke: global IPC ---
150
+ // --- Shared WebSocket transport ---
151
+
152
+ const eventListeners = new Map<string, Set<(data: any) => void>>();
153
+ const pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void; timeout: ReturnType<typeof setTimeout> }>();
154
+ let socket: WebSocket | null = null;
155
+ let listenerAttached = false;
156
+
157
+ function attachListener(ws: WebSocket) {
158
+ if (listenerAttached) return;
159
+ listenerAttached = true;
160
+ ws.addEventListener("message", async (event) => {
161
+ try {
162
+ const decrypted = await buniteDecrypt(new Uint8Array(event.data as ArrayBuffer));
163
+ const packet = mpDecode(decrypted) as any;
164
+ if (packet?.type === "response" && packet.scope === "global") {
165
+ const p = pending.get(packet.id);
166
+ if (p) {
167
+ pending.delete(packet.id);
168
+ clearTimeout(p.timeout);
169
+ packet.success ? p.resolve(packet.payload) : p.reject(new Error(packet.error || "Unknown error"));
170
+ }
171
+ } else if (packet?.type === "event") {
172
+ const set = eventListeners.get(packet.channel);
173
+ if (set) for (const fn of set) fn(packet.data);
174
+ }
175
+ } catch { /* ignore malformed frames */ }
176
+ });
177
+ ws.addEventListener("close", () => { listenerAttached = false; socket = null; });
178
+ }
151
179
 
152
- w.bunite = w.__bunite;
153
- w.bunite.invoke = (() => {
154
- let socket: WebSocket | null = null;
155
- let nextId = 1;
156
- const pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void; timeout: ReturnType<typeof setTimeout> }>();
157
-
158
- function ensureSocket(): WebSocket {
159
- // Reuse socket opened by BuniteView if available
160
- const existing = w.__bunite?._socket;
161
- if (existing && existing.readyState <= WebSocket.OPEN && existing !== socket) {
162
- socket = existing;
163
- attachListener(existing);
164
- return existing;
165
- }
166
- if (socket && socket.readyState <= WebSocket.OPEN) return socket;
167
- socket = new WebSocket(
168
- `ws://localhost:${__buniteRpcSocketPort}/socket?webviewId=${__buniteWebviewId}`
169
- );
170
- socket.binaryType = "arraybuffer";
171
- w.__bunite._socket = socket;
172
- attachListener(socket);
173
- return socket;
180
+ function ensureSocket(): WebSocket {
181
+ const existing = w.__bunite?._socket;
182
+ if (existing && existing.readyState <= WebSocket.OPEN && existing !== socket) {
183
+ socket = existing;
184
+ attachListener(existing);
185
+ return existing;
174
186
  }
187
+ if (socket && socket.readyState <= WebSocket.OPEN) return socket;
188
+ socket = new WebSocket(
189
+ `ws://localhost:${__buniteRpcSocketPort}/socket?webviewId=${__buniteWebviewId}`
190
+ );
191
+ socket.binaryType = "arraybuffer";
192
+ w.__bunite._socket = socket;
193
+ attachListener(socket);
194
+ return socket;
195
+ }
175
196
 
176
- function attachListener(ws: WebSocket) {
177
- ws.addEventListener("message", async (event) => {
178
- try {
179
- const decrypted = await buniteDecrypt(new Uint8Array(event.data as ArrayBuffer));
180
- const packet = mpDecode(decrypted) as any;
181
- if (packet?.type === "response" && packet.scope === "global") {
182
- const p = pending.get(packet.id);
183
- if (p) {
184
- pending.delete(packet.id);
185
- clearTimeout(p.timeout);
186
- packet.success ? p.resolve(packet.payload) : p.reject(new Error(packet.error || "Unknown error"));
187
- }
188
- }
189
- } catch { /* ignore malformed frames */ }
190
- });
191
- }
197
+ // --- bunite.on/off: main→renderer events ---
198
+
199
+ w.bunite = w.__bunite;
200
+ w.bunite.on = (channel: string, handler: (data: any) => void) => {
201
+ ensureSocket();
202
+ let set = eventListeners.get(channel);
203
+ if (!set) { set = new Set(); eventListeners.set(channel, set); }
204
+ set.add(handler);
205
+ return () => { set!.delete(handler); };
206
+ };
207
+ w.bunite.off = (channel: string, handler: (data: any) => void) => {
208
+ eventListeners.get(channel)?.delete(handler);
209
+ };
210
+
211
+ // --- bunite.invoke: global IPC ---
212
+
213
+ let nextId = 1;
192
214
 
193
- return (method: string, params?: unknown) =>
215
+ w.bunite.invoke = (method: string, params?: unknown) =>
194
216
  new Promise((resolve, reject) => {
195
217
  const ws = ensureSocket();
196
218
  const id = nextId++;
@@ -212,4 +234,5 @@ w.bunite.invoke = (() => {
212
234
  ws.addEventListener("open", () => doSend(), { once: true });
213
235
  }
214
236
  });
215
- })();
237
+
238
+ import "./webviewElement";
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "compilerOptions": {
3
+ "composite": true,
3
4
  "target": "ES2022",
4
5
  "module": "ES2022",
5
6
  "moduleResolution": "bundler",
@@ -9,5 +10,5 @@
9
10
  "skipLibCheck": true,
10
11
  "noEmit": true
11
12
  },
12
- "include": ["runtime.ts"]
13
+ "include": ["runtime.ts", "webviewElement.ts"]
13
14
  }