From e41e00c0be3512e36ec0058f419fc0501ed6ab99 Mon Sep 17 00:00:00 2001 From: Sergey Moiseev Date: Fri, 11 Apr 2025 00:26:14 +0300 Subject: [PATCH 1/4] 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 b31e581b..1bab3371 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 7eef75eb..1fecda29 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 2d525f2f..9c0d029b 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 6c67f91e87e4f8b724841262a40d63bdb5c79d15 Mon Sep 17 00:00:00 2001 From: Sergey Moiseev Date: Sun, 27 Apr 2025 15:57:05 +0300 Subject: [PATCH 2/4] 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 7b88c5f1..629ea492 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 3fec438a21b0bbab93eee0d78c0b4707c2de6640 Mon Sep 17 00:00:00 2001 From: Sergey Moiseev Date: Sun, 27 Apr 2025 16:52:23 +0300 Subject: [PATCH 3/4] 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 1bab3371..f5be585b 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 629ea492..6beb65ba 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 9c0d029b..94f3ed84 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 bcfcb602ecc86ba689ed1b2bd1d5165ddc06528e Mon Sep 17 00:00:00 2001 From: Sergey Moiseev Date: Sun, 27 Apr 2025 16:54:19 +0300 Subject: [PATCH 4/4] Update readme --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 3b959906..7dde503e 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