This is the first of what has turned into a 3-post mini-series:
Be sure to read this post first though.
I’m in the midst of doing some technical validation for a project that I’m working on. Based on the requirements, it’s going to have to be:
- Cross-platform C++ with a “friendly” scripting language (yes, a C-plugin interface would be good, but it’s not exactly friendly).
- Web-technology with a native host leveraging JavaScript (or a variant) as a scripting language.
I think for the performance that I’m looking for, a “native” web-app will be more than sufficient. I’m also taking the opportunity to finally do deep dive into ClojureScript. There are a lot of reasons why I made that choice, but those are out-of-scope for this post.
One thing I really try to do is to minimize dependencies. So whenever possible, I’ll get rid of them. I would rather know the debt that I’m creating up-front and know how to fix the inevitable issues that will arise, instead of getting broken mid-project pulling my hair out with little options left.
This is even more true with dependencies that introduce an architectural dependency. While things like React are great in their own space, I’m very cautious to introduce those into my projects that I plan on maintaining for years to come.
That’s a bit of preamble to give a little context why I’m not simply using tool like descjop for ClojureScript + Electron templates. At the time of writing this blog, the last commit was over 6 months ago. Again, not a dig at the author at all. However, it is something that I need to think about.
Getting Started
The first thing we understand is a bit how Electron works. I’ll leave that to the Quick Start guide. The important thing to note is that there are two processes that we need to care about:
- Main Process – this is the code that is fed from the
package.json
file. - Renderer Process – these are the individual pages and their related JavaScript code.
Keeping in mind that I want to minimize dependencies as much as possible, I’m going to be creating two different targets relating to each of the process types. Because the renderer is the basic equivalent of pages being hosted by a server, we can decouple all of the main UI from the the hosting process.
Project Layout
I’m creating a “monorepo” for this project, so I’ll go ahead a layout my source as follows:
├── app/
├── ui/
├── Gruntfile.js
├── package.json
└── project.clj
So… I’m actually using three dependencies here… yeah yeah.
- Grunt – this is primarily used for easy downloading of the Electron distributable projects.
- NPM – to download most of the dependencies and create a single
npm install
step to do so. - The
lein
build tool. - Java SDK – yeah… this is annoying. However, Clojure and ClojureScript require this to build.
The two folders represent the two targets that we’ll be creating:
app
– this corresponds to the “main process” of Electronui
– this corresponds to the “renderer process” of Electron
Gruntfile.js
This is an extremely basic Grunt file. All it does is download the Electron shell for us. This is also why I have no real concerns with adopting this. Even if the grunt-download-electron
task stops working for some reason or another, this is “single-depth” dependency that can be easily swapped out with any other downloading tool.
module.exports = function (grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON("package.json"),
"download-electron": {
version: "",
outputDir: "",
rebuild: true
}
});
grunt.loadNpmTasks("grunt-download-electron");
};
The only other thing to note here is that I’ve factored out all of the details from this file, so if a new version of Electron comes out, there’s only a single place to update.
package.json
The only purpose for this file is to allow us to easily download the Grunt dependencies. It’s probably possible to integrate this into the project.clj
file (the Leiningen build file), but I’ve not looked too much into this.
{
"name": "",
"version": "",
"description": "",
"devDependencies": {
"grunt": "^1.0.0",
"grunt-download-electron": "^2.1.4"
},
"license": "",
"repository": "",
"config": {
"electron": {
"version": "1.5.0",
"installDir": ".deps/electron"
}
},
"scripts": {
"postinstall": "mkdir -p .deps/electron; grunt download-electron"
}
}
As you can see, the Electron data is stored in the config
section under the electron
key. Pretty straight-forward. The postinstall
script is used to actually perform the installation of the Electron shell. There are two things of note:
- There is a
mkdir -p
command as the Grunt task doesn’t actually create the intermediate folder structure. This is baffling to me as nearly all Grunt commands actually do this already… - I typically put all of my “built” or other output in hidden folders to help reduce the visible noise in the project structure.
As long as you already have npm
, you can simply run npm install
. This will download Grunt and it’s requirements and download the Electron shell for you.
Setting Up The Project
That gets us up and running and ready to start actually building our project now. A little bit involved, but not too bad.
Before we create the project.clj
file, it’s important to understand the steps that we want to create. Also, it’s also important to note that the Leiningen tasks are really about atomic actions. We can use aliases to chain together multiple tasks.
So these are things we’ll want to do:
- Generate a
package.json
manifest file. This is what Electron uses to know what JavaScript file to load. - Generate the
app.js
that is referenced by thepackage.json
file. This simply loads the prerequisite libraries and the main entry point for the “main process”. - Compile the “main process” code.
- Compile the “renderer process” code.
To get started, create the project.clj
file:
(defproject
blog-post "0.1.0"
:description ""
:url ""
:license {:name ""}
:dependencies [[org.clojure/clojure "1.8.0"]
[org.clojure/clojurescript "1.9.456"]]
:plugins [[lein-cljsbuild "1.1.5"]])
This is the base version of the file. Obviously, there are some holes to fill in, but basically it just sets us up to build using version 1.8 of Clojure and 1.9.456 of ClojureScript. The lein-cljsbuild
is a plugin that adds the cljsbuild
task to lein
. Without it, we would only have the language .jar
file (it’s a Java bundle) and no way to compile with lein
.
Generate the Manifest
Next up is to generate the package.json
file for the Electron bundle. You could skip this step if you’d like and simply have a hard-coded file as the contents is simply:
{
"name": "",
"version": "",
"main": "electron-host"
}
However, since I don’t like duplicating information, I’d rather just generate this file. Also, since we already have Grunt, this is a straight-forward process.
- Add some additional details in our
package.json
file:
"electron": {
"version": "1.5.0",
"installDir": ".deps/electron",
"manifestDir": ".out/app",
"main": "electron-host"
}
- The
manifestDir
property is the output path for the file. - The
main
property is the path for the JavaScript file we’ll load. This is relative tomanifestDir
.
- Add a new task to our
Gruntfile.js
:
grunt.registerTask("generate-manifest", "Generate the Electron manifest.", function () {
grunt.config.requires("generate-manifest.name");
grunt.config.requires("generate-manifest.version");
grunt.config.requires("generate-manifest.main");
grunt.config.requires("generate-manifest.manifestDir");
var config = grunt.config("generate-manifest");
var json = {
name: config.name,
version: config.version,
main: config.main
};
var manifestFile = config.manifestDir + "/package.json";
grunt.file.write(config.manifestDir, JSON.stringify(json, null, 2));
});
Additionally, you’ll want to add in a config setting for this as well:
"generate-manifest": {
name: "",
version: "",
main: "",
manifestDir: ""
}
I’m not going to explain all of this, but basically it just:
- Reads in the
package.json
file. - Creates the configuration blob by parsing out the contents of the file.
- Ensures all of the configuration blocks are set.
- Writes the contents of the manifest file out to disk.
You can test this out:
$ grunt generate-manifest
This should create a package.json
file at the path with all of the content.
Update the Project File
It’s time to get to our actual build file. What we need is a way to build are particular targets: main and renderer.
We need to add the following as a new key in our project.clj
file:
:cljsbuild {:builds {:main {:source-paths ["app/src"]
:incremental true
:assert true
:compiler {:output-to ".out/app/electron-host.js"
:warnings true
:elide-asserts true
:target :nodejs
:optmizations :simple
:pretty-print true
:output-wrapper true}}}})
This enables us to actually try and build our project!
$ lein cljsbuild once main
Well… you’ll notice two things happen:
- Nothing is compiled
- A
target
directory is created
The first should be no real surprise as we don’t have any sources yet. However, the second is a bit more annoying. This target
directory contains, what are essentially, a bunch of the intermediate output. Fortunately, you can move that if you’d like by providing an output-dir
.
There is a very important thing to note here: for all of your build configurations, “main” in this case, each has to have it’s own unique
output-dir
.
I like to place all of my intermediate files in a .tmp
directory that matches the output folder location.
:output-to ".out/app/electron-host.js"
:output-dir ".tmp/app"
:warnings true
Add Our First Source File
Under the app/src
folder, we are going to create our host.cljs
file. This is the file that will ultimately be loaded into Electron in the “main process”.
(ns blog-post.electron
(:require [cljs.nodejs :as nodejs]))
(def Electron (nodejs/require "electron"))
(def app (.-app Electron))
(def BrowserWindow (.-BrowserWindow Electron))
(def path (nodejs/require "path"))
(def url (nodejs/require "url"))
(def *win* (atom nil))
(def darwin? (= (.-platform nodejs/process) "darwin"))
(defn create-window []
(reset! *win* (BrowserWindow. (clj->js {:width 800 :height 600})))
(.openDevTools (.-webContents @*win*))
(.on app "closed" (fn [] (reset! *win* nil))))
(defn -main []
(.on app "ready" (fn [] (create-window)))
(.on app "window-all-closed"
(fn [] (when-not darwin? (.quit app))))
(.on app "activate"
(fn [] (when darwin? (create-window)))))
(nodejs/enable-util-print!)
(.log js/console "App has started!")
(set! *main-cli-fn* -main)
This is basically a ClojureScript transcription from the Electron Quick Start guide.
Now when we run:
$ lein cljsbuild once main
You should see some output that looks like:
Compiling ClojureScript...
Compiling ".out/app/electron-host.js" from ["app/src"]...
Successfully compiled ".out/app/electron-host.js" in 10.542 seconds.
Hopefully you see that!
Create the Aliases
At this point, we actually have all of the components to launch Electron with our “main process”. However, let’s hook it all up so we don’t have to do any of the steps manually.
We’ll start off by creating an “alias” in our project.clj
file:
:aliases {"electron-main" ["do"
["shell" "grunt" "generate-manifest"]
["cljsbuild" "once" "main"]]}
Next, we need to add the following to our plugins
list: [lein-shell "0.5.0"]
.
Adding this as a top-level key in our project file allows us to simply run:
$ lein electron-main
And get the following output:
Running "generate-manifest" task
Done.
Compiling ClojureScript...
Compiling ".out/app/electron-host.js" from ["app/src"]...
Successfully compiled ".out/app/electron-host.js" in 10.938 seconds.
Create the Main File
Lastly, remember we have that pesky main.js
file that we still need created. We’ll create a task for that! Then we’ll add this to our new alias: ["shell" "grunt" "generate-mainjs"]
Over in our a Gruntfile.js
we’ll need this:
grunt.registerTask("generate-mainjs",
"Generate the Electron main.js file.",
function() {
grunt.config.requires("generate-mainjs.main");
grunt.config.requires("generate-mainjs.manifestDir");
var config = grunt.config("generate-mainjs");
var content = "require('./" + config.main + "');\n";
var manifestFile = config.manifestDir + "/main.js";
grunt.file.write(manifestFile, content);
});
And you’ll need to add this configuration block:
"generate-mainjs": {
main: "",
manifestDir: ""
}
Now, this is mostly a by-produce with how Electron works. Based on the optimization settings, one of two different main.js
files will need to be created. This is annoying, and this is something that we don’t want to ever think or care about. That’s why we are creating this task. We’ll need to create a different version for when the optimization value is :none
, but for now, this works.
With all the updates, you should be able to do this now:
$ lein electron-main
And get:
Running "generate-manifest" task
Done.
Compiling ClojureScript...
Compiling ".out/app/electron-host.js" from ["app/src"]...
Successfully compiled ".out/app/electron-host.js" in 6.89 seconds.
Running "generate-mainjs" task
Done.
Testing It Out!
It’s finally time to test out that Electron is actually working!
From the root of your project, if you run this:
$ ./.deps/electron/Electron.app/Contents/MacOS/Electron ./.out/app
You should see this:

