Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions lib/react_on_rails/doctor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ def check_development
check_procfile_dev
check_bin_dev_script
check_gitignore
check_async_usage
end

def check_javascript_bundles
Expand Down Expand Up @@ -1146,6 +1147,136 @@ def safe_display_config_value(label, config, method_name)
checker.add_info(" #{label}: <error reading value: #{e.message}>")
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 = /<!--.*javascript_pack_tag/

def check_async_usage
# When Pro is installed, async is fully supported and is the default behavior
# No need to check for async usage in this case
return if ReactOnRails::Utils.react_on_rails_pro?

async_issues = []

# Check 1: javascript_pack_tag with :async in view files
view_files_with_async = scan_view_files_for_async_pack_tag
unless view_files_with_async.empty?
async_issues << "javascript_pack_tag with :async found in view files:"
view_files_with_async.each do |file|
async_issues << " • #{file}"
end
end

# Check 2: generated_component_packs_loading_strategy = :async
if config_has_async_loading_strategy?
async_issues << "config.generated_component_packs_loading_strategy = :async in initializer"
end

return if async_issues.empty?

# Report errors if async usage is found without Pro
checker.add_error("🚫 :async usage detected without React on Rails Pro")
async_issues.each { |issue| checker.add_error(" #{issue}") }
checker.add_info(" 💡 :async can cause race conditions. Options:")
checker.add_info(" 1. Upgrade to React on Rails Pro (recommended for :async support)")
checker.add_info(" 2. Change to :defer or :sync loading strategy")
checker.add_info(" 📖 https://www.shakacode.com/react-on-rails/docs/guides/configuration/")
end

def scan_view_files_for_async_pack_tag
view_patterns = ["app/views/**/*.erb", "app/views/**/*.haml"]
files_with_async = view_patterns.flat_map { |pattern| scan_pattern_for_async(pattern) }
files_with_async.compact
rescue Errno::ENOENT, Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError => e
# Log the error if Rails logger is available
log_debug("Error scanning view files for async: #{e.message}")
[]
end

def scan_pattern_for_async(pattern)
Dir.glob(pattern).filter_map do |file|
next unless File.exist?(file)

content = File.read(file)
next if content_has_only_commented_async?(content)
next unless file_has_async_pack_tag?(content)

relativize_path(file)
end
end

def file_has_async_pack_tag?(content)
# Match javascript_pack_tag with :async symbol or async: true hash syntax
# Examples that should match:
# - javascript_pack_tag "app", :async
# - javascript_pack_tag "app", async: true
# - javascript_pack_tag "app", :async, other_option: value
# Examples that should NOT match:
# - javascript_pack_tag "app", defer: "async" (async is a string value, not the option)
# - javascript_pack_tag "app", :defer
# Note: Theoretical edge case `data: { async: true }` would match but is extremely unlikely
# in real code and represents a harmless false positive (showing a warning when not needed)
# Use word boundary \b to ensure :async is not part of a longer symbol like :async_mode
# [^<]* allows matching across newlines within ERB tags but stops at closing ERB tag
content.match?(/javascript_pack_tag[^<]*(?::async\b|async:\s*true)/)
end

def content_has_only_commented_async?(content)
# Check if all occurrences of javascript_pack_tag with :async are in comments
# Returns true if ONLY commented async usage exists (no active async usage)
# Note: We need to check the full content first (for multi-line tags) before line-by-line filtering

# First check if there's any javascript_pack_tag with :async in the full content
return true unless file_has_async_pack_tag?(content)

# Now check line-by-line to see if all occurrences are commented
# For multi-line tags, we check if the starting line is commented
has_uncommented_async = content.each_line.any? do |line|
# Skip lines that don't contain javascript_pack_tag
next unless line.include?("javascript_pack_tag")

# Skip ERB comments (<%# ... %>)
next if line.match?(ERB_COMMENT_PATTERN)
# Skip HAML comments (-# ...)
next if line.match?(HAML_COMMENT_PATTERN)
# Skip HTML comments (<!-- ... -->)
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
Comment on lines +1226 to +1252
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical false positive bug: line 1238 doesn't verify :async presence.

The current logic produces false positives when a file has an uncommented javascript_pack_tag without :async alongside a commented one with :async.

Example scenario causing false positive:

<%= javascript_pack_tag "app", defer: true %>
<%# javascript_pack_tag "other", :async %>

Current behavior: Reports async usage (false positive)
Expected behavior: Should not report (only :async is commented)

Root cause: Line 1238 checks line.include?("javascript_pack_tag") but doesn't verify the line contains :async, so any uncommented pack tag triggers detection even when it has no async option.

Apply this fix:

     has_uncommented_async = content.each_line.any? do |line|
-      # Skip lines that don't contain javascript_pack_tag
-      next unless line.include?("javascript_pack_tag")
+      # Skip lines that don't contain javascript_pack_tag with :async
+      next unless file_has_async_pack_tag?(line)

       # Skip ERB comments (<%# ... %>)
       next if line.match?(ERB_COMMENT_PATTERN)

This ensures only lines with both javascript_pack_tag AND async indicators are considered, eliminating false positives.

Note: This issue was previously flagged in past review comments but remains unfixed.


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
Loading