Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Phoenix with esbuild, fortawesome, and tailwindcss

Using fortawesome (font-awesome) and TailwindCSS (JIT mode) with Phoenix 1.6 and esbuild

Setup

  1. Navigate to the assets dir cd assets/
  2. Install fortawesome, esbuild esbuild-sass-plugins, chokidar (used for watching), tailwind, postcss, and the phoenix deps npm install --save-dev fs path chokidar esbuild esbuild-sass-plugin postcss autoprefixer tailwindcss @fortawesome/fontawesome-free ../deps/phoenix ../deps/phoenix_html ../deps/phoenix_live_view
  3. Create a build.js file with the contents of the build.js file in this gist (I prefer to put this inside of a scripts directory inside of assets. If you leave it at the root, you will need to update the build.js file relative paths)
  4. Create the tailwind.config.js in assets root with the contents of the tailwind.config.js file in this gist
  5. Rename app.css to app.scss
  6. At the top of the app.scss file, add the following (also in this gist as 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 "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
  1. Update the phoenix configs to use the script file instead of esbuild cli. config/config.exs
config :esbuild,
  version: "0.12.18",
  node: [
    "build.js",
    cd: Path.expand("../assets/scripts/", __DIR__),
    env: %{"ESBUILD_LOG_LEVEL" => "silent", "ESBUILD_WATCH" => "1", "NODE_ENV" => "development"}
  ]

config/dev.exs

watchers: [
    node: [
      "build.js",
      cd: Path.expand("../assets/scripts/", __DIR__),
      env: %{"ESBUILD_LOG_LEVEL" => "silent", "ESBUILD_WATCH" => "1", "NODE_ENV" => "development"}
    ]

mix.exs

defp aliases do
    [
      setup: ["deps.get"],
      "assets.deploy": [
        "cmd --cd assets NODE_ENV=production node scripts/build.js",
        "phx.digest"
      ]
    ]
  end
  1. Update assets/js/app.js for the new scss file extension import ../css/app.scss
  2. Use the icons in your html <i class="fas fa-{ICON}></i>
  3. Use tailwind in your eex/leex/heex/html templates or inside of css:
<main class="container">content</main>
<button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
  Button
</button>
main {
  @apply container;
}

.btn-blue {
  @apply bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded;
}
$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 "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
const fs = require('fs');
const path = require('path');
const { watch } = require('chokidar');
const { sassPlugin } = require("esbuild-sass-plugin");
const postcss = require('postcss');
const autoprefixer = require('autoprefixer');
const tailwindcss = require('tailwindcss');
// config
const ENTRY_FILE = 'app.js';
const OUTPUT_DIR = path.resolve(__dirname, '../../priv/static/assets');
const OUTPUT_FILE = 'app.js';
const MODE = process.env['NODE_ENV'] || 'production';
const TARGET = 'es2016'
// build
function build(entryFile, outFile) {
console.log(`[+] Starting static assets build with esbuild. Build mode ${MODE}...`)
require('esbuild').build({
entryPoints: [entryFile],
outfile: outFile,
minify: MODE === 'dev' || MODE === 'development' ? false : true, // if dev mode, don't minify
watch: false,
bundle: true,
target: TARGET,
logLevel: 'silent',
loader: { // built-in loaders: js, jsx, ts, tsx, css, json, text, base64, dataurl, file, binary
'.ttf': 'file',
'.otf': 'file',
'.svg': 'file',
'.eot': 'file',
'.woff': 'file',
'.woff2': 'file'
},
plugins: [
sassPlugin({
async transform(source, resolveDir) {
const { css } = await postcss(
autoprefixer,
tailwindcss(path.resolve(__dirname, "../tailwind.config.js"))
).process(source)
return css
}
})
], // optional
define: {
'process.env.NODE_ENV': MODE === 'dev' || MODE === 'development' ? '"development"' : '"production"',
'global': 'window'
},
sourcemap: MODE === 'dev' || MODE === 'development' ? true : false
})
.then(() => { console.log(`[+] Esbuild ${entryFile} to ${outFile} succeeded.`) })
.catch((e) => {
console.log('[-] Error building:', e.message);
process.exit(1)
})
}
// helpers
function mkDirSync(dir) {
if (fs.existsSync(dir)) {
return;
}
try {
fs.mkdirSync(dir);
} catch (err) {
if (err.code === 'ENOENT') {
mkDirSync(path.dirname(dir))
mkDirSync(dir)
}
}
}
// make sure build directory exists
mkDirSync(OUTPUT_DIR);
// build initial
build(path.join(__dirname, '..', "js", ENTRY_FILE), `${OUTPUT_DIR}/${OUTPUT_FILE}`)
// watcher
if (MODE === 'dev' || MODE === 'development') {
const watcher = watch(['../../lib/**/*.*eex*', '../js/*.js*', '../css/*.*css*']);
watcher.on('change', () => {
build(path.join(__dirname, '..', "js", ENTRY_FILE), `${OUTPUT_DIR}/${OUTPUT_FILE}`);
})
}
Phoenix esbuild with Tailwind+Fontawesome
module.exports = {
mode: 'jit',
purge: [
'../../lib/**/*.ex',
'../../lib/**/*.leex',
'../../lib/**/*.heex',
'../../lib/**/*.lexs',
'../../lib/**/*.exs',
'../../lib/**/*.eex',
'../js/**/*.js'
],
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
};
@cvkmohan
Copy link

cvkmohan commented Sep 16, 2021

npm install ../deps/phoenix ../deps/phoenix_html ../deps/phoenix_live_view

is also necessary. Otherwise, those imports in the app.js will fail.

@ks2211
Copy link
Author

ks2211 commented Sep 16, 2021

npm install ../deps/phoenix ../deps/phoenix_html ../deps/phoenix_live_view

is also necessary. Otherwise, those imports in the app.js will fail.

Thanks @krushi123 I updated the gist

@simplyist
Copy link

simplyist commented Sep 29, 2021

does this copy static assets like images folder, I guess not

@ks2211
Copy link
Author

ks2211 commented Sep 29, 2021

does this copy static assets like images folder, I guess not

@simplyist
It does not but with the esbuild plugin system, you can easily write your own or use a community built package

https://www.npmjs.com/package/@es-pack/copy-plugin

@hammsvietro
Copy link

hammsvietro commented Nov 7, 2021

A great addition to your script would be:

- const watcher = watch(['../../lib/**/*.*eex*', '../js/*.js*', '../css/*.*css*']);
+ const watcher = watch(['../../lib/**/*.*eex*', '../js/*.js*', '../css/**/*.*css*']);

to cover multiple css folders

and

const ENTRY_FILE = 'app.js';
const OUTPUT_DIR = path.resolve(__dirname, '../../priv/static/assets');
const OUTPUT_FILE = 'app.js';
+const ENTRY_CSS_FILE = 'app.scss';
+const OUTPUT_CSS_FILE = 'app.scss';

...
build(path.join(__dirname, '..', "js", ENTRY_FILE), `${OUTPUT_DIR}/${OUTPUT_FILE}`);
+ build(path.join(__dirname, '..', "css", ENTRY_CSS_FILE), `${OUTPUT_DIR}/${OUTPUT_CSS_FILE}`)

to update the assets folder with the builded css

@Calamari
Copy link

Calamari commented Dec 11, 2021

There is a typo in there. This line:

'../lib/**/*.eex',

should be

'../../lib/**/*.eex',

like the others.

@Liberatys
Copy link

Liberatys commented Dec 18, 2021

@ks2211 in tailwind 3.0 the 'purge' option has been renamed to 'content'. tailwindlabs/tailwindcss#6019.

Thank you for this gist 🙏

@ks2211
Copy link
Author

ks2211 commented Dec 19, 2021

@Calamari thanks for pointing it out, fixed it!

@Liberatys got it, I haven't had a chance to try tailwind3.0 yet, once I have a chance to upgrade, I'll make the necessary update here, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment