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.
Quick start
Section titled “Quick start”-
In an empty folder (or a fresh npm project), run
init:Terminal window npx jops-core init -
Pick 4. Electron JOPS Desktop App.
initwrites an Electron Forge project into the current folder, merges its dependencies and scripts into an existingpackage.json(existing entries are kept), vendors the library and runsnpm install. -
Run the app:
Terminal window npm start -
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:infoandfiles: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
- main.html sidebar and
Directorylib/
- jops-core.min.js vendored library
- main.js main process: creates the window, answers
- forge.config.js
- package.json
Adding JOPS to an existing Electron project
Section titled “Adding JOPS to an existing Electron project”npm i -D jops-corenpx jops-core initinit 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 anindex.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 toindex.forge.htmland replaced by a JOPSindex.htmlwith 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-coremoves todevDependenciesand ajops:updatescript is added. There is nobuildorpostinstallscript: Electron Forge packages the web folder itself.
After upgrading jops-core, copy the new library into the app:
npm i -D jops-core@latestnpm run jops:updatePaths under file://
Section titled “Paths under file://”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 /:
<script type="module" src="lib/jops-core.min.js" data-app="App.js"></script><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 ./:
<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.
Calling the main process (IPC)
Section titled “Calling the main process (IPC)”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.
-
Handle the call in the main process:
src/main.js ipcMain.handle("files:list", async () => {// read the folder…return { dir, entries };}); -
Expose a named function in the preload:
src/preload.js const { contextBridge, ipcRenderer } = require("electron");contextBridge.exposeInMainWorld("api", {listFiles: () => ipcRenderer.invoke("files:list"),}); -
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 browserconst { dir, entries } = await window.api.listFiles();// …}}
Checking window.api first lets the same view open in a regular browser too.
Events from the main process
Section titled “Events from the main process”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:
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():
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.
Content-Security-Policy
Section titled “Content-Security-Policy”init writes this policy into index.html, so Electron shows no security warning:
<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.
Conventions
Section titled “Conventions”- 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, nonodeIntegration). The views need nothing else. jops-coreis a devDependency. The app uses the vendored copy, so the npm package (with its CLI and templates) stays out ofapp.asar.
Packaging
Section titled “Packaging”Electron Forge packages the web folder as it is; there is no JOPS build step (npx jops-core build is skipped in Electron projects).
npm run makeThe 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.