by clemens (01.11.2021)

One way to handle static asset files in Phoenix 1.6

So I’ve recently upgraded one of our projects to Phoenix 1.6. While I was at it, I’ve also replaced the legacy webpack pipeline with the new default esbuild pipeline. One problem I’ve stumbled across when recreating the new layout for static files, i.e. all static files simply are put directly into the priv/static folder, was that it is now easy to accidentally commit digested files when running git add .; git commit because those files are no longer ignored by git, only the files in priv/static/assets are.

So for our projects I’ve now come up with the following solution. Instead of putting all files into the priv/static folder, I’ve moved all static files into assets/static and am now using watchexec for watching that folder and copying all changes made there into priv/static during development.

Development setup

For managing watchexec we are using asdf, since we are already using it for managing our Erlang and Elixir versions. However any other installation method will do as well.

In your .tool-versions add:

watchexec 1.15.3

In order to prevent zombie watchers, we must run the watchexec executable using the wrapper script mentioned in the docs. In our case we put it into assets/watcher:

#!/usr/bin/env bash

# See https://hexdocs.pm/elixir/Port.html#module-zombie-operating-system-processes

# Start the program in the background
exec "$@" &
pid1=$!

# Silence warnings from here on
exec >/dev/null 2>&1

# Read from stdin in the background and
# kill running program when stdin closes
exec 0<&0 $(
  while read; do :; done
  kill -KILL $pid1
) &
pid2=$!

# Clean up
wait $pid1
ret=$?
kill -KILL $pid2
exit $ret

Don’t forget to make it executable: chmod 744 assets/watcher!

Next we can move all static files from priv/static to a new folder assets/static and a watcher in our config/dev.exs:

assets_dir = Path.expand("../assets", __DIR__)

config :shop, MyApp.Endpoint,
  ...
  watchers: [
    ...
    {Path.join(assets_dir, "watcher"),
     [
       "watchexec",
       "-w",
       "static",
       "rsync -r --delete --exclude /assets static/ ../priv/static/",
       cd: assets_dir
     ]}
  ]

And finally add the complete priv/static folder to your .gitignore:

# Ignore static folder -> is generated on each build
/priv/static

Build pipeline

Now all that is left to do is to change your build pipeline to also copy the files from assets/static before bundling the release. At our company we are using a simple bash script run with jenkins, but by adjusting the assets.deploy mix task this should also work for other setups.

In your mix.exs change the assets.deploy to include cleaning and copying of the static files, and only then build the js and css files.

  defp aliases do
    [
      "assets.deploy": [
        "cmd rm -rf priv/static/*",
        "cmd cp -r assets/static/* priv/static/",
        "cmd --cd assets npm run deploy",
        "esbuild default --minify",
        "phx.digest"
      ]
    ]
  end

And finally we can no longer commit the digested files by accident 🤩.

Alternative options

One alternative option I’ve tried was to modify our git pre-commit so that it will run mix phx.digest.clean --all before each commit. However that does not prevent git from making the commit, the only result is that after the commit all those files are shown as deleted if you run git status. And because mix phx.digest.clean --all always returns an exit code of 0, I did not find an easy solution to prevent git from making the commit. Maybe there’s an easy solution, but I just couldn’t find one. so