@zappdev/cli 0.5.0-alpha.9 → 0.6.0-alpha.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/bootstrap/codegen.ts +4 -9
- package/bootstrap/worker.ts +99 -12
- package/dist/config.js +25 -0
- package/native/app/app.zc +33 -10
- package/native/app/app_events.zc +7 -11
- package/native/build.zc +0 -2
- package/native/window/callbacks.zc +5 -6
- package/native/worker/engines/jsc.h +0 -14
- package/native/worker/engines/jsc.m +189 -159
- package/native/worker/engines/txiki.c +14 -7
- package/package.json +7 -3
- package/src/build-config.ts +29 -0
- package/src/config.ts +6 -1
- package/src/init.ts +11 -5
- package/src/native.ts +3 -1
- package/src/zapp-cli.ts +5 -1
- package/bootstrap/backend.ts +0 -139
package/bootstrap/codegen.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Generates two functions:
|
|
6
6
|
* - zapp_webview_bootstrap_script() — WebView bridge (invoke, events, workers, sync, drag)
|
|
7
|
-
* - zapp_worker_bootstrap_script() —
|
|
7
|
+
* - zapp_worker_bootstrap_script() — Unified worker bridge (used by both webview-spawned
|
|
8
|
+
* workers and headless workers)
|
|
8
9
|
*
|
|
9
10
|
* Usage: called by CLI during build, or standalone:
|
|
10
11
|
* bun bootstrap/codegen.ts [outputDir]
|
|
@@ -45,14 +46,13 @@ export async function generateBootstrap(outputDir: string): Promise<string> {
|
|
|
45
46
|
await mkdir(outputDir, { recursive: true });
|
|
46
47
|
|
|
47
48
|
// Bundle all bootstraps in parallel
|
|
48
|
-
const [webviewEscaped, workerEscaped
|
|
49
|
+
const [webviewEscaped, workerEscaped] = await Promise.all([
|
|
49
50
|
bundleAndEscape(path.join(bootstrapDir, "webview.ts")),
|
|
50
51
|
bundleAndEscape(path.join(bootstrapDir, "worker.ts")),
|
|
51
|
-
bundleAndEscape(path.join(bootstrapDir, "backend.ts")),
|
|
52
52
|
]);
|
|
53
53
|
|
|
54
54
|
const zcContent = `// AUTO-GENERATED by zapp CLI. Do not edit.
|
|
55
|
-
// Source: bootstrap/webview.ts, bootstrap/worker.ts
|
|
55
|
+
// Source: bootstrap/webview.ts, bootstrap/worker.ts
|
|
56
56
|
|
|
57
57
|
fn zapp_webview_bootstrap_script() -> string {
|
|
58
58
|
raw { return "${webviewEscaped}"; }
|
|
@@ -63,11 +63,6 @@ fn zapp_worker_bootstrap_script() -> string {
|
|
|
63
63
|
raw { return "${workerEscaped}"; }
|
|
64
64
|
return "";
|
|
65
65
|
}
|
|
66
|
-
|
|
67
|
-
fn zapp_backend_bootstrap_script() -> string {
|
|
68
|
-
raw { return "${backendEscaped}"; }
|
|
69
|
-
return "";
|
|
70
|
-
}
|
|
71
66
|
`;
|
|
72
67
|
|
|
73
68
|
const outPath = path.join(outputDir, "zapp_bootstrap.zc");
|
package/bootstrap/worker.ts
CHANGED
|
@@ -1,25 +1,112 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Worker bootstrap — injected into JSC worker
|
|
2
|
+
* Worker bootstrap — injected into every JSC worker context (webview-spawned
|
|
3
|
+
* and headless alike). All workers share the same API surface; the only
|
|
4
|
+
* difference between a headless worker and a webview-owned one is whether
|
|
5
|
+
* they have an owner window.
|
|
3
6
|
*
|
|
4
|
-
* Host objects
|
|
7
|
+
* Host objects set up by native (see jsc_setup_bridge in jsc.m):
|
|
5
8
|
* - invokeService(method, args) → JSValue (sync, direct C call)
|
|
6
|
-
* - postToWebview(data) → void
|
|
7
|
-
* - syncWait(key, timeoutMs)
|
|
8
|
-
* -
|
|
9
|
+
* - postToWebview(data) → void (no-op in headless workers)
|
|
10
|
+
* - syncWait(key, timeoutMs), syncNotify(key, count)
|
|
11
|
+
* - dispatchEventToAll(name, payload) — broadcast to every webview
|
|
12
|
+
* - createWindow(opts) → number (returns windowId; works from any worker)
|
|
13
|
+
* - quit() — terminate the app
|
|
14
|
+
* - showNotification(title, body)
|
|
15
|
+
* - subscribeWindowEvent(windowId, eventId)
|
|
9
16
|
*
|
|
10
|
-
* This bootstrap adds
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
13
|
-
* - self.
|
|
17
|
+
* This bootstrap adds:
|
|
18
|
+
* - A runtime bridge exposed via Symbol.for("zapp.bridge") so
|
|
19
|
+
* @zappdev/runtime's getBridge() works uniformly across contexts.
|
|
20
|
+
* - Channel API (self.send / self.receive) for webview-worker messaging.
|
|
21
|
+
* - App event listener registry + _dispatchAppEvent callback for headless
|
|
22
|
+
* workers that want to hear app:* lifecycle events.
|
|
23
|
+
* - dispatchSyncResult glue for Sync.wait()'s promise resolution.
|
|
14
24
|
*/
|
|
15
25
|
|
|
16
26
|
(function () {
|
|
17
27
|
const bridge = (self as any).__zappBridge;
|
|
18
28
|
if (!bridge) return;
|
|
19
29
|
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
30
|
+
// Zero-overhead principle: we mutate __zappBridge in place to add the
|
|
31
|
+
// methods the runtime expects (on, emit, invoke, post, _onEvent,
|
|
32
|
+
// _dispatchAppEvent) and expose *that same object* under
|
|
33
|
+
// Symbol.for("zapp.bridge"). No wrapper, no indirection — when the
|
|
34
|
+
// runtime calls getBridge().invoke(...), it's hitting the native host
|
|
35
|
+
// object's own property directly.
|
|
36
|
+
//
|
|
37
|
+
// The hottest path (Services.invoke → __zappBridge.invokeService) already
|
|
38
|
+
// bypasses getBridge() entirely in runtime/services.ts, so it remains a
|
|
39
|
+
// direct C call regardless. This setup ensures the remaining
|
|
40
|
+
// runtime-API calls from worker contexts pay at most one JS property
|
|
41
|
+
// read + function call, not a whole wrapper object hop.
|
|
42
|
+
|
|
43
|
+
const listeners: Record<string, Array<(data: unknown) => void>> = {};
|
|
44
|
+
|
|
45
|
+
const windowEventIds: Record<string, number> = {
|
|
46
|
+
"window:ready": 0, "window:focus": 1, "window:blur": 2,
|
|
47
|
+
"window:resize": 3, "window:move": 4, "window:close": 5,
|
|
48
|
+
"window:minimize": 6, "window:maximize": 7, "window:restore": 8,
|
|
49
|
+
"window:fullscreen": 9, "window:unfullscreen": 10,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
bridge.on = function (event: string, handler: (data: unknown) => void) {
|
|
53
|
+
if (!listeners[event]) listeners[event] = [];
|
|
54
|
+
listeners[event].push(handler);
|
|
55
|
+
const eventId = windowEventIds[event];
|
|
56
|
+
if (eventId !== undefined && typeof bridge.subscribeWindowEvent === "function") {
|
|
57
|
+
bridge.subscribeWindowEvent(-1, eventId); // -1 = all windows
|
|
58
|
+
}
|
|
59
|
+
return () => {
|
|
60
|
+
listeners[event] = (listeners[event] || []).filter((h) => h !== handler);
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Runtime's Events.emit calls bridge.emit — alias to the direct host.
|
|
65
|
+
// No wrapper, just rename.
|
|
66
|
+
bridge.emit = bridge.dispatchEventToAll;
|
|
67
|
+
|
|
68
|
+
// Runtime's getBridge().invoke() — pure alias to invokeService (zero JS
|
|
69
|
+
// overhead). Runtime APIs that need something other than a user service
|
|
70
|
+
// (Window.create, Notification.*, Dock.*) detect worker context and call
|
|
71
|
+
// the appropriate host dispatcher directly, so there's no branching here.
|
|
72
|
+
bridge.invoke = bridge.invokeService;
|
|
73
|
+
|
|
74
|
+
// Headless workers have no webview to post to; webview-owned workers
|
|
75
|
+
// use postToWebview. Alias so getBridge().post(...) works either way.
|
|
76
|
+
bridge.post = bridge.postToWebview ?? function () {};
|
|
77
|
+
|
|
78
|
+
// Native-driven event dispatch callbacks.
|
|
79
|
+
bridge._dispatchAppEvent = function (eventId: number, dataJson: string) {
|
|
80
|
+
const eventMap: Record<number, string> = {
|
|
81
|
+
100: "app:started", 101: "app:shutdown",
|
|
82
|
+
102: "app:notification-click", 103: "app:notification-action",
|
|
83
|
+
104: "app:reopen", 105: "app:open-url",
|
|
84
|
+
106: "app:active", 107: "app:inactive",
|
|
85
|
+
};
|
|
86
|
+
const name = eventMap[eventId];
|
|
87
|
+
if (!name) return;
|
|
88
|
+
let data: unknown = dataJson;
|
|
89
|
+
try { data = JSON.parse(dataJson); } catch {}
|
|
90
|
+
for (const h of listeners[name] || []) {
|
|
91
|
+
try { h(data); } catch (e) { console.error("[worker]", e); }
|
|
92
|
+
}
|
|
93
|
+
if (eventId === 102) {
|
|
94
|
+
for (const h of listeners["__notif:click"] || []) try { h(data); } catch (e) { console.error("[worker]", e); }
|
|
95
|
+
} else if (eventId === 103) {
|
|
96
|
+
for (const h of listeners["__notif:action"] || []) try { h(data); } catch (e) { console.error("[worker]", e); }
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
bridge._onEvent = function (name: string, payload: string) {
|
|
101
|
+
let parsed: unknown = payload;
|
|
102
|
+
try { parsed = JSON.parse(payload); } catch {}
|
|
103
|
+
for (const h of listeners[name] || []) {
|
|
104
|
+
try { h(parsed); } catch (e) { console.error("[worker]", e); }
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Expose __zappBridge itself (not a wrapper) under the symbol the
|
|
109
|
+
// runtime looks up. getBridge() returns the native host object directly.
|
|
23
110
|
(globalThis as any)[Symbol.for("zapp.bridge")] = bridge;
|
|
24
111
|
|
|
25
112
|
// Channel handler registry
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
function defineConfig(config) {
|
|
4
|
+
return config;
|
|
5
|
+
}
|
|
6
|
+
async function loadConfig(root) {
|
|
7
|
+
const configPath = path.join(root, "zapp.config.ts");
|
|
8
|
+
try {
|
|
9
|
+
const mod = await import(configPath);
|
|
10
|
+
const config = typeof mod.default === "function" ? mod.default() : mod.default;
|
|
11
|
+
return {
|
|
12
|
+
...config,
|
|
13
|
+
assetDir: config.assetDir ?? "./dist"
|
|
14
|
+
};
|
|
15
|
+
} catch {
|
|
16
|
+
return {
|
|
17
|
+
name: path.basename(root),
|
|
18
|
+
assetDir: "./dist"
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
loadConfig,
|
|
24
|
+
defineConfig
|
|
25
|
+
};
|
package/native/app/app.zc
CHANGED
|
@@ -66,10 +66,35 @@ import "../worker/engines/jsc.h" as jsc_engine;
|
|
|
66
66
|
@cfg(ZAPP_WORKER_ENGINE_TXIKI)
|
|
67
67
|
import "../worker/engines/txiki.h" as txiki_engine;
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
fn
|
|
69
|
+
// zapp_start_headless_workers is provided by .zapp/zapp_headless_workers.zc,
|
|
70
|
+
// generated by the CLI from zappConfig.headless. When no headless workers are
|
|
71
|
+
// configured the generated function is an empty body.
|
|
72
|
+
extern fn zapp_start_headless_workers() -> void;
|
|
73
|
+
|
|
74
|
+
// Helper the generated headless startup file calls for each configured worker.
|
|
75
|
+
// Owner ID is empty — headless workers have no parent window.
|
|
76
|
+
fn zapp_start_headless_worker(id: string, script_url: string) -> void {
|
|
77
|
+
let app = (App*)app_get_active();
|
|
78
|
+
if app == NULL { return; }
|
|
79
|
+
worker_create(app, script_url, "", id);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Called from workers via the createWindow host object. Always runs on the
|
|
83
|
+
// main queue (the ObjC-side dispatch_sync handles that). Builds a
|
|
84
|
+
// WindowOptions and creates the window using the normal WindowManager path.
|
|
85
|
+
fn zapp_worker_create_window(title: string, url: string, width: int, height: int) -> int {
|
|
86
|
+
let app = (App*)app_get_active();
|
|
87
|
+
if app == NULL { return -1; }
|
|
88
|
+
let opts = WindowOptions::create(title);
|
|
89
|
+
raw {
|
|
90
|
+
if (url && url[0] != '\0') opts.url = url;
|
|
91
|
+
}
|
|
92
|
+
if width > 0 { opts.width = width; }
|
|
93
|
+
if height > 0 { opts.height = height; }
|
|
94
|
+
opts.visible = true;
|
|
95
|
+
let win = app.window.create(&opts);
|
|
96
|
+
return win.id;
|
|
97
|
+
}
|
|
73
98
|
|
|
74
99
|
// Dispatch worker message to a single window by owner_id
|
|
75
100
|
fn worker_dispatch_to_window(worker_id: string, data_json: string, owner_id: string) -> void {
|
|
@@ -182,7 +207,6 @@ struct AppConfig {
|
|
|
182
207
|
webContentInspectable: ZappInspectable;
|
|
183
208
|
maxWorkers: int;
|
|
184
209
|
qjsStackSize: int;
|
|
185
|
-
backend: bool; // enable backend worker (src/backend.ts)
|
|
186
210
|
}
|
|
187
211
|
|
|
188
212
|
// ---------------------------------------------------------------------------
|
|
@@ -247,13 +271,12 @@ impl App {
|
|
|
247
271
|
self.window.app = (App*)self;
|
|
248
272
|
app_set_active((App*)self);
|
|
249
273
|
|
|
250
|
-
// Run service startup (in registration order, before windows
|
|
274
|
+
// Run service startup (in registration order, before windows)
|
|
251
275
|
service_run_startup_all();
|
|
252
276
|
|
|
253
|
-
// Start
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
277
|
+
// Start configured headless workers. Generated function — empty body
|
|
278
|
+
// when zappConfig.headless is empty or omitted.
|
|
279
|
+
zapp_start_headless_workers();
|
|
257
280
|
|
|
258
281
|
return platform_run(self.config.applicationShouldTerminateAfterLastWindowClosed);
|
|
259
282
|
}
|
package/native/app/app_events.zc
CHANGED
|
@@ -52,7 +52,9 @@ raw {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
// Layer 2:
|
|
55
|
+
// Layer 2: Broadcast to every active worker (headless + webview-owned).
|
|
56
|
+
// Each worker's bootstrap sets up `_dispatchAppEvent` on __zappBridge
|
|
57
|
+
// and fans the event out to any listeners registered via bridge.on().
|
|
56
58
|
{
|
|
57
59
|
const char* safe_data = data ? data : "";
|
|
58
60
|
char buf[4096];
|
|
@@ -62,18 +64,12 @@ raw {
|
|
|
62
64
|
event_id, safe_data);
|
|
63
65
|
|
|
64
66
|
#if defined(ZAPP_WORKER_ENGINE_TXIKI)
|
|
65
|
-
extern
|
|
66
|
-
|
|
67
|
-
if (txiki_backend_is_running()) {
|
|
68
|
-
txiki_backend_eval_js(buf);
|
|
69
|
-
}
|
|
67
|
+
extern void txiki_broadcast_eval_js(const char* js);
|
|
68
|
+
txiki_broadcast_eval_js(buf);
|
|
70
69
|
#endif
|
|
71
70
|
#if defined(ZAPP_WORKER_ENGINE_JSC)
|
|
72
|
-
extern
|
|
73
|
-
|
|
74
|
-
if (jsc_backend_is_running()) {
|
|
75
|
-
jsc_backend_eval_js(buf);
|
|
76
|
-
}
|
|
71
|
+
extern void jsc_broadcast_eval_js(const char* js);
|
|
72
|
+
jsc_broadcast_eval_js(buf);
|
|
77
73
|
#endif
|
|
78
74
|
}
|
|
79
75
|
|
package/native/build.zc
CHANGED
|
@@ -37,7 +37,6 @@ fn zapp_build_accept_first_mouse() -> int { return 1; } // drag regions
|
|
|
37
37
|
// Bootstrap stub (standalone — normally generated by CLI via bootstrap/codegen.ts)
|
|
38
38
|
fn zapp_webview_bootstrap_script() -> string { return ""; }
|
|
39
39
|
fn zapp_worker_bootstrap_script() -> string { return ""; }
|
|
40
|
-
fn zapp_backend_bootstrap_script() -> string { return ""; }
|
|
41
40
|
|
|
42
41
|
// --- Test entry point ---
|
|
43
42
|
|
|
@@ -56,7 +55,6 @@ fn main() -> int {
|
|
|
56
55
|
webContentInspectable: Zapp::inspectable_on(),
|
|
57
56
|
maxWorkers: 0,
|
|
58
57
|
qjsStackSize: 0,
|
|
59
|
-
backend: false,
|
|
60
58
|
};
|
|
61
59
|
let app = App::new(config);
|
|
62
60
|
|
|
@@ -149,14 +149,13 @@ raw {
|
|
|
149
149
|
ename, window_id);
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
// Broadcast to every active worker (headless + webview-owned)
|
|
152
153
|
#if defined(ZAPP_WORKER_ENGINE_TXIKI)
|
|
153
|
-
extern
|
|
154
|
-
|
|
155
|
-
if (txiki_backend_is_running()) txiki_backend_eval_js(__be_buf);
|
|
154
|
+
extern void txiki_broadcast_eval_js(const char* js);
|
|
155
|
+
txiki_broadcast_eval_js(__be_buf);
|
|
156
156
|
#elif defined(ZAPP_WORKER_ENGINE_JSC)
|
|
157
|
-
extern
|
|
158
|
-
|
|
159
|
-
if (jsc_backend_is_running()) jsc_backend_eval_js(__be_buf);
|
|
157
|
+
extern void jsc_broadcast_eval_js(const char* js);
|
|
158
|
+
jsc_broadcast_eval_js(__be_buf);
|
|
160
159
|
#endif
|
|
161
160
|
#endif
|
|
162
161
|
}
|
|
@@ -24,18 +24,4 @@ void jsc_worker_terminate_owner(const char* owner_id);
|
|
|
24
24
|
// Implemented in Zen-C (bridge/dispatch.zc), declared here for jsc.m to call.
|
|
25
25
|
extern void worker_dispatch_to_webview(char* worker_id, char* data_json);
|
|
26
26
|
|
|
27
|
-
// --- Backend worker (privileged, app-level JS context) ---
|
|
28
|
-
|
|
29
|
-
// Create the backend worker. Loads script from path. Starts on serial queue.
|
|
30
|
-
bool jsc_backend_create(const char* script_path);
|
|
31
|
-
|
|
32
|
-
// Terminate the backend worker.
|
|
33
|
-
void jsc_backend_terminate(void);
|
|
34
|
-
|
|
35
|
-
// Evaluate JS in the backend worker context.
|
|
36
|
-
void jsc_backend_eval_js(const char* js);
|
|
37
|
-
|
|
38
|
-
// Check if backend worker is running.
|
|
39
|
-
bool jsc_backend_is_running(void);
|
|
40
|
-
|
|
41
27
|
#endif
|
|
@@ -294,6 +294,180 @@ static void jsc_setup_bridge(JSContext* ctx, NSString* workerId) {
|
|
|
294
294
|
darwin_sync_handle("notify", [json UTF8String]);
|
|
295
295
|
};
|
|
296
296
|
|
|
297
|
+
// --- Privileged host objects available in every worker ---
|
|
298
|
+
|
|
299
|
+
// quit — terminate the app from any worker
|
|
300
|
+
bridge[@"quit"] = ^{
|
|
301
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
302
|
+
exit(0);
|
|
303
|
+
});
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// subscribeWindowEvent — register this worker for window events
|
|
307
|
+
bridge[@"subscribeWindowEvent"] = ^(JSValue* windowIdVal, JSValue* eventIdVal) {
|
|
308
|
+
int wId = [windowIdVal toInt32];
|
|
309
|
+
int eId = [eventIdVal toInt32];
|
|
310
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
311
|
+
extern void zapp_window_set_backend_listener(int id, int event_id, int has_listener);
|
|
312
|
+
if (wId < 0) {
|
|
313
|
+
for (int i = 0; i < 64; i++) {
|
|
314
|
+
zapp_window_set_backend_listener(i, eId, 1);
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
zapp_window_set_backend_listener(wId, eId, 1);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// showNotification — fire-and-forget system notification
|
|
323
|
+
bridge[@"showNotification"] = ^(NSString* title, NSString* body) {
|
|
324
|
+
const char* t = title ? [title UTF8String] : "";
|
|
325
|
+
const char* b = body ? [body UTF8String] : "";
|
|
326
|
+
char* tc = strdup(t);
|
|
327
|
+
char* bc = strdup(b);
|
|
328
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
329
|
+
extern void darwin_notification_show_typed(const char*, const char*, const char*, const char*);
|
|
330
|
+
darwin_notification_show_typed(tc, "", bc, "default");
|
|
331
|
+
free(tc);
|
|
332
|
+
free(bc);
|
|
333
|
+
});
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
// createWindow — synchronously create a window from any worker.
|
|
337
|
+
// Dispatches to main queue (window creation requires main thread on macOS),
|
|
338
|
+
// waits for result, returns an object with windowId.
|
|
339
|
+
bridge[@"createWindow"] = ^JSValue*(JSValue* optsVal) {
|
|
340
|
+
JSContext* currentCtx = [JSContext currentContext];
|
|
341
|
+
NSString* title = @"Window";
|
|
342
|
+
NSString* url = @"";
|
|
343
|
+
__block int width = 0;
|
|
344
|
+
__block int height = 0;
|
|
345
|
+
if (optsVal && ![optsVal isUndefined] && ![optsVal isNull]) {
|
|
346
|
+
JSValue* t = optsVal[@"title"];
|
|
347
|
+
if (t && ![t isUndefined] && ![t isNull]) title = [t toString];
|
|
348
|
+
JSValue* u = optsVal[@"url"];
|
|
349
|
+
if (u && ![u isUndefined] && ![u isNull]) url = [u toString];
|
|
350
|
+
JSValue* w = optsVal[@"width"];
|
|
351
|
+
if (w && ![w isUndefined] && ![w isNull]) width = [w toInt32];
|
|
352
|
+
JSValue* h = optsVal[@"height"];
|
|
353
|
+
if (h && ![h isUndefined] && ![h isNull]) height = [h toInt32];
|
|
354
|
+
}
|
|
355
|
+
const char* titleC = strdup([title UTF8String]);
|
|
356
|
+
const char* urlC = strdup([url UTF8String]);
|
|
357
|
+
__block int windowId = -1;
|
|
358
|
+
dispatch_sync(dispatch_get_main_queue(), ^{
|
|
359
|
+
extern int zapp_worker_create_window(const char* title, const char* url, int width, int height);
|
|
360
|
+
windowId = zapp_worker_create_window(titleC, urlC, width, height);
|
|
361
|
+
});
|
|
362
|
+
free((void*)titleC);
|
|
363
|
+
free((void*)urlC);
|
|
364
|
+
JSValue* result = [JSValue valueWithNewObjectInContext:currentCtx];
|
|
365
|
+
result[@"windowId"] = [NSString stringWithFormat:@"win-%d", windowId];
|
|
366
|
+
return result;
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
// notif(action, args) — dispatcher for all notification operations.
|
|
370
|
+
// Runtime's Notification.* methods call this directly in worker contexts,
|
|
371
|
+
// bypassing getBridge().invoke() entirely for zero-overhead calls.
|
|
372
|
+
bridge[@"notif"] = ^JSValue*(NSString* action, JSValue* argsVal) {
|
|
373
|
+
JSContext* currentCtx = [JSContext currentContext];
|
|
374
|
+
extern const char* darwin_notification_get_permission(void);
|
|
375
|
+
extern void darwin_notification_show_typed(const char*, const char*, const char*, const char*);
|
|
376
|
+
extern void darwin_notification_schedule_typed(const char*, const char*, double);
|
|
377
|
+
extern void darwin_notification_cancel(const char*);
|
|
378
|
+
extern void darwin_notification_cancel_all(void);
|
|
379
|
+
extern void darwin_notification_remove_delivered(const char*);
|
|
380
|
+
extern void darwin_notification_remove_all_delivered(void);
|
|
381
|
+
extern void darwin_notification_update(const char*, const char*, const char*, const char*);
|
|
382
|
+
|
|
383
|
+
NSString* act = action ?: @"";
|
|
384
|
+
if ([act isEqualToString:@"getPermission"]) {
|
|
385
|
+
const char* st = darwin_notification_get_permission();
|
|
386
|
+
JSValue* r = [JSValue valueWithNewObjectInContext:currentCtx];
|
|
387
|
+
r[@"status"] = st ? [NSString stringWithUTF8String:st] : @"notDetermined";
|
|
388
|
+
return r;
|
|
389
|
+
}
|
|
390
|
+
if ([act isEqualToString:@"show"]) {
|
|
391
|
+
NSString* title = argsVal[@"title"] ? [argsVal[@"title"] toString] : @"";
|
|
392
|
+
NSString* subtitle = argsVal[@"subtitle"] ? [argsVal[@"subtitle"] toString] : @"";
|
|
393
|
+
NSString* body = argsVal[@"body"] ? [argsVal[@"body"] toString] : @"";
|
|
394
|
+
NSString* sound = argsVal[@"sound"] ? [argsVal[@"sound"] toString] : @"default";
|
|
395
|
+
darwin_notification_show_typed([title UTF8String], [subtitle UTF8String], [body UTF8String], [sound UTF8String]);
|
|
396
|
+
// Return an ID (clients use it for update/cancel). Workers don't get the native-generated ID in this
|
|
397
|
+
// sync path — generate a client-side UUID-ish value so the API contract holds.
|
|
398
|
+
JSValue* r = [JSValue valueWithNewObjectInContext:currentCtx];
|
|
399
|
+
r[@"id"] = [NSString stringWithFormat:@"notif-%llu-%u", (unsigned long long)[[NSDate date] timeIntervalSince1970] * 1000, arc4random()];
|
|
400
|
+
return r;
|
|
401
|
+
}
|
|
402
|
+
if ([act isEqualToString:@"schedule"]) {
|
|
403
|
+
NSString* title = argsVal[@"title"] ? [argsVal[@"title"] toString] : @"";
|
|
404
|
+
NSString* body = argsVal[@"body"] ? [argsVal[@"body"] toString] : @"";
|
|
405
|
+
double delay = argsVal[@"delaySeconds"] ? [argsVal[@"delaySeconds"] toDouble] : 0;
|
|
406
|
+
darwin_notification_schedule_typed([title UTF8String], [body UTF8String], delay);
|
|
407
|
+
JSValue* r = [JSValue valueWithNewObjectInContext:currentCtx];
|
|
408
|
+
r[@"id"] = [NSString stringWithFormat:@"notif-%llu-%u", (unsigned long long)[[NSDate date] timeIntervalSince1970] * 1000, arc4random()];
|
|
409
|
+
return r;
|
|
410
|
+
}
|
|
411
|
+
if ([act isEqualToString:@"cancel"]) {
|
|
412
|
+
NSString* id_ = argsVal[@"id"] ? [argsVal[@"id"] toString] : @"";
|
|
413
|
+
darwin_notification_cancel([id_ UTF8String]);
|
|
414
|
+
return [JSValue valueWithUndefinedInContext:currentCtx];
|
|
415
|
+
}
|
|
416
|
+
if ([act isEqualToString:@"cancelAll"]) {
|
|
417
|
+
darwin_notification_cancel_all();
|
|
418
|
+
return [JSValue valueWithUndefinedInContext:currentCtx];
|
|
419
|
+
}
|
|
420
|
+
if ([act isEqualToString:@"removeDelivered"]) {
|
|
421
|
+
NSString* id_ = argsVal[@"id"] ? [argsVal[@"id"] toString] : @"";
|
|
422
|
+
darwin_notification_remove_delivered([id_ UTF8String]);
|
|
423
|
+
return [JSValue valueWithUndefinedInContext:currentCtx];
|
|
424
|
+
}
|
|
425
|
+
if ([act isEqualToString:@"removeAllDelivered"]) {
|
|
426
|
+
darwin_notification_remove_all_delivered();
|
|
427
|
+
return [JSValue valueWithUndefinedInContext:currentCtx];
|
|
428
|
+
}
|
|
429
|
+
if ([act isEqualToString:@"update"]) {
|
|
430
|
+
NSString* id_ = argsVal[@"id"] ? [argsVal[@"id"] toString] : @"";
|
|
431
|
+
NSString* title = argsVal[@"title"] ? [argsVal[@"title"] toString] : @"";
|
|
432
|
+
NSString* subtitle = argsVal[@"subtitle"] ? [argsVal[@"subtitle"] toString] : @"";
|
|
433
|
+
NSString* body = argsVal[@"body"] ? [argsVal[@"body"] toString] : @"";
|
|
434
|
+
darwin_notification_update([id_ UTF8String], [title UTF8String], [subtitle UTF8String], [body UTF8String]);
|
|
435
|
+
return [JSValue valueWithUndefinedInContext:currentCtx];
|
|
436
|
+
}
|
|
437
|
+
return [JSValue valueWithUndefinedInContext:currentCtx];
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
// dock(action, args) — dispatcher for all dock operations. All are sync,
|
|
441
|
+
// fire-and-forget (they post to the main UI thread internally via the
|
|
442
|
+
// Zen-C bridge and return immediately).
|
|
443
|
+
bridge[@"dock"] = ^(NSString* action, JSValue* argsVal) {
|
|
444
|
+
extern void darwin_dock_show_icon(void);
|
|
445
|
+
extern void darwin_dock_hide_icon(void);
|
|
446
|
+
extern void darwin_dock_set_badge(const char*);
|
|
447
|
+
extern void darwin_dock_remove_badge(void);
|
|
448
|
+
extern void darwin_dock_bounce(int);
|
|
449
|
+
extern void darwin_dock_set_icon(const char*);
|
|
450
|
+
extern void darwin_dock_reset_icon(void);
|
|
451
|
+
|
|
452
|
+
NSString* act = action ?: @"";
|
|
453
|
+
if ([act isEqualToString:@"showIcon"]) { darwin_dock_show_icon(); return; }
|
|
454
|
+
if ([act isEqualToString:@"hideIcon"]) { darwin_dock_hide_icon(); return; }
|
|
455
|
+
if ([act isEqualToString:@"setBadge"]) {
|
|
456
|
+
NSString* label = argsVal[@"label"] ? [argsVal[@"label"] toString] : @"";
|
|
457
|
+
darwin_dock_set_badge([label UTF8String]); return;
|
|
458
|
+
}
|
|
459
|
+
if ([act isEqualToString:@"removeBadge"]) { darwin_dock_remove_badge(); return; }
|
|
460
|
+
if ([act isEqualToString:@"bounce"]) {
|
|
461
|
+
int t = argsVal[@"type"] ? [argsVal[@"type"] toInt32] : 0;
|
|
462
|
+
darwin_dock_bounce(t); return;
|
|
463
|
+
}
|
|
464
|
+
if ([act isEqualToString:@"setIcon"]) {
|
|
465
|
+
NSString* p = argsVal[@"path"] ? [argsVal[@"path"] toString] : @"";
|
|
466
|
+
darwin_dock_set_icon([p UTF8String]); return;
|
|
467
|
+
}
|
|
468
|
+
if ([act isEqualToString:@"resetIcon"]) { darwin_dock_reset_icon(); return; }
|
|
469
|
+
};
|
|
470
|
+
|
|
297
471
|
ctx[@"__zappBridge"] = bridge;
|
|
298
472
|
|
|
299
473
|
// setTimeout / setInterval
|
|
@@ -555,165 +729,21 @@ void jsc_worker_terminate_owner(const char* owner_id) {
|
|
|
555
729
|
}
|
|
556
730
|
}
|
|
557
731
|
|
|
558
|
-
//
|
|
559
|
-
//
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
static BOOL jsc_backend_running = NO;
|
|
564
|
-
|
|
565
|
-
bool jsc_backend_create(const char* script_path) {
|
|
566
|
-
if (jsc_backend_running) return false;
|
|
567
|
-
if (!script_path) return false;
|
|
568
|
-
jsc_ensure_init();
|
|
569
|
-
|
|
570
|
-
NSString* scriptPath = [NSString stringWithUTF8String:script_path];
|
|
571
|
-
jsc_backend_queue = dispatch_queue_create("com.zapp.backend", DISPATCH_QUEUE_SERIAL);
|
|
572
|
-
|
|
573
|
-
dispatch_async(jsc_backend_queue, ^{
|
|
574
|
-
JSContext* ctx = [[JSContext alloc] initWithVirtualMachine:jsc_vm];
|
|
575
|
-
ctx.name = @"Zapp Backend";
|
|
576
|
-
ctx.exceptionHandler = ^(JSContext* c, JSValue* exception) {
|
|
577
|
-
(void)c;
|
|
578
|
-
NSLog(@"[backend ERROR] %@", exception);
|
|
579
|
-
};
|
|
580
|
-
|
|
581
|
-
// Make inspectable
|
|
582
|
-
if (app_get_bootstrap_web_content_inspectable()) {
|
|
583
|
-
if ([ctx respondsToSelector:@selector(setInspectable:)]) {
|
|
584
|
-
[ctx setInspectable:YES];
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// Set up standard bridge (console, invokeService, syncWait/Notify)
|
|
589
|
-
jsc_setup_bridge(ctx, @"__backend__");
|
|
590
|
-
|
|
591
|
-
// Backend-specific host objects
|
|
592
|
-
JSValue* bridge = ctx[@"__zappBridge"];
|
|
593
|
-
|
|
594
|
-
// quit — terminate the app
|
|
595
|
-
bridge[@"quit"] = ^{
|
|
596
|
-
dispatch_async(dispatch_get_main_queue(), ^{
|
|
597
|
-
exit(0);
|
|
598
|
-
});
|
|
599
|
-
};
|
|
600
|
-
|
|
601
|
-
// subscribeWindowEvent — register backend for window events
|
|
602
|
-
bridge[@"subscribeWindowEvent"] = ^(JSValue* windowIdVal, JSValue* eventIdVal) {
|
|
603
|
-
int wid = [windowIdVal toInt32];
|
|
604
|
-
int eid = [eventIdVal toInt32];
|
|
605
|
-
dispatch_async(dispatch_get_main_queue(), ^{
|
|
606
|
-
extern void zapp_window_set_backend_listener(int id, int event_id, int has_listener);
|
|
607
|
-
if (wid < 0) {
|
|
608
|
-
// Subscribe all windows (wid == -1)
|
|
609
|
-
for (int i = 0; i < 64; i++) {
|
|
610
|
-
zapp_window_set_backend_listener(i, eid, 1);
|
|
611
|
-
}
|
|
612
|
-
} else {
|
|
613
|
-
zapp_window_set_backend_listener(wid, eid, 1);
|
|
614
|
-
}
|
|
615
|
-
});
|
|
616
|
-
};
|
|
617
|
-
|
|
618
|
-
// showNotification — fire-and-forget
|
|
619
|
-
bridge[@"showNotification"] = ^(NSString* title, NSString* body) {
|
|
620
|
-
const char* t = title ? [title UTF8String] : "";
|
|
621
|
-
const char* b = body ? [body UTF8String] : "";
|
|
622
|
-
// Copy strings for async block
|
|
623
|
-
char* tc = strdup(t);
|
|
624
|
-
char* bc = strdup(b);
|
|
625
|
-
dispatch_async(dispatch_get_main_queue(), ^{
|
|
626
|
-
extern void darwin_notification_show_typed(const char*, const char*, const char*, const char*);
|
|
627
|
-
darwin_notification_show_typed(tc, "", bc, "default");
|
|
628
|
-
free(tc);
|
|
629
|
-
free(bc);
|
|
630
|
-
});
|
|
631
|
-
};
|
|
632
|
-
|
|
633
|
-
jsc_backend_ctx = ctx;
|
|
634
|
-
|
|
635
|
-
// Load backend bootstrap
|
|
636
|
-
extern const char* zapp_backend_bootstrap_script(void);
|
|
637
|
-
const char* bootstrap = zapp_backend_bootstrap_script();
|
|
638
|
-
if (bootstrap && bootstrap[0] != '\0') {
|
|
639
|
-
[ctx evaluateScript:[NSString stringWithUTF8String:bootstrap]];
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
// Load user backend script — try embedded first, then filesystem.
|
|
643
|
-
// scriptPath is the canonical URL form ("/_workers/backend.mjs").
|
|
644
|
-
NSString* script = nil;
|
|
645
|
-
|
|
646
|
-
extern int zapp_build_use_embedded_assets(void);
|
|
647
|
-
extern int zapp_embedded_assets_count;
|
|
648
|
-
extern ZappEmbeddedAsset zapp_embedded_assets[];
|
|
649
|
-
if (zapp_build_use_embedded_assets() && zapp_embedded_assets_count > 0) {
|
|
650
|
-
for (int ai = 0; ai < zapp_embedded_assets_count; ai++) {
|
|
651
|
-
NSString* assetPath = [NSString stringWithUTF8String:zapp_embedded_assets[ai].path];
|
|
652
|
-
if ([assetPath isEqualToString:scriptPath]) {
|
|
653
|
-
if (zapp_embedded_assets[ai].is_brotli && zapp_embedded_assets[ai].uncompressed_len > 0) {
|
|
654
|
-
uint8_t* out = malloc(zapp_embedded_assets[ai].uncompressed_len + 1);
|
|
655
|
-
size_t decoded = compression_decode_buffer(
|
|
656
|
-
out, zapp_embedded_assets[ai].uncompressed_len,
|
|
657
|
-
zapp_embedded_assets[ai].data, zapp_embedded_assets[ai].len,
|
|
658
|
-
NULL, COMPRESSION_BROTLI);
|
|
659
|
-
out[decoded] = '\0';
|
|
660
|
-
script = [[NSString alloc] initWithBytesNoCopy:out length:decoded
|
|
661
|
-
encoding:NSUTF8StringEncoding freeWhenDone:YES];
|
|
662
|
-
} else {
|
|
663
|
-
script = [[NSString alloc] initWithBytes:zapp_embedded_assets[ai].data
|
|
664
|
-
length:zapp_embedded_assets[ai].len encoding:NSUTF8StringEncoding];
|
|
665
|
-
}
|
|
666
|
-
NSLog(@"[zapp] backend loaded from embedded: %@", scriptPath);
|
|
667
|
-
break;
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
if (!script) {
|
|
673
|
-
// Dev: Vite plugin writes workers to .zapp/workers/<basename>
|
|
674
|
-
char cwd[1024];
|
|
675
|
-
NSString* base = getcwd(cwd, sizeof(cwd)) ? [NSString stringWithUTF8String:cwd] : @".";
|
|
676
|
-
NSString* basename = [scriptPath lastPathComponent];
|
|
677
|
-
NSString* fullPath = [[base stringByAppendingPathComponent:@".zapp/workers"]
|
|
678
|
-
stringByAppendingPathComponent:basename];
|
|
679
|
-
NSError* err = nil;
|
|
680
|
-
script = [NSString stringWithContentsOfFile:fullPath encoding:NSUTF8StringEncoding error:&err];
|
|
681
|
-
if (script) NSLog(@"[zapp] backend worker started: %@", fullPath);
|
|
682
|
-
else NSLog(@"[zapp] backend script not found: %@", fullPath);
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
if (script) {
|
|
686
|
-
// Async IIFE wrapper enables top-level await — see jsc_worker_create
|
|
687
|
-
// for rationale.
|
|
688
|
-
NSString* wrapped = [NSString stringWithFormat:
|
|
689
|
-
@"(async () => {\n%@\n})().catch(e => { console.error('[backend error]', e && e.stack ? e.stack : e); });",
|
|
690
|
-
script];
|
|
691
|
-
[ctx evaluateScript:wrapped withSourceURL:[NSURL URLWithString:@"backend.mjs"]];
|
|
692
|
-
}
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
jsc_backend_running = YES;
|
|
696
|
-
return true;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
void jsc_backend_terminate(void) {
|
|
700
|
-
if (!jsc_backend_running) return;
|
|
701
|
-
jsc_backend_ctx = nil;
|
|
702
|
-
jsc_backend_queue = nil;
|
|
703
|
-
jsc_backend_running = NO;
|
|
704
|
-
NSLog(@"[zapp] backend worker terminated");
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
void jsc_backend_eval_js(const char* js) {
|
|
708
|
-
if (!jsc_backend_running || !jsc_backend_ctx || !jsc_backend_queue || !js) return;
|
|
732
|
+
// Broadcast a JS snippet to every active worker (headless + webview-owned).
|
|
733
|
+
// Used by native event dispatch to deliver app/window events to every worker
|
|
734
|
+
// that may have subscribed. Each worker evaluates on its own serial queue.
|
|
735
|
+
void jsc_broadcast_eval_js(const char* js) {
|
|
736
|
+
if (!js) return;
|
|
709
737
|
NSString* script = [NSString stringWithUTF8String:js];
|
|
710
|
-
|
|
711
|
-
if (
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
738
|
+
for (int i = 0; i < JSC_MAX_WORKERS; i++) {
|
|
739
|
+
if (!jsc_workers[i].active) continue;
|
|
740
|
+
NSString* wid = [NSString stringWithUTF8String:jsc_workers[i].worker_id];
|
|
741
|
+
dispatch_queue_t queue = jsc_queues[wid];
|
|
742
|
+
JSContext* ctx = jsc_contexts[wid];
|
|
743
|
+
if (!queue || !ctx) continue;
|
|
744
|
+
dispatch_async(queue, ^{
|
|
745
|
+
[ctx evaluateScript:script];
|
|
746
|
+
});
|
|
747
|
+
}
|
|
715
748
|
}
|
|
716
749
|
|
|
717
|
-
bool jsc_backend_is_running(void) {
|
|
718
|
-
return jsc_backend_running;
|
|
719
|
-
}
|
|
@@ -945,13 +945,9 @@ static void* txiki_backend_thread(void* arg) {
|
|
|
945
945
|
JS_FreeValue(ctx, bridge);
|
|
946
946
|
JS_FreeValue(ctx, global);
|
|
947
947
|
|
|
948
|
-
//
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
if (bootstrap && bootstrap[0] != '\0') {
|
|
952
|
-
JSValue r = JS_Eval(ctx, bootstrap, strlen(bootstrap), "<backend-bootstrap>", JS_EVAL_TYPE_GLOBAL);
|
|
953
|
-
JS_FreeValue(ctx, r);
|
|
954
|
-
}
|
|
948
|
+
// Note: dead code path — app_start_backend no longer dispatches here.
|
|
949
|
+
// Backend bootstrap was merged into the worker bootstrap; this function
|
|
950
|
+
// is left in place until the full txiki backend removal lands.
|
|
955
951
|
|
|
956
952
|
// Load user backend script — try embedded first, then filesystem.
|
|
957
953
|
// slot->script_url is the canonical URL form ("/_workers/backend.mjs").
|
|
@@ -1085,3 +1081,14 @@ void txiki_backend_eval_js(const char* js) {
|
|
|
1085
1081
|
bool txiki_backend_is_running(void) {
|
|
1086
1082
|
return txiki_backend_running != 0;
|
|
1087
1083
|
}
|
|
1084
|
+
|
|
1085
|
+
// Broadcast a JS snippet to every active worker. Counterpart of
|
|
1086
|
+
// jsc_broadcast_eval_js — used by native event dispatch to deliver app and
|
|
1087
|
+
// window events to every worker. TODO: txiki uses a message-queue/libuv
|
|
1088
|
+
// dispatch pattern rather than direct eval; a full implementation requires
|
|
1089
|
+
// adding an "eval this JS" message type handled in on_async_message. For now
|
|
1090
|
+
// this is a stub so the JSC engine path works; txiki headless workers will
|
|
1091
|
+
// not receive forwarded events until this is wired up.
|
|
1092
|
+
void txiki_broadcast_eval_js(const char* js) {
|
|
1093
|
+
(void)js;
|
|
1094
|
+
}
|
package/package.json
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zappdev/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0-alpha.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"zapp": "./src/zapp-cli.ts"
|
|
7
7
|
},
|
|
8
8
|
"exports": {
|
|
9
|
-
"./config":
|
|
9
|
+
"./config": {
|
|
10
|
+
"types": "./src/config.ts",
|
|
11
|
+
"default": "./dist/config.js"
|
|
12
|
+
}
|
|
10
13
|
},
|
|
11
14
|
"files": [
|
|
12
15
|
"src/",
|
|
16
|
+
"dist/",
|
|
13
17
|
"native/",
|
|
14
18
|
"bootstrap/",
|
|
15
19
|
"assets/",
|
|
16
20
|
"vendor/webview2/"
|
|
17
21
|
],
|
|
18
22
|
"scripts": {
|
|
19
|
-
"prepack": "cp -r ../native ./native && cp -r ../bootstrap ./bootstrap && cp -r ../assets ./assets && mkdir -p ./vendor && cp -r ../vendor/webview2 ./vendor/webview2",
|
|
23
|
+
"prepack": "bun build src/config.ts --outdir dist --format esm --target node && cp -r ../native ./native && cp -r ../bootstrap ./bootstrap && cp -r ../assets ./assets && mkdir -p ./vendor && cp -r ../vendor/webview2 ./vendor/webview2",
|
|
20
24
|
"postpack": "rm -rf ./native ./bootstrap ./assets ./vendor"
|
|
21
25
|
},
|
|
22
26
|
"dependencies": {
|
package/src/build-config.ts
CHANGED
|
@@ -52,6 +52,35 @@ fn zapp_build_accept_first_mouse() -> int { return ${config.macos?.acceptFirstMo
|
|
|
52
52
|
return outPath;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
// Generate .zapp/zapp_headless_workers.zc — the function native/app/app.zc
|
|
56
|
+
// declares as `extern fn zapp_start_headless_workers()`. Body contains one
|
|
57
|
+
// call per entry in zappConfig.headless; empty body when no headless workers
|
|
58
|
+
// are configured.
|
|
59
|
+
export async function generateHeadlessWorkers(opts: { root: string; headless?: Record<string, string> }): Promise<string> {
|
|
60
|
+
const { root, headless } = opts;
|
|
61
|
+
const zappDir = path.join(root, ".zapp");
|
|
62
|
+
await mkdir(zappDir, { recursive: true });
|
|
63
|
+
|
|
64
|
+
const entries = Object.entries(headless ?? {});
|
|
65
|
+
const calls = entries
|
|
66
|
+
.map(([id]) => ` zapp_start_headless_worker("h-${id}", "/_workers/_headless_${id}.mjs");`)
|
|
67
|
+
.join("\n");
|
|
68
|
+
|
|
69
|
+
const body = calls || ` // No headless workers configured.`;
|
|
70
|
+
|
|
71
|
+
const content = `// AUTO-GENERATED by zapp CLI. Do not edit.
|
|
72
|
+
// Implementation for native/app/app.zc's extern fn zapp_start_headless_workers().
|
|
73
|
+
|
|
74
|
+
fn zapp_start_headless_workers() -> void {
|
|
75
|
+
${body}
|
|
76
|
+
}
|
|
77
|
+
`;
|
|
78
|
+
|
|
79
|
+
const outPath = path.join(zappDir, "zapp_headless_workers.zc");
|
|
80
|
+
await Bun.write(outPath, content);
|
|
81
|
+
return outPath;
|
|
82
|
+
}
|
|
83
|
+
|
|
55
84
|
// Generate .zapp/zapp_platform.zc with .m file cflags
|
|
56
85
|
export async function generatePlatformConfig(root: string, buildFile?: string): Promise<string> {
|
|
57
86
|
const zappDir = path.join(root, ".zapp");
|
package/src/config.ts
CHANGED
|
@@ -26,7 +26,12 @@ export interface ZappConfig {
|
|
|
26
26
|
version?: string;
|
|
27
27
|
assetDir?: string; // Default: "./dist" (Vite), configurable for static sites
|
|
28
28
|
devPort?: number; // Default: 5173
|
|
29
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Headless workers to start at app boot, keyed by ID.
|
|
31
|
+
* Example: `{ db: "src/workers/db.ts", sync: "src/workers/sync.ts" }`
|
|
32
|
+
* IDs are used for termination via `Workers.terminate(id)`.
|
|
33
|
+
*/
|
|
34
|
+
headless?: Record<string, string>;
|
|
30
35
|
deepLinkSchemes?: string[]; // e.g. ["myapp"] → registers myapp:// URL scheme
|
|
31
36
|
macos?: MacOSConfig;
|
|
32
37
|
security?: SecurityConfig;
|
package/src/init.ts
CHANGED
|
@@ -110,6 +110,12 @@ export default defineConfig({
|
|
|
110
110
|
name: "${name}",
|
|
111
111
|
identifier: "${identifier}",
|
|
112
112
|
version: "0.1.0",
|
|
113
|
+
// Add headless TypeScript workers that start when the app boots.
|
|
114
|
+
// Keys are worker IDs (used for termination); values are source paths.
|
|
115
|
+
//
|
|
116
|
+
// headless: {
|
|
117
|
+
// db: "src/workers/db.ts",
|
|
118
|
+
// },
|
|
113
119
|
});
|
|
114
120
|
`);
|
|
115
121
|
|
|
@@ -127,12 +133,12 @@ export default defineConfig({
|
|
|
127
133
|
|
|
128
134
|
pkgObj.dependencies = {
|
|
129
135
|
...(pkgObj.dependencies ?? {}),
|
|
130
|
-
"@zappdev/runtime": "^0.
|
|
136
|
+
"@zappdev/runtime": "^0.6.0-alpha.0",
|
|
131
137
|
};
|
|
132
138
|
pkgObj.devDependencies = {
|
|
133
139
|
...(pkgObj.devDependencies ?? {}),
|
|
134
|
-
"@zappdev/cli": "^0.
|
|
135
|
-
"@zappdev/vite": "^0.
|
|
140
|
+
"@zappdev/cli": "^0.6.0-alpha.0",
|
|
141
|
+
"@zappdev/vite": "^0.6.0-alpha.0",
|
|
136
142
|
};
|
|
137
143
|
|
|
138
144
|
await Bun.write(pkgPath, JSON.stringify(pkgObj, null, 2));
|
|
@@ -171,7 +177,7 @@ export default defineConfig({
|
|
|
171
177
|
if (!viteConfig.includes("zappWorkers(")) {
|
|
172
178
|
viteConfig = viteConfig.replace(
|
|
173
179
|
/plugins:\s*\[/,
|
|
174
|
-
"plugins: [zappWorkers({
|
|
180
|
+
"plugins: [zappWorkers({ headless: zappConfig.headless }), "
|
|
175
181
|
);
|
|
176
182
|
}
|
|
177
183
|
|
|
@@ -183,7 +189,7 @@ import { zappWorkers } from "@zappdev/vite";
|
|
|
183
189
|
import zappConfig from "./zapp.config";
|
|
184
190
|
|
|
185
191
|
export default defineConfig({
|
|
186
|
-
plugins: [zappWorkers({
|
|
192
|
+
plugins: [zappWorkers({ headless: zappConfig.headless })],
|
|
187
193
|
});
|
|
188
194
|
`);
|
|
189
195
|
}
|
package/src/native.ts
CHANGED
|
@@ -91,13 +91,14 @@ interface CompileOptions {
|
|
|
91
91
|
buildConfigFile: string; // .zapp/zapp_build_config.zc
|
|
92
92
|
bootstrapFile?: string; // .zapp/zapp_bootstrap.zc
|
|
93
93
|
assetsFile?: string; // .zapp/zapp_assets.zc (embedded brotli assets)
|
|
94
|
+
headlessFile?: string; // .zapp/zapp_headless_workers.zc
|
|
94
95
|
output: string; // Binary output path
|
|
95
96
|
nativeDir: string; // Framework source dir
|
|
96
97
|
optimize: boolean; // Size optimizations
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
export async function compileNative(opts: CompileOptions): Promise<void> {
|
|
100
|
-
const { root, buildFile, buildConfigFile, bootstrapFile, assetsFile, output, nativeDir, optimize } = opts;
|
|
101
|
+
const { root, buildFile, buildConfigFile, bootstrapFile, assetsFile, headlessFile, output, nativeDir, optimize } = opts;
|
|
101
102
|
|
|
102
103
|
// Generate platform config with .m file paths
|
|
103
104
|
const { generatePlatformConfig } = await import("./build-config");
|
|
@@ -112,6 +113,7 @@ export async function compileNative(opts: CompileOptions): Promise<void> {
|
|
|
112
113
|
platformFile,
|
|
113
114
|
...(bootstrapFile ? [bootstrapFile] : []),
|
|
114
115
|
...(assetsFile ? [assetsFile] : []),
|
|
116
|
+
...(headlessFile ? [headlessFile] : []),
|
|
115
117
|
"-I", nativeDir,
|
|
116
118
|
"-o", output,
|
|
117
119
|
// Suppress C compiler warnings by default. The framework and zc stdlib
|
package/src/zapp-cli.ts
CHANGED
|
@@ -4,7 +4,7 @@ import process from "node:process";
|
|
|
4
4
|
import { mkdir } from "node:fs/promises";
|
|
5
5
|
import { existsSync } from "node:fs";
|
|
6
6
|
import { loadConfig } from "./config";
|
|
7
|
-
import { generateBuildConfig, generatePlatformConfig } from "./build-config";
|
|
7
|
+
import { generateBuildConfig, generatePlatformConfig, generateHeadlessWorkers } from "./build-config";
|
|
8
8
|
import { generateBindings } from "./generate";
|
|
9
9
|
import { compileNative, ensureTxikiBuilt, hasTxikiEnabled, hasAnyWorkerEngine } from "./native";
|
|
10
10
|
import { resolveNativeDir, resolveBootstrapDir } from "./paths";
|
|
@@ -71,6 +71,7 @@ async function runDev(root: string) {
|
|
|
71
71
|
// 3. Generate build config + bootstrap (dev mode)
|
|
72
72
|
const buildConfigFile = await generateBuildConfig({ root, config, mode: "dev", devUrl });
|
|
73
73
|
const platformFile = await generatePlatformConfig(root);
|
|
74
|
+
const headlessFile = await generateHeadlessWorkers({ root, headless: config.headless });
|
|
74
75
|
const zappDir = path.join(root, ".zapp");
|
|
75
76
|
process.stdout.write("[zapp] generating bootstrap...\n");
|
|
76
77
|
const bootstrapFile = await generateBootstrap(zappDir);
|
|
@@ -137,6 +138,7 @@ async function runDev(root: string) {
|
|
|
137
138
|
buildConfigFile,
|
|
138
139
|
bootstrapFile,
|
|
139
140
|
assetsFile,
|
|
141
|
+
headlessFile,
|
|
140
142
|
output: nativeOut,
|
|
141
143
|
nativeDir,
|
|
142
144
|
optimize: false,
|
|
@@ -260,6 +262,7 @@ async function runBuild(root: string) {
|
|
|
260
262
|
// 5. Generate build config + bootstrap (prod mode, embedded assets)
|
|
261
263
|
const buildConfigFile = await generateBuildConfig({ root, config, mode: "prod", embedAssets: true });
|
|
262
264
|
const platformFile = await generatePlatformConfig(root);
|
|
265
|
+
const headlessFile = await generateHeadlessWorkers({ root, headless: config.headless });
|
|
263
266
|
const bootstrapFile = await generateBootstrap(zappDir);
|
|
264
267
|
|
|
265
268
|
// 5. Compile native binary (assets embedded in binary)
|
|
@@ -276,6 +279,7 @@ async function runBuild(root: string) {
|
|
|
276
279
|
buildConfigFile,
|
|
277
280
|
bootstrapFile,
|
|
278
281
|
assetsFile,
|
|
282
|
+
headlessFile,
|
|
279
283
|
output: nativeOut,
|
|
280
284
|
nativeDir,
|
|
281
285
|
optimize: true,
|
package/bootstrap/backend.ts
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Backend worker bootstrap — injected into the privileged backend JS context.
|
|
3
|
-
*
|
|
4
|
-
* Sets up event dispatch and the `Zapp` global for user-friendly API.
|
|
5
|
-
* Host objects available on __zappBridge:
|
|
6
|
-
* - invokeService(method, args) → JSValue
|
|
7
|
-
* - syncWait(key, timeoutMs), syncNotify(key, count)
|
|
8
|
-
* - showNotification(title, body)
|
|
9
|
-
* - createWindow(opts)
|
|
10
|
-
* - quit()
|
|
11
|
-
*
|
|
12
|
-
* JSC only for now. txiki.js opt-in later for web APIs (fetch, WebSocket, timers).
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
(function () {
|
|
16
|
-
const bridge = (self as any).__zappBridge;
|
|
17
|
-
if (!bridge) return;
|
|
18
|
-
|
|
19
|
-
const listeners: Record<string, Array<(data: unknown) => void>> = {};
|
|
20
|
-
|
|
21
|
-
// Event registration
|
|
22
|
-
bridge.on = function (event: string, handler: (data: unknown) => void) {
|
|
23
|
-
if (!listeners[event]) listeners[event] = [];
|
|
24
|
-
listeners[event].push(handler);
|
|
25
|
-
return () => {
|
|
26
|
-
listeners[event] = (listeners[event] || []).filter((h) => h !== handler);
|
|
27
|
-
};
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
// Called by native when app events fire (via jsc_backend_eval_js)
|
|
31
|
-
bridge._dispatchAppEvent = function (eventId: number, dataJson: string) {
|
|
32
|
-
const eventMap: Record<number, string> = {
|
|
33
|
-
100: "app:started",
|
|
34
|
-
101: "app:shutdown",
|
|
35
|
-
102: "app:notification-click",
|
|
36
|
-
103: "app:notification-action",
|
|
37
|
-
104: "app:reopen",
|
|
38
|
-
105: "app:open-url",
|
|
39
|
-
106: "app:active",
|
|
40
|
-
107: "app:inactive",
|
|
41
|
-
};
|
|
42
|
-
const name = eventMap[eventId];
|
|
43
|
-
if (!name) return;
|
|
44
|
-
let data: unknown = dataJson;
|
|
45
|
-
try {
|
|
46
|
-
data = JSON.parse(dataJson);
|
|
47
|
-
} catch {}
|
|
48
|
-
const handlers = listeners[name] || [];
|
|
49
|
-
for (let i = 0; i < handlers.length; i++) {
|
|
50
|
-
try {
|
|
51
|
-
handlers[i](data);
|
|
52
|
-
} catch (e) {
|
|
53
|
-
console.error("[backend]", e);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Also dispatch notification events under __notif: names
|
|
58
|
-
// so Notification.on("click"/"action") from @zappdev/runtime works
|
|
59
|
-
if (eventId === 102) {
|
|
60
|
-
const notifHandlers = listeners["__notif:click"] || [];
|
|
61
|
-
for (const h of notifHandlers) try { h(data); } catch (e) { console.error("[backend]", e); }
|
|
62
|
-
} else if (eventId === 103) {
|
|
63
|
-
const notifHandlers = listeners["__notif:action"] || [];
|
|
64
|
-
for (const h of notifHandlers) try { h(data); } catch (e) { console.error("[backend]", e); }
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
// Window event name → event ID mapping (for backend subscription)
|
|
69
|
-
const windowEventIds: Record<string, number> = {
|
|
70
|
-
"window:ready": 0, "window:focus": 1, "window:blur": 2,
|
|
71
|
-
"window:resize": 3, "window:move": 4, "window:close": 5,
|
|
72
|
-
"window:minimize": 6, "window:maximize": 7, "window:restore": 8,
|
|
73
|
-
"window:fullscreen": 9, "window:unfullscreen": 10,
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
// Expose as Symbol.for('zapp.bridge') so @zappdev/runtime works in the backend.
|
|
77
|
-
// The runtime's getBridge() looks for this symbol on globalThis.
|
|
78
|
-
const runtimeBridge = {
|
|
79
|
-
on(name: string, handler: (data: unknown) => void) {
|
|
80
|
-
const off = bridge.on(name, handler);
|
|
81
|
-
|
|
82
|
-
// If subscribing to a window event, tell native to forward to backend
|
|
83
|
-
const eventId = windowEventIds[name];
|
|
84
|
-
if (eventId !== undefined && bridge.subscribeWindowEvent) {
|
|
85
|
-
// Subscribe all windows (pass -1 for "all")
|
|
86
|
-
bridge.subscribeWindowEvent(-1, eventId);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return off;
|
|
90
|
-
},
|
|
91
|
-
emit(name: string, payload?: Record<string, unknown>) {
|
|
92
|
-
// Broadcast to every webview via the native dispatchEventToAll bridge.
|
|
93
|
-
// Each webview's bridge._onEvent picks it up and fans out to listeners
|
|
94
|
-
// registered with Events.on(name, ...).
|
|
95
|
-
if (typeof bridge.dispatchEventToAll === "function") {
|
|
96
|
-
bridge.dispatchEventToAll(name, payload ?? {});
|
|
97
|
-
}
|
|
98
|
-
},
|
|
99
|
-
invoke(method: string, args?: Record<string, unknown>) {
|
|
100
|
-
// Use host object for sync service invocation
|
|
101
|
-
const result = bridge.invokeService(method, args);
|
|
102
|
-
return Promise.resolve(result);
|
|
103
|
-
},
|
|
104
|
-
post(msg: string) {
|
|
105
|
-
// No WebView to post to — no-op in backend
|
|
106
|
-
},
|
|
107
|
-
// Sync primitives — forward to the native host objects so Sync.wait /
|
|
108
|
-
// Sync.notify work from the backend the same way they work in workers.
|
|
109
|
-
syncWait(key: string, timeoutMs?: number | null) {
|
|
110
|
-
if (typeof bridge.syncWait !== "function") {
|
|
111
|
-
throw new Error("Sync bridge is unavailable.");
|
|
112
|
-
}
|
|
113
|
-
return bridge.syncWait(key, timeoutMs ?? -1);
|
|
114
|
-
},
|
|
115
|
-
syncNotify(key: string, count?: number) {
|
|
116
|
-
if (typeof bridge.syncNotify !== "function") return;
|
|
117
|
-
bridge.syncNotify(key, count ?? 1);
|
|
118
|
-
},
|
|
119
|
-
// Called by native Layer 3 when dispatching window events to backend
|
|
120
|
-
_onEvent(name: string, payload: string) {
|
|
121
|
-
const handlers = listeners[name] || [];
|
|
122
|
-
let parsed: unknown = payload;
|
|
123
|
-
try { parsed = JSON.parse(payload); } catch {}
|
|
124
|
-
for (let i = 0; i < handlers.length; i++) {
|
|
125
|
-
try { handlers[i](parsed); } catch (e) { console.error("[backend]", e); }
|
|
126
|
-
}
|
|
127
|
-
},
|
|
128
|
-
};
|
|
129
|
-
(globalThis as any)[Symbol.for("zapp.bridge")] = runtimeBridge;
|
|
130
|
-
|
|
131
|
-
// Convenience global — the user-facing API (legacy, still works)
|
|
132
|
-
(self as any).Zapp = {
|
|
133
|
-
on: bridge.on,
|
|
134
|
-
invokeService: bridge.invokeService,
|
|
135
|
-
createWindow: bridge.createWindow,
|
|
136
|
-
showNotification: bridge.showNotification,
|
|
137
|
-
quit: bridge.quit,
|
|
138
|
-
};
|
|
139
|
-
})();
|