Desktop
An Electron renderer can run the same HTML and application module as the web build. The Electron main process loads the output produced by Vite.
Configure the Vite build
Use a relative base so generated script, stylesheet, and asset URLs resolve from the built index.html:
import { defineConfig } from "vite";
export default defineConfig({
base: "./"
});Application code should also resolve the asset catalog from the document:
const runtime = await Runtime.create("canvas", {
assets: {
catalog: new URL("assets.json", document.baseURI)
}
});Create the Electron main process
Create electron/main.js:
import { fileURLToPath } from "node:url";
import path from "node:path";
import { app, BrowserWindow } from "electron";
const currentDirectory = path.dirname(fileURLToPath(import.meta.url));
function createWindow() {
const window = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
sandbox: true
}
});
void window.loadFile(
path.join(currentDirectory, "../dist/index.html")
);
}
app.whenReady().then(() => {
createWindow();
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});The runtime does not need Node.js integration or a preload script. If the game needs desktop-only capabilities, expose named operations through a preload script instead of forwarding arbitrary IPC channels. Electron's context isolation guide shows that pattern.
Add the package scripts
Point the application entry at the Electron main process:
{
"main": "electron/main.js",
"scripts": {
"build": "vite build",
"start": "pnpm run build && electron ."
}
}$ npm startloadFile() is enough for a small local wrapper. Electron recommends a restricted custom protocol for packaged applications because it can limit which local files the renderer may request.