@gjsify/webview2-native 0.45.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.
Files changed (2) hide show
  1. package/README.md +253 -0
  2. package/package.json +59 -0
package/README.md ADDED
@@ -0,0 +1,253 @@
1
+ # @gjsify/webview2-native
2
+
3
+ **This package installs a typelib that answers to `gi://WebKit` version 6.0, and
4
+ the engine behind it is Chromium.** It is Microsoft's WebView2 wearing
5
+ WebKitGTK's API, on Windows only, so that
6
+ [`@gjsify/iframe`](../iframe/README.md) — and anything else written against
7
+ `WebKit.WebView` — runs there without a backend seam or an OS branch. The name
8
+ says WebView2 because the engine does; the namespace says WebKit because the API
9
+ shape does. Nothing about that is accidental, and the reasoning is
10
+ [ADR 0035](../../../docs/adr/0035-web-view-on-win32.md).
11
+
12
+ Sibling: [`@gjsify/webkit-native`](../webkit-native/README.md) does the same job
13
+ on macOS with Apple's WebKit behind it (ADR 0022). On Linux there is no shim —
14
+ `gi://WebKit` 6.0 is the real WebKitGTK.
15
+
16
+ > **State: stage 1 is implemented and demonstrated.** The win32 load test runs
17
+ > on `windows-latest` on every change to this package and passes 13 assertions in
18
+ > 7.18 s: `gi://WebKit` 6.0 resolves, `new WebKit.WebView()` is a `Gtk.Widget`
19
+ > reporting `HostingMode.OVERLAY`, the message pump reports `ATTACHED`,
20
+ > `load_html()` reaches `LoadEvent.FINISHED`, `evaluate_javascript()` reads a
21
+ > marker back **out of the DOM**, `get_snapshot()` returns a 640×480 texture that
22
+ > encodes to 5966 B of PNG, and the page's own `postMessage` arrives as
23
+ > `script-message-received`.
24
+ >
25
+ > **What is not proven is the hosted path itself.** The probe never presents a
26
+ > toplevel — an SSH session on Windows lands in session 0, where `present()` dies
27
+ > with `0xC0000005` — so all of the above ran against the hidden parking window.
28
+ > Re-parenting the child `HWND` under a real GTK toplevel, bounds tracking in
29
+ > device pixels, hiding on unmap, and the claim that input, focus and
30
+ > accessibility come from the OS are **untested**. Stage 2 does not exist. Treat
31
+ > this as "a full-page document in a window works, headlessly, provably" and not
32
+ > yet as "a web view widget on Windows".
33
+
34
+ ## Why this exists
35
+
36
+ WebKit's **GTK port** targets X11 and Wayland; there is no Windows GTK port
37
+ upstream, `gvsbuild` (the project that builds GTK for Windows) has no WebKit
38
+ project at all, and WebKit's **WinCairo** port — which does build — ships neither
39
+ a GObject-Introspection typelib nor a GTK widget. So `gi://WebKit` 6.0 has no
40
+ win32 provider, and not for packaging reasons.
41
+
42
+ What Windows does have is a complete, supported, already-installed web engine:
43
+ **WebView2**, the Evergreen runtime that ships with Windows 11 and with Edge on
44
+ Windows 10. That solves the engine half before this package starts, exactly as
45
+ `WebKit.framework` solved it on darwin. What it does not solve is the widget
46
+ half, and that is what stage 1 below is honest about.
47
+
48
+ ## What ships, and what it is
49
+
50
+ | exposed | backed by |
51
+ |---|---|
52
+ | `WebKit.WebView` (a real `Gtk.Widget`, derivable) | `ICoreWebView2Controller` on a child `HWND` |
53
+ | `WebKit.UserContentManager` / `UserScript` | `AddScriptToExecuteOnDocumentCreated` |
54
+ | `WebKit.Settings` | `ICoreWebView2Settings` |
55
+ | `load-changed` + `WebKit.LoadEvent` | `NavigationStarting` / `ContentLoading` / `NavigationCompleted` |
56
+ | `script-message-received::<name>` | `window.chrome.webview.postMessage`, behind a `window.webkit.messageHandlers` object injected once per view |
57
+ | `evaluate_javascript()` → a value with `to_string()` | `ExecuteScript` (which returns JSON — see below) |
58
+ | `get_snapshot()` → `Gdk.Texture` | `CapturePreview` (PNG) decoded by `gdk_texture_new_from_bytes()` |
59
+
60
+ That is the surface `@gjsify/iframe` actually uses, not WebKitGTK's — ADR 0035
61
+ decision 4, which now counts **six** instance methods rather than the four ADR
62
+ 0022 recorded: `get_uri()` and `get_user_content_manager()` are live call sites
63
+ too, and a backend built to the shorter list would have compiled, installed and
64
+ broken the consumer at run time. Alongside them stage 1 carries eight WebKitGTK
65
+ parity names (`reload`, `is_loading`, `remove_all_scripts`,
66
+ `unregister_script_message_handler`, `user_script_new_for_world`,
67
+ `Value.is_string` / `is_null` / `is_undefined`) so code written against the real
68
+ thing does not meet an `undefined`, and nothing wider than that.
69
+
70
+ ## The three things that are not obvious
71
+
72
+ ### 1. It is an OS-composited OVERLAY, not a widget GSK draws
73
+
74
+ This is stage 1 of a two-stage plan, and the stage boundary is exactly here. The
75
+ web content is a child `HWND` under the GTK toplevel's own `HWND`, positioned and
76
+ sized to the widget's allocation, hidden when the widget is unmapped. Input,
77
+ focus and accessibility therefore come from the OS for free — which is the whole
78
+ reason this staging is the reverse of darwin's, where the widget was the
79
+ expensive half.
80
+
81
+ The price is that the content is **outside GSK's scene graph**:
82
+
83
+ - an ancestor cannot clip it — a `GtkScrolledWindow` scrolls the widget and not
84
+ the page, a rounded corner does nothing;
85
+ - nothing can be drawn over it;
86
+ - opacity and transforms are not applied.
87
+
88
+ GTK's failure mode for all of that is exit 0, so this package says so out loud
89
+ rather than in a doc comment:
90
+
91
+ ```js
92
+ view.get_hosting_mode() // WebKit.HostingMode.OVERLAY
93
+ view.get_overlay_constraints() // the arrangements it is in that it cannot honour
94
+ ```
95
+
96
+ Each detected constraint is also warned once per view, naming the ancestor. What
97
+ the detector does **not** see is a CSS `border-radius` that reaches the widget —
98
+ that is not readable from GTK's public API — so the list is the arrangements that
99
+ have actually been reported, not a proof of absence.
100
+
101
+ Stage 2 (composition hosting plus `Windows.Graphics.Capture` into a
102
+ `Gdk.Texture`) is what changes the answer, and it is not in this release.
103
+
104
+ ### 2. The Win32 message queue has to be dispatched, and only for some calls
105
+
106
+ WebView2 delivers content-level callbacks through the thread's Win32 message
107
+ queue, and `g_main_loop_run()` does not dispatch that queue. Measured on
108
+ `windows-latest` against Evergreen 151.0.4129.101
109
+ ([`docs/poc/webview2-win32-probe.cpp`](../../../docs/poc/webview2-win32-probe.cpp)):
110
+
111
+ | call | needs the queue pumped |
112
+ |---|---|
113
+ | `CreateCoreWebView2EnvironmentWithOptions` | no |
114
+ | `CreateCoreWebView2Controller` | no |
115
+ | `NavigationCompleted` | **yes** — 8000 ms timeout without, immediate with |
116
+
117
+ That asymmetry is the interesting part, and it decided the design. A backend that
118
+ installed its bridge at the first need would install it after the only two calls
119
+ that do not have one — so the widget would exist, the view would exist, and
120
+ nothing would load, eight seconds and one abstraction layer from the cause.
121
+
122
+ So the pump `GSource` is attached when the **first view is constructed**, held
123
+ while any view is alive, and its absence is a **named error on every
124
+ content-level call** instead of a timeout:
125
+
126
+ ```js
127
+ view.get_message_pump_state() // WebKit.MessagePumpState.ATTACHED | DETACHED | FOREIGN_THREAD
128
+ ```
129
+
130
+ GDK's own Win32 backend pumps the same queue when a display is open. That is not
131
+ a conflict — both sources call `PeekMessage(PM_REMOVE)` on one queue, so a
132
+ message is removed once — and it is not a substitute either, because a view with
133
+ no display and no toplevel still has to reach `NavigationCompleted`. The CI proof
134
+ runs in exactly that shape.
135
+
136
+ ### 3. `evaluate_javascript` speaks JSON
137
+
138
+ `ExecuteScript` returns its result as JSON text where WebKitGTK returns a live
139
+ `JSCValue`. A JSON string is unquoted and unescaped, so a string result reads the
140
+ same on both backends; everything else comes back as its JSON text, which matches
141
+ `JSCValue.to_string()` for numbers and booleans and is a real divergence for
142
+ objects. WebView2 also returns `"null"` for both `null` and `undefined` — they
143
+ are not distinguishable in JSON — so `is_null()` is true for both and
144
+ `is_undefined()` is never true.
145
+
146
+ ## What stage 1 does not do
147
+
148
+ Each fails loudly rather than silently, which is the difference between a subset
149
+ and a lie:
150
+
151
+ - **Named script worlds are ignored, with a warning from every entry point that
152
+ takes one** — `UserScript`, `evaluate_javascript`,
153
+ `register_script_message_handler`, `unregister_script_message_handler`.
154
+ WebView2 has no public isolated-world API; `new_for_world()` exists so the call
155
+ site stays portable and says what it did. The darwin backend HONOURS the same
156
+ argument (`WKContentWorld`), so the same call is isolated there and not here —
157
+ a divergence, not a no-op, which is why silence was the wrong answer.
158
+ - **A user script carrying an allow or block list is REFUSED, with a warning.**
159
+ WebView2's injection point has no URL filter. Warning and injecting anyway is
160
+ precisely the failure a block list exists to prevent, so this narrows in the
161
+ safe direction. Porting the darwin backend's in-script guard is what closes
162
+ this; it is outside ADR 0035 decision 4's counted subset.
163
+ - **`UserScriptInjectionTime.END` is approximated** by a document-start script
164
+ that defers itself to `DOMContentLoaded` — WebView2 has one injection point.
165
+ - **`SnapshotRegion.FULL_DOCUMENT` returns the viewport, with a warning.**
166
+ `CapturePreview` captures what is laid out, not the whole scrollable document.
167
+ - **`SnapshotOptions` other than `NONE` are ignored, with a warning.**
168
+ `CapturePreview` has no transparent-background and no selection-highlighting
169
+ option. Both snapshot divergences are reported by the portable layer at the
170
+ call, not by the engine, so a caller whose snapshot never arrives for want of
171
+ an engine or a pump still learns the arguments would not have been honoured.
172
+ - **`Settings.allow-file-access-from-file-urls` is not offered.** It existed,
173
+ was writable, and reached nothing — WebView2's equivalent is a
174
+ browser-command-line switch on the process-wide environment, not a per-view
175
+ setting. An absent property raises a GJS warning at the call; a present one
176
+ that does nothing raises none.
177
+ - **`evaluate_javascript`'s `source_uri` is ignored, and it is the one divergence
178
+ here that does not warn.** `ExecuteScript` has no source-URI parameter, and the
179
+ argument changes nothing observable except the text attributed to a script in
180
+ an error — warning for a cosmetic loss is how the behavioural warnings beside
181
+ it get tuned out.
182
+ - **`Settings.enable-write-console-messages-to-stdout` is not honoured.**
183
+ WebView2 has no console-forwarding API short of a DevTools Protocol session;
184
+ `@gjsify/iframe`'s console-capture user script works on every backend.
185
+ - **An UNREGISTERED message channel accepts `postMessage` instead of throwing.**
186
+ WebKitGTK leaves `window.webkit.messageHandlers.foo` `undefined` until a
187
+ manager registers it, so a page posting to it gets a TypeError. Here one
188
+ auto-vivifying object is installed per view, ahead of every user script,
189
+ because WebView2 runs document-start scripts in registration order and a
190
+ per-handler shim would run *after* a bootstrap script that uses it — which is
191
+ the whole `@gjsify/iframe` bridge. The host warns once per unknown channel,
192
+ naming it, rather than discarding the message in silence.
193
+ - **`x64` only.** `gvsbuild` publishes no arm64 GTK, so ADR 0024's `--arch arm64`
194
+ refusal on Windows already forecloses the question.
195
+
196
+ ## The runtime closure is bigger than the tarball
197
+
198
+ `gjsifywebview2.dll` links GTK4, GLib and GObject, and Windows has no system copy
199
+ of any of them — so this prebuild is usable next to
200
+ [`@gjsify/gtk-runtime-win32-x64`](../../node-gi/gtk-runtime-win32-x64/README.md)'s
201
+ batteries-included bundle, which every win32 consumer already has because it is
202
+ how `@gjsify/node-gi` gets GObject at all. That bundle is **not** pulled in
203
+ automatically (node-gi declares no `optionalDependencies` on it, deliberately —
204
+ ADR 0023); install it explicitly. Duplicating those DLLs into this tarball would
205
+ put a second copy of each on the process's search path, which is a worse failure
206
+ than the one it solves.
207
+
208
+ Measured on the win11-gjsify VM against `@gjsify/gtk-runtime-win32-x64@0.45.0`:
209
+ 45 typelibs, **none of them `WebKit`**, no `JavaScriptCore`, no `Soup`, and no
210
+ `webkit*` DLL among the 52 in `gtk/bin`. There is nothing on Windows to wire this
211
+ namespace up to — which is why the package exists.
212
+
213
+ ## The Evergreen runtime is a dependency, not an assumption
214
+
215
+ `CreateCoreWebView2Environment` answers
216
+ `HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)` on a machine with no runtime, and this
217
+ package turns that into a message naming the runtime and the Fixed Version
218
+ redistributable rather than a bare HRESULT. Declaring it in the `.msi` that
219
+ `gjsify ship windows` produces — detection at install time, the redistributable
220
+ as the opt-out — is ADR 0035 decision 5 and is **not** in this release.
221
+
222
+ **Whoever writes that detection: read both registry views.** Measured on Windows
223
+ 11 build 26200 with the runtime installed (152.0.4191.53), the Evergreen client
224
+ key exists only under
225
+ `HKLM\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-…}` — the
226
+ 64-bit view does not have it. A detector reading the 64-bit path alone reports
227
+ "not installed" on a machine that has it: green in CI, wrong at the user. The
228
+ view-independent answer is `GetAvailableCoreWebView2BrowserVersionString`. This
229
+ backend reads no registry key of its own — it lets
230
+ `CreateCoreWebView2EnvironmentWithOptions` do the lookup and names the HRESULT —
231
+ so that detection is code the installer still owes.
232
+
233
+ ## Building
234
+
235
+ There is no host that can build all of this at once, and that is structural
236
+ rather than a limitation of anybody's machine:
237
+
238
+ | half | needs | where |
239
+ |---|---|---|
240
+ | `WebKit-6.0.gir` | `g-ir-scanner`, which builds and RUNS a dumper against the library | Fedora (`ghcr.io/gjsify/ci-fedora:43`) |
241
+ | `gjsifywebview2.dll` + `WebKit-6.0.typelib` | MSVC, the WebView2 SDK, gvsbuild's GTK4 | `windows-latest` |
242
+
243
+ Both halves are two jobs of ONE `prebuilds.yml` run, so drift between them is
244
+ structurally impossible rather than merely unlikely. On Fedora the library links
245
+ against `src/c/gjsify-webview2-unsupported.c`, which registers no behaviour and
246
+ fails every call loudly; nothing it produces is ever staged, because `win32-x64`
247
+ is this package's only declared target and `stage-prebuild.mjs` refuses any
248
+ other host.
249
+
250
+ ```bash
251
+ # the GIR half, on any host with gtk4-devel + gobject-introspection-devel
252
+ gjsify workspace @gjsify/webview2-native run build:meson
253
+ ```
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@gjsify/webview2-native",
3
+ "version": "0.45.0",
4
+ "description": "Microsoft's WebView2 (Chromium) behind a GObject API shaped like WebKitGTK 6.0 — the win32 backend for @gjsify/iframe (ADR 0035). Deliberately claims the WebKit-6.0 namespace; the engine is Chromium. Contains no JavaScript; the prebuilt library and typelib arrive through per-target optionalDependencies.",
5
+ "type": "module",
6
+ "files": [],
7
+ "gjsify": {
8
+ "platforms": [
9
+ "win32-x64"
10
+ ],
11
+ "runtimes": {
12
+ "gjs": "polyfill",
13
+ "node": "none",
14
+ "browser": "none",
15
+ "nativescript": "none"
16
+ },
17
+ "os": {
18
+ "linux": "none",
19
+ "darwin": "none",
20
+ "win32": "supported"
21
+ },
22
+ "osNotes": {
23
+ "linux": "Binds Microsoft's WebView2, which exists only on Windows. Linux has the real thing: @gjsify/iframe imports gi://WebKit 6.0 there and never reaches this package. The Linux build of this library exists only so g-ir-scanner can produce WebKit-6.0.gir, and it is never staged or shipped.",
24
+ "darwin": "Binds Microsoft's WebView2, which exists only on Windows. macOS is served by @gjsify/webkit-native, which claims the same namespace with Apple's WebKit behind it (ADR 0022)."
25
+ },
26
+ "tier": 1
27
+ },
28
+ "scripts": {
29
+ "clear": "gjsify clear build _gir _install",
30
+ "check": "echo 'no TypeScript in this package'",
31
+ "init:meson": "meson setup build .",
32
+ "init:meson:wipe": "gjsify run init:meson --wipe",
33
+ "build": "echo 'native only — see build:prebuilds'",
34
+ "build:meson": "gjsify run init:meson && meson compile -C build",
35
+ "build:prebuilds": "gjsify run build:meson && node ../../../scripts/stage-prebuild.mjs .",
36
+ "test": "echo 'covered by the win32 leg of prebuilds.yml'"
37
+ },
38
+ "keywords": [
39
+ "gjs",
40
+ "webview2",
41
+ "chromium",
42
+ "webkit",
43
+ "win32",
44
+ "windows"
45
+ ],
46
+ "license": "MIT",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/gjsify/gjsify.git",
50
+ "directory": "packages/framework/webview2-native"
51
+ },
52
+ "bugs": {
53
+ "url": "https://github.com/gjsify/gjsify/issues"
54
+ },
55
+ "homepage": "https://github.com/gjsify/gjsify/tree/main/packages/framework/webview2-native#readme",
56
+ "optionalDependencies": {
57
+ "@gjsify/webview2-native-win32-x64": "0.45.0"
58
+ }
59
+ }