ruby-on-railstailwind-cssrails-enginesruby-on-rails-7

How to use tailwind css gem in a rails 7 engine?


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.


Solution

  • 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
      # # The default behaviour merge both the parent's app task with this one. If needed, you can override the task completely by clearing it before redefining.
      # Rake::Task[:watch].clear
      # Rake::Task[:build].clear
    
      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