This is a fllow-up on a previous post. I decided to post this one as an update with some slight improvements.
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. For example you might want to use custom plugins (in esbuild or vite). 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. I’m using pnpm in this post, but if you use a npm or yarn you’ll need to adjust the commants.
Run these commands in assets
folder:
pnpm install -D vite tailwindcss @tailwindcss/vite @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)]}
+ pnpm: ["vite", "build", "--mode", "development", "--watch", "--config", "vite.config.js", cd: Path.expand("../assets", __DIR__)]
]
This uses pnpm
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 { loadEnv, defineConfig } from "vite";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), "");
return {
publicDir: false,
plugins: [tailwindcss()],
build: {
outDir: "../priv/static",
target: ["es2022"],
rollupOptions: {
input: "js/app.js",
output: {
assetFileNames: "assets/[name][extname]",
chunkFileNames: "[name].js",
entryFileNames: "assets/[name].js",
},
},
commonjsOptions: {
exclude: [],
include: ["vendor/topbar.js"],
},
},
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",
- ocrm: [
- 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.3",
- ocrm: [
- 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>
Another important step is to adjust Tailwind configs. The new version of Tailwind (v4) now uses css variables for the configurations.
We need to change the app.css
file:
-@import "tailwindcss/base";
-@import "tailwindcss/components";
-@import "tailwindcss/utilities";
+@import "tailwindcss";
+
+@config "../tailwind.config.js";
+
+@theme {
+ --color-brand: #fd4f00;
+}
+
+@plugin "@tailwindcss/forms";
+@custom-variant phx-click-loading (&:where(.phx-click-loading, .phx-click-loading *));
+@custom-variant phx-submit-loading (&:where(.phx-submit-loading, .phx-submit-loading *));
+@custom-variant phx-change-loading (&:where(.phx-change-loading, .phx-change-loading *));
We can then remove all the content from assets/tailwind.config.js
and replace it with the following content:
module.exports = {
content: ["./js/**/*.js", "../lib/**/*.*ex"],
};
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 pnpm install"],
- "assets.build": ["tailwind journal", "esbuild journal"],
+ "assets.build": ["cmd --cd assets pnpm vite build --config vite.config.js"],
"assets.deploy": [
- "tailwind journal --minify",
- "esbuild journal --minify",
+ "cmd --cd assets pnpm vite build --mode production --config vite.config.js",
"phx.digest"
]
Happy Hacking!