This is the Electron shell with the devtools automatically opened. Now, at this point, you can see that it’s complaining that the devtools are disconnected. The reason for this is simple: you cannot debug the “main process” from within the Electron shell. You can only debug the “renderer process”, and since we haven’t loaded any HTML files yet, we don’t have any “renderer process”.
Creating the Renderer Process
When building a UI out a webapp, you basically have three components: HTML, CSS, and JavaScript. With Electron, it is no different.
For now, I’m going to use this structure:
└── ui/
├── public/
└──── landing.html
└── src/
└── landing.cljs
The reason I set things up this way is that this allows me to easily copy over all of the “public” assets into the output location. Anything that needs to get built will go through a tool and live in a different folder structure.
Landing Page
The landing page will be super simple:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Hello World!</title> | |
</head> | |
<body> | |
<h1>Hello World!</h1> | |
We are using node.js <script>document.write(process.version)</script> | |
and Electron <script>document.write(process.versions['electron'])</script>. | |
<div id="app"> | |
<p>Script was not loaded.</p> | |
</div> | |
</body> | |
</html> |
To handle this content, we need to publish the assets over. However, instead of copying over a potentially very set of content, we’ll simply create a symlink to the public
folder. In order to that, we’ll need a new Grunt plugin, and we’ll need to add it to our alias of steps to do.
First, update our package.json
file to add the dependency:
"devDependencies": {
"grunt": "^1.0.0",
"grunt-download-electron": "^2.1.4",
"grunt-contrib-symlink": "^1.0.0"
},
Now, run npm install
to get the latest dependencies.
Next, update our Gruntfile.js
:
grunt.loadNpmTasks("grunt-download-electron");
grunt.loadNpmTasks("grunt-contrib-symlink");
Another section needs to be added to the initConfig
section:
"symlink": {
options: {
overwrite: true
},
explicit: {
src: "",
dest: ""
}
Next, update the package.json
file again to add our configuration bits:
"symlink": {
"src": "ui/public",
"dest": ".out/app/public"
}
Now, if you run grunt symlink
, the symlink is created in the output folder.
We can also add this step to our alias list:
["shell" "grunt" "symlink"]
And finally, we need to actually load the HTML file! To do that, we need to update our host.cljs
file. Update the create-window
function to this:
(defn create-window []
(reset! *win* (BrowserWindow. (clj->js {:width 800 :height 600})))
(let [u (.format url (clj->js {:pathname (.join path
(js* "__dirname")
"public"
"index.html")
:protocol "file:"
:slashes true}))]
(.loadURL @*win* u))
(.openDevTools (.-webContents @*win*))
(.on app "closed" (fn [] (reset! *win* nil))))
This will load the index.html
file when the window is loaded.
$ lein electron-main
$ ./.deps/electron/Electron.app/Contents/MacOS/Electron ./.out/app
And you should see this:

Landing Page Code
It’s a bit worthless to simply render HTML, we want some code running!
First, we’ll add the following to landing.cljs
:
(ns blog-post.landing)
(let [elem (.getElementById js/document "app")]
(set! (.innerHTML elem) "Script LOADED!")))))
Next, we’ll update the HTML page to actually load and call the function:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Hello World!</title> | |
</head> | |
<body> | |
<h1>Hello World!</h1> | |
We are using node.js <script>document.write(process.version)</script> | |
and Electron <script>document.write(process.versions['electron'])</script>. | |
<div id="app"> | |
<p>Script was not loaded.</p> | |
</div> | |
<script type="text/javascript" src="../../lib/ui/goog/base.js"></script> | |
<script type="text/javascript" src="../ui.js"></script> | |
<script type="text/javascript">goog.require('blog_post.landing')</script> | |
</body> | |
</html> |
And finally, we’ll actually update our project.clj
file so we can build the “renderer process” layer.
The entire file looks like this:
(defproject
blog-post "0.1.0"
:description "Test configuration"
:url "http://owensd.io"
:license {:name "MIT"}
:dependencies [[org.clojure/clojure "1.8.0"]
[org.clojure/clojurescript "1.9.456"]]
:plugins [[lein-cljsbuild "1.1.5"]
[lein-shell "0.5.0"]]
:aliases {"electron-main" ["do"
["shell" "grunt" "generate-manifest"]
["cljsbuild" "once" "main"]
["shell" "grunt" "generate-mainjs"]
["shell" "grunt" "symlink"]]
"electron-ui" ["do"
["cljsbuild" "once" "ui"]]
"electron" ["do"
["shell" "grunt" "generate-manifest"]
["cljsbuild" "once" "main"]
["shell" "grunt" "generate-mainjs"]
["shell" "grunt" "symlink"]
["cljsbuild" "once" "ui"]]}
:cljsbuild {:builds {:main {:source-paths ["app/src"]
:incremental true
:assert true
:compiler {:output-to ".out/app/electron-host.js"
:output-dir ".tmp/app"
:warnings true
:elide-asserts true
:target :nodejs
:optimizations :simple
:pretty-print true
:output-wrapper true}}
:ui {:source-paths ["ui/src"]
:incremental true
:assert true
:compiler {:output-to ".out/app/ui.js"
:output-dir ".out/lib/ui"
:warnings true
:elide-asserts true
:optimizations :none
:pretty-print true
:output-wrapper true}}}})
As you can see, there are two new aliases created and a new ui
build target.
Now, when you build and run Electron, you should get this:

Conclusion
It’s easy to get bogged down in the details. However, the process is simply a set of rote steps:
- Install our pre-reqs: Java and Leiningen.
- Create the node package file to track our dependencies.
- Create the Grunt configuration file to help us with some of the automation tasks.
- Write the code for the “main process”.
- Write the code for the “renderer process”.
When someone new to the project onboards, after the pre-reqs are installed, they only need to clone the repo and run npm install
. After that, they’ll be up-and-running!
Now, I also did a few other things because I wanted to reduce the amount of places that needed to be updated. Right now, there are only two places that need to be modified for the basic configuration data: package.json
and project.clj
. I’m not really sure the best way to get rid of those duplicate pieces of information.
Also, there is still one remaining task: handle when :optimizations :none
is true for the “main process”. That will have to come later as this blog post is already fair too long.
Lastly, if you want to see all of the code in one easy view, you can check out the repo here: https://github.com/owensd/electron-blog-post-sample/tree/getting-started.
Any particular reason to use grunt do download electron rather than using the npm package electron-prebuilt (recently renamed to electron) https://github.com/electron-userland/electron-prebuilt ?
Could save you one more dependency.
LikeLike
Well, I use Grunt for other things too, so I’d need to convert those over as well. However, this is everything that comes down with
electron
(also, the one you pointed to is now deprecated):There are 174 projects that npm brings down when Electron is added vs. the 139 projects with just the Grunt dependencies.
So yeah… npm will be the first dependency to go, not Grunt.
LikeLike
[…] ClojureScript + Electron […]
LikeLike
[…] ClojureScript + Electron […]
LikeLike