From 5c88f6eb55c862988dd6969b448ea895d32c4cd0 Mon Sep 17 00:00:00 2001 From: Sergey Moiseev Date: Mon, 7 Apr 2025 21:08:08 +0300 Subject: [PATCH 1/6] Engines pt.2 --- lib/tailwindcss/commands.rb | 48 ++++++----- lib/tasks/build.rake | 19 +++-- test/lib/tailwindcss/commands_test.rb | 110 +++++++++++++------------- 3 files changed, 91 insertions(+), 86 deletions(-) diff --git a/lib/tailwindcss/commands.rb b/lib/tailwindcss/commands.rb index b31e581b..0356c4b5 100644 --- a/lib/tailwindcss/commands.rb +++ b/lib/tailwindcss/commands.rb @@ -3,13 +3,20 @@ 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 remove_tempfile! + @@tempfile.unlink if @@tempfile + end + + 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", rails_root.join("app/assets/tailwind/application.css").to_s, + "-i", application_css, "-o", rails_root.join("app/assets/builds/tailwind.css").to_s, ] @@ -21,6 +28,21 @@ def compile_command(debug: false, **kwargs) command end + def application_css + if engines_roots.any? + @@tempfile = Tempfile.new("tailwind.application.css") + engines_roots.each do |root| + @@tempfile.puts "@import \"#{root}\";" + end + @@tempfile.puts "\n@import \"#{rails_root.join('app/assets/tailwind/application.css')}\";" + @@tempfile.close + + @@tempfile.path + else + rails_root.join("app/assets/tailwind/application.css").to_s + end + end + def watch_command(always: false, poll: false, **kwargs) compile_command(**kwargs).tap do |command| command << "-w" @@ -39,7 +61,7 @@ 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) Rails::Engine.subclasses.select do |engine| @@ -51,27 +73,11 @@ def engines_tailwindcss_roots end end.map do |engine| [ - Rails.root.join("app/assets/tailwind/#{engine.engine_name}/application.css"), + 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 7eef75eb..4d15f981 100644 --- a/lib/tasks/build.rake +++ b/lib/tasks/build.rake @@ -5,12 +5,11 @@ namespace :tailwindcss do verbose = args.extras.include?("verbose") command = Tailwindcss::Commands.compile_command(debug: debug) - Tailwindcss::Commands.enhance_command(command) do |transformed_command| - env = Tailwindcss::Commands.command_env(verbose: verbose) - puts "Running: #{Shellwords.join(command)}" if verbose + env = Tailwindcss::Commands.command_env(verbose: verbose) + puts "Running: #{Shellwords.join(command)}" if verbose - system(env, *command, exception: true) - end + system(env, *command, exception: true) + remove_tempfile! end desc "Watch and build your Tailwind CSS on file changes" @@ -21,14 +20,14 @@ namespace :tailwindcss do verbose = args.extras.include?("verbose") command = Tailwindcss::Commands.watch_command(always: always, debug: debug, poll: poll) - Tailwindcss::Commands.enhance_command(command) do |transformed_command| - env = Tailwindcss::Commands.command_env(verbose: verbose) - puts "Running: #{Shellwords.join(command)}" if verbose + env = Tailwindcss::Commands.command_env(verbose: verbose) + puts "Running: #{Shellwords.join(command)}" if verbose - system(env, *command) - end + system(env, *command) rescue Interrupt puts "Received interrupt, exiting tailwindcss:watch" if args.extras.include?("verbose") + ensure + remove_tempfile! end end diff --git a/test/lib/tailwindcss/commands_test.rb b/test/lib/tailwindcss/commands_test.rb index 2d525f2f..fdab6d6f 100644 --- a/test/lib/tailwindcss/commands_test.rb +++ b/test/lib/tailwindcss/commands_test.rb @@ -9,6 +9,14 @@ def setup @executable = Tailwindcss::Ruby.executable end + def teardown + super + if Tailwindcss::Commands.class_variable_defined?(:@@application_css) + Tailwindcss::Commands.remove_tempfile! + Tailwindcss::Commands.remove_class_variable(:@@application_css) + end + end + test ".compile_command" do Rails.stub(:root, File) do # Rails.root won't work in this test suite actual = Tailwindcss::Commands.compile_command @@ -127,15 +135,15 @@ def setup end end - test ".engines_tailwindcss_roots when there are no engines" do + test ".engines_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 + assert_empty Tailwindcss::Commands.engines_roots end end end - test ".engines_tailwindcss_roots when there are engines" do + test ".engines_roots when there are engines" do Dir.mktmpdir do |tmpdir| root = Pathname.new(tmpdir) @@ -173,20 +181,14 @@ def setup 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) + + [engine1_css, engine2_css, engine3_css].each do |css_path| + FileUtils.mkdir_p(File.dirname(css_path)) + FileUtils.touch(css_path) + end find_by_name_results = { "test_engine1" => spec1, @@ -197,7 +199,7 @@ def setup 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 + roots = Tailwindcss::Commands.engines_roots assert_equal 2, roots.size assert_includes roots, engine1_css.to_s @@ -212,63 +214,61 @@ def setup end end - test ".enhance_command when there are no engines" do + test ".application_css creates tempfile when engines exist" 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] + # Create necessary files + app_css = root.join("app/assets/tailwind/application.css") + FileUtils.mkdir_p(File.dirname(app_css)) + FileUtils.touch(app_css) + + engine_css = root.join("app/assets/tailwind/test_engine/application.css") + FileUtils.mkdir_p(File.dirname(engine_css)) + FileUtils.touch(engine_css) Rails.stub(:root, root) do - Tailwindcss::Commands.stub(:engines_tailwindcss_roots, []) do - Tailwindcss::Commands.enhance_command(command) do |actual| - assert_equal command, actual - end + Tailwindcss::Commands.stub(:engines_roots, [engine_css.to_s]) do + css_path = Tailwindcss::Commands.application_css + assert Tailwindcss::Commands.tempfile_path?(css_path) + + content = File.read(css_path) + assert_match "@import \"#{engine_css}\";", content + assert_match "@import \"#{app_css}\";", content end end end end - test ".enhance_command when there are engines" do + test ".application_css uses application.css when no engines exist" 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) + app_css = root.join("app/assets/tailwind/application.css") + FileUtils.mkdir_p(File.dirname(app_css)) + FileUtils.touch(app_css) - # 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) + Rails.stub(:root, root) do + Tailwindcss::Commands.stub(:engines_roots, []) do + css_path = Tailwindcss::Commands.application_css + assert_equal app_css.to_s, css_path + end + end + end + end - command = ["tailwindcss", "-i", input_path.to_s, "-o", output_path.to_s] + test ".remove_tempfile! cleans up temporary file" do + Dir.mktmpdir do |tmpdir| + root = Pathname.new(tmpdir) 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 + Tailwindcss::Commands.stub(:engines_roots, ["dummy_engine"]) do + css_path = Tailwindcss::Commands.application_css + assert File.exist?(css_path) + + debugger + Tailwindcss::Commands.remove_tempfile! + refute File.exist?(css_path) end end end From 0bd65bc8eb79a81baa493b1d354bbe3b759566de Mon Sep 17 00:00:00 2001 From: Sergey Moiseev Date: Mon, 7 Apr 2025 21:34:59 +0300 Subject: [PATCH 2/6] WiP --- lib/tailwindcss/commands.rb | 25 +++++++++++++------------ test/lib/tailwindcss/commands_test.rb | 6 ++---- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/lib/tailwindcss/commands.rb b/lib/tailwindcss/commands.rb index 0356c4b5..eef6356f 100644 --- a/lib/tailwindcss/commands.rb +++ b/lib/tailwindcss/commands.rb @@ -8,7 +8,10 @@ def rails_root end def remove_tempfile! - @@tempfile.unlink if @@tempfile + if @@tempfile + @@tempfile.unlink + @@tempfile = nil + end end def compile_command(input: application_css, debug: false, **kwargs) @@ -16,7 +19,7 @@ def compile_command(input: application_css, debug: false, **kwargs) command = [ Tailwindcss::Ruby.executable(**kwargs), - "-i", application_css, + "-i", application_css.to_s, "-o", rails_root.join("app/assets/builds/tailwind.css").to_s, ] @@ -29,18 +32,16 @@ def compile_command(input: application_css, debug: false, **kwargs) end def application_css - if engines_roots.any? - @@tempfile = Tempfile.new("tailwind.application.css") - engines_roots.each do |root| - @@tempfile.puts "@import \"#{root}\";" - end - @@tempfile.puts "\n@import \"#{rails_root.join('app/assets/tailwind/application.css')}\";" - @@tempfile.close + return rails_root.join("app/assets/tailwind/application.css").to_s if engines_roots.empty? - @@tempfile.path - else - rails_root.join("app/assets/tailwind/application.css").to_s + @@tempfile = Tempfile.new("tailwind.application.css") + engines_roots.each do |root| + @@tempfile.puts "@import \"#{root}\";" end + @@tempfile.puts "\n@import \"#{rails_root.join('app/assets/tailwind/application.css')}\";" + @@tempfile.close + + @@tempfile.path end def watch_command(always: false, poll: false, **kwargs) diff --git a/test/lib/tailwindcss/commands_test.rb b/test/lib/tailwindcss/commands_test.rb index fdab6d6f..da78761e 100644 --- a/test/lib/tailwindcss/commands_test.rb +++ b/test/lib/tailwindcss/commands_test.rb @@ -11,9 +11,8 @@ def setup def teardown super - if Tailwindcss::Commands.class_variable_defined?(:@@application_css) + if Tailwindcss::Commands.class_variable_defined?(:@@tempfile) Tailwindcss::Commands.remove_tempfile! - Tailwindcss::Commands.remove_class_variable(:@@application_css) end end @@ -230,7 +229,7 @@ def teardown Rails.stub(:root, root) do Tailwindcss::Commands.stub(:engines_roots, [engine_css.to_s]) do css_path = Tailwindcss::Commands.application_css - assert Tailwindcss::Commands.tempfile_path?(css_path) + assert_equal css_path, Tailwindcss::Commands.class_variable_get(:@@tempfile).path content = File.read(css_path) assert_match "@import \"#{engine_css}\";", content @@ -266,7 +265,6 @@ def teardown css_path = Tailwindcss::Commands.application_css assert File.exist?(css_path) - debugger Tailwindcss::Commands.remove_tempfile! refute File.exist?(css_path) end From b34e6323406d2e7aef636398085bb7c2b1ae59a4 Mon Sep 17 00:00:00 2001 From: Sergey Moiseev Date: Tue, 8 Apr 2025 02:03:26 +0300 Subject: [PATCH 3/6] Fix tests --- lib/tailwindcss/commands.rb | 24 +++-- lib/tailwindcss/engine.rb | 4 + test/lib/tailwindcss/commands_test.rb | 135 +++++++++++++++----------- 3 files changed, 92 insertions(+), 71 deletions(-) diff --git a/lib/tailwindcss/commands.rb b/lib/tailwindcss/commands.rb index eef6356f..5c06c52c 100644 --- a/lib/tailwindcss/commands.rb +++ b/lib/tailwindcss/commands.rb @@ -8,9 +8,9 @@ def rails_root end def remove_tempfile! - if @@tempfile - @@tempfile.unlink - @@tempfile = nil + if class_variable_defined?(:@@tempfile) && @@tempfile + @@tempfile.unlink if File.exist?(@@tempfile.path) + remove_class_variable(:@@tempfile) end end @@ -19,7 +19,7 @@ def compile_command(input: application_css, debug: false, **kwargs) command = [ Tailwindcss::Ruby.executable(**kwargs), - "-i", application_css.to_s, + "-i", input.to_s, "-o", rails_root.join("app/assets/builds/tailwind.css").to_s, ] @@ -35,10 +35,13 @@ def application_css return rails_root.join("app/assets/tailwind/application.css").to_s if engines_roots.empty? @@tempfile = Tempfile.new("tailwind.application.css") + + # Write content to tempfile engines_roots.each do |root| - @@tempfile.puts "@import \"#{root}\";" + @@tempfile.write("@import \"#{root}\";\n") end - @@tempfile.puts "\n@import \"#{rails_root.join('app/assets/tailwind/application.css')}\";" + @@tempfile.write("\n@import \"#{rails_root.join('app/assets/tailwind/application.css')}\";\n") + @@tempfile.flush @@tempfile.close @@tempfile.path @@ -65,13 +68,8 @@ def rails_css_compressor? def engines_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 + Rails::Engine.descendants.select do |engine| + engine.engine_name.in?(Rails.application.config.tailwindcss_rails.engines) end.map do |engine| [ rails_root.join("app/assets/tailwind/#{engine.engine_name}/application.css"), diff --git a/lib/tailwindcss/engine.rb b/lib/tailwindcss/engine.rb index 7b88c5f1..3bc91b99 100644 --- a/lib/tailwindcss/engine.rb +++ b/lib/tailwindcss/engine.rb @@ -2,6 +2,10 @@ module Tailwindcss class Engine < ::Rails::Engine + initializer 'tailwindcss.add_engines_roots_config' do + Rails.application.config.tailwindcss_rails.engines = [] + 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 da78761e..a9aee044 100644 --- a/test/lib/tailwindcss/commands_test.rb +++ b/test/lib/tailwindcss/commands_test.rb @@ -1,5 +1,8 @@ require "test_helper" require "minitest/mock" +require "tmpdir" +require "ostruct" +require "active_support/core_ext/class/subclasses" class Tailwindcss::CommandsTest < ActiveSupport::TestCase attr_accessor :executable @@ -16,8 +19,31 @@ def teardown end end + def with_rails_mocks(root: Pathname.new(Dir.mktmpdir), engines: []) + mock_config = Object.new + def mock_config.tailwindcss_rails + @tailwindcss_rails ||= OpenStruct.new(engines: @engines) + end + def mock_config.assets + @assets ||= OpenStruct.new(css_compressor: nil) + end + mock_config.instance_variable_set(:@engines, engines) + + mock_application = Object.new + def mock_application.config + @config + end + mock_application.instance_variable_set(:@config, mock_config) + + Rails.stub(:root, root) do + Rails.stub(:application, mock_application) do + yield + end + end + end + test ".compile_command" do - Rails.stub(:root, File) do # Rails.root won't work in this test suite + with_rails_mocks do actual = Tailwindcss::Commands.compile_command assert_kind_of(Array, actual) assert_equal(executable, actual.first) @@ -27,7 +53,7 @@ def teardown end test ".compile_command debug flag" do - Rails.stub(:root, File) do # Rails.root won't work in this test suite + with_rails_mocks do actual = Tailwindcss::Commands.compile_command assert_kind_of(Array, actual) assert_equal(executable, actual.first) @@ -42,7 +68,7 @@ def teardown test ".compile_command debug environment variable" do begin - Rails.stub(:root, File) do # Rails.root won't work in this test suite + with_rails_mocks do ENV["TAILWINDCSS_DEBUG"] = "" actual = Tailwindcss::Commands.compile_command assert_kind_of(Array, actual) @@ -67,7 +93,7 @@ def teardown end test ".compile_command when Rails compression is on" do - Rails.stub(:root, File) do # Rails.root won't work in this test suite + with_rails_mocks do Tailwindcss::Commands.stub(:rails_css_compressor?, true) do actual = Tailwindcss::Commands.compile_command assert_kind_of(Array, actual) @@ -84,13 +110,14 @@ def teardown test ".compile_command when postcss.config.js exists" do Dir.mktmpdir do |tmpdir| - Rails.stub(:root, Pathname.new(tmpdir)) do # Rails.root won't work in this test suite + root = Pathname.new(tmpdir) + with_rails_mocks(root: root) do actual = Tailwindcss::Commands.compile_command assert_kind_of(Array, actual) assert_equal(executable, actual.first) refute_includes(actual, "--postcss") - config_file = Rails.root.join("postcss.config.js") + config_file = root.join("postcss.config.js") FileUtils.touch(config_file) actual = Tailwindcss::Commands.compile_command assert_kind_of(Array, actual) @@ -103,7 +130,7 @@ def teardown end test ".watch_command" do - Rails.stub(:root, File) do # Rails.root won't work in this test suite + with_rails_mocks do actual = Tailwindcss::Commands.watch_command assert_kind_of(Array, actual) assert_equal(executable, actual.first) @@ -135,7 +162,7 @@ def teardown end test ".engines_roots when there are no engines" do - Rails.stub(:root, Pathname.new("/dummy")) do + with_rails_mocks do Rails::Engine.stub(:subclasses, []) do assert_empty Tailwindcss::Commands.engines_roots end @@ -169,16 +196,6 @@ def teardown define_singleton_method(:root) { engine_root3 } 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 engine1_css = engine_root1.join("app/assets/tailwind/test_engine1/application.css") engine2_css = root.join("app/assets/tailwind/test_engine2/application.css") @@ -189,27 +206,16 @@ def teardown FileUtils.touch(css_path) end - 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_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 + with_rails_mocks(root: root, engines: %w[test_engine1 test_engine2]) do + Rails::Engine.stub(:descendants, [engine1, engine2, engine3]) do + roots = Tailwindcss::Commands.engines_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 - - spec1.verify - spec2.verify end end @@ -217,23 +223,36 @@ def teardown Dir.mktmpdir do |tmpdir| root = Pathname.new(tmpdir) - # Create necessary files + # Create engine files + engine_root = root.join('engine1') + FileUtils.mkdir_p(engine_root) + + engine = Class.new(Rails::Engine) do + define_singleton_method(:engine_name) { "test_engine" } + define_singleton_method(:root) { engine_root } + end + app_css = root.join("app/assets/tailwind/application.css") - FileUtils.mkdir_p(File.dirname(app_css)) - FileUtils.touch(app_css) + engine_css = engine_root.join("app/assets/tailwind/test_engine/application.css") - engine_css = root.join("app/assets/tailwind/test_engine/application.css") + FileUtils.mkdir_p(File.dirname(app_css)) FileUtils.mkdir_p(File.dirname(engine_css)) + FileUtils.touch(app_css) FileUtils.touch(engine_css) - Rails.stub(:root, root) do - Tailwindcss::Commands.stub(:engines_roots, [engine_css.to_s]) do + with_rails_mocks(root: root, engines: ["test_engine"]) do + Rails::Engine.stub(:descendants, [engine]) do + Tailwindcss::Commands.remove_tempfile! css_path = Tailwindcss::Commands.application_css - assert_equal css_path, Tailwindcss::Commands.class_variable_get(:@@tempfile).path + assert File.exist?(css_path), "Tempfile should exist" content = File.read(css_path) - assert_match "@import \"#{engine_css}\";", content - assert_match "@import \"#{app_css}\";", content + expected_content = <<~CSS + @import "#{engine_css}"; + + @import "#{app_css}"; + CSS + assert_equal expected_content, content end end end @@ -247,11 +266,9 @@ def teardown FileUtils.mkdir_p(File.dirname(app_css)) FileUtils.touch(app_css) - Rails.stub(:root, root) do - Tailwindcss::Commands.stub(:engines_roots, []) do - css_path = Tailwindcss::Commands.application_css - assert_equal app_css.to_s, css_path - end + with_rails_mocks(root: root) do + css_path = Tailwindcss::Commands.application_css + assert_equal app_css.to_s, css_path end end end @@ -259,15 +276,17 @@ def teardown test ".remove_tempfile! cleans up temporary file" do Dir.mktmpdir do |tmpdir| root = Pathname.new(tmpdir) + app_css = root.join("app/assets/tailwind/application.css") + FileUtils.mkdir_p(File.dirname(app_css)) + FileUtils.touch(app_css) - Rails.stub(:root, root) do - Tailwindcss::Commands.stub(:engines_roots, ["dummy_engine"]) do - css_path = Tailwindcss::Commands.application_css - assert File.exist?(css_path) + with_rails_mocks(root: root, engines: ["test_engine"]) do + Tailwindcss::Commands.remove_tempfile! + css_path = Tailwindcss::Commands.application_css + assert File.exist?(css_path), "Tempfile should exist before removal" - Tailwindcss::Commands.remove_tempfile! - refute File.exist?(css_path) - end + Tailwindcss::Commands.remove_tempfile! + refute File.exist?(css_path), "Tempfile should not exist after removal" end end end From 8bb73e8a07e14e1456db3f3036ea63c52c4f3b36 Mon Sep 17 00:00:00 2001 From: Sergey Moiseev Date: Tue, 8 Apr 2025 02:57:58 +0300 Subject: [PATCH 4/6] More fixes --- lib/tailwindcss/commands.rb | 10 +- test/lib/tailwindcss/commands_test.rb | 419 ++++++++++++-------------- 2 files changed, 196 insertions(+), 233 deletions(-) diff --git a/lib/tailwindcss/commands.rb b/lib/tailwindcss/commands.rb index 5c06c52c..87925b3a 100644 --- a/lib/tailwindcss/commands.rb +++ b/lib/tailwindcss/commands.rb @@ -8,10 +8,11 @@ def rails_root end def remove_tempfile! - if class_variable_defined?(:@@tempfile) && @@tempfile - @@tempfile.unlink if File.exist?(@@tempfile.path) - remove_class_variable(:@@tempfile) - end + return unless class_variable_defined?(:@@tempfile) && @@tempfile + + @@tempfile.close unless @@tempfile.closed? + @@tempfile.unlink if File.exist?(@@tempfile.path) + remove_class_variable(:@@tempfile) end def compile_command(input: application_css, debug: false, **kwargs) @@ -67,6 +68,7 @@ def rails_css_compressor? def engines_roots return [] unless defined?(Rails) + return [] unless Rails.application&.config&.tailwindcss_rails&.engines Rails::Engine.descendants.select do |engine| engine.engine_name.in?(Rails.application.config.tailwindcss_rails.engines) diff --git a/test/lib/tailwindcss/commands_test.rb b/test/lib/tailwindcss/commands_test.rb index a9aee044..4d1ebd71 100644 --- a/test/lib/tailwindcss/commands_test.rb +++ b/test/lib/tailwindcss/commands_test.rb @@ -1,293 +1,254 @@ require "test_helper" -require "minitest/mock" -require "tmpdir" require "ostruct" -require "active_support/core_ext/class/subclasses" +require "tmpdir" +require "rails/engine" class Tailwindcss::CommandsTest < ActiveSupport::TestCase - attr_accessor :executable - - 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 - def teardown - super - if Tailwindcss::Commands.class_variable_defined?(:@@tempfile) - Tailwindcss::Commands.remove_tempfile! + 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 includes basic options" do + with_rails_app do + command = Tailwindcss::Commands.compile_command + + assert_equal @executable, command.first + assert_includes command, "-i" + assert_includes command, "-o" + assert_includes command, "--minify" end end - def with_rails_mocks(root: Pathname.new(Dir.mktmpdir), engines: []) - mock_config = Object.new - def mock_config.tailwindcss_rails - @tailwindcss_rails ||= OpenStruct.new(engines: @engines) + test "compile command respects debug flag" do + with_rails_app do + debug_command = Tailwindcss::Commands.compile_command(debug: true) + refute_includes debug_command, "--minify" end - def mock_config.assets - @assets ||= OpenStruct.new(css_compressor: nil) + end + + test "compile command respects TAILWINDCSS_DEBUG env var" do + with_rails_app do + ENV["TAILWINDCSS_DEBUG"] = "1" + command = Tailwindcss::Commands.compile_command + refute_includes command, "--minify" + ensure + ENV.delete("TAILWINDCSS_DEBUG") end - mock_config.instance_variable_set(:@engines, engines) + end + + test "watch command includes correct options" do + with_rails_app do + command = Tailwindcss::Commands.watch_command - mock_application = Object.new - def mock_application.config - @config + assert_equal @executable, command.first + assert_includes command, "-w" + assert_includes command, "--minify" + refute_includes command, "-p" end - mock_application.instance_variable_set(:@config, mock_config) + end - Rails.stub(:root, root) do - Rails.stub(:application, mock_application) do - yield - end + test "watch command with poll option" do + with_rails_app do + command = Tailwindcss::Commands.watch_command(poll: true) + assert_includes command, "-p" end end - test ".compile_command" do - with_rails_mocks do - actual = Tailwindcss::Commands.compile_command - assert_kind_of(Array, actual) - assert_equal(executable, actual.first) - assert_includes(actual, "-i") - assert_includes(actual, "-o") + 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 ".compile_command debug flag" do - with_rails_mocks do - actual = Tailwindcss::Commands.compile_command - assert_kind_of(Array, actual) - assert_equal(executable, actual.first) - assert_includes(actual, "--minify") - - actual = Tailwindcss::Commands.compile_command(debug: true) - assert_kind_of(Array, actual) - assert_equal(executable, actual.first) - refute_includes(actual, "--minify") + 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 ".compile_command debug environment variable" do - begin - with_rails_mocks do - ENV["TAILWINDCSS_DEBUG"] = "" - actual = Tailwindcss::Commands.compile_command - assert_kind_of(Array, actual) - assert_includes(actual, "--minify") - - actual = Tailwindcss::Commands.compile_command(debug: true) - assert_kind_of(Array, actual) - assert_includes(actual, "--minify") - - ENV["TAILWINDCSS_DEBUG"] = "any non-blank value" - actual = Tailwindcss::Commands.compile_command - assert_kind_of(Array, actual) - refute_includes(actual, "--minify") - - actual = Tailwindcss::Commands.compile_command(debug: true) - assert_kind_of(Array, actual) - refute_includes(actual, "--minify") - end - ensure - ENV.delete('TAILWINDCSS_DEBUG') + test ".rails_css_compressor? when css_compressor is not configured" do + with_rails_app do + Rails.application.config.assets.css_compressor = nil + refute Tailwindcss::Commands.rails_css_compressor? end end - test ".compile_command when Rails compression is on" do - with_rails_mocks do - Tailwindcss::Commands.stub(:rails_css_compressor?, true) do - actual = Tailwindcss::Commands.compile_command - assert_kind_of(Array, actual) - refute_includes(actual, "--minify") - end + test ".command_env with verbose flag" do + env = Tailwindcss::Commands.command_env(verbose: true) + assert_equal "1", env["DEBUG"] + end - Tailwindcss::Commands.stub(:rails_css_compressor?, false) do - actual = Tailwindcss::Commands.compile_command - assert_kind_of(Array, actual) - assert_includes(actual, "--minify") - end - end + test ".command_env without verbose flag" do + env = Tailwindcss::Commands.command_env(verbose: false) + assert_empty env end - test ".compile_command when postcss.config.js exists" do - Dir.mktmpdir do |tmpdir| - root = Pathname.new(tmpdir) - with_rails_mocks(root: root) do - actual = Tailwindcss::Commands.compile_command - assert_kind_of(Array, actual) - assert_equal(executable, actual.first) - refute_includes(actual, "--postcss") - - config_file = root.join("postcss.config.js") - FileUtils.touch(config_file) - actual = Tailwindcss::Commands.compile_command - assert_kind_of(Array, actual) - assert_equal(executable, actual.first) - assert_includes(actual, "--postcss") - postcss_index = actual.index("--postcss") - assert_equal(actual[postcss_index + 1], config_file.to_s) - end + test ".application_css creates tempfile when engines exist" do + within_engine_configs do |engine1, engine2| + css_path = Tailwindcss::Commands.application_css + + assert_match(/tailwind\.application\.css/, css_path) + assert File.exist?(css_path) + + 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 - test ".watch_command" do - with_rails_mocks do - actual = Tailwindcss::Commands.watch_command - assert_kind_of(Array, actual) - assert_equal(executable, actual.first) - assert_includes(actual, "-w") - refute_includes(actual, "-p") - assert_includes(actual, "--minify") - - actual = Tailwindcss::Commands.watch_command(debug: true) - assert_kind_of(Array, actual) - assert_equal(executable, actual.first) - assert_includes(actual, "-w") - refute_includes(actual, "-p") - refute_includes(actual, "--minify") - - actual = Tailwindcss::Commands.watch_command(poll: true) - assert_kind_of(Array, actual) - assert_equal(executable, actual.first) - assert_includes(actual, "-w") - refute_includes(actual, "always") - assert_includes(actual, "-p") - assert_includes(actual, "--minify") - - actual = Tailwindcss::Commands.watch_command(always: true) - assert_kind_of(Array, actual) - assert_equal(executable, actual.first) - assert_includes(actual, "-w") - assert_includes(actual, "always") + test ".application_css returns application.css path when no engines" do + with_rails_app do + expected_path = Rails.root.join("app/assets/tailwind/application.css").to_s + assert_equal expected_path, Tailwindcss::Commands.application_css end end - test ".engines_roots when there are no engines" do - with_rails_mocks do - Rails::Engine.stub(:subclasses, []) do - assert_empty Tailwindcss::Commands.engines_roots - end + test ".application_css handles tempfile cleanup" do + within_engine_configs do + css_path = Tailwindcss::Commands.application_css + assert File.exist?(css_path) + + Tailwindcss::Commands.remove_tempfile! + refute File.exist?(css_path) end end - test ".engines_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 can be configured via ActiveSupport.on_load" do + with_rails_app do + # Create a test engine + test_engine = Class.new(Rails::Engine) do + def self.engine_name + "test_engine" + end - engine2 = Class.new(Rails::Engine) do - define_singleton_method(:engine_name) { "test_engine2" } - define_singleton_method(:root) { engine_root2 } + def self.root + Pathname.new(Dir.mktmpdir) + end end - engine3 = Class.new(Rails::Engine) do - define_singleton_method(:engine_name) { "test_engine3" } - define_singleton_method(:root) { engine_root3 } - end + # 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) - # Set up file structure - engine1_css = engine_root1.join("app/assets/tailwind/test_engine1/application.css") - engine2_css = root.join("app/assets/tailwind/test_engine2/application.css") - engine3_css = engine_root2.join("app/assets/tailwind/test_engine3/application.css") + # 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) - [engine1_css, engine2_css, engine3_css].each do |css_path| - FileUtils.mkdir_p(File.dirname(css_path)) - FileUtils.touch(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 - with_rails_mocks(root: root, engines: %w[test_engine1 test_engine2]) do - Rails::Engine.stub(:descendants, [engine1, engine2, engine3]) do - roots = Tailwindcss::Commands.engines_roots + # Trigger the hook manually + ActiveSupport.run_load_hooks(:tailwindcss_rails, hook) - 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 + # 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 ".application_css creates tempfile when engines exist" do - Dir.mktmpdir do |tmpdir| - root = Pathname.new(tmpdir) + private + def with_rails_app + Object.send(:remove_const, :Rails) if Object.const_defined?(:Rails) + Object.const_set(:Rails, setup_mock_rails) + yield + end - # Create engine files - engine_root = root.join('engine1') - FileUtils.mkdir_p(engine_root) + def setup_mock_rails + mock_engine = Class.new do + class << self + attr_accessor :engine_name, :root - engine = Class.new(Rails::Engine) do - define_singleton_method(:engine_name) { "test_engine" } - define_singleton_method(:root) { engine_root } + def descendants + @descendants ||= [] + end + end end - app_css = root.join("app/assets/tailwind/application.css") - engine_css = engine_root.join("app/assets/tailwind/test_engine/application.css") - - FileUtils.mkdir_p(File.dirname(app_css)) - FileUtils.mkdir_p(File.dirname(engine_css)) - FileUtils.touch(app_css) - FileUtils.touch(engine_css) - - with_rails_mocks(root: root, engines: ["test_engine"]) do - Rails::Engine.stub(:descendants, [engine]) do - Tailwindcss::Commands.remove_tempfile! - css_path = Tailwindcss::Commands.application_css - - assert File.exist?(css_path), "Tempfile should exist" - content = File.read(css_path) - expected_content = <<~CSS - @import "#{engine_css}"; - - @import "#{app_css}"; - CSS - assert_equal expected_content, content + mock_rails = Class.new do + class << self + attr_accessor :root, :application + + def const_get(const_name) + return Engine if const_name == :Engine + super + end end end - end - end - test ".application_css uses application.css when no engines exist" do - Dir.mktmpdir do |tmpdir| - root = Pathname.new(tmpdir) - - app_css = root.join("app/assets/tailwind/application.css") - FileUtils.mkdir_p(File.dirname(app_css)) - FileUtils.touch(app_css) + 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 + + 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 - with_rails_mocks(root: root) do - css_path = Tailwindcss::Commands.application_css - assert_equal app_css.to_s, css_path + yield(*engine_configs) end end - end - test ".remove_tempfile! cleans up temporary file" do - Dir.mktmpdir do |tmpdir| - root = Pathname.new(tmpdir) - app_css = root.join("app/assets/tailwind/application.css") - FileUtils.mkdir_p(File.dirname(app_css)) - FileUtils.touch(app_css) - - with_rails_mocks(root: root, engines: ["test_engine"]) do - Tailwindcss::Commands.remove_tempfile! - css_path = Tailwindcss::Commands.application_css - assert File.exist?(css_path), "Tempfile should exist before removal" - - Tailwindcss::Commands.remove_tempfile! - refute File.exist?(css_path), "Tempfile should not exist after removal" + 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 df08588547c1fce6d7e4ca0c2aaf68d651ef1452 Mon Sep 17 00:00:00 2001 From: Sergey Moiseev Date: Tue, 8 Apr 2025 21:09:35 +0300 Subject: [PATCH 5/6] Fix few issues --- lib/tailwindcss/engine.rb | 9 +++++++-- lib/tasks/build.rake | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/tailwindcss/engine.rb b/lib/tailwindcss/engine.rb index 3bc91b99..b17d8baa 100644 --- a/lib/tailwindcss/engine.rb +++ b/lib/tailwindcss/engine.rb @@ -2,8 +2,13 @@ module Tailwindcss class Engine < ::Rails::Engine - initializer 'tailwindcss.add_engines_roots_config' do - Rails.application.config.tailwindcss_rails.engines = [] + config.before_configuration do |app| + app.config.tailwindcss_rails = ActiveSupport::OrderedOptions.new + app.config.tailwindcss_rails.engines = [] + end + + initializer 'tailwindcss.load_hook' do |app| + ActiveSupport.run_load_hooks(:tailwindcss_rails, app) end initializer "tailwindcss.disable_generator_stylesheets" do diff --git a/lib/tasks/build.rake b/lib/tasks/build.rake index 4d15f981..2b6b1ebf 100644 --- a/lib/tasks/build.rake +++ b/lib/tasks/build.rake @@ -9,7 +9,7 @@ namespace :tailwindcss do puts "Running: #{Shellwords.join(command)}" if verbose system(env, *command, exception: true) - remove_tempfile! + Tailwindcss::Commands.remove_tempfile! end desc "Watch and build your Tailwind CSS on file changes" @@ -27,7 +27,7 @@ namespace :tailwindcss do rescue Interrupt puts "Received interrupt, exiting tailwindcss:watch" if args.extras.include?("verbose") ensure - remove_tempfile! + Tailwindcss::Commands.remove_tempfile! end end From 15ba6327f61c45b2713fb9c29a591943353baa94 Mon Sep 17 00:00:00 2001 From: Sergey Moiseev Date: Tue, 8 Apr 2025 21:16:45 +0300 Subject: [PATCH 6/6] Update docs --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 3b959906..e5304fb5 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,9 @@ * [Using Tailwind plugins](#using-tailwind-plugins) * [Using with PostCSS](#using-with-postcss) * [Custom inputs or outputs](#custom-inputs-or-outputs) +- [Rails Engines support](#rails-engines-support) - [Troubleshooting](#troubleshooting) + * [The `watch` command is hanging](#the-watch-command-is-hanging) * [Lost keystrokes or hanging when using terminal-based debugging tools (e.g. IRB, Pry, `ruby/debug`...etc.) with the Puma plugin](#lost-keystrokes-or-hanging-when-using-terminal-based-debugging-tools-eg-irb-pry-rubydebugetc-with-the-puma-plugin) * [Running in a docker container exits prematurely](#running-in-a-docker-container-exits-prematurely) * [Conflict with sassc-rails](#conflict-with-sassc-rails) @@ -406,6 +408,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