Skip to content

Use Vite in Phoenix project

Published: at 09:30 AM

As of now (v1.7) new projects created in Phoennix framework by default use two packages (esbuild and tailwind). They are essentially wrappers that take care of installing npm packages (esbuild and tailwind) and running respective scripts. This works great and you don’t have to think much about Javascript setup, and if you need, there are some configurations available.

But you might need to use a different bundling system, different UI framework or just have more access to the configurations of the currently used libraries. Or you just might want to take more control over things! Here I’ll explain how to do that :)

First things first, let’s make it look like a real Javascript app ;) Create an empty package.json file in the assets folder:

{
  "type": "module",
  "dependencies": {
    "phoenix": "file:../deps/phoenix",
    "phoenix_html": "file:../deps/phoenix_html",
    "phoenix_live_view": "file:../deps/phoenix_live_view"
  }
}

This ensures the imports from phoenix, phoenix_html and phoenix_live_view will be handled correctly by the bundler. Now let’s start with adding the JS dependencies. Run these commands in your root folder:

npm --prefix assets install vite
npm --prefix assets install -D tailwindcss postcss autoprefixer @tailwindcss/forms

Now we need to start using it and get rid of the esbuild and tailwind (the Elixir wrapper ones of course). First let’s change the watcher command in dev.exs file:

  watchers: [
-    esbuild: {Esbuild, :install_and_run, [:journal, ~w(--sourcemap=inline --watch)]},
-    tailwind: {Tailwind, :install_and_run, [:journal, ~w(--watch)]}
+    npx: ["vite", "build", "--mode", "development", "--watch", "--config", "vite.config.js", cd: Path.expand("../assets", __DIR__)]
  ]

This uses npx to run the build command for Vite.js in watch mode. Notice that we need a config file for Vite as well. Create the file assets/vite.config.js and put this inside:

import { defineConfig, loadEnv } from "vite";
import tailwindcss from "tailwindcss";
import autoprefixer from "autoprefixer";

export default defineConfig(({ command, mode }) => {
  const env = loadEnv(mode, process.cwd(), "");

  return {
    publicDir: false,
    build: {
      outDir: "../priv/static",
      emptyOutDir: false,
      target: ["es2020"],
      manifest: false,
      rollupOptions: {
        input: "js/app.js",
        output: {
          assetFileNames: "assets/[name][extname]",
          chunkFileNames: "[name].js",
          entryFileNames: "assets/[name].js",
        },
      },
      commonjsOptions: {
        exclude: [],
        include: ["vendor/topbar.js"],
      },
    },
    css: {
      postcss: {
        plugins: [tailwindcss, autoprefixer],
      },
    },
    define: {
      __APP_ENV__: env.APP_ENV,
    },
  };
});

We should also remove the configurations related to esbuild and tailwind from the config.exs file as they are no longer needed:


-# Configure esbuild (the version is required)
-config :esbuild,
-  version: "0.17.11",
-  journal: [
-    args:
-      ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
-    cd: Path.expand("../assets", __DIR__),
-    env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
-  ]

-# Configure tailwind (the version is required)
-config :tailwind,
-  version: "3.4.0",
-  journal: [
-    args: ~w(
-      --config=tailwind.config.js
-      --input=css/app.css
-      --output=../priv/static/assets/app.css
-    ),
-    cd: Path.expand("../assets", __DIR__)
-  ]

Speaking of cleanups!, we should now remove the esbuild and tailwind from our mix packages.

mix deps.unlock esbuild tailwind
mix deps.clean esbuild tailwind

Don’t forget to remove them from the mix.exs file as well:

-      {:esbuild, "~> 0.8", runtime: Mix.env() == :dev},
-      {:tailwind, "~> 0.2", runtime: Mix.env() == :dev},

Alright now we’ll adjust our assets to make sure they’re working fine in this new system. First change is in our root template file root.html.heex:

-    <script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
+    <script defer phx-track-static type="module" src={~p"/assets/app.js"}>
    </script>

We’ll then change the app.css file:

-@import "tailwindcss/base";
-@import "tailwindcss/components";
-@import "tailwindcss/utilities";
+@tailwind base;
+@tailwind components;
+@tailwind utilities;

/* This file is for your main application CSS */

And as the last bit, add one line at the top of your app.js file:

import "../css/app.css";

That’s it! You can now run

iex -S mix phx.server

and see it working like a charm. There is one more small step to have the app fully ready, and that’s adjusting the mix scripts for setting up and building the application. Change the mix.exs file like this:

-      "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"],
+      "assets.setup": ["cmd --cd assets npm install"],
-      "assets.build": ["tailwind journal", "esbuild journal"],
+      "assets.build": ["cmd --cd assets npx vite build --config vite.config.js"],
      "assets.deploy": [
-        "tailwind journal --minify",
-        "esbuild journal --minify",
+        "cmd --cd assets npx vite build --mode production --config vite.config.js",
        "phx.digest"
      ]

Happy Hacking!