7

How to use tailwind in a rails engine? According to the documentation supplying a css argument to the Rails generator should work

Rails 7.0.2.2 engine generated using

rails plugin new tailtest --mountable --full -d postgresql --css tailwind

This generates the engine with Postgresql but does nothing with tailwind at all, and following manual installation instructions fail too.

Running, as per documentation, bundle add tailwindcss-rails adds tailwind to the gemfile rather than the engines tailtest.gemspec So after adding the dependency to the gemspec

spec.add_dependency "tailwindcss-rails", "~> 2.0"

and running bundle install does install the engine however the rest of the manual installation fails

then adding the require to lib/engine.rb

require "tailwindcss-rails"
module Tailtest
  class Engine < ::Rails::Engine
    isolate_namespace Tailtest
  end
end

then running the install process fails

rails tailwindcss:install
Resolving dependencies...
rails aborted!
Don't know how to build task 'tailwindcss:install' (See the list of available tasks with `rails --tasks`)
Did you mean?  app:tailwindcss:install

Obviously the app:tailwindcss:install command fails too.

So I am probably missing an initializer of some sort in the engine.rb file but no idea on what it should be.

jamesc
  • 12,423
  • 15
  • 74
  • 113
  • 1
    This just wasn't implemented yet, at all. You would have to do everything manually, to a point that you would have to replicate build tasks like `rails tailwindcss:build` – user9114945 Feb 24 '22 at 10:27
  • @user9114945 Thank you for your pointer, I'll give that a go and if I manage to achieve this manually then I'll answer my own question, off to do some research on the steps used in the build tasks. Becoming very delusioned with Rails hasty implementations lately – jamesc Feb 25 '22 at 04:23
  • 1
    By the way, I think it's not just Rails 7 but also the Tailwind Gem itself that doesn't support engines. For example the tailwindcss:build task uses only `Rails.root` paths.. I suspect that the tailwind gem only supports builds for the Root app anyway (where it searches for tailwind class definitions to generate the slimmed down tailwind css file). I have the same exact problem and I made it work by just manually installing tailwind in the Engine, and then adding tailwind.css to the mainfest.js file. This works, but without stripping/compiling. Might as well use CDN Tailwind instead of that.. – user9114945 Feb 25 '22 at 04:33
  • @user9114945, could you document the manual process you followed as your answer to this question please and I'll mark it as accepted. Much appreciated – jamesc Feb 26 '22 at 10:49
  • Sorry just saw this now. Did you ever find a solution? Mine turned out to be a mess – user9114945 Mar 16 '22 at 14:30
  • @user9114945, no, doesn't seem possible right now – jamesc Mar 18 '22 at 10:00
  • I'm just using it via CDN at the moment.. sad – user9114945 Mar 22 '22 at 01:54
  • @user9114945 it feels like something has changed in the Rails team, not for the better, the tailwind gem is not the only thing suffering and I'm starting to wonder about the future, I'm starting to think about finding an alternative language/framework, the thing is, I love Ruby so much and really hope they sort themselves out quickly – jamesc Mar 22 '22 at 09:02
  • Yes exactly! Honestly, we are probably at the last station of the Rails train – user9114945 Mar 28 '22 at 04:40
  • @user9114945 I now have a solution as per the extremely detailed and accepted answer from Alex – jamesc May 13 '22 at 14:29
  • Yes, remarkable, however my head hurts just looking at it ;). Did you have success with it? – user9114945 Jun 09 '22 at 08:34

2 Answers2

9

It is the same idea as How to set up importmap-rails in Rails 7 engine?. We don't need to use the install task. Even if you're able to run it, it's not helpful in the engine (see the end of the answer for explanation).

Also rails plugin new doesn't have a --css option. To see available options: rails plugin new -h.

Update engine's gemspec file:

# my_engine/my_engine.gemspec

spec.add_dependency "tailwindcss-rails"

Update engine.rb:

# my_engine/lib/my_engine/engine.rb

module MyEngine
  class Engine < ::Rails::Engine
    isolate_namespace MyEngine

    # NOTE: add engine manifest to precompile assets in production, if you don't have this yet.
    initializer "my-engine.assets" do |app|
      app.config.assets.precompile += %w[my_engine_manifest]
    end
  end
end

Update assets manifest:

# my_engine/app/assets/config/my_engine_manifest.js

//= link_tree ../builds/ .css

Update engine's layout:

# my_engine/app/views/layouts/my_engine/application.html.erb

<!DOCTYPE html>
<html>
  <head>
   <%# 
       NOTE: make sure this name doesn't clash with anything in the main app.
             think of it as `require` and `$LOAD_PATH`,
             but instead it is `stylesheet_link_tag` and `manifest.js`.
    %>
    <%= stylesheet_link_tag "my_engine", "data-turbo-track": "reload" %>
  </head>
  <body> <%= yield %> </body>
</html>

bundle show command will give us the path where the gem is installed, so we can copy a few files:

$ bundle show tailwindcss-rails
/home/alex/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/tailwindcss-rails-2.0.8-x86_64-linux

Copy tailwind.config.js file from tailwindcss-rails:

$ cp $(bundle show tailwindcss-rails)/lib/install/tailwind.config.js config/tailwind.config.js

Copy application.tailwind.css file into any directory to fit your setup:

$ cp $(bundle show tailwindcss-rails)/lib/install/application.tailwind.css app/assets/stylesheets/application.tailwind.css

Because tailwindcss-rails uses standalone executable, we don't need node or rails to compile the stylesheets. We just need to get to the executable itself.

Executable is located here https://github.com/rails/tailwindcss-rails/tree/v2.0.8/exe/. Instead of running the build task https://github.com/rails/tailwindcss-rails/blob/v2.0.8/lib/tasks/build.rake we can just call the executable directly.

$ $(bundle show tailwindcss-rails)/exe/tailwindcss -i app/assets/stylesheets/application.tailwind.css -o app/assets/builds/my_engine.css -c config/tailwind.config.js --minify

Use -w option to start watch mode.

$ $(bundle show tailwindcss-rails)/exe/tailwindcss -i app/assets/stylesheets/application.tailwind.css -o app/assets/builds/my_engine.css -c config/tailwind.config.js --minify -w

The output file should match the name in stylesheet_link_tag "my_engine".

Now that you have a plain my_engine.css file, do with it what you want. Use it in the layout, require it from the main app application.css. The usual rails asset pipeline rules apply.

If you want to put all that into a task, use Engine.root to get the paths.

# my_engine/lib/tasks/my_engine.rake

task :tailwind_engine_watch do
  require "tailwindcss-rails"
  # NOTE: tailwindcss-rails is an engine
  system "#{Tailwindcss::Engine.root.join("exe/tailwindcss")} \
         -i #{MyEngine::Engine.root.join("app/assets/stylesheets/application.tailwind.css")} \
         -o #{MyEngine::Engine.root.join("app/assets/builds/my_engine.css")} \
         -c #{MyEngine::Engine.root.join("config/tailwind.config.js")} \
         --minify -w"
end

From the engine directory:

$ bin/rails app:tailwind_engine_watch
+ /home/alex/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/tailwindcss-rails-2.0.8-x86_64-linux/exe/x86_64-linux/tailwindcss -i /home/alex/code/stackoverflow/my_engine/app/assets/stylesheets/application.tailwind.css -o /home/alex/code/stackoverflow/my_engine/app/assets/builds/my_engine.css -c /home/alex/code/stackoverflow/my_engine/config/tailwind.config.js --minify -w

Rebuilding...
Done in 549ms.

Make your own install task if you have a lot of engines to set up:

desc "Install tailwindcss into our engine"
task :tailwind_engine_install do
  require "tailwindcss-rails"

  # NOTE: use default app template, which will fail to modify layout, manifest,
  #       and the last command that compiles the initial `tailwind.css`.
  #       It will also add `bin/dev` and `Procfile.dev` which we don't need.
  #       Basically, it's useless in the engine as it is.
  template = Tailwindcss::Engine.root.join("lib/install/tailwindcss.rb")

  # TODO: better to copy the template from 
  #       https://github.com/rails/tailwindcss-rails/blob/v2.0.8/lib/install/tailwindcss.rb
  #       and customize it
  # template = MyEngine::Engine.root("lib/install/tailwindcss.rb")

  require "rails/generators"
  require "rails/generators/rails/app/app_generator"
  
  # NOTE: because the app template uses `Rails.root` it will run the install
  #       on our engine's dummy app. Just override `Rails.root` with our engine
  #       root to run install in the engine directory.
  Rails.configuration.root = MyEngine::Engine.root

  generator = Rails::Generators::AppGenerator.new [Rails.root], {}, { destination_root: Rails.root }
  generator.apply template
end

Install task reference:
https://github.com/rails/rails/blob/v7.0.2.4/railties/lib/rails/tasks/framework.rake#L8
https://github.com/rails/tailwindcss-rails/blob/v2.0.8/lib/tasks/install.rake

Watch task reference:
https://github.com/rails/tailwindcss-rails/blob/v2.0.8/lib/tasks/build.rake#L10


Update How to merge two tailwinds.

Above setup assumes the engine is its own separate thing, like admin backend, it has its own routes, templates, and styles. If an engine functionality is meant to be mixed with the main app, like a view_component collection, then tailwind styles will override each other. In this case isolating engine styles with a prefix could work:
https://tailwindcss.com/docs/configuration#prefix

The reason that tailwind styles don't mix is because most of the selectors have the same specificity and the order is very important.

So here is an example. Main app with an engine, both using tailwind, both compile styles separately, tailwind configs are only watching one file from the engine and one from the main app, only using @tailwind utilities; directive:

Engine template, that we want to use in the main app, should work fine:

<!-- blep/app/views/blep/_partial.html.erb -->

<div class="bg-red-500 sm:bg-blue-500"> red never-blue </div>

But when rendered in the main app it never turns blue. Here is the demonstration set up:

<!-- app/views/home/index.html.erb -->

<%= stylesheet_link_tag "blep",     "data-turbo-track": "reload" %>
<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %>

<!-- output generated css in the same order as above link tags -->
<% require "open-uri" %>
<b>Engine css</b>
<pre><%= URI.open(asset_url("blep")).read %></pre>
<b>Main app css</b>
<pre><%= URI.open(asset_url("tailwind")).read %></pre>

<div class="bg-red-500"> red </div> <!-- this generates another bg-red-500 -->
<br>
<%= render "blep/partial" %>

And it looks like this:

/* Engine css */
.bg-red-500 {
  --tw-bg-opacity: 1;
  background-color: rgb(239 68 68 / var(--tw-bg-opacity))
}

@media (min-width: 640px) {
  .sm\:bg-blue-500 {
    --tw-bg-opacity: 1;
    background-color: rgb(59 130 246 / var(--tw-bg-opacity))
  }
}

/* Main app css */
.bg-red-500 {
  --tw-bg-opacity: 1;
  background-color: rgb(239 68 68 / var(--tw-bg-opacity))
}
<div class="bg-red-500"> red </div>
<br>
<div class="bg-red-500 sm:bg-blue-500"> red never-blue </div>

^ you can hit run and click "full page". Main app bg-red-500 selector is last so it overrides engines sm:bg-blue-500 selector, media queries don't add to specificity score. It's the same reason you can't override, say, mt-1 with m-2, margin top comes later in the stylesheet. This is why @layer directives are important.

The only way around this is to watch the engine directory when running tailwind in the main app, so that styles are compiled together and in the correct order. Which means you don't really need tailwind in the engine:

module.exports = {
  content: [
    "./app/**/*",
    "/just/type/the/path/to/engine/views",
    "/or/see/updated/task/below",
  ],
}

Other ways I tried, like running 6 tailwind commands for each layer for main app and engine, so that I can put them in order, better but was still out of order a bit and duplicated. Or doing an @import and somehow letting postcss-import know where to look for engine styles (I don't know, I just symlinked it into node_modules to test), but this still required tailwind to watch engine files.


I did some more digging, tailwind cli has a --content option, which will override content from tailwind.config.js. We can use it to setup a new task:

namespace :tailwindcss do
  desc "Build your Tailwind CSS + Engine"
  task :watch do |_, args|
    # NOTE: there have been some updates, there is a whole Commands class now
    #       lets copy paste and modify.          (debug = no --minify)
    command = Tailwindcss::Commands.watch_command(debug: true, poll: false)

    # --content /path/to/app/**/*,/path/to/engine/**/*
    command << "--content"
    command << [
      Rails.root.join("app/views/home/*"),
      Blep::Engine.root.join("app/views/**/*.erb")
    ].join(",")

    p command
    system(*command)
  end

  # same for build, just call `compile_command`
  # task :build do |_, args|
  #   command = Tailwindcss::Commands.compile_command(debug: false)
  #   ...
end

https://github.com/rails/tailwindcss-rails/blob/v2.0.21/lib/tasks/build.rake#L11

Alex
  • 16,409
  • 6
  • 40
  • 56
  • Hi Alex, how can I contact you? – jamesc May 13 '22 at 14:04
  • 1
    Hi Alex, I wanted to thank you for your dedication in tracking down my unanswered questions and providing solutions for me. I want to share the results of the efforts of all your hard work. I am a member of a WhatsApp group in the UK dedicated to helping Ukranian refugees and those sponsoring refugees and opening up their homes this providing a means of escape. Group members have had a lot of national press across all media National T.V. news and local radio. – jamesc May 13 '22 at 14:06
  • 1
    We so far have helped uncountable people to reach the UK, We have members on the polish boarders helping to match the displaced with UK sponsors and we are in contact with Ukranians that are in desperate need of help and hiding out in occupied towns. The site that I have written is https://together-for-ukraine.co.uk/about it is a CMS site and volunteers in the group are working on the content. You have made the development of this project so much easier for me and I thought you deserved to know how much of a difference your help has made.Thank you from the bottom of my heart – jamesc May 13 '22 at 14:06
  • Great work and patience in explaining it. I had even figured most of this out, alas... Normal assets rules do not seem to apply with the engines generated css, WHEN the app also uses tailwind. Whatever i do, i can not get the apps tailwind styles AND the engines tailwind styles to work at the same time. I tried switching preflight off in both, ordered them this and that way, but to no avail. The problem seems to be that some media classes, eg .lg\:grid-cols-5 just don't apply, even they are there. Any help or insight appreciated – Torsten Dec 08 '22 at 22:05
  • @Torsten see the update, I only have an explanation but no good solution. I'm pretty sure the only way is to compile styles as one by watching the engine directory. – Alex Dec 09 '22 at 12:09
  • 1
    @alex good of you to have a look. My step 2 is to the same effect, just in javascript. – Torsten Dec 10 '22 at 16:18
  • such a great answer! – Ben Feb 26 '23 at 00:14
5

That answer by Alex is really good, i wish i had it when starting out. (But i didn't even have the question to google) Just want to add two things:

1- a small simplification. I just made a script to run tailwind in the engine

#!/usr/bin/env sh
# Since tailwind does not install into the engine, this will
# watch and recompile during development
# tailwindcss executable must exist (by bundling tailwindcss-rails eg)

tailwindcss -i app/assets/stylesheets/my_engine.tailwind.css \
        -o app/assets/stylesheets/my_engine/my_engine.css \
        -c config/tailwind.config.js \
        -w

2- For usage in an app, that obviously also uses tailwind, i was struggling, since the two generated css's were biting each other and i could not get both styles to work in one page. Always one or the other (app or engine) was not styled right. Until i got the app's tailwind to pick up the engines classes. Like so:

Add to the app's tailwind.config.js: before the module

const execSync = require('child_process').execSync;
const output = execSync('bundle show my_engine', { encoding: 'utf-8' });

And then inside the content as last line

    output.trim() + '/app/**/*.{erb,haml,html,rb}'

Then just include the apps generated tailwind css in the layout, like the installer will. Don't include the engines stylesheet in the layout, or add it to the asset

Torsten
  • 425
  • 4
  • 7
  • Just for anyone actually attempting this, i would recommend staying away from solution 1. Ie don't have a separate engine stylesheet at al (unless you develop a admin of sorts, that is really seperate from the app). Just go with solution 2, let the app's tailwind get all styles. The reasonn being that i keep forgetting to actually maually start the script, and so the styles don't get updated, and i wonder and wonder. Whereas with 2, foreman does it in the course of normal procedure – Torsten Dec 13 '22 at 16:57