DEV Community

Cover image for JOPS on the Desktop: Class-Based Electron Apps, No Bundler
Arik Wald
Arik Wald

Posted on Originally published at ui-db.com

JOPS on the Desktop: Class-Based Electron Apps, No Bundler

When we shipped Next.js support in JOPS 1.1.0, we said the desktop was next. After a short detour through WordPress, we've arrived: with jops-core 1.1.6, a JOPS single-page app can be the UI of an Electron desktop app, tested on macOS and Windows, with no bundler and no changes to how you write views.

One command creates a working desktop app. The same class-based views, layouts and Router you use in the browser run in Electron's window, and they reach the file system and the rest of the operating system through Electron's own preload bridge.

Why JOPS fits the desktop

Desktop apps are where class-based UI has always lived: WinForms, WPF, Swing, Cocoa, Qt. A window with a sidebar, screens that keep their state, a lifecycle that tells each screen when it comes to the front: that's the model JOPS was built on, borrowed from Android.

Electron brings the other half: a real window, native menus, installers, and full access to the operating system through Node.js in the main process. JOPS doesn't need a build step inside it. The app's files are packaged exactly as you wrote them.

A desktop app in one command

In an empty folder:

npx jops-core init
# choose: 4. Electron JOPS Desktop App
npm start          # run the app
npm run make       # package it (out/make/)
Enter fullscreen mode Exit fullscreen mode

init writes an Electron Forge project, vendors the JOPS library into the app, and runs npm install. What you get:

File Process What it does
src/main.js main Creates the window and answers two IPC calls: app:info and files:list
src/preload.js preload Exposes only those two functions to the page, as window.api.*
src/index.html renderer Content-Security-Policy, import map and the JOPS script
src/App.js, src/layout/main.html renderer Root view: a sidebar and <Router jops screens>
src/HomeView.js, src/FilesView.js renderer Two screens that call the main process
src/lib/jops-core.min.js renderer The JOPS library, vendored

The app follows the system's light or dark theme, and the template comes with a Content-Security-Policy that keeps Electron's security warning away: only the app's own files load, and remote scripts and eval stay blocked.

Already have an Electron project? Run npm i -D jops-core and npx jops-core init in it. init recognises Electron, finds the page your main process loads, and adds JOPS next to it. It works with Electron Forge's default template and other projects that load a plain HTML page; Forge's vite and webpack templates bundle the page and aren't supported.

Calling the operating system

JOPS adds no IPC layer of its own. Electron already has a good one: the main process handles requests, the preload script exposes a few named functions through contextBridge, and JOPS views call them, the same way a hybrid mobile app calls Cordova plugins.

// src/main.js (main process)
ipcMain.handle("files:list", async () => {
  // read the folder with Node's fs module
  return { dir, entries };
});

// src/preload.js
contextBridge.exposeInMainWorld("api", {
  listFiles: () => ipcRenderer.invoke("files:list"),
});

// src/FilesView.js (a JOPS view)
async onResume() {
  if (!window.api) return; // opened in a regular browser
  const { dir, entries } = await window.api.listFiles();
  // render the list
}
Enter fullscreen mode Exit fullscreen mode

Events from the main process, such as progress or live data, follow the same pattern: the preload registers a listener and returns an unsubscribe function. A view subscribes in onResume() and unsubscribes in onPause(), so a screen in the background never handles events it can't show. Electron's secure defaults stay on: context isolation, the sandbox, and no Node.js in the page.

Screens that behave like a desktop app

Building a real desktop app on JOPS showed where the Router needed work. In a fixed window with a sidebar, every route should fill the content area, keep its own scroll position, and cover the previous screen cleanly during a transition. So 1.1.4 added screens mode:

<nav class="sidebar">…</nav>
<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>
Enter fullscreen mode Exit fullscreen mode

Each route renders as a full-size screen that scrolls on its own, so Back returns to exactly where a long list was scrolled. It's opt-in: a Router without screens works as it always did, which keeps every existing web app unchanged.

One rule for paths

Electron loads the page from disk (file://), where a leading / means the root of the drive. So in an Electron app, every path JOPS loads is written relative to the page, with no leading ./ or /:

<script type="module" src="lib/jops-core.min.js" data-app="App.js"></script>
Enter fullscreen mode Exit fullscreen mode

Packaging changes nothing. The files are read from inside app.asar with the same relative paths, so the app you run with npm start is the app you ship.

What we tested it on

We built a full desktop dashboard on JOPS before writing the template: a sidebar, several routes, a file browser backed by the main process, and a startup splash. It ran under npm start and as packaged apps, a macOS .app and a Windows .exe, both built from a Mac with Electron Forge.

Component Version
Electron 45
Electron Forge 8
Platforms macOS (Apple silicon) and Windows (x64)

The template itself was tested in fresh projects, in light and dark mode, unpackaged and packaged, with no console or Content-Security-Policy errors. We also checked that the packaged app contains the vendored library and none of the npm package: jops-core is a dev dependency, so its CLI and templates stay out of what you ship.

A real application found real bugs. Seven rendering and Router fixes from this work shipped in 1.1.3, and every JOPS app benefits from them, in the browser, on mobile, in Next.js and in WordPress.

Know before you start

  • Page-relative paths everywhere JOPS loads something, as above. The import map is the one exception: the browser reads it, so its value keeps the ./.
  • Expose named functions only in the preload, never ipcRenderer itself, and validate arguments in the main process.
  • After upgrading jops-core, run npm run jops:update to copy the new library into the app.
  • Calling a remote API? Add its host to the Content-Security-Policy, for example connect-src 'self' https://api.example.com.

Try it

One codebase, five places to run

JOPS now runs in the browser, in hybrid iOS and Android apps, inside Next.js, inside WordPress, and on the desktop with Electron. The views are the same classes everywhere; only the host changes. We'd like to hear what you build with it, and where you'd like JOPS to run next.


JOPS is built and maintained at UIDB, a boutique team of senior engineers who build custom systems with our clients, side by side. If you're planning a desktop tool or a system that needs to hold up for years, we'd be glad to talk.

Top comments (0)