Skip to content

Instantly share code, notes, and snippets.

@ks2211
Last active July 30, 2024 01:50
Show Gist options
  • Save ks2211/75af6cddc051f5e261a29fc25eed5789 to your computer and use it in GitHub Desktop.
Save ks2211/75af6cddc051f5e261a29fc25eed5789 to your computer and use it in GitHub Desktop.
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

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

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

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

There is a typo in there. This line:

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

should be

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

like the others.

Copy link

@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!

@dennym
Copy link

dennym commented Feb 7, 2022

It shouldn't make much of a difference but for the sake of cleanness and following the official documentation:
change

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

to

npm install --save-dev fs path chokidar esbuild esbuild-sass-plugin postcss autoprefixer tailwindcss @fortawesome/fontawesome-free
npm install --save ../deps/phoenix ../deps/phoenix_html ../deps/phoenix_live_view

@lessless
Copy link

lessless commented May 3, 2022

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

@hammsvietro I didn't notice any difference after adding this change - the css/app.scss is still built because of it's imported in the app.js.

Can you please eplain why is this needed?

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