Setup
Serve and update Webview Bundle archives in a Deno desktop webview through a Deno.serve request handler.
Deno Desktop renders a native webview with Deno.BrowserWindow and serves it over Deno.serve. Webview Bundle becomes that server's handler, so the window loads your packed .wvb assets offline instead of fetching them over the network.
import { webviewBundle, bundleProtocol } from '@wvb/deno-desktop';
const wvb = await webviewBundle({
protocols: [bundleProtocol('app')],
});
const server = Deno.serve(wvb.fetch);
const win = new Deno.BrowserWindow({ url: `http://localhost:${server.addr.port}` });
await win.closed;@wvb/deno-desktop is the integration layer; @wvb/deno is the FFI peer of @wvb/node that drives the shared Rust core.
How it fits together
The window points at a single local origin served by Deno.serve, so the integration is single-origin: it allows exactly one protocol. See Platform integration for how the core reaches each platform and Protocol handling for the protocol model.
@wvb/deno-desktop
Call webviewBundle(config) — aliased as wvb, backed by the WebviewBundle class — to build a BundleSource (plus an optional Remote and Updater) and get back a Deno.serve-compatible fetch handler. Add an updater to pull newer bundles from a remote:
import { webviewBundle, bundleProtocol } from '@wvb/deno-desktop';
const wvb = await webviewBundle({
protocols: [bundleProtocol('app')],
updater: {
remote: { endpoint: 'https://bundles.example.com' },
},
});
const server = Deno.serve(wvb.fetch);
const win = new Deno.BrowserWindow({ url: `http://localhost:${server.addr.port}` });
await win.closed;Bindings
Call registerBindings(win, wvb) to expose native commands to your web app. It registers one Deno.BrowserWindow binding named wvbInvoke, reachable from the page as window.bindings.wvbInvoke, that dispatches every @wvb/bridge command in the source.*, remote.*, and updater.* groups.
import { webviewBundle, bundleProtocol, registerBindings } from '@wvb/deno-desktop';
const wvb = await webviewBundle({
protocols: [bundleProtocol('app')],
});
const server = Deno.serve(wvb.fetch);
const win = new Deno.BrowserWindow({ url: `http://localhost:${server.addr.port}` });
registerBindings(win, wvb); // wires window.bindings.wvbInvoke
await win.closed;The binding never throws across the FFI boundary. Each call resolves to an InvokeResult envelope:
type InvokeResult =
| { ok: true; value: unknown }
| { ok: false; error: { code?: string; message: string } };The @wvb/bridge client unwraps this envelope once it detects the deno platform, so web code keeps calling invoke(), source.*, remote.*, and updater.* exactly as on other platforms:
import { updater } from '@wvb/bridge';
const update = await updater.getUpdate('my-app');
if (update.isAvailable) {
await updater.download('my-app');
await updater.install('my-app', update.version);
}@wvb/deno
@wvb/deno is the FFI peer of @wvb/node. It loads a prebuilt Rust cdylib through Deno.dlopen and exposes the same core classes: BundleProtocol, LocalProtocol, Remote, BundleSource, and Updater.
Each class is Disposable — free it explicitly with free(), or let a using declaration call [Symbol.dispose] at scope exit:
import { BundleProtocol } from '@wvb/deno';
// `lib` and `source` come from the load + BundleSource steps below.
// Explicit cleanup.
const protocol = new BundleProtocol(lib, source);
const res = await protocol.handle('get', 'app://my-app/index.html');
protocol.free();
// Or scope-bound cleanup with `using`.
{
using scoped = new BundleProtocol(lib, source);
await scoped.handle('get', 'app://my-app/index.html');
} // [Symbol.dispose]() runs hereLoad the native library
Load the cdylib from an explicit path, or download a SHA-256-verified prebuilt via @denosaurs/plug:
import { loadLib, loadLibViaPlug } from '@wvb/deno';
// 1. Load a library already on disk.
const lib = loadLib('./vendor/wvb/libwvb_deno.dylib');
// 2. Download a sha256-verified prebuilt at runtime.
const libViaPlug = await loadLibViaPlug();Point loadLib at a file with the WVB_DENO_LIB environment variable instead of hard-coding the path:
WVB_DENO_LIB=./vendor/wvb/libwvb_deno.dylib deno run -A main.tsOr vendor the library ahead of time with the installer subcommand:
deno run -A jsr:@wvb/deno/install --out vendor/wvb@wvb/deno/install downloads the cdylib from GitHub Releases and verifies its SHA-256 checksum by default. Supported targets:
| Target triple |
|---|
aarch64-apple-darwin |
x86_64-apple-darwin |
aarch64-unknown-linux-gnu |
x86_64-unknown-linux-gnu |
x86_64-pc-windows-msvc |
Vendor a specific target with --target:
deno run -A jsr:@wvb/deno/install --out vendor/wvb --target x86_64-unknown-linux-gnuFor the full class and method reference, see the Deno API reference.
Limitations
Two gaps apply to the Deno bindings today.
Custom verifier callbacks are not supported. The Updater accepts only the declarative signatureVerifier — a SignatureVerifierOptions with an algorithm and a key:
const wvb = await webviewBundle({
protocols: [bundleProtocol('app')],
updater: {
remote: { endpoint: 'https://bundles.example.com' },
integrityPolicy: 'strict',
signatureVerifier: {
algorithm: 'ed25519',
key: { format: 'raw', data: publicKeyBytes },
},
},
});The custom integrityChecker / signatureVerifier function callbacks available in @wvb/node cannot cross the FFI boundary.
HttpOptions.defaultHeaders is not supported on the Deno Remote. Other HttpOptions fields still apply:
const wvb = await webviewBundle({
protocols: [bundleProtocol('app')],
updater: {
remote: {
endpoint: 'https://bundles.example.com',
http: { userAgent: 'my-app/1.0', timeout: 120_000 },
},
},
});For how integrity and signatures work across platforms, see Remote bundles.