Electron Apps with PureScript

Posted on July 5, 2016

I’ve recently taken another look at the Electron Framework for building native applications using web technologies, in particular, the PureScript programming language.

To my enjoyment there has been lots of progress since I last checked and starting an Electron project with PureScript is simpler than ever. In this blogpost I’ll show you how to:

  1. Set up a new Electron project
  2. Add a little PureScript
  3. Freely mix Node and Browser API’s
  4. Add the Thermite library and build a minimal React application

Prerequisits

To follow along you’ll want to install:

Setting up a new Electron Project

Following the Getting started path for Electron leads to the electron-quick-start repository. We can use it to bootstrap a new Electron project like so:

# Clone the Quick Start repository
$ git clone https://github.com/electron/electron-quick-start

# Go into the repository
$ cd electron-quick-start

# Install the dependencies and run
$ npm install && npm start

This should greet you with a simple Electron Application like so:

A few things to note right away:

Let’s take a look at the directory structure of our project:

> ls -1
LICENSE.md
README.md
index.html
main.js
node_modules
package.json
renderer.js

Pretty simple, huh? We’ve got the entry point for our application main.js, an HTML file we can load up in the window that main.js creates, and an empty JS file renderer.js that gets require’d from that HTML file.

I’ll not go into detail about any of those however, since this setup is already enough for us to…

Add a little PureScript

The usual way to start a PureScript project is pulp init, but since we’ve already done some scaffolding that won’t work this time. Instead we’ll do the few steps by hand.

We start by setting up a bower file to manage our PureScript dependencies:

$ bower init

After confirming, or modifying some of bower’s questions we can install the purescript-console library to prepare for our Hello Electron.

$ bower install --save purescript-console

Next up we’ll create a simple PureScript module at src/Main.purs.

module Main where

import Control.Monad.Eff.Console (log)

main = log "Hello, Electron"

and compile it with

$ pulp build

By default the PureScript compilers emits CommonJS modules into the output/ directory. If we want to run these in a browser we usually have to use a bundling tool like Webpack or psc-bundle. Luckily Electron knows how to deal with the CommonJS format and so we can just put the following line into the renderer.js file and reload our Electron application.

require('./output/Main').main()

If you open the Console tab inside the Chrome Dev Tools you should be greeted with Hello, Electron. YAY!

Effects and Environments

In PureScript we use the built-in rowtypes to track the effects of our programs. A program with a signature like Eff (dom :: DOM) Unit tells us, that the program accesses the DOM (Document Object Model). It also tells us that we cannot use this program inside a node interpreter, since it relies on the DOM being present. On the other hand a signature like Eff (fs :: FS) Unit, where FS is the file system effect, tells us that this program can not be run inside the browser, as file system access is only available inside a nodejs interpreter.

So usually when we encounter a signature like Eff (fs :: FS, dom :: DOM) Unit we are looking at a useless program that can neither be run in the browser, nor inside the nodejs interpreter.1

Enter Electron

Electron’s secret sauce, is that it allows you to access both the Node and the Browser API’s in your programs at the same time. Let’s see how can we utilize these capabilities in a seamless manner with PureScript. We’re going to write a program, that reads the current directory and displays all the found files as an unordered list. Eventhough it’s totally overkill for this example we’ll be using the purescript-thermite library to render our webpage.

Thermite is a lightweight wrapper (350 lines including docstrings) around the React library. Since we’re looking to access the file system we’ll also need to install some node bindings.

$ npm install --save react react-dom
$ bower install --save purescript-thermite 
$ bower install --save purescript-node-fs

Next up is modifying our Main module. PureScript’s ecosystem embraces modularity, which means we have to get a few imports out of the way first.

  module Main where

  import Prelude
  import React as R
  import ReactDOM as RDOM
  import Thermite as T
  import Control.Monad.Eff (Eff)
  import Control.Monad.Eff.Exception (try)
  import DOM (DOM)
  import DOM.HTML (window) as DOM
  import DOM.HTML.Types (htmlDocumentToParentNode) as DOM
  import DOM.HTML.Window (document) as DOM
  import DOM.Node.ParentNode (querySelector) as DOM
  import Data.Either (either)
  import Data.Maybe (fromJust)
  import Data.Nullable (toMaybe)
  import Node.FS (FS)
  import Node.FS.Sync (readdir)
  import Partial.Unsafe (unsafePartial)
  import React.DOM (text, li', ul')

We’ll use React’s props mechanism to pass the list of file names to our component, so we’ll define a type for that first

  type FileNames = {names :: Array String}

Next up we’re defining the Spec for our directory listing component. Because we don’t support any actions, we just pass defaultPerformAction and our render function is pretty simple aswell. We use map to turn our list of names into a list of <li>’s and wrap it inside a <ul>.

  dirListingComponent 
    :: forall eff. T.Spec eff Unit FileNames Unit
  dirListingComponent =
    T.simpleSpec T.defaultPerformAction render
    where render _ props _ _ =
            [ ul'
              (map (\file -> li' [text file]) props.names)
            ]

Last is our main function in which we first grab all the file names inside the current directory.

  main :: Eff (fs :: FS, dom :: DOM) Unit
  main = void do
    fileNames <- either (const []) id <$> try (readdir ".")

We then create a React class out of our Spec and since our component is stateless, we just pass unit as the initial state.

    let component = T.createClass dirListingComponent unit

Next up is some boilerplate to grab the body element of of our page. This is considered bad practice and React will warn us, but it could easily be fixed by adding a container element inside index.html and grabbing that for rendering. For the sake of this blogpost body is fine though.

    document <- DOM.window >>= DOM.document
    container <-
      unsafePartial
      (fromJust <<< toMaybe
      <$> DOM.querySelector "body"
      (DOM.htmlDocumentToParentNode document))

Finally we tell React to render our component into the body of the webpage, and pass the file names that we grabbed earlier.

    RDOM.render
      (R.createFactory component {names: fileNames})
      container

If everything went well, all that’s left now, is to run pulp build and refresh our Electron application.

Tada!

Wrapping up

I think Electron is an amazing opportunity for PureScript. It allows us to take our m browser-libraries, our n node-libraries and create n x m new libraries and applications. PureScript’s openness and good FFI story let us benefit from all the amazing work the JS community brings forth.

I was surprised at how easy it was it integrate Thermite into Electron. There is no need for a bundler at all. This is a lot different from when I last attempted to combine the two technologies and I’m super excited and motivated to continue exploring and writing native applications in PureScript.

The code for this blogpost is uploaded to Github.


  1. Interpreting the effect system this way is also called Coeffects, since we’re specifying the capabilities an Environment needs to provide, instead of the effects our program will have on the world when run. I’d highly recommend you check out Coeffects by Thomas Petricek if you want to read more about this kind of reasoning.