The rampart-webview module¶
Preface¶
Acknowledgment¶
The rampart-webview module embeds the webview library, a tiny cross-platform abstraction over each operating system’s native browser engine. The authors of Rampart extend their thanks to the webview project and to the underlying engine vendors. The library binds to:
- Linux — GTK + WebKitGTK
- macOS — Cocoa + WebKit
The headless JavaScript features described below are provided by the JavaScriptCore (JSC)
engine that ships with WebKitGTK on Linux and with the system JavaScriptCore.framework on
macOS.
License¶
The rampart-webview module is released under the MIT license.
What does it do?¶
The rampart-webview module provides two related capabilities:
- A native webview — an in-process desktop window backed by the operating system’s own browser engine, for building graphical applications whose user interface is written in HTML, CSS, and JavaScript. Rampart functions can be bound so that page JavaScript calls back into the host, and the host can inject and evaluate JavaScript inside the page.
- A headless JavaScript engine — direct access to the JavaScriptCore engine bundled with WebKit, allowing Rampart to run modern JavaScript (ES2020+) for libraries that need features beyond Duktape’s ES5.1 support. No window is created; this is a pure data-transformation engine.
The rampart-chromeview module is the complementary tool for the inverse case: instead of embedding a browser engine in-process to build a GUI, rampart-chromeview drives a separate Chromium process over the Chrome DevTools Protocol for automation, scraping, server-side rendering, and headless PDF or screenshot generation.
Typical applications include:
- Desktop GUI applications with an HTML/CSS/JavaScript front end and a Rampart back end.
- Local dashboards and tools that present a rampart-server application in a dedicated window.
- Automated screenshot generation and visual testing (combined with a virtual display on Linux).
- Running data-transformation libraries that require a modern JavaScript engine, without a GUI.
How does it work?¶
The WebView
constructor creates a native window and wires up a bidirectional bridge between Rampart’s
Duktape engine and the browser’s JavaScript engine. Values crossing the bridge — both arguments
to bound functions and results of evaluated expressions — are carried by a tagged-JSON protocol
that preserves rich types (Dates, Buffers, RegExps, Maps, Sets, NaN/Infinity, undefined, and cyclic
references) in both directions.
w.run() enters the
platform’s native event loop and blocks until the window is closed. All setup
— binding functions, registering event handlers, loading content — must therefore be done
before run() is
called.
The headless JSCContext and jscExec() interfaces do not create a window and do not enter an event loop;
they evaluate code in a JavaScriptCore context and return results immediately.
Loading and Using the Module¶
Prerequisites¶
The module is built against the host’s native browser engine, so the appropriate development packages must be present to build it, and the corresponding runtime must be present to use it.
Linux (Debian/Ubuntu):
sudo apt install libgtk-3-dev libwebkit2gtk-4.1-dev
macOS: WebKit ships with the operating system; only the Xcode command line tools are required to build.
Loading¶
Loading the module is a simple matter of using the require() function:
var webview = require("rampart-webview");
Quick Start¶
A complete script that opens a window, binds a host function the page can call, and runs until the window is closed:
var webview = require("rampart-webview");
var w = new webview.WebView({
title: "Hello",
width: 480,
height: 320,
html: '<button onclick="window.quit()">Close me</button>'
});
w.bind("quit", function() {
w.terminate();
});
w.run(); /* blocks until the window closes */
w.destroy();
The WebView GUI¶
Creating a WebView¶
var w = new webview.WebView({
title: "My App", // window title (default: "Rampart WebView")
width: 800, // width in pixels (default: 800)
height: 600, // height in pixels (default: 600)
debug: true, // enable browser developer tools (default: false)
html: "<h1>Hello</h1>", // initial HTML content
// url: "https://example.com" // OR navigate to a URL
});
All options are optional. If neither html nor url is supplied, the window opens with a blank page.
Methods¶
w.setSize(width,
height [, hint])¶
Update the window size. The optional hint controls the sizing behavior and may be a string ("none", "min", "max", "fixed") or the equivalent
constant (see Constants).
w.setSize(1024, 768); // set default size
w.setSize(400, 300, "min"); // set minimum size
w.setSize(1920, 1080, "max"); // set maximum size
w.setSize(640, 480, "fixed"); // fixed (non-resizable) size
w.init(js)¶
Inject JavaScript that runs automatically before every page load. Useful for installing global functions or polyfills that each page should see.
w.init("window.myGlobal = 'available on every page';");
w.eval(js [, callback])¶
Evaluate JavaScript inside the webview. js is always a string; it
executes in the browser’s JavaScript engine, not in Duktape.
In the fire-and-forget form, no value is returned:
w.eval("document.title = 'Changed from Rampart';");
When a callback is supplied, the value of the last expression is captured and passed to the
callback as (result,
error). Rich types are preserved through the tagged-JSON
protocol, and Promises are awaited automatically:
w.eval("document.title", function(title, err) {
if (err) console.log("eval error:", err.message);
else console.log("title:", title);
});
/* Promises are awaited automatically */
w.eval("fetch('/api/data').then(r => r.json())", function(data, err) {
// ...
});
Evaluation should be triggered after the page has loaded (see w.on("load", ...) below). Evaluating before a page is loaded does nothing.
w.getContents(callback)¶
Fetch the current DOM as an HTML string and deliver it to the callback. This is shorthand for
w.eval("document.documentElement.outerHTML", callback).
w.on("load", function() {
w.getContents(function(html, err) {
rampart.utils.fprintf("page.html", "%s", html);
});
});
w.on(event, handler)¶
Subscribe to a webview event. on() must be called before run() — and before the first
setHtml()/navigate() if you want to receive events from the initial page load.
Multiple handlers may be registered for the same event; they are called in registration
order.
Two events are supported:
“load” fires when a page finishes loading. The handler receives the page URL:
w.on("load", function(url) {
console.log("page loaded:", url);
});
“console” fires for every console.log/warn/error/info/debug call made inside the webview. The
handler receives the level and an array of stringified arguments:
w.on("console", function(level, args) {
console.log("[page " + level + "]", args.join(" "));
});
w.bind(name, callback)¶
Bind a Rampart (Duktape) function so it can be called from JavaScript running inside the
webview. The bound function appears as window.<name>() in the
page and returns a Promise.
w.bind("add", function(a, b) {
return a + b;
});
From within the page:
<script>
async function doAdd() {
var result = await window.add(3, 4);
console.log(result); // 7
}
</script>
The callback can accept and return rich types: numbers, strings, objects, arrays, booleans,
null, Dates,
Buffers/ArrayBuffers, TypedArrays, RegExps, Maps, Sets, NaN, Infinity, and undefined. Cyclic object
references are preserved. If the callback throws, the Promise in the page is rejected with
the error message.
w.run()¶
Start the webview event loop. This call blocks until the window is closed,
either by the user or by a call to w.terminate() from a bound callback. All setup must be completed before
run() is called.
w.run();
// Execution resumes here after the window is closed
w.terminate()¶
Stop the event loop, causing w.run() to return. Typically called from within a bound callback:
w.bind("quit", function() {
w.terminate();
});
w.destroy()¶
Explicitly destroy the webview instance and free its resources. This is also done automatically by the garbage collector when the object goes out of scope.
w.destroy();
w.snapshot([region])¶
Capture the webview’s rendered content as a PNG image and return it as a Buffer. Available on Linux
(via WebKitGTK) and macOS (via WKWebView).
The optional region argument may be "full" to capture the entire
document, including content scrolled out of view. The default captures only the visible
window region.
var png = w.snapshot(); // visible region only
var fullPng = w.snapshot("full"); // entire document
rampart.utils.fprintf("page.png", "%s", png);
Snapshots are typically taken inside a bound callback after the page has finished loading:
w.bind("ready", function() {
var png = w.snapshot();
// ... save or process png ...
w.terminate();
});
w.setHtml('<body><h1>Hello</h1>\
<script>window.addEventListener("load",function(){window.ready()})</script>\
</body>');
w.run();
Combined with the headless.sh helper (see Running Without a Display), this enables automated
screenshot generation and visual testing without a GUI session.
w.setCookie(name,
value)¶
Set a cookie in the webview’s cookie store for the current page’s host. Available on Linux
(WebKitGTK cookie manager) and macOS (WKHTTPCookieStore). The page must be loaded over a real
URL scheme (http://, https://); pages at about:blank or behind
data: URIs cannot
hold cookies.
w.on("load", function() {
w.setCookie("session", "abc123");
w.setCookie("user", "alice");
});
w.navigate("https://example.com/");
w.getCookies([uri])¶
Return the cookies that would be sent for uri — or for the current
page’s URI when uri is omitted — as a plain object of {name: value, ...}. Host-suffix, path-prefix, and
secure-flag filtering are applied so the result matches what the browser would actually
attach to a request.
Available on Linux and macOS. Throws if no URI is passed and no page has been loaded yet.
w.on("load", function() {
var cookies = w.getCookies();
console.log(cookies.session); // "abc123"
/* Cookies for a different origin: */
var other = w.getCookies("https://other.example.com/path");
});
w.getAllCookies()¶
Return every cookie currently in the webview’s cookie store as a plain object of {name: value, ...}, with no host/path/secure filtering.
This is useful when the page was loaded via setHtml() (and so has no
resolvable host for getCookies() to filter against), or when you want to inspect the full
store across origins.
Available on macOS, and on Linux when the underlying WebKitGTK ABI exports webkit_cookie_manager_get_all_cookies (true on Debian Bullseye and newer,
and on modern macOS). On Debian Buster’s stock WebKitGTK the property is simply absent from
the webview object; guard with typeof w.getAllCookies ===
"function" if you need to support that platform.
w.on("load", function() {
if (typeof w.getAllCookies === "function") {
var everything = w.getAllCookies();
console.log(Object.keys(everything).length, "cookies in store");
}
});
w.setUserAgent(string)¶
Set the User-Agent string sent with HTTP requests and returned by navigator.userAgent. Available
on Linux and macOS. Call before navigate() to affect the first request.
w.setUserAgent("MyApp/1.0");
w.navigate("https://example.com/");
Constants¶
The window-sizing hints accepted by setSize() are available both as strings and as module constants.
| Constant | Value | Description |
|---|---|---|
webview.HINT_NONE
|
0 | Default window size |
webview.HINT_MIN
|
1 | Minimum size constraint |
webview.HINT_MAX
|
2 | Maximum size constraint |
webview.HINT_FIXED
|
3 | Fixed (non-resizable) size |
Accessing Local Servers¶
If a webview page needs to reach a local HTTP server (for example http://127.0.0.1:8088/api/data.json), the browser’s same-origin policy may
block the request depending on how the page was loaded. Pages loaded via setHtml() or a file:// URL have a null origin, so fetch/XHR requests to http:// URLs are treated as
cross-origin. There are three ways to handle this.
Option 1: Serve the page from the same server¶
If you are running rampart-server, navigate to it directly. Page and API then share an origin and no CORS issue arises:
w.navigate("http://127.0.0.1:8088/myapp/index.html");
Option 2: Add CORS headers on the server¶
Configure the local server to send permissive CORS headers, allowing pages loaded via
setHtml() or
file:// to make
requests:
// In your rampart-server route handler:
req.header("Access-Control-Allow-Origin", "*");
Option 3: Use w.bind() as a proxy¶
Expose a Rampart function that performs the HTTP request on the host side, bypassing browser security entirely. This is the most flexible approach and works regardless of how the page was loaded:
// Rampart side
var curl = require("rampart-curl");
w.bind("fetchJson", function(url) {
var res = curl.fetch(url);
return JSON.parse(res.body);
});
<!-- Webview side -->
<script>
async function loadData() {
var data = await window.fetchJson("http://127.0.0.1:8088/apps/myapp.json");
console.log(data);
}
</script>
The request happens in Rampart, free of browser restrictions, and the result is returned to the page as a resolved Promise.
Running Without a Display¶
The webview requires a display connection (X11 on Linux, the WindowServer on macOS). For automated tasks that do not need a visible window — generating screenshots, running bound callbacks — a virtual display can be used.
Linux: The included headless.sh script wraps rampart with Xvfb (the X virtual framebuffer).
Install Xvfb first:
sudo apt install xvfb # Debian/Ubuntu
sudo dnf install xorg-x11-server-Xvfb # Fedora/RHEL
Then run any webview script headlessly:
./headless.sh my_script.js
./headless.sh --dim 1920x1080 my_script.js
The webview renders fully inside the virtual display; snapshot() and all bound
callbacks work normally.
macOS: Headless operation is not directly supported, because Cocoa applications require an active WindowServer (a login session). CI services such as GitHub Actions and CircleCI handle this by keeping a user logged in on their macOS runners.
Headless JavaScriptCore¶
In addition to the GUI webview, the module provides direct access to the JavaScriptCore (JSC) engine bundled with WebKit, so Rampart can execute modern JavaScript (ES2020+) without creating a window. This gives Rampart a full JIT-compiled JS engine for running third-party libraries that need features beyond Duktape’s ES5.1 support.
The JSC features are available on Linux (via WebKitGTK) and macOS (via the system JavaScriptCore.framework).
Two interfaces are provided: jscExec() for one-shot evaluation, and JSCContext for a persistent
interpreter where you can load modules, maintain state, and call methods on JSC objects directly.
Compatibility¶
The JSC context is a pure JavaScript engine. It provides ECMAScript builtins (Math, JSON, Date, Promise, Map, Set, RegExp, TypedArrays, etc.) but
does not include Web Platform APIs (fetch, setTimeout, WebSocket, localStorage, the DOM, etc.) or
Node.js APIs (fs,
http, require, etc.). Promises and
async/await work correctly; the
microtask queue drains between calls.
As a general rule: if a library’s job is transforming data rather than
performing I/O or rendering, it will work. The following libraries have been tested and
verified: lodash, mathjs, marked, ajv, papaparse, handlebars, js-yaml, fuse.js, and
validator.js. Libraries that need network access, timers, a DOM, or Node.js modules will not
work. Use Rampart’s own modules (rampart-curl, rampart-server, rampart-lmdb, etc.) for I/O, and
pass data into JSC via jsc.set() for processing.
On callbacks: Duktape functions cannot be called from within JSC — the two are
separate engines. Data values pass freely in both directions, but any operation that requires a
callback function as an argument must be performed inside jsc.eval(), where the callback
is itself a JSC function. Calling JSC methods with simple arguments (strings, numbers, objects,
arrays) works directly from Rampart.
webview.jscExec(code)¶
Evaluate JavaScript in a temporary JSC context and return the result. A fresh context is created and destroyed on each call. The return value is deep-converted to a native Duktape value using the rich type mapping below:
| JSC type | Duktape result |
|---|---|
| number, string, boolean, null, undefined |
Native equivalent (including NaN, Infinity)
|
| Date |
Duktape Date (instanceof Date works)
|
| ArrayBuffer | Node.js-style Buffer |
| TypedArray (Uint8Array, Float64Array, etc.) | Matching Duktape TypedArray |
| RegExp | Duktape RegExp (source + flags preserved) |
| Error / TypeError / etc. | Duktape Error (name + message preserved) |
| Map |
Duktape Map (instanceof Map; entries converted
recursively)
|
| Set |
Duktape Set (instanceof Set; values converted
recursively)
|
| Array, Object | Recursive deep conversion |
| Function |
Its toString()
source text
|
var webview = require("rampart-webview");
webview.jscExec("40 + 2"); // 42
webview.jscExec("new Date('2026-01-01')"); // Date object
webview.jscExec("new Uint8Array([0xCA, 0xFE])"); // TypedArray
webview.jscExec("/^hello$/gi"); // RegExp
webview.jscExec("[...new Set([1,2,2,3])]"); // [1, 2, 3]
Thrown exceptions propagate as Duktape errors:
try {
webview.jscExec("throw new TypeError('oops')");
} catch(e) {
console.log(e.message); // contains "TypeError: oops"
}
new webview.JSCContext()¶
Create a persistent JavaScriptCore context. State is preserved across calls, and objects returned from JSC are wrapped in Duktape proxies so their properties and methods can be accessed directly.
var jsc = new webview.JSCContext();
jsc.eval(code)¶
Evaluate code in the persistent context and return the value of the last expression. Primitives, Dates, RegExps, and Buffers are auto-converted to native Duktape values; objects and functions are returned as live proxies backed by the JSC engine.
jsc.eval("var x = 10;");
jsc.eval("x * 4"); // 40 (state persists)
var obj = jsc.eval("({greet: function(n) { return 'Hello ' + n; }})");
obj.greet("World"); // "Hello World"
jsc.loadScript(path)¶
Read a JavaScript file and evaluate it in the persistent context. Returns the value of the
last expression (usually not meaningful for library scripts). Use getGlobal() to reach the
globals the script defines.
jsc.loadScript("/path/to/library.js");
var lib = jsc.getGlobal("LibraryName");
jsc.require(path)¶
Load a JavaScript file with a CommonJS-compatible shim. The file is wrapped in a function(module, exports, require){ ...
} closure and module.exports is returned.
This works with UMD bundles and CommonJS modules, which covers most npm packages. There is
also limited, experimental support for ES module syntax (export default, export function, export {
... }); if the CommonJS shim fails
with a SyntaxError, require will attempt to transform export statements
automatically.
Both the CommonJS and ESM paths expect single-file bundles. Multi-file
modules with import or require dependencies between files are not resolved — use a bundler (for
example esbuild
lib.js --bundle --format=cjs) to produce one file first.
var math = jsc.require("/path/to/math.js");
math.add(2, 3); // 5
jsc.set(name,
value)¶
Set a global variable in the JSC context. The value is converted from Duktape to JSC; numbers, strings, booleans, arrays, objects, Buffers, Dates, and already-wrapped JSC values are all supported.
jsc.set("config", {debug: true, maxRetries: 3});
jsc.eval("config.maxRetries"); // 3
jsc.getGlobal(name)¶
Get a global variable from the JSC context as a wrapped value.
jsc.loadScript("library.js");
var lib = jsc.getGlobal("Library");
lib.someMethod();
jsc.destroy()¶
Destroy the JSC context and free its resources. Also done automatically by the garbage collector.
jsc.destroy();
Working with Wrapped JSC Objects¶
Objects returned from eval(), require(), getGlobal(), or JSC method calls are Duktape proxies that resolve
properties lazily from JSC:
var math = jsc.require("math.js");
// Property access resolves from JSC
math.pi; // 3.141592653589793
// Method calls marshal arguments to JSC and convert the result
math.sqrt(144); // 12
math.add(3, 4); // 7
// Returned objects are also proxied
var m = math.matrix([[1, 2], [3, 4]]);
math.det(m); // -2
math.inv(m).toString(); // "[[-2, 1], [1.5, -0.5]]"
JSC objects passed as arguments to other JSC functions are unwrapped automatically — no manual conversion is needed:
var c1 = math.complex(3, 4);
var c2 = math.complex(1, -2);
math.add(c1, c2).toString(); // "4 + 2i"
.toValue()¶
Deep-convert a wrapped JSC object to a plain Duktape value (using the same rich type mapping
as jscExec()).
Useful when you need a native object for JSON.stringify(), for passing
to another Rampart module, and so on:
var result = math.evaluate("[1, 2, 3]");
var arr = result.toValue(); // plain Duktape array [1, 2, 3]
JSON.stringify(arr); // "[1,2,3]"
.toString()¶
Get the string representation of a wrapped JSC value.
math.matrix([[1, 0], [0, 1]]).toString(); // "[[1, 0], [0, 1]]"
Examples¶
WebView: Counter App¶
A small GUI application. The page calls a bound host function on each click and a second bound function to quit.
var webview = require("rampart-webview");
var w = new webview.WebView({
title: "Counter App",
width: 400,
height: 300,
debug: false,
html: '<html>\
<body style="font-family: sans-serif; text-align: center; padding: 40px;">\
<h1 id="count">0</h1>\
<button onclick="increment()">Increment</button>\
<button onclick="window.quit()">Quit</button>\
<script>\
var count = 0;\
async function increment() {\
count = await window.addOne(count);\
document.getElementById("count").textContent = count;\
}\
</script>\
</body>\
</html>'
});
w.bind("addOne", function(n) {
console.log("addOne called:", n);
return n + 1;
});
w.bind("quit", function() {
w.terminate();
});
w.run();
w.destroy();
JSCContext: Fetching and Using a JS Library¶
This example uses rampart-curl
to download mathjs from a CDN, saves it to a file, then loads it into a JSCContext and runs several
operations:
rampart.globalize(rampart.utils);
var curl = require("rampart-curl");
var wv = require("rampart-webview");
var url = "https://cdn.jsdelivr.net/npm/mathjs@13.2.2/lib/browser/math.js";
var file = "/tmp/math.js";
/* Fetch the library */
printf("Fetching mathjs from CDN... ");
var res = curl.fetch({location: true}, url);
if (res.status !== 200) {
printf("FAILED (status %d)\n", res.status);
process.exit(1);
}
printf("%d bytes, status %d\n", res.body.length, res.status);
/* Save to file */
rampart.utils.fprintf(file, "%s", res.body);
printf("Saved to %s\n\n", file);
/* Load into JSC and run some tests */
var jsc = new wv.JSCContext();
var math = jsc.require(file);
printf("mathjs version: %s\n\n", math.version);
/* Linear algebra */
printf("Linear algebra:\n");
var m = math.matrix([[2, 1, 0], [1, 3, 1], [0, 1, 2]]);
printf(" matrix: %s\n", m.toString());
printf(" determinant: %s\n", math.det(m));
printf(" inverse: %s\n", math.inv(m).toString());
/* Expression parser */
printf("\nExpression parser:\n");
printf(" e^(i*pi) + 1 = %s\n", math.evaluate("e^(i*pi) + 1").toString());
printf(" 3 inches in cm = %s\n", math.evaluate("3 inch to cm").toString());
printf(" derivative of sin(x)*x^2 = %s\n",
math.derivative("sin(x)*x^2", "x").toString());
/* Statistics on data passed from Duktape */
printf("\nStatistics (data passed from Duktape to JSC):\n");
var data = [2, 4, 6, 8, 10, 12, 14, 16, 18, 20];
printf(" data: %s\n", JSON.stringify(data));
printf(" mean: %s\n", math.mean(data));
printf(" median: %s\n", math.median(data));
printf(" std: %s\n", math.std(data));
jsc.destroy();
printf("\nDone.\n");