Getting started with Rails 6.1, TailwindCSS JIT, Webpacker & PostCSS 8
Published: 2021-06-09 01:51:54

This is my first post here and we are going to look at getting TailwindCSS with the awesome JIT (just in time) compilation setup in Rails 6.1 app via Webpacker and using PostCSS 8.0



### Create a new Rails app

```bash
rails new tails_on_rails
```

**In your Gemfile update webpacker**

```bash
# Replace
gem 'webpacker', '~> 5.0'

# With
gem 'webpacker', '~> 5.4.0'
```

**Run bundler**

```bash
bundle
```

Remove the default webpacker

```bash
yarn remove @rails/webpacker
```

Add the required version of Webpacker and other dependancies

```bash
yarn add @rails/webpacker@5.4.0 @fullhuman/postcss-purgecss@^4.0.3 postcss@^8.2.10 postcss-loader@^4.0.3 sass@^1.32.7 autoprefixer@^10.2.6
```

Your `package.json` should look something like this.

```json
{
  "name": "tails-on-rails",
  "private": true,
  "dependencies": {
     "@fullhuman/postcss-purgecss": "^4.0.3",
     "@rails/actioncable": "^6.0.0",
     "@rails/activestorage": "^6.0.0",
     "@rails/ujs": "^6.0.0",
     "@rails/webpacker": "5.4.0",
     "postcss": "^8.2.10",
     "postcss-loader": "^4.0.3",
     "sass": "^1.32.7",
     "turbolinks": "^5.2.0",
     "webpack": "^4.46.0",
     "webpack-cli": "^3.3.12"
    },
  "version": "0.1.0",
  "devDependencies": {
    "webpack-dev-server": "^3.11.2"
 }
}
```

Install TailwindsCSS and the official plugins.

```bash
yarn add tailwindcss @tailwindcss/forms @tailwindcss/typography @tailwindcss/aspect-ratio @tailwindcss/typography @tailwindcss/line-clamp
```

Your `package.json` should look something like this.

```json
{
  "name": "tails-on-rails",
  "private": true,
  "dependencies": {
    "@fullhuman/postcss-purgecss": "^4.0.3",
    "@rails/actioncable": "^6.0.0",
    "@rails/activestorage": "^6.0.0",
    "@rails/ujs": "^6.0.0",
    "@rails/webpacker": "5.4.0",
    "@tailwindcss/aspect-ratio": "^0.2.1",
    "@tailwindcss/forms": "^0.3.3",
    "@tailwindcss/typography": "^0.4.1",
    "postcss": "^8.2.10",
    "postcss-loader": "^4.0.3",
    "sass": "^1.32.7",
    "tailwindcss": "^2.1.4",
    "turbolinks": "^5.2.0",
    "webpack": "^4.46.0",
    "webpack-cli": "^3.3.12"
  },
  "version": "0.1.0",
  "devDependencies": {
    "webpack-dev-server": "^3.11.2"
  }
}
```

Create the custom CSS file.

```bash
mkdir app/javascript/stylesheets && touch app/javascript/stylesheets/application.scss

```

In `app/javascript/stylesheets/application.scss` add the following.

```scss
@import "tailwindcss/base";

@import "tailwindcss/components";

@import "tailwindcss/utilities";
```

While we are on here we will add a custom component to validate that the `@apply` directive works.

Add the following to `app/javascript/stylesheets/application.scss` 

```scss
.btn {
  @apply px-4 py-2 bg-blue-600 text-white rounded; 
}
```

Your `application.scss` should look something like this.

```scss
@import "tailwindcss/base";

@import "tailwindcss/components";

.btn {
  @apply px-4 py-2 bg-blue-600 text-white rounded; 
}

@import "tailwindcss/utilities";
```

In `app/javascript/packs/application.js` add the following;

```jsx
import "stylesheets/application.scss"
```

Your `application.js` should look something like this

```jsx
import Rails from "@rails/ujs"
import Turbolinks from "turbolinks"
import * as ActiveStorage from "@rails/activestorage"
import "channels"
import "stylesheets/application.scss"

Rails.start()
Turbolinks.start()
ActiveStorage.start()
```

In the app root file `postcss.config.js` require TailwindCSS

```jsx
require('tailwindcss'),
```

Your `postcss.config.js` should look something like this.

```jsx
module.exports = {
  plugins: [
	require('tailwindcss'),
	require('postcss-import'),
	require('postcss-flexbugs-fixes'),
	require('postcss-preset-env')({
	  autoprefixer: {
		flexbox: 'no-2009'
	  },
	  stage: 3
	})
  ]
}
```

