Update (2018): While the ideas in this post are still valid elm-live will get you started a lot faster. Also check out the Elm guide section on minification.

elm-reactor is good for iterating quickly on self-contained programs. You run elm-reactor, you navigate to the source file that contains the entry point to your program and you are up and running. elm-reactor automatically compiles your program and embeds it in an HTML page. You refresh that page when you want to try out modifications to your program or read the compiler errors.

When you start a bigger project you may need to customize that bootstrap page to, say, include external CSS or JavaScript. The Elm 0.17 reactor gives you no control over that. You need to compile your program to a JavaScript file with elm-make using the --output flag and then reference that in your custom HTML. Now there is an extra step in the development workflow.

When I started developing a single page application in Elm, I decided to make this workflow as nice as possible for myself by automating the build process. I came up with something that works well for me and that I want to share. Before we get into the specifics, however, let's see what needs my tooling solves.

Requirements and assumptions

Bootstrapping Elm

In a new project directory, create the npm configuration file package.json:

npm init

Install Elm as an npm package.

npm install --save-dev elm

I usually create the Makefile at this point and add a single target: setup. That is the convention I use for installing dependencies in projects that use make.

setup:
	npm install

Make sure to document in the project's README that the project is set up with make setup (or just npm install if you didn't add a setup target in the Makefile).

Hello, world!

Create a src/ directory to hold your Elm program. Write a simple main module in src/Main.elm for testing:

module Main exposing (main)

import Html

main =
    Html.text "Hello, world!"

Compile it with:

./node_modules/.bin/elm-make src/Main.elm

This will ask to install some Elm packages before it can compile. It will automatically create the default elm-package.json. Update it to point to src/ as a source directory. Otherwise, once you start adding new modules, elm make will complain about not finding them. It should look something like this (the important bit is "source-directories"):

{
    "version": "1.0.0",
    "summary": "Project summary",
    "repository": "https://github.com/user/project.git",
    "license": "BSD3",
    "source-directories": [
        "src/"
    ],
    "exposed-modules": [],
    "dependencies": {
        "elm-lang/core": "4.0.3 <= v < 5.0.0",
        "elm-lang/html": "1.1.0 <= v < 2.0.0"
    },
    "elm-version": "0.17.1 <= v < 0.18.0"
}

Now we can add a compilation step to the Makefile.

BIN = ./node_modules/.bin
SRC = src
BUILD = build

build: build-directory js

build-directory:
	mkdir -p $(BUILD)

js:
	$(BIN)/elm make src/Main.elm --output $(BUILD)/app.js

I declared some variables at the top, because we are going to use these paths a lot. Now running make compiles our program and writes it to build/app.js. This is everything we need for the Elm part even as the project grows. The compiler will bundle all imported modules.

Let's add our custom src/index.html that imports app.js.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Elm app</title>
  </head>
  <body>
  </body>
  <script src="app.js"></script>
  <script>Elm.Main.fullscreen();</script>
</html>

The corresponding Makefile section simply copies that file to the build directory.

html:
	cp $(SRC)/index.html $(BUILD)/index.html

When adding new build steps, don't forget to add them as dependencies to the build target in the Makefile. Right now, it should look like this:

build: build-directory js html

Automatic rebuilds on file changes

The chokidar-cli package provides a command-line utility that watches files for changes and runs a command in reaction. In this case, we want to run make when files in the src/ or assets/ directories are modified. src/ contains the source code and all the stylesheets, web fonts and images are stored in assets/.

watch:
	@$(BIN)/chokidar "$(SRC)" "assets" -c "make"

Cool! We can use our elm-reactor-like workflow by running make watch and opening build/index.html. Whenever we save a file, we can see the changes by refreshing the browser page. One difference is that the compiler errors are displayed in the terminal instead of the browser.

Now is a good time to document the effects of make and make watch in your README.

Live reloading

Figwheel provides a wonderful development experience for ClojureScript projects. We can get close to it thanks to BrowserSync.

build: build-directory html js css fonts images
	$(BIN)/browser-sync reload

browser-sync:
	$(BIN)/browser-sync start --server $(BUILD)

Running make browser-sync will open the app in your browser and automatically refresh it after each build (note that we added the command browser-sync reload to the build target). What's more, if you access the same URL from another browser, even other devices, all instances will be refreshed without manual intervention.

Depending on the way your application derives its state you may lose state between reloads, but that is already the case with manual reloads. How you can achieve seamless reloads is a topic for another time.

