Phoenix 1.6.0 LiveView + esbuild + Tailwind JIT + AlpineJS - A brief tutorial.

Phoenix 1.6.0 is nearing release, and when it comes out it will ditch Webpack for esbuild. It's a smaller integration, more predictable and just more productive.

There's very little material out there describing how to set up a liveview, tailwind jit and alpine project using Phoenix 1.6.0

Here's a little guide I pieced together out of forum posts, tweets, github issues and IRC. I can't thank everyone who helped me enough, they did the work, I just compiled it here for posterity.

If you're impatient just look at the code commits, it's this article step by step.

github.com/sergiotapia/golden

Update #1, November 11, 2021:

Alpine ^3.5.0 now targets es2017 so make sure you update config/config.exs. Check the last commit in the repo for a quick fix.


At the time of writing, Phoenix 1.6.0-rc.0 is out! phoenixframework.org/blog/phoenix-1.6-relea..

Create your project.

mix phx.new golden --live

In the assets folder, install dev dependencies, also install alpinejs as a prod dependency.

cd assets/
npm install autoprefixer postcss postcss-import postcss-cli tailwindcss --save-dev
npm install alpinejs

Configuring the Javascript pipeline.

We use esbuild to build our javascript payload.

Make sure you comment out line 3 of app.js, if you don't esbuild will also compile your css and will thrash any CSS pipelines you set up for tailwind jit. We will use postcss for CSS building later in this article.

// We import the CSS which is extracted to its own file by esbuild.
// Remove this line if you add a your own CSS build pipeline (e.g postcss).
// import "../css/app.css"

Add alpinejs to your app.js file and make sure you configure the livesocket to call alpine on dom updates.

// import Alpine
import Alpine from "alpinejs";

// Add this before your liveSocket call.
window.Alpine = Alpine;
Alpine.start();

---
// Add dom update support for Alpine.
// before:
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}})

// after:
let hooks = {};
let liveSocket = new LiveSocket("/live", Socket, {
  params: { _csrf_token: csrfToken },
  hooks: hooks,
  dom: {
    onBeforeElUpdated(from, to) {
      if (from._x_dataStack) {
        window.Alpine.clone(from, to);
      }
    },
  },
});

Configuring the CSS pipeline

This one is pretty complicated but bear with me.

Create postcss.config.js file in assets folder.

module.exports = {
  plugins: {
    'postcss-import': {},
    tailwindcss: {},
    autoprefixer: {},
  }
}

Create tailwind.config.js file in assets folder.

module.exports = {
  mode: "jit",
  purge: ["./js/**/*.js", "../lib/*_web/**/*.*ex"],
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
};

Add Tailwind's basic css imports to the top of your app.css file.

@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

Finally, add a watcher to dev.exs so your compiled app.css is automatically reloaded as you work on your project.

# dev.exs should ultimately end up looking like this:
config :golden, GoldenWeb.Endpoint,
  # Binding to loopback ipv4 address prevents access from other machines.
  # Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
  http: [ip: {127, 0, 0, 1}, port: 4000],
  debug_errors: true,
  code_reloader: true,
  check_origin: false,
  watchers: [
    # Start the esbuild watcher by calling Esbuild.install_and_run(:default, args)
    esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]},
    npx: [
      "tailwindcss",
      "--input=css/app.css",
      "--output=../priv/static/assets/app.css",
      "--postcss",
      "--watch",
      cd: Path.expand("../assets", __DIR__)
    ]
  ]

At this point you should have nice working development environment.

Just run mix phx.server and visit localhost:4000

Let's figure out deployment to production next.

Create a deploy script in package.json

"scripts": {
    "deploy": "NODE_ENV=production postcss css/app.css -o ../priv/static/assets/app.css"
  },

Modify the assets.deploy alias in mix.exs

defp aliases do
    [
      setup: ["deps.get", "ecto.setup", "cmd --cd assets npm install"],
      "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
      "ecto.reset": ["ecto.drop", "ecto.setup"],
      test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
      "assets.deploy": [
        "cmd --cd assets npm run deploy",
        "esbuild default --minify",
        "phx.digest"
      ]
    ]
  end

Finally let's build a build.sh at root folder deploy script. This will build our app using Elixir releases in prod MIX_ENV and digest our static js and css assets.

#!/usr/bin/env bash
# exit on error
set -o errexit

# Install deps
npm install --prefix ./assets
mix deps.get --only prod

# Initial setup
MIX_ENV=prod mix assets.deploy
MIX_ENV=prod mix compile

# Migrate the database
MIX_ENV=prod mix ecto.migrate

# Build the release and overwrite the existing release directory
MIX_ENV=prod mix release --overwrite

Give that script run permissions.

chmod a+x build.sh

Run the build script.

./build.sh

Note: This command WILL FAIL because you need to set up a database url environment variable for prod MIX_ENV. Set that up using your database of choosing.