Skip to content

Commit 044d7de

Browse files
justin808claude
andcommitted
Add doctor checks to detect :async usage without React on Rails Pro
Adds proactive detection of :async loading strategy usage in projects without React on Rails Pro, which can cause component registration race conditions. The doctor now checks for: 1. javascript_pack_tag with :async in view files 2. config.generated_component_packs_loading_strategy = :async in initializer When detected without Pro, provides clear guidance: - Upgrade to React on Rails Pro (recommended) - Change to :defer or :sync loading strategy This complements PR #1993's configuration validation by adding runtime doctor checks that help users identify and fix async usage issues during development. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 8a91778 commit 044d7de

File tree

2 files changed

+223
-0
lines changed

2 files changed

+223
-0
lines changed

lib/react_on_rails/doctor.rb

Lines changed: 67 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,72 @@ 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+
def check_async_usage
1152+
# When Pro is installed, async is fully supported and is the default behavior
1153+
# No need to check for async usage in this case
1154+
return if ReactOnRails::Utils.react_on_rails_pro?
1155+
1156+
async_issues = []
1157+
1158+
# Check 1: javascript_pack_tag with :async in view files
1159+
view_files_with_async = scan_view_files_for_async_pack_tag
1160+
unless view_files_with_async.empty?
1161+
async_issues << "javascript_pack_tag with :async found in view files:"
1162+
view_files_with_async.each do |file|
1163+
async_issues << " • #{file}"
1164+
end
1165+
end
1166+
1167+
# Check 2: generated_component_packs_loading_strategy = :async
1168+
if config_has_async_loading_strategy?
1169+
async_issues << "config.generated_component_packs_loading_strategy = :async in initializer"
1170+
end
1171+
1172+
return if async_issues.empty?
1173+
1174+
# Report errors if async usage is found without Pro
1175+
checker.add_error("🚫 :async usage detected without React on Rails Pro")
1176+
async_issues.each { |issue| checker.add_error(" #{issue}") }
1177+
checker.add_info(" 💡 :async can cause race conditions. Options:")
1178+
checker.add_info(" 1. Upgrade to React on Rails Pro (recommended for :async support)")
1179+
checker.add_info(" 2. Change to :defer or :sync loading strategy")
1180+
checker.add_info(" 📖 https://www.shakacode.com/react-on-rails/docs/guides/configuration/")
1181+
end
1182+
1183+
def scan_view_files_for_async_pack_tag
1184+
files_with_async = []
1185+
1186+
# Scan app/views for .erb and .haml files
1187+
view_patterns = ["app/views/**/*.erb", "app/views/**/*.haml"]
1188+
1189+
view_patterns.each do |pattern|
1190+
Dir.glob(pattern).each do |file|
1191+
next unless File.exist?(file)
1192+
1193+
content = File.read(file)
1194+
# Look for javascript_pack_tag with :async or "async"
1195+
if content.match?(/javascript_pack_tag.*:async/) || content.match?(/javascript_pack_tag.*["']async["']/)
1196+
files_with_async << relativize_path(file)
1197+
end
1198+
end
1199+
end
1200+
1201+
files_with_async
1202+
rescue StandardError
1203+
[]
1204+
end
1205+
1206+
def config_has_async_loading_strategy?
1207+
config_path = "config/initializers/react_on_rails.rb"
1208+
return false unless File.exist?(config_path)
1209+
1210+
content = File.read(config_path)
1211+
# Check if generated_component_packs_loading_strategy is set to :async
1212+
content.match?(/config\.generated_component_packs_loading_strategy\s*=\s*:async/)
1213+
rescue StandardError
1214+
false
1215+
end
11491216
end
11501217
# rubocop:enable Metrics/ClassLength
11511218
end

spec/lib/react_on_rails/doctor_spec.rb

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,4 +188,160 @@
188188
end
189189
end
190190
end
191+
192+
describe "#check_async_usage" do
193+
let(:checker) { instance_double(ReactOnRails::SystemChecker) }
194+
195+
before do
196+
allow(doctor).to receive(:checker).and_return(checker)
197+
allow(checker).to receive_messages(add_error: true, add_warning: true, add_info: true)
198+
allow(File).to receive(:exist?).and_call_original
199+
allow(Dir).to receive(:glob).and_return([])
200+
end
201+
202+
context "when Pro gem is installed" do
203+
before do
204+
allow(ReactOnRails::Utils).to receive(:react_on_rails_pro?).and_return(true)
205+
end
206+
207+
it "skips the check" do
208+
doctor.send(:check_async_usage)
209+
expect(checker).not_to have_received(:add_error)
210+
expect(checker).not_to have_received(:add_warning)
211+
end
212+
end
213+
214+
context "when Pro gem is not installed" do
215+
before do
216+
allow(ReactOnRails::Utils).to receive(:react_on_rails_pro?).and_return(false)
217+
end
218+
219+
context "when async is used in view files" do
220+
before do
221+
allow(Dir).to receive(:glob).with("app/views/**/*.erb").and_return(["app/views/layouts/application.html.erb"])
222+
allow(Dir).to receive(:glob).with("app/views/**/*.haml").and_return([])
223+
allow(File).to receive(:exist?).with("app/views/layouts/application.html.erb").and_return(true)
224+
allow(File).to receive(:read).with("app/views/layouts/application.html.erb")
225+
.and_return('<%= javascript_pack_tag "application", :async %>')
226+
allow(File).to receive(:exist?).with("config/initializers/react_on_rails.rb").and_return(false)
227+
allow(doctor).to receive(:relativize_path).with("app/views/layouts/application.html.erb")
228+
.and_return("app/views/layouts/application.html.erb")
229+
end
230+
231+
it "reports an error" do
232+
doctor.send(:check_async_usage)
233+
expect(checker).to have_received(:add_error).with("🚫 :async usage detected without React on Rails Pro")
234+
expect(checker).to have_received(:add_error)
235+
.with(" javascript_pack_tag with :async found in view files:")
236+
end
237+
end
238+
239+
context "when generated_component_packs_loading_strategy is :async" do
240+
before do
241+
allow(File).to receive(:exist?).with("config/initializers/react_on_rails.rb").and_return(true)
242+
allow(File).to receive(:read).with("config/initializers/react_on_rails.rb")
243+
.and_return("config.generated_component_packs_loading_strategy = :async")
244+
end
245+
246+
it "reports an error" do
247+
doctor.send(:check_async_usage)
248+
expect(checker).to have_received(:add_error).with("🚫 :async usage detected without React on Rails Pro")
249+
expect(checker).to have_received(:add_error)
250+
.with(" config.generated_component_packs_loading_strategy = :async in initializer")
251+
end
252+
end
253+
254+
context "when no async usage is detected" do
255+
before do
256+
allow(File).to receive(:exist?).with("config/initializers/react_on_rails.rb").and_return(true)
257+
allow(File).to receive(:read).with("config/initializers/react_on_rails.rb")
258+
.and_return("config.generated_component_packs_loading_strategy = :defer")
259+
end
260+
261+
it "does not report any issues" do
262+
doctor.send(:check_async_usage)
263+
expect(checker).not_to have_received(:add_error)
264+
expect(checker).not_to have_received(:add_warning)
265+
end
266+
end
267+
end
268+
end
269+
270+
describe "#scan_view_files_for_async_pack_tag" do
271+
before do
272+
allow(Dir).to receive(:glob).and_call_original
273+
allow(File).to receive(:exist?).and_call_original
274+
allow(File).to receive(:read).and_call_original
275+
end
276+
277+
context "when view files contain javascript_pack_tag with :async" do
278+
before do
279+
allow(Dir).to receive(:glob).with("app/views/**/*.erb")
280+
.and_return(["app/views/layouts/application.html.erb"])
281+
allow(Dir).to receive(:glob).with("app/views/**/*.haml").and_return([])
282+
allow(File).to receive(:exist?).with("app/views/layouts/application.html.erb").and_return(true)
283+
allow(File).to receive(:read).with("app/views/layouts/application.html.erb")
284+
.and_return('<%= javascript_pack_tag "app", :async %>')
285+
allow(doctor).to receive(:relativize_path).with("app/views/layouts/application.html.erb")
286+
.and_return("app/views/layouts/application.html.erb")
287+
end
288+
289+
it "returns files with async" do
290+
files = doctor.send(:scan_view_files_for_async_pack_tag)
291+
expect(files).to include("app/views/layouts/application.html.erb")
292+
end
293+
end
294+
295+
context "when view files do not contain async" do
296+
before do
297+
allow(Dir).to receive(:glob).with("app/views/**/*.erb")
298+
.and_return(["app/views/layouts/application.html.erb"])
299+
allow(Dir).to receive(:glob).with("app/views/**/*.haml").and_return([])
300+
allow(File).to receive(:exist?).with("app/views/layouts/application.html.erb").and_return(true)
301+
allow(File).to receive(:read).with("app/views/layouts/application.html.erb")
302+
.and_return('<%= javascript_pack_tag "app" %>')
303+
end
304+
305+
it "returns empty array" do
306+
files = doctor.send(:scan_view_files_for_async_pack_tag)
307+
expect(files).to be_empty
308+
end
309+
end
310+
end
311+
312+
describe "#config_has_async_loading_strategy?" do
313+
context "when config file has :async strategy" do
314+
before do
315+
allow(File).to receive(:exist?).with("config/initializers/react_on_rails.rb").and_return(true)
316+
allow(File).to receive(:read).with("config/initializers/react_on_rails.rb")
317+
.and_return("config.generated_component_packs_loading_strategy = :async")
318+
end
319+
320+
it "returns true" do
321+
expect(doctor.send(:config_has_async_loading_strategy?)).to be true
322+
end
323+
end
324+
325+
context "when config file has different strategy" do
326+
before do
327+
allow(File).to receive(:exist?).with("config/initializers/react_on_rails.rb").and_return(true)
328+
allow(File).to receive(:read).with("config/initializers/react_on_rails.rb")
329+
.and_return("config.generated_component_packs_loading_strategy = :defer")
330+
end
331+
332+
it "returns false" do
333+
expect(doctor.send(:config_has_async_loading_strategy?)).to be false
334+
end
335+
end
336+
337+
context "when config file does not exist" do
338+
before do
339+
allow(File).to receive(:exist?).with("config/initializers/react_on_rails.rb").and_return(false)
340+
end
341+
342+
it "returns false" do
343+
expect(doctor.send(:config_has_async_loading_strategy?)).to be false
344+
end
345+
end
346+
end
191347
end

0 commit comments

Comments
 (0)