Migrating my Phoenix app to use esbuild turned out to be more involved than I'd expected. The main difficulties were:
- Handling static assets like fonts and all the different file extensions in my app with esbuild
- Maintaining the ability to use the considerable amount of SCSS already written in my stylesheets
- Not breaking Font Awesome or Bootstrap (which I'm mostly but not 100% migrated away from)
What worked for me
My package.json
{
"scripts": {
"build": "vite build",
"watch": "vite build --watch --minify false --emptyOutDir false --clearScreen false --mode development"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@popperjs/core": "^2.10.2",
"axios": "^0.24.0",
"bootstrap": "^5.1.2",
"bootstrap.native": "^4.0.6",
"highlight.js": "^11.3.1",
"phoenix": "^1.6.5",
"phoenix_html": "^3.1.0",
"phoenix_live_view": "^0.17.5"
},
"devDependencies": {
"autoprefixer": "^10.4.1",
"chokidar": "^3.5.2",
"esbuild": "^0.14.9",
"esbuild-sass-plugin": "^2.0.0",
"fs": "^0.0.1-security",
"path": "^0.12.7",
"postcss": "^8.4.5",
"postcss-nested": "^5.0.6",
"postcss-preset-env": "^6.7.0",
"postcss-url": "^10.1.3",
"sass": "^1.42.1",
"tailwindcss": "^3.0.8",
"vite": "^2.7.9"
}
}
The endpoint watcher's config in dev.exs
:
watchers: [
node: [
"node_modules/vite/bin/vite.js",
cd: Path.expand("../assets", __DIR__)
]
]
vite.config.js
:
import { defineConfig } from "vite";
export default defineConfig(({ command }) => {
const isDev = command !== "build";
if (isDev) {
// Terminate the watcher when Phoenix quits
process.stdin.on("close", () => {
process.exit(0);
});
process.stdin.resume();
}
return {
publicDir: "static",
// plugins: [react()],
build: {
target: "esnext", // build for recent browsers
outDir: "../priv/static", // emit assets to priv/static
emptyOutDir: true,
sourcemap: isDev, // enable source map in dev build
manifest: false, // do not generate manifest.json
rollupOptions: {
input: {
app: "./js/app.js"
},
output: {
entryFileNames: "assets/[name].js", // remove hash
chunkFileNames: "assets/[name].js",
assetFileNames: chunkAssets
}
}
}
};
});
/*
* Watches and closes stdin when the main process closes.
* This avoids zombie esbuild processes accumulating in the system.
*/
function maybeCloseStdin(command) {
if (command == "build") return
process.stdin.on("close", () => { process.exit(0) })
process.stdin.resume()
}
/*
* Maps asset chunks to their corresponding folder.
* Keys are regexes that match the file name and values should be a asset folder.
* This first match is always used, so make sure to put the most specific regex first.
*/
const assetChunkMappings = {
"\.webfonts/\.(woff2?|ttf|eot|svg)": "fonts/[name][extname]",
"\.(woff2?|ttf|eot)$": "fonts/[name][extname]",
"\.(s?css)$": "assets/[name][extname]",
}
function chunkAssets(info) {
return Object.entries(assetChunkMappings)
.filter(([key, _value]) => info.name.match(key))
.map(([_key, value]) => value)[0] || "[ext]/[name][extname]"
}
My app.scss
:
$fa-font-path: "@fortawesome/fontawesome-free/webfonts/";
@import "@fortawesome/fontawesome-free/scss/fontawesome";
@import "@fortawesome/fontawesome-free/scss/regular";
@import "@fortawesome/fontawesome-free/scss/solid";
@import "@fortawesome/fontawesome-free/scss/brands";
@import "@fortawesome/fontawesome-free/scss/v4-shims";
// @import "bootstrap-sass";
@import "custom";
@import "highlight.js/scss/base16/tomorrow-night";
The root template
This part was in the resources I mentioned in the video but I didn't explain it specifically.
If you're using Vite with Phoenix in dev mode, then you'll also need to set up your root.html.heex
template to hook into Vite's output. The important part here is the if-else-end block:
<!DOCTYPE html>
<html lang="en">
<head>
<%= render_existing(
Phoenix.Controller.view_module(@conn),
"meta." <> Phoenix.Controller.view_template(@conn),
Map.merge(assigns, %{
view_module: Phoenix.Controller.view_module(@conn),
view_template: Phoenix.Controller.view_template(@conn)
})
) || render CampsiteWeb.LayoutView, "meta.html", assigns %>
<%= if Application.get_env(:campsite, :mix_env) == :dev do %>
<script type="module" src="http://localhost:3000/@vite/client"></script>
<script defer type="module" src="http://localhost:3000/js/app.js"></script>
<% else %>
<script defer phx-track-static type="module" src={Routes.static_path(@conn, "/assets/app.js")}></script>
<% end %>
<link phx-track-static rel="stylesheet" href={Routes.static_path(@conn, "/assets/app.css")}/>
<%# <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.3.1/styles/xt256.min.css" /> %>
</head>
<body>
<%= @inner_content %>
</body>
</html>
The reason I can get :mix_env
from my application environment is because I added it via this line in config.exs
:
config :campsite, mix_env: Mix.env()
Change :campsite
to whatever the name of your application is.
No Comments