From 4042f0cbe5b1e16398d17bdb011cd111a1f973f3 Mon Sep 17 00:00:00 2001 From: Sergey Moiseev Date: Wed, 29 Jan 2025 20:52:43 +0200 Subject: [PATCH 1/8] Implement Engines support --- README.md | 6 ++++++ lib/tailwindcss/commands.rb | 34 ++++++++++++++++++++++++++++++++++ lib/tasks/build.rake | 16 ++++++++++------ 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 158b175..41f6900 100644 --- a/README.md +++ b/README.md @@ -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//application.css` file or your application must have overridden file in the same location of your application root. ## Troubleshooting diff --git a/lib/tailwindcss/commands.rb b/lib/tailwindcss/commands.rb index 99ad30e..b31e581 100644 --- a/lib/tailwindcss/commands.rb +++ b/lib/tailwindcss/commands.rb @@ -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) + 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) + 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 diff --git a/lib/tasks/build.rake b/lib/tasks/build.rake index 603c805..7eef75e 100644 --- a/lib/tasks/build.rake +++ b/lib/tasks/build.rake @@ -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" @@ -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 From 7d5419cbddf2868b2284494f2aed6880bf2136b3 Mon Sep 17 00:00:00 2001 From: Sergey Moiseev Date: Mon, 10 Mar 2025 20:14:21 +0200 Subject: [PATCH 2/8] Add tests for command enhancement and Tailwind roots --- test/lib/tailwindcss/commands_test.rb | 139 ++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/test/lib/tailwindcss/commands_test.rb b/test/lib/tailwindcss/commands_test.rb index d09481a..012d42d 100644 --- a/test/lib/tailwindcss/commands_test.rb +++ b/test/lib/tailwindcss/commands_test.rb @@ -126,4 +126,143 @@ def setup assert_includes(actual, "always") end end + + test ".engines_tailwindcss_roots when there are no engines" do + Rails.stub(:root, Pathname.new("/dummy")) do + Rails::Engine.stub(:subclasses, []) do + assert_empty Tailwindcss::Commands.engines_tailwindcss_roots + end + end + end + + test ".engines_tailwindcss_roots when there are engines" do + Dir.mktmpdir do |tmpdir| + root = Pathname.new(tmpdir) + + # Create two engines + engine_root1 = root.join('engine1') + engine_root2 = root.join('engine2') + FileUtils.mkdir_p(engine_root1) + FileUtils.mkdir_p(engine_root2) + + engine1 = Class.new(Rails::Engine) do + define_singleton_method(:engine_name) { "test_engine1" } + define_singleton_method(:root) { engine_root1 } + end + + engine2 = Class.new(Rails::Engine) do + define_singleton_method(:engine_name) { "test_engine2" } + define_singleton_method(:root) { engine_root2 } + end + + # Create mock specs for both engines + spec1 = Minitest::Mock.new + spec1.expect(:dependencies, [Gem::Dependency.new("tailwindcss-rails")]) + + spec2 = Minitest::Mock.new + spec2.expect(:dependencies, [Gem::Dependency.new("tailwindcss-rails")]) + + spec3 = Minitest::Mock.new + spec3.expect(:dependencies, []) + + # Set up file structure + # Engine 1: CSS in engine root + engine1_css = engine_root1.join("app/assets/tailwind/test_engine1/application.css") + FileUtils.mkdir_p(File.dirname(engine1_css)) + FileUtils.touch(engine1_css) + + # Engine 2: CSS in Rails root + engine2_css = root.join("app/assets/tailwind/test_engine2/application.css") + FileUtils.mkdir_p(File.dirname(engine2_css)) + FileUtils.touch(engine2_css) + + # Engine 3: CsS in engine root, but no tailwindcss-rails dependency + engine3_css = engine_root2.join("app/assets/tailwind/test_engine3/application.css") + FileUtils.mkdir_p(File.dirname(engine3_css)) + FileUtils.touch(engine3_css) + + find_by_name_results = { + "test_engine1" => spec1, + "test_engine2" => spec2 + } + + Gem::Specification.stub(:find_by_name, ->(name) { find_by_name_results[name] }) do + Rails.stub(:root, root) do + Rails::Engine.stub(:subclasses, [engine1, engine2]) do + roots = Tailwindcss::Commands.engines_tailwindcss_roots + + assert_equal 2, roots.size + assert_includes roots, engine1_css.to_s + assert_includes roots, engine2_css.to_s + assert_not_includes roots, engine3_css.to_s + end + end + end + + spec1.verify + spec2.verify + end + end + + test ".enhance_command when there are no engines" do + Dir.mktmpdir do |tmpdir| + root = Pathname.new(tmpdir) + input_path = root.join("app/assets/tailwind/application.css") + output_path = root.join("app/assets/builds/tailwind.css") + + command = ["tailwindcss", "-i", input_path.to_s, "-o", output_path.to_s] + + Rails.stub(:root, root) do + Tailwindcss::Commands.stub(:engines_tailwindcss_roots, []) do + Tailwindcss::Commands.enhance_command(command) do |actual| + assert_equal command, actual + end + end + end + end + end + + test ".enhance_command when there are engines" do + Dir.mktmpdir do |tmpdir| + root = Pathname.new(tmpdir) + input_path = root.join("app/assets/tailwind/application.css") + output_path = root.join("app/assets/builds/tailwind.css") + + # Create necessary files + FileUtils.mkdir_p(File.dirname(input_path)) + FileUtils.touch(input_path) + + # Create engine CSS file + engine_css_path = root.join("app/assets/tailwind/test_engine/application.css") + FileUtils.mkdir_p(File.dirname(engine_css_path)) + FileUtils.touch(engine_css_path) + + command = ["tailwindcss", "-i", input_path.to_s, "-o", output_path.to_s] + + Rails.stub(:root, root) do + Tailwindcss::Commands.stub(:engines_tailwindcss_roots, [engine_css_path.to_s]) do + Tailwindcss::Commands.enhance_command(command) do |actual| + # Command should be modified to use a temporary file + assert_equal command[0], actual[0] # executable + assert_equal command[1], actual[1] # -i flag + assert_equal command[3], actual[3] # -o flag + assert_equal command[4], actual[4] # output path + + temp_path = Pathname.new(actual[2]) + refute_equal command[2], temp_path.to_s # input path should be different + assert_match(/tailwind\.css/, temp_path.basename.to_s) # should use temp file + assert_includes [Dir.tmpdir, '/tmp'], temp_path.dirname.to_s # should be in temp directory + + # Check temp file contents + temp_content = File.read(temp_path) + expected_content = <<~CSS + @import "#{engine_css_path}"; + @import "#{input_path}"; + CSS + assert_equal expected_content.strip, temp_content.strip + end + end + end + end + end end From 5ba881bd493d7fed43bb59889a0b3033dbe594a2 Mon Sep 17 00:00:00 2001 From: Sergey Moiseev Date: Mon, 10 Mar 2025 20:23:23 +0200 Subject: [PATCH 3/8] Test correction --- test/lib/tailwindcss/commands_test.rb | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/test/lib/tailwindcss/commands_test.rb b/test/lib/tailwindcss/commands_test.rb index 012d42d..2d525f2 100644 --- a/test/lib/tailwindcss/commands_test.rb +++ b/test/lib/tailwindcss/commands_test.rb @@ -139,11 +139,13 @@ def setup Dir.mktmpdir do |tmpdir| root = Pathname.new(tmpdir) - # Create two engines + # Create multiple engines engine_root1 = root.join('engine1') engine_root2 = root.join('engine2') + engine_root3 = root.join('engine3') FileUtils.mkdir_p(engine_root1) FileUtils.mkdir_p(engine_root2) + FileUtils.mkdir_p(engine_root3) engine1 = Class.new(Rails::Engine) do define_singleton_method(:engine_name) { "test_engine1" } @@ -155,7 +157,12 @@ def setup define_singleton_method(:root) { engine_root2 } end - # Create mock specs for both engines + engine3 = Class.new(Rails::Engine) do + define_singleton_method(:engine_name) { "test_engine3" } + define_singleton_method(:root) { engine_root3 } + end + + # Create mock specs for engines spec1 = Minitest::Mock.new spec1.expect(:dependencies, [Gem::Dependency.new("tailwindcss-rails")]) @@ -183,7 +190,8 @@ def setup find_by_name_results = { "test_engine1" => spec1, - "test_engine2" => spec2 + "test_engine2" => spec2, + "test_engine3" => spec3, } Gem::Specification.stub(:find_by_name, ->(name) { find_by_name_results[name] }) do From bcc59b5de358d96c3f2eba6c0e786799b911b71d Mon Sep 17 00:00:00 2001 From: Sergey Moiseev Date: Fri, 11 Apr 2025 00:26:14 +0300 Subject: [PATCH 4/8] Implement with_dynamic_input --- lib/tailwindcss/commands.rb | 21 +++++++++-------- lib/tasks/build.rake | 8 +++---- test/lib/tailwindcss/commands_test.rb | 34 +++++++++------------------ 3 files changed, 26 insertions(+), 37 deletions(-) diff --git a/lib/tailwindcss/commands.rb b/lib/tailwindcss/commands.rb index b31e581..1bab337 100644 --- a/lib/tailwindcss/commands.rb +++ b/lib/tailwindcss/commands.rb @@ -3,13 +3,16 @@ module Tailwindcss module Commands class << self - def compile_command(debug: false, **kwargs) + def rails_root + defined?(Rails) ? Rails.root : Pathname.new(Dir.pwd) + end + + def compile_command(input = rails_root.join("app/assets/tailwind/application.css").to_s, 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", rails_root.join("app/assets/tailwind/application.css").to_s, + "-i", input, "-o", rails_root.join("app/assets/builds/tailwind.css").to_s, ] @@ -21,8 +24,8 @@ def compile_command(debug: false, **kwargs) command end - def watch_command(always: false, poll: false, **kwargs) - compile_command(**kwargs).tap do |command| + def watch_command(input = rails_root.join("app/assets/tailwind/application.css").to_s, always: false, poll: false, **kwargs) + compile_command(input, **kwargs).tap do |command| command << "-w" command << "always" if always command << "-p" if poll @@ -57,19 +60,17 @@ def engines_tailwindcss_roots end.compact end - def enhance_command(command) + def with_dynamic_input 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? + yield file.path if block_given? end else - yield command if block_given? + yield rails_root.join("app/assets/tailwind/application.css").to_s if block_given? end end end diff --git a/lib/tasks/build.rake b/lib/tasks/build.rake index 7eef75e..1fecda2 100644 --- a/lib/tasks/build.rake +++ b/lib/tasks/build.rake @@ -4,8 +4,8 @@ namespace :tailwindcss do debug = args.extras.include?("debug") verbose = args.extras.include?("verbose") - command = Tailwindcss::Commands.compile_command(debug: debug) - Tailwindcss::Commands.enhance_command(command) do |transformed_command| + Tailwindcss::Commands.with_dynamic_input do |input| + command = Tailwindcss::Commands.compile_command(input, debug: debug) env = Tailwindcss::Commands.command_env(verbose: verbose) puts "Running: #{Shellwords.join(command)}" if verbose @@ -20,8 +20,8 @@ namespace :tailwindcss do always = args.extras.include?("always") verbose = args.extras.include?("verbose") - command = Tailwindcss::Commands.watch_command(always: always, debug: debug, poll: poll) - Tailwindcss::Commands.enhance_command(command) do |transformed_command| + Tailwindcss::Commands.with_dynamic_input do |input| + command = Tailwindcss::Commands.watch_command(input, always: always, debug: debug, poll: poll) env = Tailwindcss::Commands.command_env(verbose: verbose) puts "Running: #{Shellwords.join(command)}" if verbose diff --git a/test/lib/tailwindcss/commands_test.rb b/test/lib/tailwindcss/commands_test.rb index 2d525f2..9c0d029 100644 --- a/test/lib/tailwindcss/commands_test.rb +++ b/test/lib/tailwindcss/commands_test.rb @@ -11,7 +11,7 @@ def setup test ".compile_command" do Rails.stub(:root, File) do # Rails.root won't work in this test suite - actual = Tailwindcss::Commands.compile_command + actual = Tailwindcss::Commands.compile_command("app/assets/tailwind/application.css") assert_kind_of(Array, actual) assert_equal(executable, actual.first) assert_includes(actual, "-i") @@ -212,29 +212,25 @@ def setup end end - test ".enhance_command when there are no engines" do + test ".with_dynamic_input when there are no engines" do Dir.mktmpdir do |tmpdir| root = Pathname.new(tmpdir) - input_path = root.join("app/assets/tailwind/application.css") - output_path = root.join("app/assets/builds/tailwind.css") - - command = ["tailwindcss", "-i", input_path.to_s, "-o", output_path.to_s] + input_path = root.join("app/assets/tailwind/application.css").to_s Rails.stub(:root, root) do Tailwindcss::Commands.stub(:engines_tailwindcss_roots, []) do - Tailwindcss::Commands.enhance_command(command) do |actual| - assert_equal command, actual + Tailwindcss::Commands.with_dynamic_input do |actual| + assert_equal input_path, actual end end end end end - test ".enhance_command when there are engines" do + test ".with_dynamic_input when there are engines" do Dir.mktmpdir do |tmpdir| root = Pathname.new(tmpdir) - input_path = root.join("app/assets/tailwind/application.css") - output_path = root.join("app/assets/builds/tailwind.css") + input_path = root.join("app/assets/tailwind/application.css").to_s # Create necessary files FileUtils.mkdir_p(File.dirname(input_path)) @@ -245,24 +241,16 @@ def setup FileUtils.mkdir_p(File.dirname(engine_css_path)) FileUtils.touch(engine_css_path) - command = ["tailwindcss", "-i", input_path.to_s, "-o", output_path.to_s] - Rails.stub(:root, root) do Tailwindcss::Commands.stub(:engines_tailwindcss_roots, [engine_css_path.to_s]) do - Tailwindcss::Commands.enhance_command(command) do |actual| - # Command should be modified to use a temporary file - assert_equal command[0], actual[0] # executable - assert_equal command[1], actual[1] # -i flag - assert_equal command[3], actual[3] # -o flag - assert_equal command[4], actual[4] # output path - - temp_path = Pathname.new(actual[2]) - refute_equal command[2], temp_path.to_s # input path should be different + Tailwindcss::Commands.with_dynamic_input do |actual| + temp_path = Pathname.new(actual) + refute_equal input_path, temp_path.to_s # input path should be different assert_match(/tailwind\.css/, temp_path.basename.to_s) # should use temp file assert_includes [Dir.tmpdir, '/tmp'], temp_path.dirname.to_s # should be in temp directory # Check temp file contents - temp_content = File.read(temp_path) + temp_content = File.read(actual) expected_content = <<~CSS @import "#{engine_css_path}"; @import "#{input_path}"; From aeeb440bc8a6a84452853fc432210617a4ebd7c4 Mon Sep 17 00:00:00 2001 From: Sergey Moiseev Date: Sun, 27 Apr 2025 15:57:05 +0300 Subject: [PATCH 5/8] Add configuration --- lib/tailwindcss/engine.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/tailwindcss/engine.rb b/lib/tailwindcss/engine.rb index 7b88c5f..629ea49 100644 --- a/lib/tailwindcss/engine.rb +++ b/lib/tailwindcss/engine.rb @@ -2,6 +2,9 @@ module Tailwindcss class Engine < ::Rails::Engine + config.tailwindcss_rails = ActiveSupport::OrderedOptions.new + config.tailwindcss_rails.engines = [] + initializer "tailwindcss.disable_generator_stylesheets" do Rails.application.config.generators.stylesheets = false end From e416f2da1c35c5878d55513773b7abd67852a926 Mon Sep 17 00:00:00 2001 From: Sergey Moiseev Date: Sun, 27 Apr 2025 16:52:23 +0300 Subject: [PATCH 6/8] Fix tests --- lib/tailwindcss/commands.rb | 12 +- lib/tailwindcss/engine.rb | 4 + test/lib/tailwindcss/commands_test.rb | 262 +++++++++++++++----------- 3 files changed, 159 insertions(+), 119 deletions(-) diff --git a/lib/tailwindcss/commands.rb b/lib/tailwindcss/commands.rb index 1bab337..f5be585 100644 --- a/lib/tailwindcss/commands.rb +++ b/lib/tailwindcss/commands.rb @@ -42,15 +42,13 @@ def rails_css_compressor? defined?(Rails) && Rails&.application&.config&.assets&.css_compressor.present? end - def engines_tailwindcss_roots + def engines_roots return [] unless defined?(Rails) + return [] unless Rails.application&.config&.tailwindcss_rails&.engines - Rails::Engine.subclasses.select do |engine| + Rails::Engine.descendants.select do |engine| begin - spec = Gem::Specification.find_by_name(engine.engine_name) - spec.dependencies.any? { |d| d.name == 'tailwindcss-rails' } - rescue Gem::MissingSpecError - false + engine.engine_name.in?(Rails.application.config.tailwindcss_rails.engines) end end.map do |engine| [ @@ -61,7 +59,7 @@ def engines_tailwindcss_roots end def with_dynamic_input - engine_roots = Tailwindcss::Commands.engines_tailwindcss_roots + engine_roots = Tailwindcss::Commands.engines_roots if engine_roots.any? Tempfile.create('tailwind.css') do |file| file.write(engine_roots.map { |root| "@import \"#{root}\";" }.join("\n")) diff --git a/lib/tailwindcss/engine.rb b/lib/tailwindcss/engine.rb index 629ea49..6beb65b 100644 --- a/lib/tailwindcss/engine.rb +++ b/lib/tailwindcss/engine.rb @@ -5,6 +5,10 @@ class Engine < ::Rails::Engine config.tailwindcss_rails = ActiveSupport::OrderedOptions.new config.tailwindcss_rails.engines = [] + initializer 'tailwindcss.load_hook' do |app| + ActiveSupport.run_load_hooks(:tailwindcss_rails, app) + end + initializer "tailwindcss.disable_generator_stylesheets" do Rails.application.config.generators.stylesheets = false end diff --git a/test/lib/tailwindcss/commands_test.rb b/test/lib/tailwindcss/commands_test.rb index 9c0d029..94f3ed8 100644 --- a/test/lib/tailwindcss/commands_test.rb +++ b/test/lib/tailwindcss/commands_test.rb @@ -1,14 +1,22 @@ require "test_helper" +require "ostruct" require "minitest/mock" class Tailwindcss::CommandsTest < ActiveSupport::TestCase - attr_accessor :executable + attr_accessor :executable, :original_rails, :tmp_dir - def setup - super + setup do + @tmp_dir = Dir.mktmpdir + @original_rails = Object.const_get(:Rails) if Object.const_defined?(:Rails) @executable = Tailwindcss::Ruby.executable end + teardown do + FileUtils.rm_rf(@tmp_dir) + Tailwindcss::Commands.remove_tempfile! if Tailwindcss::Commands.class_variable_defined?(:@@tempfile) + restore_rails_constant + end + test ".compile_command" do Rails.stub(:root, File) do # Rails.root won't work in this test suite actual = Tailwindcss::Commands.compile_command("app/assets/tailwind/application.css") @@ -127,138 +135,168 @@ def setup end end - test ".engines_tailwindcss_roots when there are no engines" do - Rails.stub(:root, Pathname.new("/dummy")) do - Rails::Engine.stub(:subclasses, []) do - assert_empty Tailwindcss::Commands.engines_tailwindcss_roots - end + test ".engines_roots when Rails is not defined" do + Object.send(:remove_const, :Rails) if Object.const_defined?(:Rails) + assert_empty Tailwindcss::Commands.engines_roots + end + + test ".engines_roots when no engines are configured" do + with_rails_app do + assert_empty Tailwindcss::Commands.engines_roots end end - test ".engines_tailwindcss_roots when there are engines" do - Dir.mktmpdir do |tmpdir| - root = Pathname.new(tmpdir) - - # Create multiple engines - engine_root1 = root.join('engine1') - engine_root2 = root.join('engine2') - engine_root3 = root.join('engine3') - FileUtils.mkdir_p(engine_root1) - FileUtils.mkdir_p(engine_root2) - FileUtils.mkdir_p(engine_root3) - - engine1 = Class.new(Rails::Engine) do - define_singleton_method(:engine_name) { "test_engine1" } - define_singleton_method(:root) { engine_root1 } - end + test ".engines_roots when there are engines" do + within_engine_configs do |engine1, engine2, engine3| + roots = Tailwindcss::Commands.engines_roots + + assert_equal 2, roots.size + assert_includes roots, engine1.css_path.to_s + assert_includes roots, engine2.css_path.to_s + refute_includes roots, engine3.css_path.to_s + end + end + + test ".with_dynamic_input yields tempfile path when engines exist" do + within_engine_configs do |engine1, engine2| + Tailwindcss::Commands.with_dynamic_input do |css_path| + assert_match(/tailwind\.css/, css_path) + assert File.exist?(css_path) - engine2 = Class.new(Rails::Engine) do - define_singleton_method(:engine_name) { "test_engine2" } - define_singleton_method(:root) { engine_root2 } + content = File.read(css_path) + assert_match %r{@import "#{engine1.css_path}";}, content + assert_match %r{@import "#{engine2.css_path}";}, content + assert_match %r{@import "#{Rails.root.join('app/assets/tailwind/application.css')}";}, content end + end + end - engine3 = Class.new(Rails::Engine) do - define_singleton_method(:engine_name) { "test_engine3" } - define_singleton_method(:root) { engine_root3 } + test ".with_dynamic_input yields application.css path when no engines" do + with_rails_app do + expected_path = Rails.root.join("app/assets/tailwind/application.css").to_s + Tailwindcss::Commands.with_dynamic_input do |css_path| + assert_equal expected_path, css_path end + end + end - # Create mock specs for engines - spec1 = Minitest::Mock.new - spec1.expect(:dependencies, [Gem::Dependency.new("tailwindcss-rails")]) - - spec2 = Minitest::Mock.new - spec2.expect(:dependencies, [Gem::Dependency.new("tailwindcss-rails")]) - - spec3 = Minitest::Mock.new - spec3.expect(:dependencies, []) - - # Set up file structure - # Engine 1: CSS in engine root - engine1_css = engine_root1.join("app/assets/tailwind/test_engine1/application.css") - FileUtils.mkdir_p(File.dirname(engine1_css)) - FileUtils.touch(engine1_css) - - # Engine 2: CSS in Rails root - engine2_css = root.join("app/assets/tailwind/test_engine2/application.css") - FileUtils.mkdir_p(File.dirname(engine2_css)) - FileUtils.touch(engine2_css) - - # Engine 3: CsS in engine root, but no tailwindcss-rails dependency - engine3_css = engine_root2.join("app/assets/tailwind/test_engine3/application.css") - FileUtils.mkdir_p(File.dirname(engine3_css)) - FileUtils.touch(engine3_css) - - find_by_name_results = { - "test_engine1" => spec1, - "test_engine2" => spec2, - "test_engine3" => spec3, - } - - Gem::Specification.stub(:find_by_name, ->(name) { find_by_name_results[name] }) do - Rails.stub(:root, root) do - Rails::Engine.stub(:subclasses, [engine1, engine2]) do - roots = Tailwindcss::Commands.engines_tailwindcss_roots - - assert_equal 2, roots.size - assert_includes roots, engine1_css.to_s - assert_includes roots, engine2_css.to_s - assert_not_includes roots, engine3_css.to_s - end + test "engines can be configured via tailwindcss_rails.engines" do + with_rails_app do + # Create a test engine + test_engine = Class.new(Rails::Engine) do + def self.engine_name + "test_engine" + end + + def self.root + Pathname.new(Dir.mktmpdir) end end - spec1.verify - spec2.verify + # Create CSS file for the engine + engine_css_path = test_engine.root.join("app/assets/tailwind/test_engine/application.css") + FileUtils.mkdir_p(File.dirname(engine_css_path)) + FileUtils.touch(engine_css_path) + + # Create application-level CSS file + app_css_path = Rails.root.join("app/assets/tailwind/test_engine/application.css") + FileUtils.mkdir_p(File.dirname(app_css_path)) + FileUtils.touch(app_css_path) + + # Register the engine + Rails::Engine.descendants << test_engine + + # Store the hook for later execution + hook = nil + ActiveSupport.on_load(:tailwindcss_rails) do + hook = self + Rails.application.config.tailwindcss_rails.engines << "test_engine" + end + + # Trigger the hook manually + ActiveSupport.run_load_hooks(:tailwindcss_rails, hook) + + # Verify the engine is included in roots + roots = Tailwindcss::Commands.engines_roots + assert_equal 1, roots.size + assert_includes roots, app_css_path.to_s + ensure + FileUtils.rm_rf(test_engine.root) if defined?(test_engine) + FileUtils.rm_rf(File.dirname(app_css_path)) if defined?(app_css_path) end end - test ".with_dynamic_input when there are no engines" do - Dir.mktmpdir do |tmpdir| - root = Pathname.new(tmpdir) - input_path = root.join("app/assets/tailwind/application.css").to_s + private + def with_rails_app + Object.send(:remove_const, :Rails) if Object.const_defined?(:Rails) + Object.const_set(:Rails, setup_mock_rails) + yield + end + + def setup_mock_rails + mock_engine = Class.new do + class << self + attr_accessor :engine_name, :root - Rails.stub(:root, root) do - Tailwindcss::Commands.stub(:engines_tailwindcss_roots, []) do - Tailwindcss::Commands.with_dynamic_input do |actual| - assert_equal input_path, actual + def descendants + @descendants ||= [] end end end - end - end - test ".with_dynamic_input when there are engines" do - Dir.mktmpdir do |tmpdir| - root = Pathname.new(tmpdir) - input_path = root.join("app/assets/tailwind/application.css").to_s + mock_rails = Class.new do + class << self + attr_accessor :root, :application - # Create necessary files - FileUtils.mkdir_p(File.dirname(input_path)) - FileUtils.touch(input_path) + def const_get(const_name) + return Engine if const_name == :Engine + super + end + end + end - # Create engine CSS file - engine_css_path = root.join("app/assets/tailwind/test_engine/application.css") - FileUtils.mkdir_p(File.dirname(engine_css_path)) - FileUtils.touch(engine_css_path) + mock_rails.const_set(:Engine, mock_engine) + mock_rails.root = Pathname.new(@tmp_dir) + mock_rails.application = OpenStruct.new( + config: OpenStruct.new( + tailwindcss_rails: OpenStruct.new(engines: []), + assets: OpenStruct.new(css_compressor: nil) + ) + ) + mock_rails + end - Rails.stub(:root, root) do - Tailwindcss::Commands.stub(:engines_tailwindcss_roots, [engine_css_path.to_s]) do - Tailwindcss::Commands.with_dynamic_input do |actual| - temp_path = Pathname.new(actual) - refute_equal input_path, temp_path.to_s # input path should be different - assert_match(/tailwind\.css/, temp_path.basename.to_s) # should use temp file - assert_includes [Dir.tmpdir, '/tmp'], temp_path.dirname.to_s # should be in temp directory - - # Check temp file contents - temp_content = File.read(actual) - expected_content = <<~CSS - @import "#{engine_css_path}"; - @import "#{input_path}"; - CSS - assert_equal expected_content.strip, temp_content.strip - end + def restore_rails_constant + Object.send(:remove_const, :Rails) if Object.const_defined?(:Rails) + Object.const_set(:Rails, @original_rails) if @original_rails + end + + def within_engine_configs + engine_configs = create_test_engines + with_rails_app do + Rails.application.config.tailwindcss_rails.engines = %w[test_engine1 test_engine2] + + # Create and register mock engine classes + engine_configs.each do |config| + engine_class = Class.new(Rails::Engine) + engine_class.engine_name = config.name + engine_class.root = Pathname.new(config.root) + Rails::Engine.descendants << engine_class end + + yield(*engine_configs) + end + end + + def create_test_engines + [1, 2, 3].map do |i| + engine = OpenStruct.new + engine.name = "test_engine#{i}" + engine.root = File.join(@tmp_dir, "engine#{i}") + engine.css_path = File.join(@tmp_dir, "app/assets/tailwind/test_engine#{i}/application.css") + FileUtils.mkdir_p(File.dirname(engine.css_path)) + FileUtils.touch(engine.css_path) + engine end end - end end From ba504f42a7a644569242c37dda823f596ba7b3b9 Mon Sep 17 00:00:00 2001 From: Sergey Moiseev Date: Sun, 27 Apr 2025 16:54:19 +0300 Subject: [PATCH 7/8] Update readme --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 41f6900..f9dad69 100644 --- a/README.md +++ b/README.md @@ -406,6 +406,14 @@ If you have Rails Engines in your application that use Tailwind CSS, they will b - The engine must have `tailwindcss-rails` as gem dependency. - The engine must have a `app/assets/tailwind//application.css` file or your application must have overridden file in the same location of your application root. +- The engine must register itself in Tailwindcss Rails: +```ruby + initializer 'your_engine.tailwindcss' do |app| + ActiveSupport.on_load(:tailwindcss_rails) do + config.tailwindcss_rails.engines << Your::Engine.engine_name + end + end +``` ## Troubleshooting From a9c9841095f9c09410aca816ac6adada9b372413 Mon Sep 17 00:00:00 2001 From: Sergey Moiseev Date: Fri, 16 May 2025 20:41:21 +0300 Subject: [PATCH 8/8] Replace OpenStruct with Struct --- Gemfile.lock | 2 +- test/lib/tailwindcss/commands_test.rb | 27 ++++++++++++++++----------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 9b0500d..3a61813 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - tailwindcss-rails (4.2.2) + tailwindcss-rails (4.2.3) railties (>= 7.0.0) tailwindcss-ruby (~> 4.0) diff --git a/test/lib/tailwindcss/commands_test.rb b/test/lib/tailwindcss/commands_test.rb index 94f3ed8..dbeacb8 100644 --- a/test/lib/tailwindcss/commands_test.rb +++ b/test/lib/tailwindcss/commands_test.rb @@ -1,5 +1,4 @@ require "test_helper" -require "ostruct" require "minitest/mock" class Tailwindcss::CommandsTest < ActiveSupport::TestCase @@ -227,6 +226,13 @@ def self.root end private + # Define Structs outside of methods to avoid redefining them + ConfigStruct = Struct.new(:engines) + AssetsStruct = Struct.new(:css_compressor) + TailwindStruct = Struct.new(:tailwindcss_rails, :assets) + AppStruct = Struct.new(:config) + EngineStruct = Struct.new(:name, :root, :css_path) + def with_rails_app Object.send(:remove_const, :Rails) if Object.const_defined?(:Rails) Object.const_set(:Rails, setup_mock_rails) @@ -257,12 +263,10 @@ def const_get(const_name) mock_rails.const_set(:Engine, mock_engine) mock_rails.root = Pathname.new(@tmp_dir) - mock_rails.application = OpenStruct.new( - config: OpenStruct.new( - tailwindcss_rails: OpenStruct.new(engines: []), - assets: OpenStruct.new(css_compressor: nil) - ) - ) + tailwind_config = ConfigStruct.new([]) + assets_config = AssetsStruct.new(nil) + app_config = TailwindStruct.new(tailwind_config, assets_config) + mock_rails.application = AppStruct.new(app_config) mock_rails end @@ -290,10 +294,11 @@ def within_engine_configs def create_test_engines [1, 2, 3].map do |i| - engine = OpenStruct.new - engine.name = "test_engine#{i}" - engine.root = File.join(@tmp_dir, "engine#{i}") - engine.css_path = File.join(@tmp_dir, "app/assets/tailwind/test_engine#{i}/application.css") + engine = EngineStruct.new( + "test_engine#{i}", + File.join(@tmp_dir, "engine#{i}"), + File.join(@tmp_dir, "app/assets/tailwind/test_engine#{i}/application.css") + ) FileUtils.mkdir_p(File.dirname(engine.css_path)) FileUtils.touch(engine.css_path) engine