ClojureScript + Electron

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:

  1. Cross-platform C++ with a “friendly” scripting language (yes, a C-plugin interface would be good, but it’s not exactly friendly).
  2. 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:

  1. Main Process – this is the code that is fed from the package.json file.
  2. 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.

  1. Grunt – this is primarily used for easy downloading of the Electron distributable projects.
  2. NPM – to download most of the dependencies and create a single npm install step to do so.
  3. The lein build tool.
  4. 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:

  1. app – this corresponds to the “main process” of Electron
  2. ui – 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:

  1. 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…
  2. 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:

  1. Generate a package.json manifest file. This is what Electron uses to know what JavaScript file to load.
  2. Generate the app.js that is referenced by the package.json file. This simply loads the prerequisite libraries and the main entry point for the “main process”.
  3. Compile the “main process” code.
  4. 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.

  1. 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 to manifestDir.
  1. 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:

  1. Reads in the package.json file.
  2. Creates the configuration blob by parsing out the contents of the file.
  3. Ensures all of the configuration blocks are set.
  4. 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:

  1. Nothing is compiled
  2. 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:

Electron shell with devtools open.
Electron shell with devtools open.

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:

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:

Electron shell with page loaded and no script running.
Electron shell with page loaded and no script running.

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:

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:

Electron with loaded script and devtools.
Electron with loaded script and devtools.

Conclusion

It’s easy to get bogged down in the details. However, the process is simply a set of rote steps:

  1. Install our pre-reqs: Java and Leiningen.
  2. Create the node package file to track our dependencies.
  3. Create the Grunt configuration file to help us with some of the automation tasks.
  4. Write the code for the “main process”.
  5. 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.

ClojureScript + Electron

4 thoughts on “ClojureScript + Electron

    1. 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):

      └─┬ electron@1.5.0 
        ├─┬ electron-download@3.3.0 
        │ ├─┬ debug@2.6.0 
        │ │ └── ms@0.7.2 
        │ ├─┬ fs-extra@0.30.0 
        │ │ ├── jsonfile@2.4.0 
        │ │ └── klaw@1.3.1 
        │ ├── home-path@1.0.3 
        │ ├─┬ nugget@2.0.1 
        │ │ ├── pretty-bytes@1.0.4 
        │ │ ├─┬ progress-stream@1.2.0 
        │ │ │ ├── speedometer@0.1.4 
        │ │ │ └─┬ through2@0.2.3 
        │ │ │   └─┬ xtend@2.1.2 
        │ │ │     └── object-keys@0.4.0 
        │ │ ├─┬ single-line-log@1.1.2 
        │ │ │ └─┬ string-width@1.0.2 
        │ │ │   ├── code-point-at@1.1.0 
        │ │ │   └── is-fullwidth-code-point@1.0.0 
        │ │ └── throttleit@0.0.2 
        │ ├─┬ rc@1.1.6 
        │ │ ├── deep-extend@0.4.1 
        │ │ ├── ini@1.3.4 
        │ │ └── strip-json-comments@1.0.4 
        │ └─┬ sumchecker@1.3.0 
        │   └── es6-promise@4.0.5 
        └─┬ extract-zip@1.6.0 
          ├─┬ concat-stream@1.5.0 
          │ ├─┬ readable-stream@2.0.6 
          │ │ ├── isarray@1.0.0 
          │ │ ├── process-nextick-args@1.0.7 
          │ │ └── util-deprecate@1.0.2 
          │ └── typedarray@0.0.6 
          ├── debug@0.7.4 
          ├─┬ mkdirp@0.5.0 
          │ └── minimist@0.0.8 
          └─┬ yauzl@2.4.1 
            └─┬ fd-slicer@1.0.1 
              └── pend@1.2.0 
      

      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.

      Like

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s