Add the following to your `app/views/layouts/application.html.erb`

```html
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">

<%= stylesheet_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
```

Your `application.html.erb` should look something like this.

```html
<!DOCTYPE html>
<html>
  <head>
	<title>TailsOnRails</title>
	<meta name="viewport" content="width=device-width,initial-scale=1">
	<%= csrf_meta_tags %>
	<%= csp_meta_tag %>

	<link rel="stylesheet" href="https://rsms.me/inter/inter.css">

	<%= stylesheet_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
	<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
	<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
	<%= yield %>
  </body>
</html>
```

Scaffold the full TailwindCSS `tailwind.config.js`

```html
npx tailwindcss init --full
```

The above will generate a large configurable TailwindCSS file.

Add the TailwindCSS plug-ins to `tailwind.config.js` (right at the bottom of the file in the `plugins: []` node.

```json
plugins: [
  require('@tailwindcss/typography'),
  require('@tailwindcss/forms'),
  require('@tailwindcss/line-clamp'),
  require('@tailwindcss/aspect-ratio'),
]
```

Add the `Inter` font to the `fontFamily` node in `tailwind.config.js` 

```json
fontFamily: {
  sans: [
	'Inter var',
	   ...
```

Enable JIT - just in time compilation at the top of the `tailwind.config.js` file

```json
module.exports = {
  mode: 'jit',
  purge: [],
```

Configure the purge option in the `tailwind.config.js` file

```json
purge: {
enabled: ["production", "staging"].includes(process.env.NODE_ENV),
content: [
  './app/views/**/*.html.erb',
  './app/helpers/**/*.rb',
  './app/javascript/**/*.js',
],
},

```

Your `tailwind.config.js` should look something like this.

```jsx
const colors = require('tailwindcss/colors')

module.exports = {
  mode: 'jit',
  purge: {
	enabled: ["production", "staging"].includes(process.env.NODE_ENV),
	content: [
	  './**/*.html.erb',
	  './app/helpers/**/*.rb',
	  './app/javascript/**/*.js',
	],
  },
  presets: [],
  darkMode: false, // or 'media' or 'class'
  theme: {
	screens: {
	  sm: '640px',
	  md: '768px',
	  lg: '1024px',
	  xl: '1280px',
	  '2xl': '1536px',
	},
	colors: {
	  transparent: 'transparent',
	  current: 'currentColor',
	 
	  // content removed ....
	 
	flexGrow: {
	  0: '0',
	  DEFAULT: '1',
	},
	flexShrink: {
	  0: '0',
	  DEFAULT: '1',
	},
	fontFamily: {
	  sans: [
		'Inter var',
		'ui-sans-serif',
		'system-ui',
		'-apple-system',
		'BlinkMacSystemFont',
		'"Segoe UI"',
		'Roboto',
		'"Helvetica Neue"',
		'Arial',
		'"Noto Sans"',
		'sans-serif',
		'"Apple Color Emoji"',
		'"Segoe UI Emoji"',
		'"Segoe UI Symbol"',
		'"Noto Color Emoji"',
	  ],
	  serif: ['ui-serif', 'Georgia', 'Cambria', '"Times New Roman"', 'Times', 'serif'],
	  mono: [
		'ui-monospace',
		'SFMono-Regular',
		'Menlo',

	  // content removed ....

	zIndex: ['responsive', 'focus-within', 'focus'],
  },
  plugins: [
	require('@tailwindcss/typography'),
	require('@tailwindcss/forms'),
	require('@tailwindcss/line-clamp'),
	require('@tailwindcss/aspect-ratio'),],
}
```

In the `babel.config.js` file set the following

```jsx
...
'@babel/plugin-transform-runtime',
{
  helpers: false,
  regenerator: true,
  corejs: false
}
```

And add the following to `babel.config.js` 

```jsx
['@babel/plugin-proposal-private-methods', { loose: true }]
```

Your `babel.config.js` should look something like this

```jsx
module.exports = function (api) {
  var validEnv = ['development', 'test', 'production']
  var currentEnv = api.env()
  var isDevelopmentEnv = api.env('development')
  var isProductionEnv = api.env('production')
  var isTestEnv = api.env('test')

  if (!validEnv.includes(currentEnv)) {
	throw new Error(
	  'Please specify a valid `NODE_ENV` or ' +
		'`BABEL_ENV` environment variables. Valid values are "development", ' +
		'"test", and "production". Instead, received: ' +
		JSON.stringify(currentEnv) +
		'.'
	)
  }

  return {
	presets: [
	  isTestEnv && [
		'@babel/preset-env',
		{
		  targets: {
			node: 'current'
		  }
		}
	  ],
	  (isProductionEnv || isDevelopmentEnv) && [
		'@babel/preset-env',
		{
		  forceAllTransforms: true,
		  useBuiltIns: 'entry',
		  corejs: 3,
		  modules: false,
		  exclude: ['transform-typeof-symbol']
		}
	  ]
	].filter(Boolean),
	plugins: [
	  'babel-plugin-macros',
	  '@babel/plugin-syntax-dynamic-import',
	  isTestEnv && 'babel-plugin-dynamic-import-node',
	  '@babel/plugin-transform-destructuring',
	  [
		'@babel/plugin-proposal-class-properties',
		{
		  loose: true
		}
	  ],
	  [
		'@babel/plugin-proposal-object-rest-spread',
		{
		  useBuiltIns: true
		}
	  ],
	  [
		'@babel/plugin-transform-runtime',
		{
		  helpers: false,
		  regenerator: true,
		  corejs: false
		}
	  ],
	  [
		'@babel/plugin-transform-regenerator',
		{
		  async: false
		}
	  ],
	  ['@babel/plugin-proposal-private-methods', { loose: true }]
	].filter(Boolean)
  }
}
```

For Webpacker to work with this version of Postcss we need to tell it to use the loader.
In `config/webpack/environment.js` add the following. Just insert it between the two existing lines.

```jsx
// Get the actual sass-loader config
const sassLoader = environment.loaders.get('sass')
const sassLoaderConfig = sassLoader.use.find(function (element) {
return element.loader == 'sass-loader'
})

// Use Dart-implementation of Sass (default is node-sass)
const options = sassLoaderConfig.options
options.implementation = require('sass')

function hotfixPostcssLoaderConfig (subloader) {
  const subloaderName = subloader.loader
  if (subloaderName === 'postcss-loader') {
	subloader.options.postcssOptions = subloader.options.config
	delete subloader.options.config
  }
}

environment.loaders.keys().forEach(loaderName => {
  const loader = environment.loaders.get(loaderName)
  loader.use.forEach(hotfixPostcssLoaderConfig)
})
```

Your `environment.js` should look something like this.

```jsx
const {environment} = require('@rails/webpacker')

// Get the actual sass-loader config
const sassLoader = environment.loaders.get('sass')
const sassLoaderConfig = sassLoader.use.find(function (element) {
return element.loader == 'sass-loader'
})

// Use Dart-implementation of Sass (default is node-sass)
const options = sassLoaderConfig.options
options.implementation = require('sass')

function hotfixPostcssLoaderConfig(subloader) {
  const subloaderName = subloader.loader
  if (subloaderName === 'postcss-loader') {
	subloader.options.postcssOptions = subloader.options.config
	delete subloader.options.config
  }
}

environment.loaders.keys().forEach(loaderName => {
const loader = environment.loaders.get(loaderName)
loader.use.forEach(hotfixPostcssLoaderConfig)
})

module.exports = environment
```

Let's test this setup

```bash
rails g controller Home index 
```

Add the following to `app/views/home/index.html.erb`

```bash
<div class="font-sans bg-white h-screen flex flex-col w-full">
  <div class="h-screen bg-gradient-to-r from-green-400 to-blue-500">
    <div class="px-4 py-48">
     <div class="relative w-full text-center">
       <h1 class="animate-pulse font-bold text-gray-200 text-2xl mb-6">
        Your TailwindCSS setup is working if this pulses...
       </h1>
    </div>
   </div>
  </div>
</div>
```

Add the root path to point at the above `/home/index` in `config/routes.rb`

```bash
root 'home#index'
```

In one terminal run `bin/webpack-dev-server`
And in another run `rails s` 

> ⚠️ Do not try running `webpack-dev-server` without the `bin/` prefix as it's likely it will fail to compile.

You should see this

![](https://j.mp/2T9MGOP)

To test the `@apply` directive replace the following from `app/views/home/index.html.erb` with `class="pulsing-text"`


```bash
class="animate-pulse font-bold text-gray-200 text-2xl mb-6"
```

and in `app/javascript/stylesheets/application.scss` add the following

```css
.pulsing-text {
@apply animate-pulse font-bold text-gray-200 text-2xl mb-6;
}
```

Your `applications.scss` should look like something like this.

```css
@import "tailwindcss/base";

@import "tailwindcss/components";

.pulsing-text {
@apply animate-pulse font-bold text-gray-200 text-2xl mb-6;
}

@import "tailwindcss/utilities";
```

When you refresh you home page in the browser you should still see this.

![](https://j.mp/2T9MGOP)

[GitHub Repo](https://github.com/davidteren/tails_on_rails)