From bc0af612146be23436fccfbdaf52c9f888728ce3 Mon Sep 17 00:00:00 2001 From: Sergey Moiseev Date: Wed, 29 Jan 2025 20:52:43 +0200 Subject: [PATCH 1/3] 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 7188adf..3b95990 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 20aa562ddee7e438d72b945de195d13b17308db9 Mon Sep 17 00:00:00 2001 From: Sergey Moiseev Date: Mon, 10 Mar 2025 20:14:21 +0200 Subject: [PATCH 2/3] 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 163b310010739016538153d9b59146f463a4140a Mon Sep 17 00:00:00 2001 From: Sergey Moiseev Date: Mon, 10 Mar 2025 20:23:23 +0200 Subject: [PATCH 3/3] 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