diff --git a/lib/react_on_rails/doctor.rb b/lib/react_on_rails/doctor.rb index efe5f80006..dd3cdbac48 100644 --- a/lib/react_on_rails/doctor.rb +++ b/lib/react_on_rails/doctor.rb @@ -173,6 +173,7 @@ def check_development check_procfile_dev check_bin_dev_script check_gitignore + check_async_usage end def check_javascript_bundles @@ -1146,6 +1147,136 @@ def safe_display_config_value(label, config, method_name) checker.add_info(" #{label}: ") end end + + # Comment patterns used for filtering out commented async usage + ERB_COMMENT_PATTERN = /<%\s*#.*javascript_pack_tag/ + HAML_COMMENT_PATTERN = /^\s*-#.*javascript_pack_tag/ + HTML_COMMENT_PATTERN = /) + next if line.match?(HTML_COMMENT_PATTERN) + + # If we reach here, this line has an uncommented javascript_pack_tag + true + end + + !has_uncommented_async + end + + def config_has_async_loading_strategy? + config_path = "config/initializers/react_on_rails.rb" + return false unless File.exist?(config_path) + + content = File.read(config_path) + # Check if generated_component_packs_loading_strategy is set to :async + # Filter out commented lines (lines starting with # after optional whitespace) + content.each_line.any? do |line| + # Skip lines that start with # (after optional whitespace) + next if line.match?(/^\s*#/) + + # Match: config.generated_component_packs_loading_strategy = :async + # Use word boundary \b to ensure :async is the complete symbol, not part of :async_mode etc. + line.match?(/config\.generated_component_packs_loading_strategy\s*=\s*:async\b/) + end + rescue Errno::ENOENT, Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError => e + # Log the error if Rails logger is available + log_debug("Error checking async loading strategy: #{e.message}") + false + end + + def log_debug(message) + return unless defined?(Rails.logger) && Rails.logger + + Rails.logger.debug(message) + end end # rubocop:enable Metrics/ClassLength end diff --git a/spec/lib/react_on_rails/doctor_spec.rb b/spec/lib/react_on_rails/doctor_spec.rb index beab9b83dd..c7005e701f 100644 --- a/spec/lib/react_on_rails/doctor_spec.rb +++ b/spec/lib/react_on_rails/doctor_spec.rb @@ -188,4 +188,272 @@ end end end + + describe "#check_async_usage" do + let(:checker) { instance_double(ReactOnRails::SystemChecker) } + + before do + allow(doctor).to receive(:checker).and_return(checker) + allow(checker).to receive_messages(add_error: true, add_warning: true, add_info: true) + allow(File).to receive(:exist?).and_call_original + allow(Dir).to receive(:glob).and_return([]) + end + + context "when Pro gem is installed" do + before do + allow(ReactOnRails::Utils).to receive(:react_on_rails_pro?).and_return(true) + end + + it "skips the check" do + doctor.send(:check_async_usage) + expect(checker).not_to have_received(:add_error) + expect(checker).not_to have_received(:add_warning) + end + end + + context "when Pro gem is not installed" do + before do + allow(ReactOnRails::Utils).to receive(:react_on_rails_pro?).and_return(false) + end + + context "when async is used in view files" do + before do + allow(Dir).to receive(:glob).with("app/views/**/*.erb").and_return(["app/views/layouts/application.html.erb"]) + allow(Dir).to receive(:glob).with("app/views/**/*.haml").and_return([]) + allow(File).to receive(:exist?).with("app/views/layouts/application.html.erb").and_return(true) + allow(File).to receive(:read).with("app/views/layouts/application.html.erb") + .and_return('<%= javascript_pack_tag "application", :async %>') + allow(File).to receive(:exist?).with("config/initializers/react_on_rails.rb").and_return(false) + allow(doctor).to receive(:relativize_path).with("app/views/layouts/application.html.erb") + .and_return("app/views/layouts/application.html.erb") + end + + it "reports an error" do + doctor.send(:check_async_usage) + expect(checker).to have_received(:add_error).with("🚫 :async usage detected without React on Rails Pro") + expect(checker).to have_received(:add_error) + .with(" javascript_pack_tag with :async found in view files:") + end + end + + context "when generated_component_packs_loading_strategy is :async" do + before do + allow(File).to receive(:exist?).with("config/initializers/react_on_rails.rb").and_return(true) + allow(File).to receive(:read).with("config/initializers/react_on_rails.rb") + .and_return("config.generated_component_packs_loading_strategy = :async") + end + + it "reports an error" do + doctor.send(:check_async_usage) + expect(checker).to have_received(:add_error).with("🚫 :async usage detected without React on Rails Pro") + expect(checker).to have_received(:add_error) + .with(" config.generated_component_packs_loading_strategy = :async in initializer") + end + end + + context "when no async usage is detected" do + before do + allow(File).to receive(:exist?).with("config/initializers/react_on_rails.rb").and_return(true) + allow(File).to receive(:read).with("config/initializers/react_on_rails.rb") + .and_return("config.generated_component_packs_loading_strategy = :defer") + end + + it "does not report any issues" do + doctor.send(:check_async_usage) + expect(checker).not_to have_received(:add_error) + expect(checker).not_to have_received(:add_warning) + end + end + end + end + + describe "#scan_view_files_for_async_pack_tag" do + before do + allow(Dir).to receive(:glob).and_call_original + allow(File).to receive(:exist?).and_call_original + allow(File).to receive(:read).and_call_original + end + + context "when view files contain javascript_pack_tag with :async" do + before do + allow(Dir).to receive(:glob).with("app/views/**/*.erb") + .and_return(["app/views/layouts/application.html.erb"]) + allow(Dir).to receive(:glob).with("app/views/**/*.haml").and_return([]) + allow(File).to receive(:exist?).with("app/views/layouts/application.html.erb").and_return(true) + allow(File).to receive(:read).with("app/views/layouts/application.html.erb") + .and_return('<%= javascript_pack_tag "app", :async %>') + allow(doctor).to receive(:relativize_path).with("app/views/layouts/application.html.erb") + .and_return("app/views/layouts/application.html.erb") + end + + it "returns files with async" do + files = doctor.send(:scan_view_files_for_async_pack_tag) + expect(files).to include("app/views/layouts/application.html.erb") + end + end + + context "when view files contain javascript_pack_tag with async: true" do + before do + allow(Dir).to receive(:glob).with("app/views/**/*.erb") + .and_return(["app/views/layouts/application.html.erb"]) + allow(Dir).to receive(:glob).with("app/views/**/*.haml").and_return([]) + allow(File).to receive(:exist?).with("app/views/layouts/application.html.erb").and_return(true) + allow(File).to receive(:read).with("app/views/layouts/application.html.erb") + .and_return('<%= javascript_pack_tag "app", async: true %>') + allow(doctor).to receive(:relativize_path).with("app/views/layouts/application.html.erb") + .and_return("app/views/layouts/application.html.erb") + end + + it "returns files with async" do + files = doctor.send(:scan_view_files_for_async_pack_tag) + expect(files).to include("app/views/layouts/application.html.erb") + end + end + + context "when view files contain defer: \"async\" (false positive check)" do + before do + allow(Dir).to receive(:glob).with("app/views/**/*.erb") + .and_return(["app/views/layouts/application.html.erb"]) + allow(Dir).to receive(:glob).with("app/views/**/*.haml").and_return([]) + allow(File).to receive(:exist?).with("app/views/layouts/application.html.erb").and_return(true) + allow(File).to receive(:read).with("app/views/layouts/application.html.erb") + .and_return('<%= javascript_pack_tag "app", defer: "async" %>') + end + + it "does not return files (async is a string value, not the option)" do + files = doctor.send(:scan_view_files_for_async_pack_tag) + expect(files).to be_empty + end + end + + context "when view files do not contain async" do + before do + allow(Dir).to receive(:glob).with("app/views/**/*.erb") + .and_return(["app/views/layouts/application.html.erb"]) + allow(Dir).to receive(:glob).with("app/views/**/*.haml").and_return([]) + allow(File).to receive(:exist?).with("app/views/layouts/application.html.erb").and_return(true) + allow(File).to receive(:read).with("app/views/layouts/application.html.erb") + .and_return('<%= javascript_pack_tag "app" %>') + end + + it "returns empty array" do + files = doctor.send(:scan_view_files_for_async_pack_tag) + expect(files).to be_empty + end + end + + context "when async is only in ERB comments" do + before do + allow(Dir).to receive(:glob).with("app/views/**/*.erb") + .and_return(["app/views/layouts/application.html.erb"]) + allow(Dir).to receive(:glob).with("app/views/**/*.haml").and_return([]) + allow(File).to receive(:exist?).with("app/views/layouts/application.html.erb").and_return(true) + allow(File).to receive(:read).with("app/views/layouts/application.html.erb") + .and_return('<%# javascript_pack_tag "app", :async %>') + end + + it "returns empty array" do + files = doctor.send(:scan_view_files_for_async_pack_tag) + expect(files).to be_empty + end + end + + context "when async is only in HAML comments" do + before do + allow(Dir).to receive(:glob).with("app/views/**/*.erb").and_return([]) + allow(Dir).to receive(:glob).with("app/views/**/*.haml") + .and_return(["app/views/layouts/application.html.haml"]) + allow(File).to receive(:exist?).with("app/views/layouts/application.html.haml").and_return(true) + allow(File).to receive(:read).with("app/views/layouts/application.html.haml") + .and_return('-# javascript_pack_tag "app", :async') + end + + it "returns empty array" do + files = doctor.send(:scan_view_files_for_async_pack_tag) + expect(files).to be_empty + end + end + + context "when async is only in HTML comments" do + before do + allow(Dir).to receive(:glob).with("app/views/**/*.erb") + .and_return(["app/views/layouts/application.html.erb"]) + allow(Dir).to receive(:glob).with("app/views/**/*.haml").and_return([]) + allow(File).to receive(:exist?).with("app/views/layouts/application.html.erb").and_return(true) + allow(File).to receive(:read).with("app/views/layouts/application.html.erb") + .and_return('') + end + + it "returns empty array" do + files = doctor.send(:scan_view_files_for_async_pack_tag) + expect(files).to be_empty + end + end + + context "when javascript_pack_tag spans multiple lines" do + before do + allow(Dir).to receive(:glob).with("app/views/**/*.erb") + .and_return(["app/views/layouts/application.html.erb"]) + allow(Dir).to receive(:glob).with("app/views/**/*.haml").and_return([]) + allow(File).to receive(:exist?).with("app/views/layouts/application.html.erb").and_return(true) + allow(File).to receive(:read).with("app/views/layouts/application.html.erb") + .and_return("<%= javascript_pack_tag \"app\",\n :async %>") + allow(doctor).to receive(:relativize_path).with("app/views/layouts/application.html.erb") + .and_return("app/views/layouts/application.html.erb") + end + + it "returns files with async" do + files = doctor.send(:scan_view_files_for_async_pack_tag) + expect(files).to include("app/views/layouts/application.html.erb") + end + end + end + + describe "#config_has_async_loading_strategy?" do + context "when config file has :async strategy" do + before do + allow(File).to receive(:exist?).with("config/initializers/react_on_rails.rb").and_return(true) + allow(File).to receive(:read).with("config/initializers/react_on_rails.rb") + .and_return("config.generated_component_packs_loading_strategy = :async") + end + + it "returns true" do + expect(doctor.send(:config_has_async_loading_strategy?)).to be true + end + end + + context "when config file has different strategy" do + before do + allow(File).to receive(:exist?).with("config/initializers/react_on_rails.rb").and_return(true) + allow(File).to receive(:read).with("config/initializers/react_on_rails.rb") + .and_return("config.generated_component_packs_loading_strategy = :defer") + end + + it "returns false" do + expect(doctor.send(:config_has_async_loading_strategy?)).to be false + end + end + + context "when config file does not exist" do + before do + allow(File).to receive(:exist?).with("config/initializers/react_on_rails.rb").and_return(false) + end + + it "returns false" do + expect(doctor.send(:config_has_async_loading_strategy?)).to be false + end + end + + context "when :async strategy is commented out" do + before do + allow(File).to receive(:exist?).with("config/initializers/react_on_rails.rb").and_return(true) + allow(File).to receive(:read).with("config/initializers/react_on_rails.rb") + .and_return("# config.generated_component_packs_loading_strategy = :async") + end + + it "returns false" do + expect(doctor.send(:config_has_async_loading_strategy?)).to be false + end + end + end end