diff --git a/lib/install/tailwindcss.rb b/lib/install/tailwindcss.rb index 413f86b8..40f18746 100644 --- a/lib/install/tailwindcss.rb +++ b/lib/install/tailwindcss.rb @@ -1,5 +1,6 @@ APPLICATION_LAYOUT_PATH = Rails.root.join("app/views/layouts/application.html.erb") CENTERING_CONTAINER_INSERTION_POINT = /^\s*<%= yield %>/.freeze +DEVELOPMENT_ENVIRONMENT_CONFIG_PATH = Rails.root.join("config/environments/development.rb") if APPLICATION_LAYOUT_PATH.exist? say "Add Tailwindcss include tags and container element in application layout" @@ -16,6 +17,15 @@ say %( Add <%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %> within the <head> tag in your custom layout.) end +if DEVELOPMENT_ENVIRONMENT_CONFIG_PATH.exist? + say "Enable Tailwindcss server process in development" + insert_into_file DEVELOPMENT_ENVIRONMENT_CONFIG_PATH.to_s, <<~ERB.indent(2), before: /^end$/ + + # Automatically watch and build Tailwindcss when 'rails server' is started. + config.tailwindcss.server_process = true + ERB +end + say "Build into app/assets/builds" empty_directory "app/assets/builds" keep_file "app/assets/builds" diff --git a/lib/tailwindcss-rails.rb b/lib/tailwindcss-rails.rb index 112809c5..867663c6 100644 --- a/lib/tailwindcss-rails.rb +++ b/lib/tailwindcss-rails.rb @@ -5,3 +5,4 @@ module Tailwindcss require_relative "tailwindcss/version" require_relative "tailwindcss/engine" require_relative "tailwindcss/commands" +require_relative "tailwindcss/server_process" diff --git a/lib/tailwindcss/engine.rb b/lib/tailwindcss/engine.rb index 4b9b9fdc..fb60f1e3 100644 --- a/lib/tailwindcss/engine.rb +++ b/lib/tailwindcss/engine.rb @@ -2,6 +2,9 @@ module Tailwindcss class Engine < ::Rails::Engine + config.tailwindcss = ActiveSupport::OrderedOptions.new + config.tailwindcss.server_process = false # Rails.env.development? + initializer "tailwindcss.assets" do Rails.application.config.assets.precompile += %w( inter-font.css ) end @@ -13,5 +16,9 @@ class Engine < ::Rails::Engine config.app_generators do |g| g.template_engine :tailwindcss end + + server do + ServerProcess.start if config.tailwindcss.server_process + end end end diff --git a/lib/tailwindcss/server_process.rb b/lib/tailwindcss/server_process.rb new file mode 100644 index 00000000..fbca382c --- /dev/null +++ b/lib/tailwindcss/server_process.rb @@ -0,0 +1,149 @@ +module Tailwindcss + class ServerProcess + attr_reader :server, :pid + + def self.start + new.start + end + + def initialize + @server = Server.new(self) + end + + def start + @pid = existing_process || start_process + server.monitor_process + server.exit_hook + end + + def stop + return if dead? + + Process.kill(:INT, pid) + Process.wait(pid) + rescue Errno::ECHILD, Errno::ESRCH + end + + def dead? + Process.wait(pid, Process::WNOHANG) + false + rescue Errno::ECHILD, Errno::ESRCH + true + end + + private + + def existing_process + if (pid = Pidfile.pid) + begin + Process.kill 0, pid + pid + rescue Errno::ESRCH + # Process does not exist + rescue Errno::EPERM + # Ignore process owned by another user + end + end + end + + def start_process + pid = fork do + Pidfile.write + monitor_server + exit_hook + # Using IO.popen(command, 'r+') will avoid watch_command read from $stdin. + # If we use system(*command) instead, IRB and Debug can't read from $stdin + # correctly bacause some keystrokes will be taken by watch_command. + IO.popen(Commands.watch_command, 'r+') do |io| + IO.copy_stream(io, $stdout) + end + ensure + Pidfile.delete + end + Process.detach pid + pid + end + + def monitor_server + Thread.new do + loop do + if server.dead? + puts "Tailwind detected server has gone away" + exit + end + sleep 2 + end + end + end + + def exit_hook + at_exit do + puts "Stopping tailwind..." + server.stop + end + end + + module Pidfile + def self.path + Rails.root.join("tmp", "pids", "tailwindcss.txt") + end + + def self.read + File.read(path, mode: "rb:UTF-8") + rescue Errno::ENOENT + # File does not exist + end + + def self.write + File.write(path, Process.pid, mode: "wb:UTF-8") + end + + def self.delete + File.exist?(path) && File.delete(path) + end + + def self.pid + Integer(read) + rescue ArgumentError, TypeError + # Invalid content + delete + end + end + + class Server + attr_reader :process, :pid + + def initialize(process) + @process = process + @pid = Process.pid + end + + def monitor_process + Thread.new do + loop do + if process.dead? + puts "Detected tailwind has gone away, stopping server..." + exit + end + sleep 2 + end + end + end + + def exit_hook + at_exit do + process.stop + end + end + + def dead? + Process.ppid != pid + end + + def stop + Process.kill(:INT, pid) + rescue Errno::ECHILD, Errno::ESRCH + end + end + end +end