webview and the app node: turning a site into a desktop app
Everything the previous forty-nine chapters produced was a "site" — open a browser, type an address. This chapter gives it a window: a class: "app" service node = a local HTTP service + a WebView2 desktop window. Configuration is all it takes, degradation is automatic, and combined with xs pack you get a double-click single-exe application.
Overview
window descriptor and a frontend lifecycle are added.50.1 Your first desktop app
Take any existing site's service node, change it to class: "app" and add a window descriptor:
{
"enabled": true, "class": "app", "name": "main",
"ip": "127.0.0.1", "port": 0,
"host_default": { "name": "app", "path": "wwwroot", "devlang": "c",
"devfile": "script/http_main.c" },
"window": {
"title": "My App", "width": 1280, "height": 800,
"url": "/", "devtools": false,
"close": "stop", "install_fallback": "ask"
}
}
Start xs: a loading window appears first (a placeholder before the service is ready); once the local HTTP service is up, the main interface at url opens automatically. Note the two defaults that differ from the http class: ip defaults to 127.0.0.1 (loopback binding does not trigger the Windows firewall popup) and port allows 0 (auto-assign, see 50.3). Field-by-field constraints for the nine window fields (including min_width / min_height and the install_fallback value table) are in the xs.json field reference.
The build variant must compile in the webview extension: build.bat webview (or all); a variant without the extension still parses the config — there is just no window (see the matrix in 50.4).
50.2 Lifecycle & close semantics
- Startup: the loading window comes first (covering the service-initialization gap) → service ready → automatic navigation to the main interface
close: "stop"(default): the user closing the window = graceful shutdown through the same chain as Ctrl+C — not a process killclose: "hide": closing only hides; the service keeps running. Launching the same app again wakes the existing window via single-instance detection (a named mutex derived from the exe path) and the new process exits immediately. v1 has no tray icon; in hide mode shutdown goes through the signal path- Crash isolation: the WebView2 renderer is a separate process — a page crash does not infect xs; any window-thread failure degrades to a log line and never pushes the engine assembly onto the fail path
50.3 port: 0 auto-assignment
A desktop app's port should be invisible to the user and should not fight other applications — port: 0 hands allocation to the OS atomically (allowed only for the app class; other classes fail fast):
- Readback: after a successful bind the actual port lands in
XS_ServerInfo.PortBound(Port is written back too, so endpoint identity matches the script view); the frontend builds its URL from it - From a script:
xsServerFind("main")->PortBound, rendered into the page so the frontend knows who it is (e.g. the WS address) - Hot-reload stickiness: a sticky table keyed by service name (surviving generations) rebinds the old port explicitly on swap — open windows never notice; if the sticky port is taken, the candidate fails and cleans up per the existing failure path
- Boundary: the sticky table is not persisted. For a port stable across restarts, configure a fixed port (especially when external systems call you back)
50.4 Degradation matrix & release
| Build variant | Platform | WebView2 | Behavior |
|---|---|---|---|
| webview extension | Windows | installed | full window functionality |
| webview extension | Windows | missing | per install_fallback: ask prompts / silent installs / headless degrades at once; any failure lands on headless |
| webview extension | Linux | — | headless, the access URL printed in the log |
| no extension | any | — | same headless plus one notice line; config parses as usual |
The release combo: xs pack (chapter 48) packs the site into the binary + xsw.exe (the no-console GUI-subsystem variant shipped with the release package) — the artifact is a double-click single-exe desktop app: loading window → interface → close-to-stop, no black console at any point. Zero runtime DLLs: the import table lists system DLLs only; WebView2Loader.dll is located at runtime from the installed Runtime directory, with install_fallback covering a missing Runtime.
Common mistakes
Exercises
Summary
class: "app" = frontend-ized http (same config + 9 window fields) · loading → ready → open URL → close-to-stop = the lifecycle · port:0 + PortBound = the invisible port (fixed for cross-restart stability) · the degradation matrix = window best-effort, service first · xs pack + xsw.exe = the double-click desktop exe