@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.
- package/README.md +253 -0
- 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
|
+
}
|