emberjs-tailwind-purgecss icon indicating copy to clipboard operation
emberjs-tailwind-purgecss copied to clipboard

A better way to integrate tailwindcss and purgecss with any ember application

Open nightire opened this issue 5 years ago • 5 comments

tl;dr, for those who don't care why it is better, just follow these steps:

  1. install all required dependencies:
$ yarn add tailwindcss @fullhuman/postcss-purgecss broccoli-source broccoli-postcss-single --dev
  1. prepare an entry css file and the tailwind config file:
# you can choose any path you like
$ mkdir -p app/tailwind
$ touch app/tailwind/index.css app/tailwind/config.js

the content of app/tailwind/index.css:

/* purgecss start ignore */
@tailwind base;
@tailwind components;
/* purgecss end ignore */
@tailwind utilities;

the content of app/tailwind/config.js is what exactly tailwindcss init command gives you, or you can change it whatever you like as long as the tailwindcss accepts it.

  1. update your ember-cli-build.js:
const EmberApp = require("ember-cli/lib/broccoli/ember-app");
const { join } = require("path");
const { WatchedDir } = require("broccoli-source");
const compilePostCSS = require("broccoli-postcss-single");

module.exports = function(defaults) {
  const app = new EmberApp(defaults, {
    // other settings
  });

  const tailwindBase = join(__dirname, "app", "tailwindcss");
  const plugins = [require("tailwindcss")(join(tailwindBase, "tailwind.js"))];

  if (EmberApp.env() === "production") {
    plugins.push({
      module: require("@fullhuman/postcss-purgecss"),
      options: {
        content: [
          join(__dirname, "app", "index.html"),
          join(__dirname, "app", "**", "*.js"),
          join(__dirname, "app", "**", "*.hbs")
        ],
        defaultExtractor: content => content.match(/[A-Za-z0-9-_:/]+/g) || [],
        whitelistPatterns: [/^_/]
      }
    });
  }

  const tailwindNode = compilePostCSS(
    new WatchedDir(tailwindBase),
    "index.css",
    "assets/tailwind.css",
    { browsers, cacheInclude: [/.*\.(css|js)$/], plugins }
  );

  return app.toTree([tailwindNode]);
};
  1. boot up your app with ember serve, after a short period of time you will see the output file in dist/assets/tailwind.css. Now, open your app/index.html, add one line above where your app loads the vendor.css:
<link integrity="" rel="stylesheet" href="{{rootURL}}assets/tailwind.css" />
  1. Voila!

So why it is better?

The core difference is to separate the building process of tailwindcss completely from the main css building process, this will bring us some benefits:

  1. The initial build is relatively faster (that depends on how complex your prior building process is)

  2. Each rebuilding will extremely faster

Integrate tailwindcss in your main css building process will dramatically slow down your building efficiency, I have tested it in a newly created ember octane app, add tailwindcss process will increase at least 1~2 seconds for each rebuilding that caused by css changes, actually, we don't even need to rebuild tailwindcss again and again, it doesn't make any sense.

  1. Only rebuild the tailwindcss on config changes

By using a dedicated broccoli node to watch the changes of app/tailwind/config.js, the content of assets/tailwind.css only updates as needed.

  1. PostCSS is optional for the main css building process

PostCSS is good, but sometimes we only want to introduce tailwindcss with customization ability and without touching the existed css building process.

As step 4. shows, you can still do whatever you like to the styles in app/styles directory, PostCSS is just a (good) option.

  1. It emits an individual css file

I like to load the assets/tailwind.css file individually, and I know some ppl like it too. Tailwindcss is huge by default, even if purgecss can help us out, it still big especially you customize it a lot, so loading it separately can be good because it gives you some space to do more optimizations.

Hope this can help you enjoy working with tailwindcss and ember together.

nightire avatar Mar 28 '20 17:03 nightire

Hi @nightire thanks very much for the detailed writeup.

It sounds very interesting and agree that the approach makes a lot of sense.

I’m going to try it on a project first and see how it works and then update the docs to reflect this change once I’m comfortable with it.

Thanks again.

chrism avatar Apr 23 '20 09:04 chrism

I found this comment, and really like the approach. Slow builds for tweaking css can drag things down and make everything feel kind of slow.

For anyone else coming to this thread, the default extractor isn't the default from tailwind the details of which are listed here: https://tailwindcss.com/docs/controlling-file-size#setting-up-purge-css-manually

      const broadMatches = content.match(/[^<>"'`\s]*[^<>"'`\s:]/g) || []

      // Capture classes within other delimiters like .block(class="w-1/2") in Pug
      const innerMatches = content.match(/[^<>"'`\s.()]*[^<>"'`\s.():]/g) || []

      return broadMatches.concat(innerMatches)

averydev avatar Aug 29 '20 21:08 averydev

Thanks for the writeup. I'm trying to update tailwind in my Ember project by phasing out ember-tailwind-cli.

The browsers variable is not defined before it is used in

const tailwindNode = compilePostCSS(
    new WatchedDir(tailwindBase),
    "index.css",
    "assets/tailwind.css",
    { browsers, cacheInclude: [/.*\.(css|js)$/], plugins }
  );

Is this an error or am I missing something?

NateDawg90 avatar Dec 10 '20 23:12 NateDawg90

Thanks for the writeup. I'm trying to update tailwind in my Ember project by phasing out ember-tailwind-cli.

The browsers variable is not defined before it is used in

const tailwindNode = compilePostCSS(
    new WatchedDir(tailwindBase),
    "index.css",
    "assets/tailwind.css",
    { browsers, cacheInclude: [/.*\.(css|js)$/], plugins }
  );

Is this an error or am I missing something?

const { browsers } = require('./config/targets');

nightire avatar Dec 10 '20 23:12 nightire

The configurations above didn't work with Tailwind's just in time (JIT) compilation, meaning I had to restart the Ember server every time I referenced a new Tailwind css class.

The approach suggested in this comment worked well for me. (I am not using embroider, and there are other comments in that thread dealing with embroider.)

To wrap it up into one place:

packages required:

npx ember install ember-cli-postcss
yarn add tailwindcss cssnano autoprefixer

ember-cli-build.js:

const EmberApp = require('ember-cli/lib/broccoli/ember-app');
const autoprefixer = require('autoprefixer');
const cssnano = require('cssnano');
const tailwindcss = require('tailwindcss');
const isProduction = EmberApp.env() === 'production';

module.exports = function (defaults) {
  let app = new EmberApp(defaults, {
    // your normal configuration here

    postcssOptions: {
      compile: {
        cacheInclude: [/.*\.(css|scss|hbs)$/, /.tailwind\.config\.js$/],
        map: false,
        plugins: [
          tailwindcss('./tailwind.config.js'),
          autoprefixer,
          ...(isProduction ? [cssnano] : []),
        ],
      },
    },
  });

tailwind.config.js:

/* global module*/
module.exports = {
  // These are the folders that Tailwind will examine to determine
  // which of its classes should be included in the copy of tailwind.css
  // that gets distributed with our app
  content: ['app/**/*.{html,js,hbs}'],
  theme: {
    extend: {},
  },
  plugins: [],
};

app.css

@tailwind base;
@tailwind components;
@tailwind utilities;

Location of files in folder structure:

./ember-cli-build.js
./tailwind.config.js
./app/css/app/css

abirtley avatar Feb 24 '22 08:02 abirtley