react_on_rails 17.0.1 → 17.1.0.rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/docs/agent/README.md +26 -0
  4. data/docs/agent/doctor-fix-loop.md +25 -0
  5. data/docs/agent/install-and-upgrade.md +162 -0
  6. data/docs/agent/rsc-adoption.md +31 -0
  7. data/docs/agent/streaming-debug.md +57 -0
  8. data/lib/generators/react_on_rails/base_generator.rb +4 -4
  9. data/lib/generators/react_on_rails/generator_helper.rb +12 -1
  10. data/lib/generators/react_on_rails/install_generator.rb +50 -5
  11. data/lib/generators/react_on_rails/js_dependency_manager.rb +1 -1
  12. data/lib/generators/react_on_rails/pro_generator.rb +5 -3
  13. data/lib/generators/react_on_rails/pro_setup.rb +51 -19
  14. data/lib/generators/react_on_rails/rsc_generator.rb +18 -1
  15. data/lib/generators/react_on_rails/rsc_setup/client_references.rb +63 -25
  16. data/lib/generators/react_on_rails/rsc_setup.rb +141 -39
  17. data/lib/generators/react_on_rails/templates/agent_files/AGENTS.md +31 -3
  18. data/lib/generators/react_on_rails/templates/base/base/{Procfile.dev-static-assets → Procfile.dev-static-assets.tt} +1 -1
  19. data/lib/generators/react_on_rails/templates/base/base/{Procfile.dev → Procfile.dev.tt} +1 -1
  20. data/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt +18 -42
  21. data/lib/react_on_rails/agent_guardrails/templates/rsc_app_safety_check.rb +215 -0
  22. data/lib/react_on_rails/agent_guardrails/templates/rsc_app_safety_skill.md +71 -0
  23. data/lib/react_on_rails/agent_guardrails.rb +223 -0
  24. data/lib/react_on_rails/dev/service_checker.rb +1 -1
  25. data/lib/react_on_rails/doctor.rb +880 -48
  26. data/lib/react_on_rails/doctor_schema.rb +192 -0
  27. data/lib/react_on_rails/engine.rb +2 -2
  28. data/lib/react_on_rails/helper.rb +17 -10
  29. data/lib/react_on_rails/length_prefixed_parser.rb +22 -14
  30. data/lib/react_on_rails/lenient_json.rb +0 -0
  31. data/lib/react_on_rails/prerender_error.rb +38 -8
  32. data/lib/react_on_rails/react_component/render_options.rb +1 -1
  33. data/lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb +168 -15
  34. data/lib/react_on_rails/version.rb +1 -1
  35. data/lib/tasks/agent_guardrails.rake +21 -0
  36. data/rakelib/run_rspec.rake +14 -0
  37. data/sig/react_on_rails/prerender_error.rbs +2 -1
  38. data/skills/doctor-fix-loop/SKILL.md +26 -0
  39. data/skills/install-and-upgrade/SKILL.md +24 -0
  40. data/skills/rsc-adoption/SKILL.md +28 -0
  41. data/skills/streaming-debug/SKILL.md +53 -0
  42. metadata +18 -4
  43. data/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-watch +0 -26
@@ -594,14 +594,10 @@ module ReactOnRails
594
594
  # those configs manually.
595
595
  return true if sections.empty?
596
596
 
597
- sections.all? do |section|
598
- body = section.fetch(:body)
599
- if rsc_plugin_body_has_top_level_scoped_client_references?(body)
600
- scoped_rsc_client_references_defined?(content)
601
- else
602
- rsc_plugin_body_has_top_level_key?(body, "clientReferences")
597
+ generated_rsc_client_references_defined?(content) &&
598
+ sections.all? do |section|
599
+ rsc_plugin_body_has_top_level_scoped_client_references?(section.fetch(:body))
603
600
  end
604
- end
605
601
  end
606
602
 
607
603
  # Counts active `new RSCWebpackPlugin(...)` calls whose first argument is present but
@@ -1229,13 +1225,13 @@ module ReactOnRails
1229
1225
  # the same comment-, string-, and brace-aware semantics as the rewrite path.
1230
1226
  def rsc_plugin_body_has_top_level_scoped_client_references?(body)
