Setup TailwindCSS, postcss and esbuild on Rails 7
Tags: rails, tailwindcss, esbuild, postcss
Published: 2022-10-12 17:54:24

Every time I run a new rails application, I’d need to configure essential tools for my front-end stack such as TailwindCSS and esbuild. I’ve adopted my configuration from my friend [Pete Hawkins](https://dev.to/phawk), 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](https://esbuild.github.io/) form [JavaScript Bundling](https://github.com/rails/jsbundling-rails) - _(this post)_
- [TailwindCSS](https://tailwindcss.com/) from CSS [Bundling for Rails](https://github.com/rails/cssbundling-rails) - _(this post)_
- [Hotwire stack](https://hotwired.dev/) which includes Turbo and Stimulus. In detail, for faster page loads ([Turbo Drive](https://turbo.hotwired.dev/reference/drive)), partial page updates with ([Turbo Frames](https://turbo.hotwired.dev/reference/frames)), reactive page updates ([Turbo Streams](https://turbo.hotwired.dev/reference/streams)), and frontend interactivity ([Stimulus](https://stimulus.hotwired.dev/handbook/introduction)) (I will dig into this in another post).
- 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 [Ruby `3.0.4`](https://www.ruby-lang.org/en/news/2022/04/12/ruby-3-0-4-released/).

### importmaps-rails, *not this time*

While I prefer using [importmaps-rails ](https://github.com/rails/importmap-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](https://github.com/rails/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` with rails form for AJAX communications, Mrujs keeps it in for you, even with Rails 7.
- [CableReady](https://cableready.stimulusreflex.com/) and [StimulusReflex](https://docs.stimulusreflex.com/) are installed for even more, server-powered frontend interactivity and reactive page updates.
- [Mrujs](https://mrujs.com/) 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 Rails…

Now we know a few essential tools, let’s generate a new rails 7 app which comes with Stimulus and Turbo by default. 

`rails new hotwire_stack -d postgresql --css=tailwind --javascript=esbuild`

==SideNote:==  *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/](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](https://github.com/ddollar/foreman) which manages Procfile-based applications. Rails automatically install foreman gem but it doesn’t bundle it because [forman recommends NOT to do](https://github.com/ddollar/foreman/wiki/Don%27t-Bundle-Foreman) 🚫

Forman starts Procfile.dev by running `foreman start -f Procfile.dev`, 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.

```dev
 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. 

```json
  "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](https://postcss.org/).

First, we need to install `postcss` and other [plugins](https://github.com/postcss/postcss/blob/main/docs/plugins.md) via yarn. From terminal:

```shell
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:

```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](https://tailwindcss.com/docs/using-with-preprocessors#build-time-imports). Besides, we can create a components folder and add any `css` files to `app/assets/stylesheets/components/*.css` and `@import` it into main `application.tailwind.css` file:

```css
@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](https://yarnpkg.com/package/@tailwindcss/typography) A Tailwind CSS plugin for automatically styling plain HTML content with beautiful typographic defaults.
- [@tailwindcss/forms](https://yarnpkg.com/package/@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.

```js
module.exports = {
  content: [
    "./app/**/*.html.erb",
    "./app/helpers/**/*.rb",
    "./app/javascript/**/*.js",
  ],
  plugins: [
    require('@tailwindcss/forms')
    require('@tailwindcss/typography')
  ],
}
```
You can [customize](https://tailwindcss.com/docs/configuration) `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](https://esbuild.github.io/). 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](https://esbuild.github.io/api/).

Here is a summary of what our customization does:
- Minify `js` bundle in production similar to tialwindcss in `package.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](https://github.com/paulmillr/chokidar) to enable watching and automatically refreshing our files.

```shell
 yarn add chokidar -D
 touch esbuild.config.js
```

And then update `esbuild.config.js`:

```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,
  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 via `esbuild` with both `incremental` and `banner` configuration options set. Incremental is useful at calling `esbuild`’s build API repeatedly, such as implementing a file watcher service. While `banner` is used to insert comments at the beginning of generated JavaScript and CSS files.
- `chokidar` watch our directories for any change, each time it occurs, `chokidar` opens a new `EventSource` connection to the web server. This kicks off `reload()` function to rebuild any JS file that had changed.
- We end the file with `if` and `else` block, it checks whether above `rebuild()` function passed to `esbuild` or run the default `esbuild.build()` function in production.

## Revisit our package.json

Earlier we mentioned, when we run `bin/div` it runs `forman` which manages `Procfile.dev` file. The latter watches any change for selected files via the script property of `package.json` file.

Now and after we created our `esbuild.config` configuration, we should update `package.json` file as below:

```json
"scripts": {
  "build": "node esbuild.config.js",
  "build:css": "tailwindcss --postcss -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/application.css"
}
```

Checkout both `package.json` before…

```json
  "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 update.

  ```json
	"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 crack on codeing, we need to update `Prockfile.dev` file and pass the `--rebuild` argument.

```dev
 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` property 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 for `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` 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, I will cover [Hotwire stack](https://hotwired.dev/), [CableReady](https://cableready.stimulusreflex.com/), [StimulusReflex](https://docs.stimulusreflex.com/), [Mrujs](https://mrujs.com/), and [UUIDs](https://www.postgresql.org/docs/current/datatype-uuid.html).

Happy Coding 😀 💻 
As always, thanks for reading! 


Originally published on http://www.ahmednadar.com/posts/setup-tailwindcss-and-esbuild-on-rails-7