@mandujs/core 0.22.1 → 0.23.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/package.json +1 -1
- package/src/config/mandu.ts +172 -110
- package/src/config/validate.ts +357 -290
- package/src/desktop/__tests__/webview-fallback.test.ts +254 -0
- package/src/desktop/__tests__/window.test.ts +79 -3
- package/src/desktop/webview-fallback.ts +583 -0
- package/src/desktop/window.ts +527 -492
- package/src/perf/hmr-markers.ts +12 -0
- package/src/runtime/server.ts +133 -8
- package/src/testing/db.ts +157 -0
- package/src/testing/index.ts +59 -1
- package/src/testing/mocks.ts +203 -0
- package/src/testing/server.ts +196 -0
- package/src/testing/session.ts +190 -0
- package/src/testing/snapshot.ts +444 -0
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 11 C / M-02 — `bun:ffi` fallback for `webview-bun`.
|
|
3
|
+
*
|
|
4
|
+
* # Why this exists
|
|
5
|
+
*
|
|
6
|
+
* `webview-bun` (tr1ckydev/webview-bun) is the primary optional peer for
|
|
7
|
+
* `@mandujs/core/desktop`. Phase 9c R0 diagnostics
|
|
8
|
+
* (`docs/bun/phase-9-diagnostics/webview-bun-ffi.md`) identified one
|
|
9
|
+
* supply-chain risk: single maintainer, ~400 stars, small contributor
|
|
10
|
+
* base. If the peer ever goes unmaintained, Mandu's desktop story breaks.
|
|
11
|
+
*
|
|
12
|
+
* The upstream C++ library (https://github.com/webview/webview) is
|
|
13
|
+
* healthy — 14k★, 636 commits, backed by continuous Microsoft + Apple
|
|
14
|
+
* platform support. The fallback binds Mandu directly to that library
|
|
15
|
+
* via `bun:ffi`, bypassing `webview-bun` as a JS layer.
|
|
16
|
+
*
|
|
17
|
+
* This file is a **prototype** — it declares the minimal FFI surface
|
|
18
|
+
* (`webview_create` / `webview_navigate` / `webview_set_title` /
|
|
19
|
+
* `webview_run` / `webview_destroy`) and wires a creator that mirrors
|
|
20
|
+
* the `webview-bun` constructor contract so `window.ts::createWindow`
|
|
21
|
+
* can substitute it.
|
|
22
|
+
*
|
|
23
|
+
* # Non-goals
|
|
24
|
+
*
|
|
25
|
+
* - NOT production-ready. Phase 11 C ships the prototype; real rollout
|
|
26
|
+
* (signed DLL mirroring + SHA-256 verification + Mandu CDN or LFS
|
|
27
|
+
* hosting) is tracked in `docs/bun/phase-11-diagnostics/completeness-sprint.md`
|
|
28
|
+
* §C-1.
|
|
29
|
+
* - NOT a replacement for `webview-bun` on the happy path. Only used
|
|
30
|
+
* when `MANDU_DESKTOP_INLINE_FFI=1` is set OR when the `webview-bun`
|
|
31
|
+
* peer's dynamic `import()` rejects with a module-not-found error.
|
|
32
|
+
* - NOT an IPC layer. `webview-bun`'s `bind()` is not mirrored — the
|
|
33
|
+
* fallback exposes `navigate` + `setHTML` + `title` + `run` + `destroy`
|
|
34
|
+
* only. Apps that use `bind()` must keep the primary peer.
|
|
35
|
+
*
|
|
36
|
+
* # Safety
|
|
37
|
+
*
|
|
38
|
+
* - `bun:ffi`'s `dlopen` throws synchronously when the library path is
|
|
39
|
+
* unreachable. All three entrypoints (`_ffiSymbols`, `loadFFILibwebview`,
|
|
40
|
+
* `createFallbackWebview`) are guarded so that importing this module
|
|
41
|
+
* never throws — the failure surface is at first use.
|
|
42
|
+
* - The library search order is: (1) `MANDU_LIBWEBVIEW_PATH` env var
|
|
43
|
+
* (absolute path), (2) `@mandujs/core` package-relative
|
|
44
|
+
* `libwebview.{dll|dylib|so}` (if mirrored by a future release
|
|
45
|
+
* pipeline), (3) system-default via `dlopen` with the bare name. When
|
|
46
|
+
* all three fail, `loadFFILibwebview()` throws an actionable error
|
|
47
|
+
* explaining each probe path that was tried.
|
|
48
|
+
* - Type definitions use `FFIType.cstring` for strings, matching the
|
|
49
|
+
* upstream C ABI (`const char*`). `destroy()` zeros the pointer so
|
|
50
|
+
* double-free is a no-op at the FFI level.
|
|
51
|
+
*
|
|
52
|
+
* # Runtime behavior
|
|
53
|
+
*
|
|
54
|
+
* On import, this module does NOT load `libwebview`. `dlopen` only runs
|
|
55
|
+
* on the first `createFallbackWebview()` call, at which point a clean
|
|
56
|
+
* "install and mirror libwebview" error is thrown if the library is
|
|
57
|
+
* unreachable. Tests exercise the FFI surface definitions without
|
|
58
|
+
* triggering the load path (see `__tests__/webview-fallback.test.ts`).
|
|
59
|
+
*
|
|
60
|
+
* # References
|
|
61
|
+
*
|
|
62
|
+
* - `docs/bun/phase-9-diagnostics/webview-bun-ffi.md` §8 (fallback design)
|
|
63
|
+
* - https://github.com/webview/webview/blob/master/webview.h (C API)
|
|
64
|
+
* - https://bun.sh/docs/api/ffi (Bun FFI docs)
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
import type {
|
|
68
|
+
WindowHandle,
|
|
69
|
+
WindowOptions,
|
|
70
|
+
WindowSizeHint,
|
|
71
|
+
} from "./types.js";
|
|
72
|
+
|
|
73
|
+
// ─── FFI symbol contract (upstream webview/webview) ────────────────────────
|
|
74
|
+
//
|
|
75
|
+
// This object is a PURE specification — it's the `dlopen` symbols map we
|
|
76
|
+
// hand to `bun:ffi`. Exported so tests can introspect the contract
|
|
77
|
+
// without triggering a real `dlopen`.
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The minimal C ABI we need from libwebview. Signatures mirror
|
|
81
|
+
* `webview.h` from the upstream C++ repo.
|
|
82
|
+
*
|
|
83
|
+
* @internal
|
|
84
|
+
*/
|
|
85
|
+
export const _ffiSymbols = Object.freeze({
|
|
86
|
+
/**
|
|
87
|
+
* `webview_t webview_create(int debug, void* window);`
|
|
88
|
+
*
|
|
89
|
+
* Create a native webview. `debug=1` enables DevTools / WebInspector.
|
|
90
|
+
* `window=null` asks libwebview to create its own shell window.
|
|
91
|
+
* Returns an opaque handle (`webview_t`) or `null` on failure.
|
|
92
|
+
*/
|
|
93
|
+
webview_create: {
|
|
94
|
+
args: ["i32", "ptr"] as const,
|
|
95
|
+
returns: "ptr" as const,
|
|
96
|
+
},
|
|
97
|
+
/**
|
|
98
|
+
* `void webview_navigate(webview_t w, const char* url);`
|
|
99
|
+
*
|
|
100
|
+
* Point the webview at a URL. Blocks until the new page's `onload`
|
|
101
|
+
* fires (or times out per platform rules).
|
|
102
|
+
*
|
|
103
|
+
* Note: `bun:ffi` arg type for C strings is `ptr` (pointer to an
|
|
104
|
+
* encoded buffer), NOT `cstring` (which is return-only in Bun 1.3).
|
|
105
|
+
* Callers must pre-encode JS strings via `_encodeCString` before
|
|
106
|
+
* passing to this symbol. See `webview-bun/src/ffi.ts` for the
|
|
107
|
+
* canonical reference.
|
|
108
|
+
*/
|
|
109
|
+
webview_navigate: {
|
|
110
|
+
args: ["ptr", "ptr"] as const,
|
|
111
|
+
returns: "void" as const,
|
|
112
|
+
},
|
|
113
|
+
/**
|
|
114
|
+
* `void webview_set_title(webview_t w, const char* title);`
|
|
115
|
+
*
|
|
116
|
+
* Set the native window title. No-op if the underlying OS window
|
|
117
|
+
* hasn't been realized yet. Second arg is a ptr — pre-encode via
|
|
118
|
+
* `_encodeCString`.
|
|
119
|
+
*/
|
|
120
|
+
webview_set_title: {
|
|
121
|
+
args: ["ptr", "ptr"] as const,
|
|
122
|
+
returns: "void" as const,
|
|
123
|
+
},
|
|
124
|
+
/**
|
|
125
|
+
* `void webview_set_size(webview_t w, int width, int height, int hints);`
|
|
126
|
+
*
|
|
127
|
+
* Hints map (from webview.h):
|
|
128
|
+
* WEBVIEW_HINT_NONE = 0 // freely resizable
|
|
129
|
+
* WEBVIEW_HINT_MIN = 1
|
|
130
|
+
* WEBVIEW_HINT_MAX = 2
|
|
131
|
+
* WEBVIEW_HINT_FIXED = 3
|
|
132
|
+
*/
|
|
133
|
+
webview_set_size: {
|
|
134
|
+
args: ["ptr", "i32", "i32", "i32"] as const,
|
|
135
|
+
returns: "void" as const,
|
|
136
|
+
},
|
|
137
|
+
/**
|
|
138
|
+
* `void webview_set_html(webview_t w, const char* html);`
|
|
139
|
+
*
|
|
140
|
+
* Second arg is a ptr — pre-encode via `_encodeCString`.
|
|
141
|
+
*/
|
|
142
|
+
webview_set_html: {
|
|
143
|
+
args: ["ptr", "ptr"] as const,
|
|
144
|
+
returns: "void" as const,
|
|
145
|
+
},
|
|
146
|
+
/**
|
|
147
|
+
* `void webview_run(webview_t w);`
|
|
148
|
+
*
|
|
149
|
+
* BLOCKING. Returns when the user closes the shell window or when
|
|
150
|
+
* `webview_terminate(w)` is called from another thread.
|
|
151
|
+
*/
|
|
152
|
+
webview_run: {
|
|
153
|
+
args: ["ptr"] as const,
|
|
154
|
+
returns: "void" as const,
|
|
155
|
+
},
|
|
156
|
+
/**
|
|
157
|
+
* `void webview_terminate(webview_t w);`
|
|
158
|
+
*
|
|
159
|
+
* Request that `webview_run` return ASAP. Safe to call from any
|
|
160
|
+
* thread (unlike `destroy`, which must follow run).
|
|
161
|
+
*/
|
|
162
|
+
webview_terminate: {
|
|
163
|
+
args: ["ptr"] as const,
|
|
164
|
+
returns: "void" as const,
|
|
165
|
+
},
|
|
166
|
+
/**
|
|
167
|
+
* `void webview_destroy(webview_t w);`
|
|
168
|
+
*
|
|
169
|
+
* Free the webview and its native resources. Must be called after
|
|
170
|
+
* `webview_run` returns.
|
|
171
|
+
*/
|
|
172
|
+
webview_destroy: {
|
|
173
|
+
args: ["ptr"] as const,
|
|
174
|
+
returns: "void" as const,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Encode a JS string as a null-terminated C string pointer, suitable for
|
|
180
|
+
* passing to `ptr`-arg FFI calls.
|
|
181
|
+
*
|
|
182
|
+
* `bun:ffi`'s `cstring` type is return-only in Bun 1.3; for C-string
|
|
183
|
+
* arguments we must encode the string to a `Buffer` and take a pointer.
|
|
184
|
+
* This helper mirrors `webview-bun/src/ffi.ts::encodeCString`.
|
|
185
|
+
*
|
|
186
|
+
* The returned pointer is valid ONLY as long as the encoding Buffer is
|
|
187
|
+
* reachable — callers MUST keep the buffer alive (e.g. by holding the
|
|
188
|
+
* reference in a local variable) until the FFI call returns.
|
|
189
|
+
*
|
|
190
|
+
* @internal
|
|
191
|
+
*/
|
|
192
|
+
export async function _encodeCString(
|
|
193
|
+
value: string,
|
|
194
|
+
): Promise<{ ptr: unknown; buffer: Uint8Array }> {
|
|
195
|
+
const { ptr } = await import("bun:ffi");
|
|
196
|
+
const buffer = new TextEncoder().encode(value + "\0");
|
|
197
|
+
return {
|
|
198
|
+
ptr: (ptr as (b: Uint8Array) => unknown)(buffer),
|
|
199
|
+
buffer,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Map the string hint to libwebview's numeric enum (matches
|
|
205
|
+
* `WEBVIEW_HINT_*` in `webview.h`).
|
|
206
|
+
*
|
|
207
|
+
* @internal
|
|
208
|
+
*/
|
|
209
|
+
export function _mapHintToInt(hint: WindowSizeHint | undefined): number {
|
|
210
|
+
switch (hint) {
|
|
211
|
+
case "min":
|
|
212
|
+
return 1;
|
|
213
|
+
case "max":
|
|
214
|
+
return 2;
|
|
215
|
+
case "fixed":
|
|
216
|
+
return 3;
|
|
217
|
+
case "none":
|
|
218
|
+
case undefined:
|
|
219
|
+
default:
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ─── Library loading ───────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* OS-specific native lib filename. Returned as a best-effort default — the
|
|
228
|
+
* real resolution path adds `MANDU_LIBWEBVIEW_PATH` and a package-relative
|
|
229
|
+
* probe before falling back to this.
|
|
230
|
+
*
|
|
231
|
+
* @internal
|
|
232
|
+
*/
|
|
233
|
+
export function _defaultLibName(): string {
|
|
234
|
+
switch (process.platform) {
|
|
235
|
+
case "win32":
|
|
236
|
+
return "libwebview.dll";
|
|
237
|
+
case "darwin":
|
|
238
|
+
return "libwebview.dylib";
|
|
239
|
+
default:
|
|
240
|
+
// Linux + BSDs + etc.
|
|
241
|
+
return "libwebview.so";
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Ordered list of candidate paths for the libwebview shared library.
|
|
247
|
+
*
|
|
248
|
+
* Exported for the test suite so we can verify the resolution contract
|
|
249
|
+
* without triggering a real `dlopen`.
|
|
250
|
+
*
|
|
251
|
+
* @internal
|
|
252
|
+
*/
|
|
253
|
+
export function _getLibraryCandidates(): string[] {
|
|
254
|
+
const candidates: string[] = [];
|
|
255
|
+
const envPath = process.env.MANDU_LIBWEBVIEW_PATH;
|
|
256
|
+
if (envPath && envPath.length > 0) {
|
|
257
|
+
candidates.push(envPath);
|
|
258
|
+
}
|
|
259
|
+
// Future-proof: a release-time mirror step will ship
|
|
260
|
+
// `packages/core/dist/native/libwebview.{dll,dylib,so}` alongside the
|
|
261
|
+
// published module. We probe this location (resolved relative to THIS
|
|
262
|
+
// module at runtime) but it is NOT guaranteed to exist in Phase 11 C.
|
|
263
|
+
//
|
|
264
|
+
// We don't fs.existsSync in this pure function — the caller does.
|
|
265
|
+
try {
|
|
266
|
+
const url = new URL("../../dist/native/" + _defaultLibName(), import.meta.url);
|
|
267
|
+
candidates.push(decodeURIComponent(url.pathname).replace(/^\//, ""));
|
|
268
|
+
} catch {
|
|
269
|
+
// URL construction must never throw in normal workflows; if it does
|
|
270
|
+
// we simply skip this candidate rather than poison the fallback.
|
|
271
|
+
}
|
|
272
|
+
// Final fallback — let `dlopen` consult the OS's dynamic-linker search
|
|
273
|
+
// path (PATH on Windows, LD_LIBRARY_PATH on Linux, DYLD_LIBRARY_PATH
|
|
274
|
+
// on macOS).
|
|
275
|
+
candidates.push(_defaultLibName());
|
|
276
|
+
return candidates;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Cached FFI handle. Populated on first successful load.
|
|
281
|
+
*/
|
|
282
|
+
interface LoadedFFI {
|
|
283
|
+
/**
|
|
284
|
+
* Bun's FFI symbols map — opaque to Mandu core, but well-defined by
|
|
285
|
+
* `bun:ffi`'s `dlopen` return type. We type this as `unknown` to keep
|
|
286
|
+
* the cross-peer contract narrow.
|
|
287
|
+
*/
|
|
288
|
+
symbols: Record<string, unknown>;
|
|
289
|
+
libraryPath: string;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
let loadedFFICache: LoadedFFI | null = null;
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Dynamically load the libwebview shared library via `bun:ffi`. Throws an
|
|
296
|
+
* actionable error when none of the candidate paths resolve.
|
|
297
|
+
*
|
|
298
|
+
* Marked async even though `dlopen` is synchronous so the call shape
|
|
299
|
+
* matches `_loadWebviewBun` in `window.ts`.
|
|
300
|
+
*
|
|
301
|
+
* @internal
|
|
302
|
+
*/
|
|
303
|
+
export async function loadFFILibwebview(): Promise<LoadedFFI> {
|
|
304
|
+
if (loadedFFICache) return loadedFFICache;
|
|
305
|
+
|
|
306
|
+
// Lazy-import `bun:ffi` so a `bun test` in an environment where `bun:ffi`
|
|
307
|
+
// isn't available (e.g. a Deno test runner, or a future isolated
|
|
308
|
+
// sandbox) still lets the module load.
|
|
309
|
+
let dlopen: unknown;
|
|
310
|
+
try {
|
|
311
|
+
// @ts-ignore -- `bun:ffi` is a Bun built-in; resolution is runtime-only.
|
|
312
|
+
const mod = await import("bun:ffi");
|
|
313
|
+
dlopen = (mod as { dlopen?: unknown }).dlopen;
|
|
314
|
+
if (typeof dlopen !== "function") {
|
|
315
|
+
throw new Error("bun:ffi.dlopen is not a function");
|
|
316
|
+
}
|
|
317
|
+
} catch (cause) {
|
|
318
|
+
throw new Error(
|
|
319
|
+
[
|
|
320
|
+
"[@mandujs/core/desktop/webview-fallback] Could not load `bun:ffi`.",
|
|
321
|
+
"This fallback requires the Bun runtime (not Node.js).",
|
|
322
|
+
"",
|
|
323
|
+
`Cause: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
324
|
+
].join("\n"),
|
|
325
|
+
{ cause: cause as Error },
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const candidates = _getLibraryCandidates();
|
|
330
|
+
const errors: string[] = [];
|
|
331
|
+
|
|
332
|
+
for (const libPath of candidates) {
|
|
333
|
+
try {
|
|
334
|
+
const loaded = (dlopen as (p: string, s: typeof _ffiSymbols) => {
|
|
335
|
+
symbols: Record<string, unknown>;
|
|
336
|
+
})(libPath, _ffiSymbols);
|
|
337
|
+
loadedFFICache = { symbols: loaded.symbols, libraryPath: libPath };
|
|
338
|
+
return loadedFFICache;
|
|
339
|
+
} catch (err) {
|
|
340
|
+
errors.push(
|
|
341
|
+
` ${libPath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
throw new Error(
|
|
347
|
+
[
|
|
348
|
+
"[@mandujs/core/desktop/webview-fallback] Failed to load libwebview from any candidate path.",
|
|
349
|
+
"",
|
|
350
|
+
"Tried (in order):",
|
|
351
|
+
...errors,
|
|
352
|
+
"",
|
|
353
|
+
"To install libwebview:",
|
|
354
|
+
" 1. Prebuilt binaries: https://github.com/webview/webview-releases",
|
|
355
|
+
" 2. Build from source: https://github.com/webview/webview#building",
|
|
356
|
+
"",
|
|
357
|
+
"Then either:",
|
|
358
|
+
" - Set MANDU_LIBWEBVIEW_PATH=<absolute path> before running the app",
|
|
359
|
+
" - Place the library on the OS dynamic-linker search path",
|
|
360
|
+
` - Default name probed: ${_defaultLibName()}`,
|
|
361
|
+
].join("\n"),
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Reset the library-load cache. Tests only.
|
|
367
|
+
*
|
|
368
|
+
* @internal
|
|
369
|
+
*/
|
|
370
|
+
export function _resetFFICache(): void {
|
|
371
|
+
loadedFFICache = null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ─── Webview factory (FFI-backed) ──────────────────────────────────────────
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Create a webview using the bun:ffi fallback path. Returns a
|
|
378
|
+
* {@link WindowHandle} that uses the same public shape as
|
|
379
|
+
* `createWindow()` — callers should not need to distinguish between
|
|
380
|
+
* the two backends.
|
|
381
|
+
*
|
|
382
|
+
* This factory is `async` to match the `webview-bun` peer loader contract
|
|
383
|
+
* (which does a dynamic import). Construction itself is synchronous
|
|
384
|
+
* beyond the initial `loadFFILibwebview()`.
|
|
385
|
+
*
|
|
386
|
+
* @example
|
|
387
|
+
* ```ts
|
|
388
|
+
* import { createFallbackWebview } from "@mandujs/core/desktop/webview-fallback";
|
|
389
|
+
*
|
|
390
|
+
* const handle = await createFallbackWebview({
|
|
391
|
+
* url: "http://127.0.0.1:3333",
|
|
392
|
+
* title: "My App",
|
|
393
|
+
* width: 1024,
|
|
394
|
+
* height: 768,
|
|
395
|
+
* });
|
|
396
|
+
* handle.run();
|
|
397
|
+
* ```
|
|
398
|
+
*/
|
|
399
|
+
export async function createFallbackWebview(
|
|
400
|
+
options: WindowOptions,
|
|
401
|
+
): Promise<WindowHandle> {
|
|
402
|
+
// Option validation is the caller's responsibility — this path is
|
|
403
|
+
// reached only from `window.ts::createWindow` after it calls
|
|
404
|
+
// `_validateOptions`. We still perform minimal defensive checks so
|
|
405
|
+
// direct callers (tests, experimental apps) don't segfault.
|
|
406
|
+
if (!options || typeof options !== "object") {
|
|
407
|
+
throw new TypeError(
|
|
408
|
+
"[@mandujs/core/desktop/webview-fallback] options must be an object.",
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
if (typeof options.url !== "string" || options.url.length === 0) {
|
|
412
|
+
throw new TypeError(
|
|
413
|
+
"[@mandujs/core/desktop/webview-fallback] 'url' must be a non-empty string.",
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const { symbols } = await loadFFILibwebview();
|
|
418
|
+
|
|
419
|
+
// `bun:ffi` exposes each symbol as a callable — the return type is
|
|
420
|
+
// `(...args: any[]) => unknown`. We narrow via cast at each callsite
|
|
421
|
+
// instead of widening the symbols map's type.
|
|
422
|
+
type PtrFn = (...args: unknown[]) => unknown;
|
|
423
|
+
const create = symbols.webview_create as PtrFn;
|
|
424
|
+
const navigate = symbols.webview_navigate as PtrFn;
|
|
425
|
+
const setTitle = symbols.webview_set_title as PtrFn;
|
|
426
|
+
const setSize = symbols.webview_set_size as PtrFn;
|
|
427
|
+
const run = symbols.webview_run as PtrFn;
|
|
428
|
+
const terminate = symbols.webview_terminate as PtrFn;
|
|
429
|
+
const destroy = symbols.webview_destroy as PtrFn;
|
|
430
|
+
|
|
431
|
+
const debug = options.debug ? 1 : 0;
|
|
432
|
+
const handle = create(debug, null) as unknown;
|
|
433
|
+
if (!handle) {
|
|
434
|
+
throw new Error(
|
|
435
|
+
"[@mandujs/core/desktop/webview-fallback] webview_create returned null (libwebview init failed).",
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Size + title BEFORE navigate, matching the `webview-bun` ctor contract.
|
|
440
|
+
const width = options.width ?? 1024;
|
|
441
|
+
const height = options.height ?? 768;
|
|
442
|
+
setSize(handle, width, height, _mapHintToInt(options.hint));
|
|
443
|
+
// The libwebview C API takes a `const char*`; `bun:ffi`'s `ptr` arg
|
|
444
|
+
// type needs a pre-encoded buffer. Keep references live across the
|
|
445
|
+
// entire sync block so the GC doesn't collect the buffer before
|
|
446
|
+
// libwebview copies out the string. `_encodeCString` returns both the
|
|
447
|
+
// ptr and the backing Uint8Array for this reason.
|
|
448
|
+
const titleEnc = await _encodeCString(options.title ?? "Mandu Desktop");
|
|
449
|
+
setTitle(handle, titleEnc.ptr);
|
|
450
|
+
const urlEnc = await _encodeCString(options.url);
|
|
451
|
+
navigate(handle, urlEnc.ptr);
|
|
452
|
+
// Keep encoding buffers reachable past the sync navigate call.
|
|
453
|
+
// libwebview copies the strings internally, so once we're past the
|
|
454
|
+
// synchronous FFI return these buffers can be GC'd — we just need
|
|
455
|
+
// them alive THROUGH the FFI call, which the local const refs ensure.
|
|
456
|
+
void titleEnc.buffer;
|
|
457
|
+
void urlEnc.buffer;
|
|
458
|
+
|
|
459
|
+
let closed = false;
|
|
460
|
+
let resolveClosed: (() => void) | null = null;
|
|
461
|
+
const closedPromise = new Promise<void>((resolve) => {
|
|
462
|
+
resolveClosed = resolve;
|
|
463
|
+
});
|
|
464
|
+
const closeCallbacks: Array<() => void> = [];
|
|
465
|
+
|
|
466
|
+
function markClosed(): void {
|
|
467
|
+
if (closed) return;
|
|
468
|
+
closed = true;
|
|
469
|
+
for (const cb of closeCallbacks) {
|
|
470
|
+
try {
|
|
471
|
+
cb();
|
|
472
|
+
} catch (error) {
|
|
473
|
+
console.error(
|
|
474
|
+
"[@mandujs/core/desktop/webview-fallback] onClose callback threw:",
|
|
475
|
+
error,
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (options.onClose) {
|
|
480
|
+
try {
|
|
481
|
+
const result = options.onClose();
|
|
482
|
+
if (result instanceof Promise) {
|
|
483
|
+
result.catch((error) =>
|
|
484
|
+
console.error(
|
|
485
|
+
"[@mandujs/core/desktop/webview-fallback] onClose (options) threw:",
|
|
486
|
+
error,
|
|
487
|
+
),
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
} catch (error) {
|
|
491
|
+
console.error(
|
|
492
|
+
"[@mandujs/core/desktop/webview-fallback] onClose (options) threw:",
|
|
493
|
+
error,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
resolveClosed?.();
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (options.onReady) {
|
|
501
|
+
queueMicrotask(() => {
|
|
502
|
+
try {
|
|
503
|
+
const result = options.onReady!();
|
|
504
|
+
if (result instanceof Promise) {
|
|
505
|
+
result.catch((error) =>
|
|
506
|
+
console.error(
|
|
507
|
+
"[@mandujs/core/desktop/webview-fallback] onReady threw:",
|
|
508
|
+
error,
|
|
509
|
+
),
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
} catch (error) {
|
|
513
|
+
console.error(
|
|
514
|
+
"[@mandujs/core/desktop/webview-fallback] onReady threw:",
|
|
515
|
+
error,
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
async close(): Promise<void> {
|
|
523
|
+
if (closed) return;
|
|
524
|
+
try {
|
|
525
|
+
terminate(handle);
|
|
526
|
+
} catch {
|
|
527
|
+
/* ignore — terminate may fail if run() hasn't started */
|
|
528
|
+
}
|
|
529
|
+
try {
|
|
530
|
+
destroy(handle);
|
|
531
|
+
} catch (error) {
|
|
532
|
+
if (options.debug) {
|
|
533
|
+
console.warn(
|
|
534
|
+
"[@mandujs/core/desktop/webview-fallback] destroy() warning:",
|
|
535
|
+
error,
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
markClosed();
|
|
540
|
+
},
|
|
541
|
+
onClose(cb: () => void): void {
|
|
542
|
+
if (closed) {
|
|
543
|
+
queueMicrotask(() => {
|
|
544
|
+
try {
|
|
545
|
+
cb();
|
|
546
|
+
} catch (error) {
|
|
547
|
+
console.error(
|
|
548
|
+
"[@mandujs/core/desktop/webview-fallback] onClose callback threw:",
|
|
549
|
+
error,
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
closeCallbacks.push(cb);
|
|
556
|
+
},
|
|
557
|
+
async eval(_js: string): Promise<void> {
|
|
558
|
+
// eval() needs `webview_eval` which requires an onmessage callback
|
|
559
|
+
// wired through webview_bind. Phase 11 C ships the fallback
|
|
560
|
+
// without the bind surface — explicit error so apps that depend
|
|
561
|
+
// on eval() stay on the primary peer.
|
|
562
|
+
throw new Error(
|
|
563
|
+
"[@mandujs/core/desktop/webview-fallback] eval() is not supported. " +
|
|
564
|
+
"Install webview-bun for full IPC (eval + bind).",
|
|
565
|
+
);
|
|
566
|
+
},
|
|
567
|
+
bind(_name: string, _fn: (...args: unknown[]) => unknown): void {
|
|
568
|
+
throw new Error(
|
|
569
|
+
"[@mandujs/core/desktop/webview-fallback] bind() is not supported. " +
|
|
570
|
+
"Install webview-bun for full IPC (eval + bind).",
|
|
571
|
+
);
|
|
572
|
+
},
|
|
573
|
+
closed: closedPromise,
|
|
574
|
+
run(): void {
|
|
575
|
+
if (closed) return;
|
|
576
|
+
try {
|
|
577
|
+
run(handle);
|
|
578
|
+
} finally {
|
|
579
|
+
markClosed();
|
|
580
|
+
}
|
|
581
|
+
},
|
|
582
|
+
};
|
|
583
|
+
}
|