DEV Community

Cover image for How to detect classes contained in ruby gems in Tailwind 4
Matouš Borák for NejŘemeslníci

Posted on

1

How to detect classes contained in ruby gems in Tailwind 4

Our main web app uses Tailwind CSS and we are happy to have recently migrated it to Tailwind version 4. Among the nice features that Tailwind 4 brings to the table is an automatic detection of CSS classes in the application source files. This allowed us to completely remove the content section from the former Tailwind v3 configuration and just forget about it.

For a moment only, that is.

As it quickly turned out, we forgot about the fact that not all CSS classes are used within our application itself. Our app includes an internal gem that implements several Flowbite components and some of the Tailwind classes it contains are unique to that gem. As the gem sources reside outside the app root directory, the Tailwind program cannot see them and doesn’t scan them for potential classes.

How we did it in Tailwind 3

In Tailwind v3 we actually had the following code in the tailwind.config.js configuration file, handling this issue:

// tailwind.config.js

const execSync = require('child_process').execSync;

function getGemPath(gem) {
  return execSync(`bundle show ${gem}`).toString().trim()
}

module.exports = {
  content: [  
    ...,
    `${getGemPath('flowbite')}/app/components/**/*.{slim,rb}`
  ]
}
Enter fullscreen mode Exit fullscreen mode

The config defined a function which called the bundle show command to get the absolute path to the gem source code folder. It then combined it with glob patterns to the actual source code files. This solution was probably inspired from here and just worked for us for several years.

But how to do it in Tailwind 4?

In Tailwind 4, things have changed dramatically. Using a JS configuration is still possible but not really recommended and we did not want to keep that tech debt in our project.

The official way now is to configure Tailwind via a CSS file. In a CSS file though, there is no way to run code dynamically, everything has to be pre-defined and static. So how can we reference gem source files when each environment that our app runs on stores them under a different path?

We came up with a solution that we are happy with using two small tricks: 1) we made the gem path static and 2) we made it accessible relative to the main app.

As it turns out, Bundler, has the concept of plugins and one of them does precisely what we need: the bundler-symlink plugin provides a post-install hook that adds symlinks to all gems of the main application under its local directory. Specifically, under the .bundle/gems/ directory of your project. This is great because all the different absolute gem paths are now accessible from a known place within the main application.

To ensure the gem is available in your setup, you can add it to the Gemfile like this:

# Gemfile

plugin "bundler-symlink"
Enter fullscreen mode Exit fullscreen mode

Then, whenever bundle install is run, the plugin adds / updates a set of symlinks to the gems:

$ bundle install

Fetching gem metadata from https://rubygems.org/..
Resolving dependencies...
Fetching bundler-symlink 0.4.0
Installing bundler-symlink 0.4.0
Installed plugin bundler-symlink
Symlinking bundled gems into /home/matous/projekty/nejremeslnici/web/.bundle/gems
Bundle complete! ...


$ ls -l .bundle/gems

lrwxrwxrwx 1 matous users 51 Mar  5 21:32 actioncable-8.0.1 -> /home/matous/.gem/ruby/3.4.1/gems/actioncable-8.0.1/
lrwxrwxrwx 1 matous users 53 Mar  5 21:32 actionmailbox-8.0.1 -> /home/matous/.gem/ruby/3.4.1/gems/actionmailbox-8.0.1/
lrwxrwxrwx 1 matous users 52 Mar  5 21:32 actionmailer-8.0.1 -> /home/matous/.gem/ruby/3.4.1/gems/actionmailer-8.0.1/
lrwxrwxrwx 1 matous users 50 Mar  5 21:32 actionpack-8.0.1 -> /home/matous/.gem/ruby/3.4.1/gems/actionpack-8.0.1/
lrwxrwxrwx 1 matous users 50 Mar  5 21:32 actiontext-8.0.1 -> /home/matous/.gem/ruby/3.4.1/gems/actiontext-8.0.1/
lrwxrwxrwx 1 matous users 50 Mar  5 21:32 actionview-8.0.1 -> /home/matous/.gem/ruby/3.4.1/gems/actionview-8.0.1/
...
Enter fullscreen mode Exit fullscreen mode

Now, you can easily add a relative gem path to the Tailwind CSS configuration file using the @source directive (the path is relative to the location of the CSS file):

/* app/assets/tailwind/application.css */

@import "tailwindcss";

@source "../../../.bundle/gems/flowbite-components-517d2087e439/app/components/**/*.{slim,rb}";
Enter fullscreen mode Exit fullscreen mode

And voila, all styles work again!

There is still one issue though: the bundler plugin creates symlinks that have the gem versions in their names so this setup would break again whenever we upgraded our Flowbite components gem. This does not seem like a viable solution, long-term.

We briefly tried to use a glob in the gem path in the Tailwind configuration (as in @source ".../.bundle/gems/flowbite-*/...") but this did not work, probably due to a limitation of the Tailwind scanner regarding symlinks.

Using the ”bare symlinks“ plugin

We ended up cloning the bundler plugin and amending its source to create symlinks that were not versioned instead. We released the new plugin under the bundler-bare_symlink name. It is exactly the same as the original 1 except for one thing: it uses the bare gem name instead of the (versioned) gem directory name in the symlink name.

So, to use it, we first uninstalled the original plugin:

$ bundle plugin uninstall bundler-symlink
Enter fullscreen mode Exit fullscreen mode

Then put our new one in the Gemfile:

# Gemfile

plugin "bundler-bare_symlink"
Enter fullscreen mode Exit fullscreen mode

And after running bundle install again, we got the following symlink structure:

$ ls -l .bundle/gems

lrwxrwxrwx 1 matous users 51 Mar  5 22:09 actioncable -> /home/matous/.gem/ruby/3.4.1/gems/actioncable-8.0.1/
lrwxrwxrwx 1 matous users 53 Mar  5 22:09 actionmailbox -> /home/matous/.gem/ruby/3.4.1/gems/actionmailbox-8.0.1/
lrwxrwxrwx 1 matous users 52 Mar  5 22:09 actionmailer -> /home/matous/.gem/ruby/3.4.1/gems/actionmailer-8.0.1/
lrwxrwxrwx 1 matous users 50 Mar  5 22:09 actionpack -> /home/matous/.gem/ruby/3.4.1/gems/actionpack-8.0.1/
lrwxrwxrwx 1 matous users 50 Mar  5 22:09 actiontext -> /home/matous/.gem/ruby/3.4.1/gems/actiontext-8.0.1/
lrwxrwxrwx 1 matous users 50 Mar  5 22:09 actionview -> /home/matous/.gem/ruby/3.4.1/gems/actionview-8.0.1/
...
Enter fullscreen mode Exit fullscreen mode

And finally, we were able to use a fully relative and static path in the Tailwind configuration file:

/* app/assets/tailwind/application.css */

@import "tailwindcss";

@source "../../../.bundle/gems/flowbite/app/components/**/*.{slim,rb}";
Enter fullscreen mode Exit fullscreen mode

Conclusion

This setup works for us on all developer machines as well as on the servers. The fact that the plugin uses a bundle install hook makes it very convenient and we did not have to explicitly install or configure it anywhere. And, most importantly, all of our web styles work again!

Would you like to read more stuff like this? Follow us on Bluesky.


  1. It even keeps the original license, the WTFPL, of course, haha. 

Top comments (0)

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay