Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Implement Engines support
  • Loading branch information
bopm committed Jun 2, 2025
commit 4042f0cbe5b1e16398d17bdb011cd111a1f973f3
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,12 @@ Then you can use yarn or npm to install the dependencies.

If you need to use a custom input or output file, you can run `bundle exec tailwindcss` to access the platform-specific executable, and give it your own build options.

## Rails Engines support

If you have Rails Engines in your application that use Tailwind CSS, they will be automatically included in the Tailwind build as long as they conform to next conventions:

- The engine must have `tailwindcss-rails` as gem dependency.
- The engine must have a `app/assets/tailwind/<engine_name>/application.css` file or your application must have overridden file in the same location of your application root.

## Troubleshooting

Expand Down
34 changes: 34 additions & 0 deletions lib/tailwindcss/commands.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,40 @@ def command_env(verbose:)
def rails_css_compressor?
defined?(Rails) && Rails&.application&.config&.assets&.css_compressor.present?
end

def engines_tailwindcss_roots
return [] unless defined?(Rails)

Rails::Engine.subclasses.select do |engine|
begin
spec = Gem::Specification.find_by_name(engine.engine_name)
Copy link
Member

@flavorjones flavorjones Apr 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is Gem::Specification.find_by_name guaranteed to return spec for the version of the gem that is loaded? If there are multiple versions installed on the system, could it return the spec from an earlier or later version?

I've not seen this pattern of using an engine's dependencies to control behavior, and it feels very indirect/implicit. I'm very hesitant to use this approach.

Help me understand why we shouldn't use a railtie and ask engines to be explicit/imperative? The engine, in its railtie file, should explicitly do something like:

ActiveSupport.on_load(:tailwindcss_rails) do
  config.tailwindcss_rails.engine_roots << "path/to/application.css"
  # or
  config.tailwindcss_rails.engines << My::Engine
end

That would greatly simplify what we do in the gem.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gem::Specification.find_by_name guaranteed to return spec for the version of the gem that is loaded

Yes, because it's overridden by Bundler, example:

rails-empty(dev)> Gem::Specification.find_by_name('rails')
=>
Gem::Specification.new do |s|
  s.name = "rails"
  s.version = Gem::Version.new("8.0.2")
  s.installed_by_version = Gem::Version.new("3.6.1")
  s.authors = ["David Heinemeier Hansson"]

spec.dependencies.any? { |d| d.name == 'tailwindcss-rails' }
rescue Gem::MissingSpecError
false
end
end.map do |engine|
[
Rails.root.join("app/assets/tailwind/#{engine.engine_name}/application.css"),
engine.root.join("app/assets/tailwind/#{engine.engine_name}/application.css")
].select(&:exist?).compact.first.to_s
end.compact
end

def enhance_command(command)
Copy link
Member

@flavorjones flavorjones Apr 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like this pattern of modifying the command string. I would prefer if #compile_command and #watch_command accepted an input file path that defaulted to a method that creates this file and returns the string. Something like:

      def compile_command(input: application_css, debug: false, **kwargs)
        debug = ENV["TAILWINDCSS_DEBUG"].present? if ENV.key?("TAILWINDCSS_DEBUG")
        rails_root = defined?(Rails) ? Rails.root : Pathname.new(Dir.pwd)

        command = [
          Tailwindcss::Ruby.executable(**kwargs),
          "-i", application_css,
          "-o", rails_root.join("app/assets/builds/tailwind.css").to_s,
        ]

        command << "--minify" unless (debug || rails_css_compressor?)

        postcss_path = rails_root.join("postcss.config.js")
        command += ["--postcss", postcss_path.to_s] if File.exist?(postcss_path)

        command
      end

      def application_css
        rails_root = defined?(Rails) ? Rails.root : Pathname.new(Dir.pwd)

        file = Tempfile.new("tailwind.application.css")
        engine_roots.each do |root|
          file.puts "@import \"#{root}\";"
        end
        file.puts "\n@import \"#{rails_root.join('app/assets/tailwind/application.css')}\";"
        file.close

        file.path
      end

This way the complication of tempfile generation doesn't bleed into the rake tasks.

Copy link
Contributor Author

@bopm bopm Apr 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tempfile generation doesn't bleed into the rake tasks.

The main reason I've done it the way I did it is because of Tempfile removal. Using Tempfile.create block guarantees that the file will be removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about this approach for solving this problem?

engine_roots = Tailwindcss::Commands.engines_tailwindcss_roots
if engine_roots.any?
Tempfile.create('tailwind.css') do |file|
file.write(engine_roots.map { |root| "@import \"#{root}\";" }.join("\n"))
file.write("\n@import \"#{Rails.root.join('app/assets/tailwind/application.css')}\";\n")
file.rewind
transformed_command = command.dup
transformed_command[2] = file.path
yield transformed_command if block_given?
end
else
yield command if block_given?
end
end
end
end
end
16 changes: 10 additions & 6 deletions lib/tasks/build.rake
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ namespace :tailwindcss do
verbose = args.extras.include?("verbose")

command = Tailwindcss::Commands.compile_command(debug: debug)
env = Tailwindcss::Commands.command_env(verbose: verbose)
puts "Running: #{Shellwords.join(command)}" if verbose
Tailwindcss::Commands.enhance_command(command) do |transformed_command|
env = Tailwindcss::Commands.command_env(verbose: verbose)
puts "Running: #{Shellwords.join(command)}" if verbose

system(env, *command, exception: true)
system(env, *command, exception: true)
end
end

desc "Watch and build your Tailwind CSS on file changes"
Expand All @@ -19,10 +21,12 @@ namespace :tailwindcss do
verbose = args.extras.include?("verbose")

command = Tailwindcss::Commands.watch_command(always: always, debug: debug, poll: poll)
env = Tailwindcss::Commands.command_env(verbose: verbose)
puts "Running: #{Shellwords.join(command)}" if verbose
Tailwindcss::Commands.enhance_command(command) do |transformed_command|
env = Tailwindcss::Commands.command_env(verbose: verbose)
puts "Running: #{Shellwords.join(command)}" if verbose

system(env, *command)
system(env, *command)
end
rescue Interrupt
puts "Received interrupt, exiting tailwindcss:watch" if args.extras.include?("verbose")
end
Expand Down