Yes, it is possible to bundle a Common Lisp web app into Electron, with any web server, so Hunchentoot is possible.
I have a working POC but was planning to write a detailed blog post with more details and a demo sorted out, so stay tuned ;)
Our method starts the Electron process, starts the Lisp web app as a subprocess on localhost on the given port, and opens this address inside the Electron window.
In a nutshell:
- follow Electron installation instructions,
- build a Lisp web app as a binary
- see https://lisp-journey.gitlab.io/blog/lisp-for-the-web-build-standalone-binaries-foreign-libraries-templates-static-assets/ (the process will be a tad simpler without Djula templates)
- bundle this binary into the final Electron build.
- and that's it.
I mean, you can run the Lisp web app from sources, but then ship all sources.
You can have a look at https://github.com/mikelevins/electron-lisp-boilerplate for this, their main.js has the pattern, using child_process.
What about Ceramic?
You can ignore the Ceramic project, unfortunately, it's a wrapper around Electron (npm) tools and is broken and outdated. It will try to install an old Electron version and fail. That being said, the Ceramic org has a few useful helper projects we can use.
Example main.js
This file is adapted from the main.js you get after installation, to run an application as a subprocess.
console.log(`Hello from Electron 👋`) const { app, BrowserWindow } = require('electron') const { spawn } = require('child_process'); // Suppose we have our app binary at the current directory. var binaryPaths = [ "./openbookstore", ]; // Define any arg required for the binary. var binaryArgs = ["--web"]; const binaryapp = null; const runLocalApp = () => { "Run our binary app locally." console.log("running our app locally…"); const binaryapp = spawn(binaryPaths[0], binaryArgs); return binaryapp; } // Start an Electron window. const createWindow = () => { const win = new BrowserWindow({ width: 800, height: 600, }) // Open localhost on the app's port. // We should read the port from an environment variable or a config file. win.loadURL('http://localhost:4242/') } // Run our app. let child = runLocalApp(); // We want to see stdout and stderr of the child process // (to see our Lisp app output). child.stdout.on('data', (data) => { console.log(`stdout:\n${data}`); }); child.stderr.on('data', (data) => { console.error(`stderr: ${data}`); }); child.on('error', (error) => { console.error(`error: ${error.message}`); }); // Handle Electron close events. child.on('close', (code) => { console.log(`openbookstore process exited with code ${code}`); }); // Open it in Electron. app.whenReady().then(() => { createWindow(); // Open a window if none are open (macOS) if (process.platform == 'darwin') { app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow() }) } }) // On Linux and Windows, quit the app main all windows are closed. app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } })
What's left as an exercise: automatically bundle the binary into the Electron release.
Then, communicate from Lisp app <-> Electron window (optional though).
Any feedback and demos welcome, this was a quick post.
Happy lisping
ps: and Tauri?
I suppose it's the same process. Prove me wrong ;)
Will test and show an example soon©, if you have experience please share in the comments 🙏
https://lisp-journey.gitlab.io/
Top comments (1)
There's a great recent project using Ceramic though! github.com/neomacs-project/neomacs/ a structural editor and browser.