Purging Tailwind CSS With Kotlin/JS

Earlier this month, I shared how to get Tailwind CSS working with Kotlin/JS. That post covered the basics of getting Tailwind CSS running. It didn’t cover an important problem: optimizing your CSS for production.

Tailwind CSS provides instructions for production optimization. Essentially, in our tailwind.config.js file, we need to specify purge options. This allows Tailwind to determine and exclude any styles that it doesn’t need. Tailwind’s sample looks like this:

// tailwind.config.js
module.exports = {
  purge: [
    './src/**/*.html',
    './src/**/*.vue',
    './src/**/*.jsx',
  ],
  theme: {},
  variants: {},
  plugins: [],
}

Out of the box, this doesn’t work with the Kotlin/JS configuration we’ve set up previously. Let’s dive into what’s going wrong.

TL;DR: See the final result. Be aware that this works, but there’s still room for improvement.

Missing Tailwind Configuration

It was pretty easy to verify that our JS bundle remained the same size, despite my purge options. Eventually, I realized that my tailwind.config.js wasn’t even being read!

After a lot of Googling, I ran across this post. It has something interesting in its postcss.config.js:

// postcss.config.js
module.exports = {
  plugins: [
    require("tailwindcss")("./tailwind.config.js"),
    require("autoprefixer"),
  ],
}

This differs from our config: it specifies a Tailwind configuration file! Attempting to do this in our own config results in Gradle build errors—Gradle complains that tailwind.config.js can’t be found.

Through trial and error, we can figure out that ./tailwind.config.js is being searched for in the same directory as webpack.config.js (that makes sense). That directory is build/js/packages/${project.name}/.

Copy Tailwind Config

We have two options: (a) specify a path back to our Tailwind config or (b) copy our config into the directory where webpack is being run. I opted for (b).

Because I’m new to using Gradle, I’m not sure what the idiomatic way to copy this file is. (If you know, please tell me!)

I ended up doing this by adding a copy block:

// build.gradle.kts
kotlin {
    js(IR) {
        // tailwind.config.js doesn't exist relative to the directory where webpack is being run.
        // This results in the PostCSS plugin not discovering the tailwind configuration.
        // We deal with this by copying it into that folder.
        copy {
            from("./tailwind.config.js")
            into("$buildDir/js/packages/${project.name}")
        }

        // ...
    }

I suspect the idiomatic way to do this is to add this copying as part of whatever task configures webpack.

Purge Options

Once we’re correctly locating our config, we need to specify the correct purge options. These should be relative to that same build directory.

Inside of build/js/packages/${project.name}/, there’s a kotlin folder. I believe this is the output of the JS compiler, before it gets run through webpack. So, we can specify all .js files in that folder.

// tailwind.config.js
module.exports = {
  purge: {
    enabled: true,
    content: [
        './kotlin/*.js',
    ]
  },
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
}

Production Mode

From the Tailwind docs:

Now whenever you compile your CSS with NODE_ENV set to production, Tailwind will automatically purge unused styles from your CSS.

We’ve overridden this above—you’ll notice that we set purge to always be enabled. We have to do this because webpack doesn’t bundle our CSS using NODE_ENV=production.

The downside of this is that we always optimize our CSS, even for development builds. Ideally we don’t purge for development builds in order to shorten build times.

There do seem to be some workarounds for this; I just haven’t explored them yet. You can find them by searching how to set environment variables/modes for Kotlin/JS, Gradle, and webpack. There are also some approaches mentioned in these issues describing the NODE_ENV issue.

More Details

In case you need more context, here’s a bit more about how everything fits together.

The Kotlin Gradle plugin defines development and production webpack tasks. In my project, these are named jsBrowserDevelopmentWebpack and jsBrowserProductionWebpack.

These specify webpack’s mode, which set process.env.NODE_ENV appropriately.

Here’s the catch: that NODE_ENV is only set in the resulting bundle. It is not set when webpack is building the bundle—which is when PostCSS is doing our Tailwind optimization. As a result, Tailwind thinks it’s being run in development mode and doesn’t do any optimization.

Final Result

Add this copy block to your build.gradle.kts:

// build.gradle.kts
kotlin {
    js(IR) {
        // tailwind.config.js doesn't exist relative to the directory where webpack is being run.
        // This results in the PostCSS plugin not discovering the tailwind configuration.
        // We deal with this by copying it into that folder.
        copy {
            from("./tailwind.config.js")
            into("$buildDir/js/packages/${project.name}")
        }

        // ...
    }

Additionally, configure your tailwind.config.js:

// tailwind.config.js
module.exports = {
  purge: {
    enabled: true,
    content: [
        './kotlin/*.js',
    ]
  },
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
}