DEV Community

Dwayne Crooks
Dwayne Crooks

Posted on

Making TodoMVC work with dwayne/elm2nix

The TodoMVC web application is one of the first web applications I ever implemented. The first time I coded it up was when I was learning Backbone.js and the second time was when I was new to Elm. Throughout the years I've iterated on it multiple times to explore a variety of ideas. Now I'm at it again trying out dwayne/elm2nix on my dwayne/elm-todos project.

There were two major highlights for me after rewriting elm-todos to use elm2nix.

  1. Having the ability to run the web application from any machine with Nix installed.
  2. Making CI more reliable by being able to use Nix with a Nix-aware caching solution.

Below I'll explain these two points and then I'd go over the interesting details of the changes that were made.

Run the web application from any machine with Nix installed

If you have Nix installed you can run the development version of the application using:

nix run github:dwayne/elm-todos
Enter fullscreen mode Exit fullscreen mode

Or, you can run the production version using:

nix run github:dwayne/elm-todos#prod
Enter fullscreen mode Exit fullscreen mode

If you don't have Nix installed, no worries, there are multiple ways to do it. The way I recommend is to use the Determinate Nix Installer.

Reliable CI

In Towards more reliable CI, Evan (evancz) explained how to make CI much more reliable and Christian Ekrem (cekrem) created a repository, cekrem/elm-ci-setup, that shows how to properly cache Elm dependencies in CI/CD pipelines. If you're not using Nix then you should definitely follow the approach they outline.

Marek Fajkus (turboMaCk), is one of the main people responsible for getting all the existing community tooling for the Elm language working with Nix with his, now defunct, turboMaCk/nix-elm-tools project. He recommends using nix as a build tool:

In the end the best option I know about is to use nix as a build tool. Nix uses immutable structure for everything and can do very granular caching because of that. By far the most robust system is to package the code using nix and instead of CI cache rely on nix-store caching or things like cachix (built using Elm btw) to immutable hash based granular caching. But it's hardly something we should be recommending to everyone.

Another Elm community member, Matt McHenry (jerith666), also shared his thoughts on Nix which I've partially quoted below:

I think it’s worth mentioning Nix in this context as well. Nix builds run in a sandbox without any network access. So when you use Nix to build your Elm code, preventing network access at build time becomes a necessity rather than a “nice to have” for added stability and performance.

I mention all this to share that once I was able get dwayne/elm-todos building with Nix using dwayne/elm2nix it was relatively easy to set up a GitHub workflow that did quality assurance on my code on every push.

Here's the workflow, check.yml:

name: Check

on:
  push

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: DeterminateSystems/nix-installer-action@main
      - uses: DeterminateSystems/magic-nix-cache-action@main
      - run: nix flake check -L
Enter fullscreen mode Exit fullscreen mode

Here are several runs. Here's a run where the cache needed to be warmed up. And, here's a run where the cache was used. In the last example, you'd notice that the flake checks didn't even need to be re-run since the derivations didn't change.

I was going to use Cachix, but I stumbled upon the Magic Nix Cache and decided to give it a try. With the Magic Nix Cache there's no signup, setup, or cost and you can use it by adding just one line to your workflow. For my simple public open source project the Magic Nix Cache seems to be a good fit. For other use cases Cachix might be the better service but this is not the post to go into comparisons between them.

The changes

I started here with the following project layout of key files:

public/
    _redirects
    index.css
    index.html
src/
    Main.elm
.gitignore
Caddyfile
elm.json
Enter fullscreen mode Exit fullscreen mode

and after several iterations, see PR: Use elm2nix, I ended here with the following layout of key files:

.github/workflows/
    check.yml
nix/
    build-css.nix
    build-html.nix
    build.nix
    htmlnano.nix
    serve.nix
public/
    _redirects
    index.css
    index.html
src/
    Main.elm
.gitignore
Caddyfile
elm.json
elm.lock
flake.lock
flake.nix
Enter fullscreen mode Exit fullscreen mode

N.B. All the .nix files in the nix/ directory follow the callPackage convention.

HTML

nix/build-html.nix returns a function that allows you to decide whether or not you want to minify the HTML. The minification is done with htmlnano, which Parcel used before switching to its own built-in minifier.

The HTML is only ever built the first time and whenever public/index.html changes.

CSS

nix/build-css.nix returns a function that allows you to decide whether or not you want to minify the CSS. The minification is done with lightningcss.

The CSS is only ever built the first time and whenever public/index.css changes.

Elm

The Elm application is built using the buildElmApplication builder from dwayne/elm2nix. Its src is filtered to include only the files required to build the application and nothing more.

Web app

The entire web application is put together in nix/build.nix. It returns a function that allows you to configure how the HTML, CSS, and Elm files are built.

If compression is enabled it compresses your HTML, CSS, and JavaScript output using brotli and zopflli. The Caddyfile is already configured to serve these compressed files locally so you can test that it works as expected.

You have the option to include Netlify redirects, public/_redirects, for proper handling of client-side routing when you're building to deploy to Netlify.

Serving locally

nix/serve.nix returns a function that creates a shell script that runs Caddy with a derivation that contains the files to be served. The flake.nix exposes these serve-based derivations as the dev and prod apps with dev being the default. See here. That's how the nix run functionality becomes available.

Quality assurance

.github/workflows/check.yml contains the workflow that runs nix flake check -L on every push. nix flake check is a command to verify whether the flake can be evaluated successfully and that the derivations specified by the flake's checks output can be built successfully.

I check that both the development and production versions of the Elm web application can be built successfully. Thanks to the Magic Nix Cache and the way I filter the dependent files I pass to the HTML, CSS, and Elm derivations I'm able to minimize what needs to rebuilt on every push.

Conclusion

Firstly, you can run your Elm web application from any machine with Nix installed.

nix run github:dwayne/elm-todos#prod
Enter fullscreen mode Exit fullscreen mode

Secondly, Nix + dwayne/elm2nix + a Nix-aware caching solution leads to reliable CI for Elm web applications.

Finally, I hope I was able to convey the benefit of using Nix to build your Elm web application. TodoMVC may be a toy application but the ideas I shared do scale to larger applications with more complicated configurations. I plan to show you more interesting projects in future posts.

Further reading

Subscribe to my newsletter

If you're interested in improving your skills with Elm then I invite you to subscribe to my newsletter, Elm with Dwayne. To learn more about it, please read this announcement.

Top comments (0)