Spa

examples/webview/spa.ts — the "pack any SPA" pattern.

A multi-asset SPA (anything that builds to a static dist/ — React/Vue/ Svelte/vanilla) is served by http.createServer running in a Worker thread (which legitimately owns its own event loop), while the main thread owns the GUI loop and navigates to it. Here dist/ is a tiny hand-written page; swap it for your framework's build output.

Build: klainmain examples/webview/spa.ts && ./examples/webview/spa Windows: klainmain examples/webview/spa.ts && examples/webview/spa.exe (WebView2; see README)

Production note: a robust app posts an *ephemeral* port (listen on 0) back from the worker via postMessage before navigating. This example uses a fixed port for clarity, with a page-reload guard injected via w.init to cover the brief window before the server socket is bound.

import { Webview } from 'klain:webview'
import { Worker } from 'worker_threads'

const PORT = 8137

// The server runs in its own thread — its listen() loop and the GUI loop each
// own a thread, exactly the coexistence contract the design calls for.
const server = new Worker('./spa_server.ts', { workerData: "" + PORT })

const w = new Webview({ title: "Klain SPA", width: 800, height: 600, debug: true })

// init() runs before every page load: if the first navigation lands before the
// server socket is up (a blank page), reload shortly after. The real page sets
// document.body.dataset.ready, which stops the reload loop.
w.init(`
  window.addEventListener('load', () => {
    setTimeout(() => {
      if (!document.body || document.body.dataset.ready !== '1') location.reload()
    }, 400)
  })
`)

w.navigate("http://127.0.0.1:" + PORT + "/")
w.run()

server.terminate()
console.log("window closed")
View source on GitHubexamples/webview/spa.ts