Skip to content

Desktop apps (Electron)

A JOPS SPA can be the UI (renderer process) of an Electron app. The page is loaded from disk with loadFile(), JOPS and your app’s files are packaged as they are (no bundler), and views reach the operating system through Electron’s preload bridge. Tested with Electron 45 and Electron Forge 8 on macOS and Windows.

  1. In an empty folder (or a fresh npm project), run init:

    Terminal window
    npx jops-core init
  2. Pick 4. Electron JOPS Desktop App. init writes an Electron Forge project into the current folder, merges its dependencies and scripts into an existing package.json (existing entries are kept), vendors the library and runs npm install.

  3. Run the app:

    Terminal window
    npm start
  4. Package it when you are ready (output in out/make/):

    Terminal window
    npm run make

The template contains:

  • Directorysrc/
    • main.js main process: creates the window, answers app:info and files:list
    • preload.js exposes only those two functions to the page, as window.api.*
    • index.html Content-Security-Policy, import map, jops-core script
    • App.js root view
    • HomeView.js screen that calls window.api
    • FilesView.js screen that calls window.api
    • index.css
    • Directorylayout/
      • main.html sidebar and <Router jops screens>
      • home_view.html
      • files_view.html
    • Directorylib/
      • jops-core.min.js vendored library
  • forge.config.js
  • package.json

Adding JOPS to an existing Electron project

Section titled “Adding JOPS to an existing Electron project”
Terminal window
npm i -D jops-core
npx jops-core init

init recognises an Electron project by electron in package.json. Pick template 1 (Empty), 2 (Hello World) or 5 (None). It works with Electron Forge’s default template (npx create-electron-app@latest my-app) and other projects that load a plain HTML page.

What init configures:

  • The web folder is the folder of the page the main process loads: the folder of "main" when it has an index.html (src/ in Forge’s template).
  • The library goes to <web folder>/lib/jops-core.min.js.
  • index.html: Forge’s sample page is moved to index.forge.html and replaced by a JOPS index.html with a Content-Security-Policy, the import map and the module script. A page you already changed is patched instead.
  • Templates 1 and 2 are copied into the web folder next to index.html (App.js, layout/, …), with their paths made page-relative (see below).
  • package.json: jops-core moves to devDependencies and a jops:update script is added. There is no build or postinstall script: Electron Forge packages the web folder itself.

After upgrading jops-core, copy the new library into the app:

Terminal window
npm i -D jops-core@latest
npm run jops:update

The page is loaded from file://, where a leading / means the root of the disk. Every path JOPS resolves (data-app, src=, layout=, loadLayout(), setStyle()) is written relative to the page, without a leading ./ or /:

src/index.html
<script type="module" src="lib/jops-core.min.js" data-app="App.js"></script>
src/layout/main.html
<Router jops screens class="content">
<HomeView jops src="HomeView.js" layout="layout/home_view.html" path="/home"></HomeView>
<FilesView jops src="FilesView.js" layout="layout/files_view.html" path="/files"></FilesView>
</Router>

The import map is the one exception. It is read by the browser, not by JOPS, and its value must look like a URL, so it keeps its ./:

src/index.html
<script type="importmap">
{ "imports": { "jops-core": "./lib/jops-core.min.js" } }
</script>

The same page-relative rule applies to <img src> and other URLs in your markup: assets/logo.svg, not /assets/logo.svg. Packaging changes nothing: the files are read from inside app.asar with the same relative paths.

JOPS adds no IPC layer. Electron’s preload script and contextBridge already are the bridge, and views call the functions it exposes, the same way Cordova plugins are called from window.

  1. Handle the call in the main process:

    src/main.js
    ipcMain.handle("files:list", async () => {
    // read the folder…
    return { dir, entries };
    });
  2. Expose a named function in the preload:

    src/preload.js
    const { contextBridge, ipcRenderer } = require("electron");
    contextBridge.exposeInMainWorld("api", {
    listFiles: () => ipcRenderer.invoke("files:list"),
    });
  3. Call it from a view:

    src/FilesView.js
    import { View } from "jops-core";
    export default class FilesView extends View {
    async onResume() {
    if (!window.api) return; // opened in a regular browser
    const { dir, entries } = await window.api.listFiles();
    // …
    }
    }

Checking window.api first lets the same view open in a regular browser too.

For notifications or live data, send with webContents.send() in the main process, and expose a function in the preload that registers a listener and returns its unsubscribe function:

src/preload.js
contextBridge.exposeInMainWorld("api", {
onProgress: (callback) => {
const listener = (event, value) => callback(value);
ipcRenderer.on("progress", listener);
return () => ipcRenderer.removeListener("progress", listener);
},
});

A view subscribes in onResume() and unsubscribes in onPause() or destroy():

src/ProgressView.js
import { View } from "jops-core";
export default class ProgressView extends View {
onResume() {
this.stopProgress = window.api.onProgress((value) => {
this.get("progress").set(String(value));
});
}
onPause() {
this.stopProgress?.();
}
}

Alternatively, forward the data into an owned EventBus subscription (EventBus.subscribe(this, …)), which destroy() removes for you. See Events.

init writes this policy into index.html, so Electron shows no security warning:

src/index.html
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' 'unsafe-inline' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data:" />

Only the app’s own files load. 'unsafe-inline' is needed for the inline import map and the on* attributes that bindEvents() wires; blob: is needed for Thread workers. Remote scripts and eval stay blocked. Add hosts as your app needs them, for example connect-src 'self' https://api.example.com before calling a remote API.

  • Screens mode. <Router jops screens> suits desktop layouts: each route fills the router area and scrolls on its own, below a fixed sidebar or toolbar. See Routing.
  • Keep Electron’s secure defaults (contextIsolation, sandbox, no nodeIntegration). The views need nothing else.
  • jops-core is a devDependency. The app uses the vendored copy, so the npm package (with its CLI and templates) stays out of app.asar.

Electron Forge packages the web folder as it is; there is no JOPS build step (npx jops-core build is skipped in Electron projects).

Terminal window
npm run make

The packaged app reads the same files from inside app.asar, with the same page-relative paths as npm start. The template’s forge.config.js is Forge’s default configuration with the zip maker enabled for macOS and Windows.