Setup TailwindCSS and esbuild on Rails 7
Every time I run a new rails application, I’d need to configure essential tools for my front-end stacks such as TailwindCSS and esbuild. I’ve adopted my configuration from my friend Pete Hawkins’s post on The best way to run Tailwind CSS on Rails. And it has been serving me well 👍🏼. I would say. Recently I’ve stepped up my game 💪🏼 at setting up my Rails apps, trying to be more efficient and have a unified configuration.
As we know, building a modern Rails application starts with installing and setting up essential and reliable tools that help us focus on building our features. We will use well-known and documented tools.
Here is our Rails 7 application's initial configuration:
esbuild form JavaScript Bundling - (this post)
TailwindCSS from CSS Bundling for Rails - (this post)
Hotwire stack which includes Turbo and Stimulus. In detail, for faster page loads (Turbo Drive), partial page updates with (Turbo Frames), reactive page updates (Turbo Streams), and frontend interactivity (Stimulus).
UUIDs are used for primary keys because I like using UUIDs. (I will dig into this in another post)
I’m using Rails
7.0.4
https://github.com/rails/rails/releases/tag/v7.0.4 and Ruby3.0.4
.
import-maps-rails
While I prefer using importmaps-rails to jsbundling-rails, I found many developers and companies want to have the ability to use JS libraries as they used with Webpack. Old habit I think.
What does it mean to use importmaps:
No more
node_modules
folder while I have the option to use external JS libraries directly from the browser. ✅Preconfigured TailwindCSS from TailwindCSS-rails ready to use out of the box, pass
--css tailwind
as an option.✅
Extra and optional
While the above tools are fantastic to start up any Rails application, the following ones are the cherries on top 🍒 I’ve been using them for a while and I’m learning.
If you miss using
remote: true
rails form for AJAX communications, Mrujs keeps it in for you, even with Rails 7.CableReady and StimulusReflex are installed for even more, server-powered frontend interactivity and reactive page updates.
Mrujs to replace a few features from Rails/UJS and for its powerful Cable Car plugin.
I will cover those in detail in another post.
Ready, Set, Run Rail…
Now we know a few essential tools, let’s generate new rails 7 app which comes with Stimulus and Turbo by default.
rails new hotwire_stack -d postgresql --css=tailwind --javascript=esbuild
Note: If you like to skip any other option such as rails test, jbuilder or mailbox, you would pass in the following --skip-system-test --skip-jbuilder --skip-mailbox
.
After creating the Rails app, go ahead in your terminal or console and cd
to our app, and type bin/dev
to run the application. It might ask you to create its database though. Once all run properly, open your browser to http://localhost:3000/. Congratulations, you have a new rails app ready for creativity 👏🏼
But before we go any further, let’s step back in time ⏪ ⏳ and review some important files.
We ran our app via bin/dev
. You can find the div
file inside ./bin/dev
folder. It is a ruby wrapper over the process manager forman which manages Procfile-based applications. Rails automatically install foreman gem but it doesn’t bundle it because forman recommends NOT to do so 🚫
Forman starts Procfile.dev by running, which isn’t obvious! Which is in our app root and it lists the processes we need to run with our rails server, tailcwindcss, and esbuild.
web: bin/rails server -p 3000
js: yarn build --watch
css: yarn build:css --watch
Procfile.dev file watches any change that occurs to our html
, css
and js
files and recompile the css
and js
files within app/assets/builds
folder. Yarn 🧶 runs build
these scripts communicate out to package.json
file.
"scripts": {
"build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=assets",
"build:css": "tailwindcss -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/application.css --minify"
}
Easy and straightforward, right? Now that we have an “overview” of how our app initial start, let’s dig deeper.
Configure Tailwind
As we write more features we would need to organize our css
files, write additional styles or wrap any tailwind components up using tailwinds @apply
helper. Currently, we can’t import other css
files into the main application.tailwind.css
file because our node-powered TailwindCSS is provided by cssbundling-rails, which by default doesn’t allow it. Luckily we can fix it, thanks to postcss.
First, we need to install postcss
other plugins via yarn. From terminal:
yarn add postcss postcss-flexbugs-fixes postcss-import postcss-nested
touch postcss.config.js
Plugins definitions:
postcss
a tool for transforming styles with JS plugins.postcss-import
inlines the stylesheets referred to by @import rules.postcss-nested
unwraps nested rules.postcss-flexbugs-fixes
fixes a few known flexbox bugs.
Now we can update postcss.config.js:
module.exports = {
plugins: [
require('postcss-import'),
require('tailwindcss'),
require('autoprefixer'),
require("postcss-nested"),
require("postcss-flexbugs-fixes"),
]
}
Likewise, update application.tailwind.css
to replace the @tailwind
directives with imports, as described in the Tailwind docs. Besides, we can create a components folder and add any css
files to app/assets/stylesheets/components/*.css
and @import
it into the main application.tailwind.css
file:
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
/* app components */
@import "./components/card.css";
As we used plugins for postcss
, we can use TailwindCSS ones likewise.
yarn add @tailwindcss/forms @tailwindcss/typography
Plugins definitions:
@tailwindcss/typography A Tailwind CSS plugin for automatically styling plain HTML content with beautiful typographic defaults.
@tailwindcss/forms A plugin that provides a basic reset for form styles that makes form elements easy to override with utilities.
After adding previous updates, add these plugins to the Tailwind config file tailwind.config.js
which is in the app directory.
module.exports = {
content: [
"./app/**/*.html.erb",
"./app/helpers/**/*.rb",
"./app/javascript/**/*.js",
],
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography')
],
}
You can customize tailwind.config.js
file to your liking by adding themes, plugins, presets, and content again.
Phew, that was a lot to do for postcss and tailwindcsss configuration. You deserve a pat on your shoulder, well done 👏🏼
Now, it’s time to write a costume configuration for esbuild. It will replace the default one which ships with jsbundling-rails gem. But, do we need to do so for rsbuild?! you might ask. The answer ‘sometimes’ is yes.
While configurations are optional, it is helpful and efficient. For example, automatic page refresh (watch and rebuild) upon a file change, it makes life easier and fun coding😎 during development. Besides, it is good to get familiar with esbuild API.
Here is a summary of what our customization does:
Minify
js
bundle in production similar to tialwindcss inpackage.json
file.Enable source maps in both development and production.
Watch and rebuild the page upon any changes to assets and views.
API definitions:
Minify. When enabled, the generated code will be minified instead of pretty-printed. Minified code downloads faster but is harder to debug. You minify code in production but not in development.
Source maps, make it easier to debug your code. They encode the information necessary to translate from a line/column offset in a generated output file back to a line/column offset in the corresponding original input file.
Watch. Enabling watch mode tells esbuild to listen for changes on the file system and to rebuild file changes that could invalidate the build.
First, we need to install chokidar to enable watching and automatically refreshing our files.
yarn add chokidar -D
touch esbuild.config.js
And then update esbuild.config.js
:
#!/usr/bin/env node
const esbuild = require('esbuild')
const path = require('path')
// Add more entrypoints, if needed
const entryPoints = [
"application.js",
]
const watchDirectories = [
"./app/javascript/**/*.js",
"./app/views/**/*.html.erb",
"./app/assets/stylesheets/*.css",
"./app/assets/stylesheets/*.scss"
]
const config = {
absWorkingDir: path.join(process.cwd(), "app/javascript"),
bundle: true,
entryPoints: entryPoints,
outdir: path.join(process.cwd(), "app/assets/builds"),
sourcemap: true
}
async function rebuild() {
const chokidar = require('chokidar')
const http = require('http')
const clients = []
http.createServer((req, res) => {
return clients.push(
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Access-Control-Allow-Origin": "*",
Connection: "keep-alive",
}),
);
}).listen(8082);
let result = await esbuild.build({
...config,
incremental: true, // use "esbuild": "~0.16.17" to avoide unsloved error with esbuild 7
banner: {
js: ' (() => new EventSource("http://localhost:8082").onmessage = () => location.reload())();',
},
})
chokidar.watch(watchDirectories).on('all', (event, path) => {
if (path.includes("javascript")) {
result.rebuild()
}
clients.forEach((res) => res.write('data: update\n\n'))
clients.length = 0
});
}
if (process.argv.includes("--rebuild")) {
rebuild()
} else {
esbuild.build({
...config,
minify: process.env.RAILS_ENV == "production",
}).catch(() => process.exit(1));
}
That’s a lot of ‘gibberish’ code ⌨️. Let’s decode it.
Set our entryPoints for
applications.js
We set
watchDirectories
for watching files we care about within assets, javascript, and views folders.Create a server and listen to it at
http://localhost:8082
Build our JS with
esbuild
bothincremental
andbanner
configuration options set. Incremental is useful at callingesbuild
’s build API repeatedly, such as implementing a file watcher service. Whilebanner
it is used to insert comments at the beginning of generated JavaScript and CSS files.chokidar
watch our directories for any change, each time it occurschokidar
opens a newEventSource
connection to the web server. This kicks offreload()
the function to rebuild any JS file that had changed.We end the file with
if
andelse
block, it checks whether the aboverebuild()
function passed toesbuild
or runs the defaultesbuild.build
()
function in production.
Revisit our package.json
Earlier we mentioned when we run bin/div
it runs forman
it manages Procfile.dev
file. The latter watches any change for selected files via the script property of package.json
the file.
Now and after we created our esbuild.config
configuration, we should update package.json
file as below:
{
"esbuild": "~0.16.17",
},
"scripts": {
"build": "node esbuild.config.js",
"build:css": "tailwindcss --postcss -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/application.css"
}
Note: Using this "esbuild": "~0.16.17"
version prevents an error caused by calling incremental. 'ERROR] Invalid option in build() call: "incremental". ' with the latest version. https://github.com/floydspace/serverless-esbuild/issues/427
Check out both package.json
before…
"scripts": {
"build": "esbuild app/javascript/*.*
--bundle
--sourcemap
--outdir=app/assets/builds
--public-path=assets",
"build:css": "tailwindcss
-i ./app/assets/stylesheets/application.tailwind.css
-o ./app/assets/builds/application.css
--minify"
}
…And after the update.
"scripts": {
"build": "node esbuild.config.js",
"build:css": "tailwindcss
--postcss
-i ./app/assets/stylesheets/application.tailwind.css
-o ./app/assets/builds/application.css"
}
One last change before we jump on coding, we need to update Prockfile.dev
file and pass the --rebuild
argument.
web: bin/rails server -p 3000
js: yarn build --rebuild
css: yarn build:css --watch
That’s all folks. Easy, right?
Before I leave to get coding, let’s do a quick recap.
Now, when Rails runs bin/dev
it calls out to Prockfile.dev
which runs yarn
build command that communicates with package.json
file. The script
properly execute build
command for both js
and css
options. And in order, both options call out to esbuild.config.js
and postcss.config.js
+ tailwind.config.js
respectively.
The recent updates package.json
save us time and manual labor to run those scripts.
At any time you are ready to start up the application, use bin/dev
the command to start the rails server which will build JavaScript and CSS all in one go for you.
That is it. We have accomplished a lot in a short time ‘relatively’ speaking. Hope this helps.
Next post, we will cover Hotwire stack, CableReady, StimulusReflex, Mrujs, and UUIDs.
Happy Coding 😀 💻
Subscribe to my newsletter
Read articles from Ahmed Nadar directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Ahmed Nadar
Ahmed Nadar
Developer and Product Design. RapidRails UI components creator Run RapidRails Agency. https://rapidrails.cc