Minification

The Google Closure compiler requires Java, so I opted for UglifyJs, which gives good results. I enabled name mangling and compression. This reduces the file size significantly (from 164Kb down to 64Kb for the Hello World example). Gzip reduces the size even further (down to 19Kb), but gzipping can be done by the web server.

js:
	$(BIN)/elm make $(SRC)/Main.elm \
		--output $(BUILD)/app.js
	$(BIN)/uglifyjs --compress --mangle \
		--output $(BUILD)/app.min.js \
		$(BUILD)/app.js 2> /dev/null
	mv $(BUILD)/app.min.js $(BUILD)/app.js

I redirect the UglifyJS error stream to /dev/null, because it just contains warnings about the minification of things in the Elm standard library: "Side effects in initialization of unused variable", "Dropping unused variable", "Dropping unused function". Ignoring warnings is generally not a good idea, but in this case:

Stylesheets

clean-css works great for my purposes. It takes your concatenated CSS files as input and creates a minified stylesheet. If you need Sass, node-sass has a command line interface that can be used in a similar way.

CSS = assets/stylesheets

css:
	cat $(CSS)/*.css | \
		$(BIN)/cleancss --output $(BUILD)/style.css

Import the stylesheet in the <head> section of src/index.html:

<link rel="stylesheet" type="text/css" href="style.css" />

Final result

The package.json configuration file looks something like this:

{
  "name": "elm-spa",
  "version": "1.0.0",
  "private": true,
  "description": "Elm single page application",
  "dependencies": {},
  "devDependencies": {
    "browser-sync": "^2.14.0",
    "chokidar-cli": "^1.2.0",
    "clean-css": "^3.4.18",
    "elm": "^0.17.1",
    "uglify-js": "^2.7.0"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "UNLICENSED"
}

The build directory contains the following after I run make:

$ tree -L 1 build
build
β”œβ”€β”€ app.js
β”œβ”€β”€ fonts
β”œβ”€β”€ images
β”œβ”€β”€ index.html
β”œβ”€β”€ setup.js
└── style.css

2 directories, 4 files

setup.js contains the JavaScript code that embeds the Elm program and sets up ports. In the final Makefile below you can see that I also compress setup.js.

BIN = ./node_modules/.bin
SRC = src
BUILD = build
CSS = assets/stylesheets
FONTS = assets/fonts
IMAGES = assets/images
HTML = src/index.html

build: build-directory html js css fonts images

build-directory:
	mkdir -p $(BUILD)

html:
	cp $(HTML) $(BUILD)/index.html

js:
	$(BIN)/elm make $(SRC)/Main.elm \
		--output $(BUILD)/app.js
	$(BIN)/uglifyjs --compress --mangle \
		--output $(BUILD)/app.min.js \
		$(BUILD)/app.js 2> /dev/null
	mv $(BUILD)/app.min.js $(BUILD)/app.js
	$(BIN)/uglifyjs --compress --mangle \
		--output $(BUILD)/setup.js \
		$(SRC)/setup.js 2> /dev/null

css:
	cat $(CSS)/*.css | \
		$(BIN)/cleancss --output $(BUILD)/style.css

fonts:
	cp -r $(FONTS) $(BUILD)

images:
	cp -r $(IMAGES) $(BUILD)

watch:
	$(BIN)/chokidar "$(SRC)" "assets" -c "make"

browser-sync:
	$(BIN)/browser-sync start \
		--server $(BUILD) --files $(BUILD)

setup:
	npm install

Make sure to use tabs for indentation or make will fail with a cryptic error message.

Possible improvements

Deployment automation

It would be good to be able to deploy the app in a single step by running

make deploy

This depends on where you deploy, so I left it out.

Cleaner make watch logs

By default make prints a command before executing it. It also prints "Entering directory" and "Leaving directory" messages. All of this creates a lot of noise and we only care about compiler errors. There are ways to silence make. For instance, prefixing a Makefile line with @ prevents make from printing it before execution. I find it best to leave the defaults for manual builds. For builds triggered by file changes during development I completely disable make output with the -s flag:

watch:
	$(BIN)/chokidar "$(SRC)" "assets" -c "make -s"

I also like to add some blank lines to distinguish the output of consecutive builds:

build: build-directory html js css fonts images
	@$(BIN)/browser-sync reload
	@echo -e "\n\n\n\n"