1231
1227
  [
1232
- /\bclientReferences\s*:\s*rscClientReferences\b/,
1233
- /(['"`])clientReferences\1\s*:\s*rscClientReferences\b/
1234
- ].any? do |pattern|
1228
+ [/\bclientReferences\b/, false],
1229
+ [/(['"`])clientReferences\1/, true]
1230
+ ].any? do |pattern, quoted|
1235
1231
  search_from = 0
1236
1232
 
1237
1233
  while (match = pattern.match(body, search_from))
1238
- return true if js_top_level_position?(body, match.begin(0))
1234
+ return true if top_level_scoped_client_references_match?(body, match, quoted:)
1239
1235
 
1240
1236
  search_from = match.end(0)
1241
1237
  end
@@ -1244,6 +1240,20 @@ module ReactOnRails
1244
1240
  false
1245
1241
  end
1246
1242
 
1243
+ def top_level_scoped_client_references_match?(body, match, quoted:)
1244
+ return false unless js_top_level_position?(body, match.begin(0))
1245
+ return false unless rsc_plugin_body_key_position?(body, match.begin(0), match.end(0), quoted:)
1246
+
1247
+ colon_index = first_js_token_index(body, match.end(0))
1248
+ return false unless colon_index && body[colon_index] == ":"
1249
+
1250
+ value_index = first_js_token_index(body, colon_index + 1)
1251
+ identifier = "rscClientReferences"
1252
+ return false unless value_index && body[value_index, identifier.length] == identifier
1253
+
1254
+ !body[value_index + identifier.length]&.match?(/[A-Za-z0-9_$]/)
1255
+ end
1256
+
1247
1257
  def splice_client_references_into_rsc_plugin_body(body, is_server_match)
1248
1258
  return splice_client_references_at_close_brace(body) if body.include?("\n")
1249
1259
  # Other options follow `isServer:` on the same line — append after the last option so
@@ -1516,20 +1526,48 @@ module ReactOnRails
1516
1526
  end
1517
1527
 
1518
1528
  def generated_rsc_client_references_defined?(content)
1519
- # Detect the generated `const rscClientReferences = (() => { ... })()` form
1520
- # structurally rather than by substring-matching template internals such as the
1521
- # `RSC_MANIFEST_CLIENT_REFERENCES_JSON` env-var name or the `rsc-client-references.json`
1522
- # filename. Those `content.include?` guards would couple detection to implementation
1523
- # details — renaming either string would silently break detection without a failing
1524
- # test — while adding no safety: `scoped_object_literal_defined?` already requires a
1525
- # real, module-scope `fallbackRscClientReferences` object literal with
1526
- # `directory: resolve(config.source_path)` (the generator-specific signature), and the
1527
- # `rsc_client_references_defined?` precondition rejects commented-out declarations — its
1528
- # `^[ \t]*(?:const|let|var)` anchor skips `//`-prefixed lines, and `js_top_level_position?`
1529
- # rejects declarations buried inside `/* ... */` blocks.
1530
- return false unless rsc_client_references_defined?(content)
1531
-
1532
- scoped_object_literal_defined?(content, "fallbackRscClientReferences")
1529
+ # A fallback directory object plus a binding named `rscClientReferences` is not enough:
1530
+ # legacy configs can wire that fallback directly into the plugin and bypass graph-derived
1531
+ # manifest selection. Verify the generated IIFE shape and its essential manifest path in
1532
+ # executable code while allowing formatter-controlled whitespace to vary.
1533
+ iife_body = generated_rsc_client_references_iife_body(content)
1534
+ return false unless iife_body
1535
+
1536
+ scoped_object_literal_defined?(content, "fallbackRscClientReferences") &&
1537
+ js_code_matches?(
1538
+ iife_body,
1539
+ %r{\bdefaultRefsJson\s*=\s*resolve\(\s*['"]ssr-generated/rsc-client-references\.json['"]\s*,?\s*\)}
1540
+ ) &&
1541
+ js_code_matches?(iife_body, /\breadManifestReferences\s*=\s*\(/) &&
1542
+ js_code_matches?(iife_body, /\breturn\s+readManifestReferences\(\s*defaultRefsJson\s*\)/)
1543
+ end
1544
+
1545
+ def generated_rsc_client_references_iife_body(content)
1546
+ declaration = /^[ \t]*(?:const|let|var)\s+rscClientReferences\s*=\s*\(\s*\(\s*\)\s*=>\s*\{/
1547
+ content.to_enum(:scan, declaration).each do
1548
+ match = Regexp.last_match
1549
+ next unless js_top_level_position?(content, match.begin(0))
1550
+
1551
+ open_brace = match.end(0) - 1
1552
+ close_brace = matching_js_closing_brace(content, open_brace)
1553
+ next unless close_brace
1554
+ next unless content[(close_brace + 1)..].match?(/\A\s*\)\s*\(\s*\)\s*;?/)
1555
+
1556
+ return content[(open_brace + 1)...close_brace]
1557
+ end
1558
+
1559
+ nil
1560
+ end
1561
+
1562
+ def js_code_matches?(content, pattern)
1563
+ search_from = 0
1564
+ while (match = pattern.match(content, search_from))
1565
+ return true if js_code_position?(content, match.begin(0))
1566
+
1567
+ search_from = match.end(0)
1568
+ end
1569
+
1570
+ false
1533
1571
  end
1534
1572
 
1535
1573
  def scoped_object_literal_defined?(content, variable_name)
@@ -183,7 +183,7 @@ module ReactOnRails
183
183
  ⚠️ Procfile.dev not found. Skipping RSC bundle watcher addition.
184
184
 
185
185
  You'll need to add the RSC bundle watcher to your process manager manually:
186
- rsc-bundle: RSC_BUNDLE_ONLY=true bin/shakapacker-watch --watch
186
+ rsc-bundle: RSC_BUNDLE_ONLY=true #{shakapacker_watch_command}
187
187
  MSG
188
188
  return
189
189
  end
@@ -198,7 +198,7 @@ module ReactOnRails
198
198
  rsc_watcher_line = <<~PROCFILE
199
199
 
200
200
  # React on Rails Pro - RSC bundle watcher
201
- rsc-bundle: RSC_BUNDLE_ONLY=true bin/shakapacker-watch --watch
201
+ rsc-bundle: RSC_BUNDLE_ONLY=true #{shakapacker_watch_command}
202
202
  PROCFILE
203
203
 
204
204
  append_to_file("Procfile.dev", rsc_watcher_line)
@@ -354,62 +354,147 @@ module ReactOnRails
354
354
  say "✅ Updated webpack configs for RSC", :green
355
355
  end
356
356
 
357
+ # Structural matchers shared by the ServerClientOrBoth (SCOB) transforms in
358
+ # update_server_client_or_both_for_rsc / update_scob_default_bundle_output and
359
+ # their verifier counterparts in check_rsc_scob_config. Each keys off a real
360
+ # JS declaration/branch/assignment shape rather than a bare substring, so:
361
+ # * a stray mention in a comment or string literal cannot suppress a
362
+ # transform (which would leave dangling `rscConfig` references →
363
+ # `ReferenceError: rscConfig is not defined` at runtime) or fool the
364
+ # verifier into reporting a partial setup as complete; and
365
+ # * linter whitespace reflow (e.g. `[ clientConfig, serverConfig ]`) is
366
+ # tolerated, matching the rest of this file.
367
+ # A transform's skip-guard and its verifier check use the *same* constant so
368
+ # the two can never disagree on what "already applied" means.
369
+ RSC_WEBPACK_CONFIG_IMPORT =
370
+ %r{const\s+rscWebpackConfig\s*=\s*require\(\s*'\./rscWebpackConfig'\s*\)}
371
+ RSC_CONFIG_DECLARATION = /const\s+rscConfig\s*=\s*rscWebpackConfig\(\s*\)/
372
+ ENVSPECIFIC_RSC_CALL = /envSpecific\(\s*clientConfig\s*,\s*serverConfig\s*,\s*rscConfig\s*\)/
373
+ # The RSC_BUNDLE_ONLY branch, matched by its `process.env.RSC_BUNDLE_ONLY`
374
+ # condition together with the `result = rscConfig` assignment in its body —
375
+ # a bare `RSC_BUNDLE_ONLY` comment mention satisfies neither.
376
+ RSC_BUNDLE_ONLY_BRANCH = /process\.env\.RSC_BUNDLE_ONLY\s*\)[^}]*?result\s*=\s*rscConfig\s*;?/
377
+ # The 2-element default result array we rewrite, and the 3-element RSC-aware
378
+ # array it becomes — both fully whitespace tolerant (inner brackets/commas).
379
+ DEFAULT_RESULT_ARRAY = /result\s*=\s*\[\s*clientConfig\s*,\s*serverConfig\s*\]\s*;?/
380
+ RSC_RESULT_ARRAY = /result\s*=\s*\[\s*clientConfig\s*,\s*serverConfig\s*,\s*rscConfig\s*\]/
381
+ # The RSC-aware default-build log, matched by its `console.log('…')` call
382
+ # shape so a bare comment mention of the message text can't no-op the rewrite.
383
+ RSC_BUNDLES_LOG =
384
+ /console\.log\(\s*'\[React on Rails\] Creating client, server, and RSC bundles\.'\s*\)/
385
+ # The bare `} else {` default-build branch: the one whose body reaches the
386
+ # 2-element default result assignment with no intervening `}`. The
387
+ # RSC_BUNDLE_ONLY branch is spliced immediately before it. `[^}]` cannot span
388
+ # an earlier else's closing brace, so an unrelated earlier `} else {` (e.g. an
389
+ # `if (envSpecific) {…} else {…}`) is never matched, and `\} else \{` never
390
+ # matches `} else if (…) {`. Keyed off the default result assignment, not the
391
+ # `// default …` comment, so comment reflow/reword/removal can't no-op it.
392
+ DEFAULT_BUILD_BRANCH_ANCHOR = /\n(\s*\}\s*else\s*\{[^}]*?#{DEFAULT_RESULT_ARRAY})/
393
+
394
+ # Pre-transform "source" shapes the gsub_file rewrites target (as opposed to
395
+ # the "already applied" markers above). Each is whitespace/semicolon tolerant
396
+ # and anchored to a complete statement so it can't match inside a larger
397
+ # construct; every rewrite is applied via replace_single_match so an
398
+ # incidental extra match in a customized file can't corrupt it.
399
+ #
400
+ # SERVER_WEBPACK_CONFIG_IMPORT tolerates a destructured `{ default: … }` or a
401
+ # plain binding AND an optional trailing `.default`
402
+ # (`require('./serverWebpackConfig').default;`), and captures through the end
403
+ # of the statement so the RSC import is inserted AFTER the complete require —
404
+ # never before a trailing `.default`, which would produce invalid JS.
405
+ SERVER_WEBPACK_CONFIG_IMPORT =
406
+ %r{(const\s+(?:\{[^\}]*\}|\w+)\s*=\s*require\(\s*'\./serverWebpackConfig'\s*\)(?:\s*\.\s*default)?\s*;?)}
407
+ SERVER_CONFIG_INVOCATION = /^(\s*const\s+serverConfig\s*=\s*serverWebpackConfig\(\s*\)\s*;?)\s*$/
408
+ ENVSPECIFIC_DEFAULT_CALL = /envSpecific\(\s*clientConfig\s*,\s*serverConfig\s*\)\s*;?/
409
+ DEFAULT_BUNDLES_LOG =
410
+ /console\.log\(\s*'\[React on Rails\] Creating both client and server bundles\.'\s*\)\s*;?/
411
+
357
412
  def update_server_client_or_both_for_rsc
358
413
  config_path = resolve_server_client_or_both_path
359
414
  return unless config_path
360
415
 
416
+ # `content` is read once as a pre-run snapshot; every guard below checks
417
+ # this snapshot rather than re-reading the file after each gsub_file.
418
+ # That is safe only because the five transforms are textually
419
+ # independent — each skip-guard matches a distinct real JS shape and none
420
+ # of the inserted strings satisfies a later guard (e.g. the import line
421
+ # `const rscWebpackConfig = require(...)` never matches RSC_CONFIG_DECLARATION,
422
+ # whose identifier is `rscConfig`). Preserve that invariant: do not add a
423
+ # transform whose output would flip a later guard, or re-read `content`.
361
424
  content = File.read(File.join(destination_root, config_path))
362
425
 
363
- # Skip if RSC is already configured
364
- return if content.include?("rscWebpackConfig")
426
+ # Insert the RSC import AFTER the complete server import statement
427
+ # (SERVER_WEBPACK_CONFIG_IMPORT captures through an optional `.default;`).
428
+ unless content.match?(RSC_WEBPACK_CONFIG_IMPORT)
429
+ replace_single_match(
430
+ config_path, content, SERVER_WEBPACK_CONFIG_IMPORT,
431
+ "\\1\nconst rscWebpackConfig = require('./rscWebpackConfig');"
432
+ )
433
+ end
365
434
 
366
- # Add RSC import after serverWebpackConfig import
367
- gsub_file(
368
- config_path,
369
- %r{(const (?:\{ default: serverWebpackConfig \}|serverWebpackConfig) = require\('\./serverWebpackConfig'\);)},
370
- "\\1\nconst rscWebpackConfig = require('./rscWebpackConfig');"
371
- )
435
+ unless content.match?(RSC_CONFIG_DECLARATION)
436
+ replace_single_match(
437
+ config_path, content, SERVER_CONFIG_INVOCATION,
438
+ "\\1\n\n const rscConfig = rscWebpackConfig();"
439
+ )
440
+ end
372
441
 
373
- # Add rscConfig variable after serverConfig (with blank line separator)
374
- gsub_file(
375
- config_path,
376
- /^(\s*const serverConfig = serverWebpackConfig\(\);)$/,
377
- "\\1\n\n const rscConfig = rscWebpackConfig();"
378
- )
442
+ unless content.match?(ENVSPECIFIC_RSC_CALL)
443
+ replace_single_match(
444
+ config_path, content, ENVSPECIFIC_DEFAULT_CALL,
445
+ "envSpecific(clientConfig, serverConfig, rscConfig);"
446
+ )
447
+ end
379
448
 
380
- # Update envSpecific call to include rscConfig
381
- gsub_file(
382
- config_path,
383
- /envSpecific\(clientConfig, serverConfig\);/,
384
- "envSpecific(clientConfig, serverConfig, rscConfig);"
385
- )
449
+ insert_rsc_bundle_only_branch(config_path, content)
450
+
451
+ update_scob_default_bundle_output(config_path, content)
452
+ end
453
+
454
+ def insert_rsc_bundle_only_branch(config_path, content)
455
+ return if content.match?(RSC_BUNDLE_ONLY_BRANCH)
386
456
 
387
- # Add RSC_BUNDLE_ONLY handling before the else block
388
457
  rsc_bundle_handling = <<~JS.chomp
389
458
  } else if (process.env.RSC_BUNDLE_ONLY) {
390
459
  // eslint-disable-next-line no-console
391
460
  console.log('[React on Rails] Creating only the RSC bundle.');
392
461
  result = rscConfig;
393
462
  JS
394
- gsub_file(
395
- config_path,
396
- %r{\n(\s*\} else \{\s*\n\s*// default is the standard client and server build)},
463
+ # Splice in the branch only when exactly one default-build branch is
464
+ # identifiable (replace_single_match bails on 0 or 2+).
465
+ replace_single_match(
466
+ config_path, content, DEFAULT_BUILD_BRANCH_ANCHOR,
397
467
  "\n #{rsc_bundle_handling}\n\\1"
398
468
  )
469
+ end
399
470
 
400
- # Update default multi-bundle output to include RSC
401
- gsub_file(
402
- config_path,
403
- /console\.log\('\[React on Rails\] Creating both client and server bundles\.'\);/,
404
- "console.log('[React on Rails] Creating client, server, and RSC bundles.');"
405
- )
406
- gsub_file(
407
- config_path,
408
- /result = \[clientConfig, serverConfig\];/,
471
+ def update_scob_default_bundle_output(config_path, content)
472
+ unless content.match?(RSC_BUNDLES_LOG)
473
+ replace_single_match(
474
+ config_path, content, DEFAULT_BUNDLES_LOG,
475
+ "console.log('[React on Rails] Creating client, server, and RSC bundles.');"
476
+ )
477
+ end
478
+
479
+ return if content.match?(RSC_RESULT_ARRAY)
480
+
481
+ replace_single_match(
482
+ config_path, content, DEFAULT_RESULT_ARRAY,
409
483
  "result = [clientConfig, serverConfig, rscConfig];"
410
484
  )
411
485
  end
412
486
 
487
+ # Apply a gsub_file rewrite only when `pattern` matches exactly once in the
488
+ # pre-run snapshot. Thor's gsub_file rewrites every textual match, so bailing
489
+ # on 0 or 2+ matches keeps a customized file (an incidental extra match, or a
490
+ # missing anchor) from being corrupted — a miss is surfaced by
491
+ # check_rsc_scob_config / manual review rather than silently mis-edited.
492
+ def replace_single_match(config_path, content, pattern, replacement)
493
+ return unless content.scan(pattern).length == 1
494
+
495
+ gsub_file(config_path, pattern, replacement)
496
+ end
497
+
413
498
  def update_server_webpack_config_for_rsc
414
499
  config_path = destination_config_path("config/webpack/serverWebpackConfig.js")
415
500
  full_path = File.join(destination_root, config_path)
@@ -578,7 +663,7 @@ module ReactOnRails
578
663
  missing.concat(stale_inactive_rsc_plugin_messages(content, "serverWebpackConfig.js"))
579
664
  warn_non_object_literal_rsc_plugin_options_for_config(content)
580
665
  unless rsc_plugin_client_references_configured?(content, is_server: true)
581
- missing << "generated scoped clientReferences in serverWebpackConfig.js"
666
+ missing << "generated manifest-backed clientReferences resolver in serverWebpackConfig.js"
582
667
  end
583
668
  elsif inactive_rsc_plugin_symbol_in_js_code?(content)
584
669
  missing << "#{rsc_plugin_class_name} in serverWebpackConfig.js " \
@@ -600,7 +685,7 @@ module ReactOnRails
600
685
  missing.concat(stale_inactive_rsc_plugin_messages(content, "clientWebpackConfig.js"))
601
686
  warn_non_object_literal_rsc_plugin_options_for_config(content)
602
687
  unless rsc_plugin_client_references_configured?(content, is_server: false)
603
- missing << "generated scoped clientReferences in clientWebpackConfig.js"
688
+ missing << "generated manifest-backed clientReferences resolver in clientWebpackConfig.js"
604
689
  end
605
690
  elsif inactive_rsc_plugin_symbol_in_js_code?(content)
606
691
  missing << "#{rsc_plugin_class_name} in clientWebpackConfig.js " \
@@ -616,7 +701,24 @@ module ReactOnRails
616
701
  return [] unless scob_path
617
702
 
618
703
  content = File.read(File.join(destination_root, scob_path))
619
- content.include?("rscWebpackConfig") ? [] : ["rscWebpackConfig in ServerClientOrBoth.js"]
704
+ missing = []
705
+ # Each check uses the SAME structural matcher as the corresponding
706
+ # transform's skip-guard, so a comment/string mention can neither suppress
707
+ # the transform nor mask its absence, and the two can never disagree.
708
+ missing << "rscWebpackConfig import in ServerClientOrBoth.js" unless content.match?(RSC_WEBPACK_CONFIG_IMPORT)
709
+ unless content.match?(RSC_CONFIG_DECLARATION)
710
+ missing << "rscConfig declaration (const rscConfig = rscWebpackConfig()) in ServerClientOrBoth.js"
711
+ end
712
+ unless content.match?(ENVSPECIFIC_RSC_CALL)
713
+ missing << "envSpecific(clientConfig, serverConfig, rscConfig) call in ServerClientOrBoth.js"
714
+ end
715
+ # The two default-branch transforms share the `} else {` anchor in
716
+ # insert_rsc_bundle_only_branch, which deliberately no-ops on a customized
717
+ # file where a single default branch can't be identified — so their success
718
+ # is NOT implied by the invocation above and a silent miss must be surfaced.
719
+ missing << "RSC_BUNDLE_ONLY branch in ServerClientOrBoth.js" unless content.match?(RSC_BUNDLE_ONLY_BRANCH)
720
+ missing << "RSC-aware default result array in ServerClientOrBoth.js" unless content.match?(RSC_RESULT_ARRAY)
721
+ missing
620
722
  end
621
723
 
622
724
  def warn_non_object_literal_rsc_plugin_options_for_config(content)
@@ -640,7 +742,7 @@ module ReactOnRails
640
742
  @non_object_literal_rsc_plugin_options_warned = true
641
743
  GeneratorMessages.add_warning(
642
744
  "#{rsc_plugin_class_name} calls use non-object-literal options in one or more bundler configs, " \
643
- "so the generator cannot verify whether scoped clientReferences are configured. " \
745
+ "so the generator cannot verify whether the generated manifest-backed resolver is configured. " \
644
746
  "Please verify your bundler configs manually."
645
747
  )
646
748
  end
@@ -149,13 +149,41 @@ A Redux store wasn't registered before a component that depends on it rendered.
149
149
 
150
150
  ## 6. Diagnose your setup
151
151
 
152
- Before guessing, run the doctor — it checks configuration, bundles, and dependencies:
152
+ Before guessing, run the doctor in its stable machine-readable mode — it checks
153
+ configuration, bundles, and dependencies and returns copy-promptable remediation:
153
154
 
154
155
  ```bash
155
- bin/rails react_on_rails:doctor
156
+ bin/rails react_on_rails:doctor FORMAT=json
156
157
  ```
157
158
 
158
- ## 7. Where the full reference lives
159
+ Exit code `0` means there are no errors (warnings may still be present); exit code
160
+ `1` means at least one error must be fixed. Use each non-passing check's stable
161
+ `id`, `severity`, `message`, and `remediation.prompt`, then rerun the command until
162
+ the report no longer contains errors. The JSON contract is documented at
163
+ https://reactonrails.com/docs/api-reference/doctor.
164
+
165
+ ## 7. Use the bundled docs and skills first
166
+
167
+ Use the installed gem directory as the canonical, reliable lookup for version-matched
168
+ guidance:
169
+
170
+ ```bash
171
+ bundle show react_on_rails
172
+ ```
173
+
174
+ From the returned directory, read `docs/agent/README.md` and load the matching workflow
175
+ skill when the task fits:
176
+
177
+ - Install or upgrade: `skills/install-and-upgrade/SKILL.md`
178
+ - Adopt React Server Components (Pro): `skills/rsc-adoption/SKILL.md`
179
+ - Debug streaming SSR (Pro): `skills/streaming-debug/SKILL.md`
180
+ - Run the doctor fix loop: `skills/doctor-fix-loop/SKILL.md`
181
+
182
+ An optional direct-dependency path is `node_modules/react-on-rails/`, but use it only when
183
+ that directory exists. Do not assume it exists: Pro installations may omit the direct base
184
+ npm dependency, and pnpm may isolate a transitive copy.
185
+
186
+ ## 8. Where the hosted reference lives
159
187
 
160
188
  - Hosted docs: https://reactonrails.com/docs/
161
189
  - Getting-started tutorial: https://reactonrails.com/docs/getting-started/tutorial/
@@ -1,2 +1,2 @@
1
1
  web: bin/rails server -p ${PORT:-3000}
2
- js: bin/shakapacker-watch --watch
2
+ js: <%= shakapacker_watch_command %>
@@ -7,4 +7,4 @@
7
7
  # For manual worktree setup (no base-port tool), see .env.example.
8
8
  rails: bundle exec rails s -p ${PORT:-3000}
9
9
  dev-server: bin/shakapacker-dev-server
10
- server-bundle: SERVER_BUNDLE_ONLY=true bin/shakapacker-watch --watch
10
+ server-bundle: SERVER_BUNDLE_ONLY=true <%= shakapacker_watch_command %>
@@ -1,6 +1,6 @@
1
1
  <%= add_documentation_reference(config[:message], "// https://github.com/shakacode/react-on-rails-demo-ssr-hmr/blob/master/config/webpack/serverWebpackConfig.js") %>
2
2
 
3
- const { merge, config } = require('shakapacker');
3
+ const { config } = require('shakapacker');
4
4
  const commonWebpackConfig = require('./commonWebpackConfig');
5
5
 
6
6
  const bundler = config.assets_bundler === 'rspack'
@@ -17,18 +17,18 @@ const rscClientReferences = {
17
17
  };
18
18
  <% end -%>
19
19
 
20
+ // Normalizes an entry of a webpack/rspack `rule.use` array to its loader path.
21
+ // Entries may be a bare string, a `{ loader, options }` object, or null.
22
+ function getLoaderPath(item) {
23
+ if (typeof item === 'string') return item;
24
+ if (item && typeof item.loader === 'string') return item.loader;
25
+ return '';
26
+ }
27
+
20
28
  <% if use_pro? -%>
21
29
  function extractLoader(rule, loaderName) {
22
30
  if (!Array.isArray(rule.use)) return null;
23
- return rule.use.find((item) => {
24
- let testValue = '';
25
- if (typeof item === 'string') {
26
- testValue = item;
27
- } else if (item && typeof item.loader === 'string') {
28
- testValue = item.loader;
29
- }
30
- return testValue.includes(loaderName);
31
- });
31
+ return rule.use.find((item) => getLoaderPath(item).includes(loaderName));
32
32
  }
33
33
 
34
34
  <% end -%>
@@ -38,10 +38,6 @@ const configureServer = (rscBundle = false) => {
38
38
  <% else -%>
39
39
  const configureServer = () => {
40
40
  <% end -%>
41
- // We need to use "merge" because the clientConfigObject, EVEN after running
42
- // toWebpackConfig() is a mutable GLOBAL. Thus any changes, like modifying the
43
- // entry value will result in changing the client config!
44
- // Using webpack-merge into an empty object avoids this issue.
45
41
  const serverWebpackConfig = commonWebpackConfig();
46
42
 
47
43
  // We just want the single server bundle entry
@@ -63,15 +59,10 @@ const configureServer = () => {
63
59
  serverWebpackConfig.module.rules.forEach((loader) => {
64
60
  if (loader.use && loader.use.filter) {
65
61
  loader.use = loader.use.filter((item) => {
66
- let testValue = '';
67
- if (typeof item === 'string') {
68
- testValue = item;
69
- } else if (item && typeof item.loader === 'string') {
70
- testValue = item.loader;
71
- }
62
+ const loaderPath = getLoaderPath(item);
72
63
  return !(
73
- testValue.includes('mini-css-extract-plugin') ||
74
- testValue.includes('cssExtractLoader') // Rspack uses this path
64
+ loaderPath.includes('mini-css-extract-plugin') ||
65
+ loaderPath.includes('cssExtractLoader') // Rspack uses this path
75
66
  );
76
67
  });
77
68
  }
@@ -162,29 +153,14 @@ const configureServer = () => {
162
153
  if (Array.isArray(rule.use)) {
163
154
  // remove the mini-css-extract-plugin and style-loader
164
155
  rule.use = rule.use.filter((item) => {
165
- let testValue = '';
166
- if (typeof item === 'string') {
167
- testValue = item;
168
- } else if (item && typeof item.loader === 'string') {
169
- testValue = item.loader;
170
- }
156
+ const loaderPath = getLoaderPath(item);
171
157
  return !(
172
- testValue.includes('mini-css-extract-plugin') ||
173
- testValue.includes('cssExtractLoader') || // Rspack uses this path
174
- testValue === 'style-loader'
158
+ loaderPath.includes('mini-css-extract-plugin') ||
159
+ loaderPath.includes('cssExtractLoader') || // Rspack uses this path
160
+ loaderPath === 'style-loader'
175
161
  );
176
162
  });
177
- const cssLoader = rule.use.find((item) => {
178
- let testValue = '';
179
-
180
- if (typeof item === 'string') {
181
- testValue = item;
182
- } else if (item && typeof item.loader === 'string') {
183
- testValue = item.loader;
184
- }
185
-
186
- return testValue.includes('css-loader');
187
- });
163
+ const cssLoader = rule.use.find((item) => getLoaderPath(item).includes('css-loader'));
188
164
  if (cssLoader && cssLoader.options && cssLoader.options.modules) {
189
165
  cssLoader.options.modules = {
190
166
  ...(typeof cssLoader.options.modules === 'object' ? cssLoader.options.modules : {}),