Skip to content

Commit cbee429

Browse files
justin808claude
andauthored
Add doctor checks for :async usage without React on Rails Pro (#2010)
## Summary Adds proactive doctor checks to detect `:async` loading strategy usage in projects without React on Rails Pro, which can cause component registration race conditions. ## Changes The doctor now checks for and reports errors when it detects: - `javascript_pack_tag` with `:async` in view files - `config.generated_component_packs_loading_strategy = :async` in initializer When detected without Pro, provides clear guidance to either upgrade to Pro or use `:defer`/`:sync` loading strategies. This complements PR #1993's configuration validation by adding runtime detection during development to help developers identify and fix async usage issues. ## Testing All new tests pass (155+ examples in doctor spec). The checks are skipped when React on Rails Pro is installed. <!-- Reviewable:start --> - - - This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/shakacode/react_on_rails/2010) <!-- Reviewable:end --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Diagnostic that detects async component-loading usage when the Pro edition is not present, reporting affected view and initializer locations, distinguishing commented/false-positive cases, and providing guidance and upgrade options. * **Tests** * Expanded tests for async-detection across template formats, comment-aware and multiline scanning, initializer strategies, error handling, and skipping when Pro is installed. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude <[email protected]>
1 parent 9013a13 commit cbee429

File tree

2 files changed

+459
-0
lines changed

2 files changed

+459
-0
lines changed

lib/react_on_rails/doctor.rb

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ def check_development
173173
check_procfile_dev
174174
check_bin_dev_script
175175
check_gitignore
176+
check_async_usage
176177
end
177178

178179
def check_javascript_bundles
@@ -1146,6 +1147,130 @@ def safe_display_config_value(label, config, method_name)
11461147
checker.add_info(" #{label}: <error reading value: #{e.message}>")
11471148
end
11481149
end
1150+
1151+
# Comment patterns used for filtering out commented async usage
1152+
ERB_COMMENT_PATTERN = /<%\s*#.*javascript_pack_tag/
1153+
HAML_COMMENT_PATTERN = /^\s*-#.*javascript_pack_tag/
1154+
SLIM_COMMENT_PATTERN = %r{^\s*/.*javascript_pack_tag}
1155+
HTML_COMMENT_PATTERN = /<!--.*javascript_pack_tag/
1156+
1157+
def check_async_usage
1158+
# When Pro is installed, async is fully supported and is the default behavior
1159+
# No need to check for async usage in this case
1160+
return if ReactOnRails::Utils.react_on_rails_pro?
1161+
1162+
async_issues = []
1163+
1164+
# Check 1: javascript_pack_tag with :async in view files
1165+
view_files_with_async = scan_view_files_for_async_pack_tag
1166+
unless view_files_with_async.empty?
1167+
async_issues << "javascript_pack_tag with :async found in view files:"
1168+
view_files_with_async.each do |file|
1169+
async_issues << " • #{file}"
1170+
end
1171+
end
1172+
1173+
# Check 2: generated_component_packs_loading_strategy = :async
1174+
if config_has_async_loading_strategy?
1175+
async_issues << "config.generated_component_packs_loading_strategy = :async in initializer"
1176+
end
1177+
1178+
return if async_issues.empty?
1179+
1180+
# Report errors if async usage is found without Pro
1181+
checker.add_error("🚫 :async usage detected without React on Rails Pro")
1182+
async_issues.each { |issue| checker.add_error(" #{issue}") }
1183+
checker.add_info(" 💡 :async can cause race conditions. Options:")
1184+
checker.add_info(" 1. Upgrade to React on Rails Pro (recommended for :async support)")
1185+
checker.add_info(" 2. Change to :defer or :sync loading strategy")
1186+
checker.add_info(" 📖 https://www.shakacode.com/react-on-rails/docs/guides/configuration/")
1187+
end
1188+
1189+
def scan_view_files_for_async_pack_tag
1190+
view_patterns = ["app/views/**/*.erb", "app/views/**/*.haml", "app/views/**/*.slim"]
1191+
files_with_async = view_patterns.flat_map { |pattern| scan_pattern_for_async(pattern) }
1192+
files_with_async.compact
1193+
rescue Errno::ENOENT, Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError => e
1194+
# Log the error if Rails logger is available
1195+
log_debug("Error scanning view files for async: #{e.message}")
1196+
[]
1197+
end
1198+
1199+
def scan_pattern_for_async(pattern)
1200+
Dir.glob(pattern).filter_map do |file|
1201+
next unless File.exist?(file)
1202+
1203+
content = File.read(file)
1204+
next if content_has_only_commented_async?(content)
1205+
next unless file_has_async_pack_tag?(content)
1206+
1207+
relativize_path(file)
1208+
end
1209+
end
1210+
1211+
def file_has_async_pack_tag?(content)
1212+
# Match javascript_pack_tag with :async symbol or async: true hash syntax
1213+
# Examples that should match:
1214+
# - javascript_pack_tag "app", :async
1215+
# - javascript_pack_tag "app", async: true
1216+
# - javascript_pack_tag "app", :async, other_option: value
1217+
# Examples that should NOT match:
1218+
# - javascript_pack_tag "app", defer: "async" (async is a string value, not the option)
1219+
# - javascript_pack_tag "app", :defer
1220+
# Note: Theoretical edge case `data: { async: true }` would match but is extremely unlikely
1221+
# in real code and represents a harmless false positive (showing a warning when not needed)
1222+
# Use word boundary \b to ensure :async is not part of a longer symbol like :async_mode
1223+
# [^<]* allows matching across newlines within ERB tags but stops at closing ERB tag
1224+
content.match?(/javascript_pack_tag[^<]*(?::async\b|async:\s*true)/)
1225+
end
1226+
1227+
def content_has_only_commented_async?(content)
1228+
# Check if all occurrences of javascript_pack_tag with :async are in comments
1229+
# Returns true if ONLY commented async usage exists (no active async usage)
1230+
1231+
# First check if there's any javascript_pack_tag with :async in the full content
1232+
return true unless file_has_async_pack_tag?(content)
1233+
1234+
# Strategy: Remove all commented lines, then check if any :async remains
1235+
# This handles both single-line and multi-line tags correctly
1236+
uncommented_lines = content.each_line.reject do |line|
1237+
line.match?(ERB_COMMENT_PATTERN) ||
1238+
line.match?(HAML_COMMENT_PATTERN) ||
1239+
line.match?(SLIM_COMMENT_PATTERN) ||
1240+
line.match?(HTML_COMMENT_PATTERN)
1241+
end
1242+
1243+
uncommented_content = uncommented_lines.join
1244+
# If no async found in uncommented content, all async usage was commented
1245+
!file_has_async_pack_tag?(uncommented_content)
1246+
end
1247+
1248+
def config_has_async_loading_strategy?
1249+
config_path = "config/initializers/react_on_rails.rb"
1250+
return false unless File.exist?(config_path)
1251+
1252+
content = File.read(config_path)
1253+
# Check if generated_component_packs_loading_strategy is set to :async
1254+
# Filter out commented lines (lines starting with # after optional whitespace)
1255+
content.each_line.any? do |line|
1256+
# Skip lines that start with # (after optional whitespace)
1257+
next if line.match?(/^\s*#/)
1258+
1259+
# Match: config.generated_component_packs_loading_strategy = :async
1260+
# Use word boundary \b to ensure :async is the complete symbol, not part of :async_mode etc.
1261+
line.match?(/config\.generated_component_packs_loading_strategy\s*=\s*:async\b/)
1262+
end
1263+
rescue Errno::ENOENT, Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError => e
1264+
# Log the error if Rails logger is available
1265+
log_debug("Error checking async loading strategy: #{e.message}")
1266+
false
1267+
end
1268+
1269+
def log_debug(message)
1270+
return unless defined?(Rails.logger) && Rails.logger
1271+
1272+
Rails.logger.debug(message)
1273+
end
11491274
end
11501275
# rubocop:enable Metrics/ClassLength
11511276
end

0 commit comments

Comments
 (0)