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
@@ -3,6 +3,7 @@
3
3
  require "open-uri"
4
4
  require "execjs"
5
5
  require "react_on_rails/length_prefixed_parser"
6
+ require "react_on_rails/lenient_json"
6
7
 
7
8
  module ReactOnRails
8
9
  module ServerRenderingPool
@@ -148,15 +149,31 @@ module ReactOnRails
148
149
 
149
150
  def read_bundle_js_code
150
151
  server_js_file = ReactOnRails::Utils.server_bundle_js_file_path
151
- if ReactOnRails::Utils.server_bundle_path_is_http?
152
+ server_bundle_path_is_http = ReactOnRails::Utils.server_bundle_path_is_http?
153
+ if server_bundle_path_is_http
152
154
  file_url_to_string(server_js_file)
153
155
  else
154
156
  File.read(server_js_file)
155
157
  end
156
158
  rescue StandardError => e
157
- msg = "You specified server rendering JS file: #{server_js_file}, but it cannot be " \
158
- "read. You may set the server_bundle_js_file in your configuration to be \"\" to " \
159
- "avoid this warning.\nError is: #{e}\n\n#{Utils.default_troubleshooting_section}"
159
+ # Sanitized: server_js_file may be an HTTP(S) URL with embedded basic-auth credentials
160
+ # (a supported config convenience, e.g. https://:password@host:3800). This is the
161
+ # public entry point that file_url_to_string is called from, so without this the
162
+ # sanitization inside file_url_to_string's own raise/rescue is moot for any real
163
+ # caller — the credential would still leak here regardless. Errors already wrapped by
164
+ # file_url_to_string have passed through the same message sanitizer; scanning that
165
+ # wrapper again would treat its diagnostic text as fresh untrusted input. Other errors
166
+ # are scrubbed here because URI::InvalidURIError embeds the raw URL in its message.
167
+ sanitized_url = sanitized_renderer_url(server_js_file)
168
+ sanitized_error = if server_bundle_path_is_http && e.is_a?(ReactOnRails::ServerBundleLoadError)
169
+ e.message
170
+ else
171
+ sanitized_renderer_error_message(e.message, server_js_file, sanitized_url)
172
+ end
173
+ msg = "You specified server rendering JS file: #{sanitized_url}, but it cannot " \
174
+ "be read. You may set the server_bundle_js_file in your configuration to be \"\" to " \
175
+ "avoid this warning.\nError is: #{sanitized_error}\n\n" \
176
+ "#{Utils.default_troubleshooting_section}"
160
177
  raise ReactOnRails::ServerBundleLoadError, msg
161
178
  end
162
179
 
@@ -386,10 +403,80 @@ module ReactOnRails
386
403
  # Strips any embedded credentials from a configured renderer URL before it is
387
404
  # interpolated into an error message, so a password in the URL (a supported config
388
405
  # convenience, e.g. https://:password@host:3800) cannot leak into logs or error
389
- # trackers. Mirrors ReactOnRailsPro::Configuration#strip_renderer_url_userinfo.
406
+ # trackers. One configured value is not free-form prose: multiple schemes or line breaks
407
+ # after its first scheme make it structurally ambiguous, so that URL suffix fails closed
408
+ # through its final @. Any non-URL prefix is preserved for useful error context.
390
409
  def sanitized_renderer_url(url)
391
410
  return url if url.nil? || url.empty?
392
411
 
412
+ scheme = url.match(%r{https?://}i)
413
+ return url unless scheme
414
+
415
+ prefix = url[...scheme.begin(0)]
416
+ configured_url = url[scheme.begin(0)..]
417
+ sanitized_url = if configured_url.match?(/[\r\n]/) || configured_url.scan(%r{https?://}i).length > 1
418
+ strip_malformed_url_userinfo(configured_url)
419
+ else
420
+ sanitized_valid_http_url(configured_url) || strip_malformed_url_userinfo(configured_url)
421
+ end
422
+
423
+ "#{prefix}#{sanitized_url}"
424
+ end
425
+
426
+ def sanitized_renderer_error_message(message, raw_url, sanitized_url = sanitized_renderer_url(raw_url))
427
+ message = message.to_s
428
+ return strip_userinfo(message) if raw_url.nil? || raw_url.empty?
429
+
430
+ # URI::InvalidURIError embeds String#inspect, while other errors may embed the raw value.
431
+ message = message.gsub(raw_url.inspect) { sanitized_url.inspect }
432
+ .gsub(raw_url) { sanitized_url }
433
+ strip_userinfo(message)
434
+ end
435
+
436
+ # Protects successfully parsed bare HTTP(S) tokens by assembling the result from spans,
437
+ # without collision-prone placeholder text. A candidate remains unprotected when any @
438
+ # follows it before the next scheme: punctuation, prose, and line breaks are not structural
439
+ # proof that the @ is unrelated to malformed userinfo. When an unresolved region precedes
440
+ # a credential-bearing candidate, that region is discarded and the candidate is sanitized
441
+ # independently rather than allowing one fallback match to consume through another scheme.
442
+ # Ambiguous text may be over-redacted for safety.
443
+ def strip_userinfo(text)
444
+ text = text.to_s
445
+ sanitized = text.dup.clear
446
+ unprotected_start = 0
447
+
448
+ text.to_enum(:scan, %r{https?://(?:(?!https?://)[^\s"'])+}i).each do
449
+ match = Regexp.last_match
450
+ protected_url = sanitized_valid_http_url(match[0])
451
+ next unless protected_url
452
+ next if userinfo_delimiter_before_next_scheme?(text, match)
453
+
454
+ unprotected = text[unprotected_start...match.begin(0)]
455
+ sanitized << sanitized_unprotected_prefix(unprotected, match[0], protected_url)
456
+ sanitized << protected_url
457
+ unprotected_start = match.end(0)
458
+ end
459
+
460
+ sanitized << strip_unprotected_userinfo(text[unprotected_start..])
461
+ end
462
+
463
+ def userinfo_delimiter_before_next_scheme?(text, match)
464
+ continuation = text[match.end(0)..]
465
+ boundary = continuation.match(%r{https?://}i)
466
+ continuation = continuation[...boundary.begin(0)] if boundary
467
+ continuation.include?("@")
468
+ end
469
+
470
+ def sanitized_unprotected_prefix(text, raw_url, sanitized_url)
471
+ unresolved_scheme = text.match(%r{https?://}i)
472
+ return strip_unprotected_userinfo(text) if raw_url == sanitized_url || unresolved_scheme.nil?
473
+
474
+ text[...unresolved_scheme.begin(0)]
475
+ end
476
+
477
+ # URI handles valid URLs structurally, so an @ in the path is never mistaken for userinfo.
478
+ # Returning nil for invalid input keeps that token unprotected for the fail-closed pass.
479
+ def sanitized_valid_http_url(url)
393
480
  uri = URI.parse(url)
394
481
  return url if uri.userinfo.nil?
395
482
 
@@ -397,30 +484,96 @@ module ReactOnRails
397
484
  uri.password = nil
398
485
  uri.user = nil
399
486
  uri.to_s
400
- rescue URI::InvalidURIError
401
- # Best-effort strip of the common user:pass@ form so a URL that URI rejects as
402
- # malformed still doesn't leak a password into the message or logs.
403
- url.to_s.gsub(%r{//[^/@]*@}, "//")
487
+ rescue URI::Error
488
+ nil
489
+ end
490
+
491
+ def strip_unprotected_userinfo(text)
492
+ text.gsub(%r{https?://(?:(?!https?://).)*@}im) { |span| strip_malformed_url_userinfo(span) }
493
+ end
494
+
495
+ # For malformed URLs or unprotected scheme-delimited spans, remove through the last @. The
496
+ # retained suffix is best-effort context; ambiguous malformed diagnostics may lose text.
497
+ def strip_malformed_url_userinfo(url)
498
+ scheme = url.match(%r{\Ahttps?://}i)
499
+ userinfo_delimiter = url.rindex("@")
500
+ return url unless scheme && userinfo_delimiter
501
+
502
+ "#{url[...scheme.end(0)]}#{url[(userinfo_delimiter + 1)..]}"
404
503
  end
405
504
 
406
505
  def file_url_to_string(url)
407
506
  response = Net::HTTP.get_response(URI.parse(url))
408
- content_type_header = response["content-type"]
409
- match = content_type_header.match(/\A.*; charset=(?<encoding>.*)\z/)
410
- encoding_type = match[:encoding]
411
- response.body.force_encoding(encoding_type)
507
+ unless response.is_a?(Net::HTTPSuccess)
508
+ raise "GET #{sanitized_renderer_url(url)} returned a non-success HTTP status: " \
509
+ "#{response.code} #{response.message}"
510
+ end
511
+
512
+ # Label the bytes with the declared charset, then transcode to UTF-8: ExecJS's
513
+ # underlying JS runtime parses source as UTF-8, so a body merely *tagged* with its
514
+ # declared encoding (e.g. ISO-8859-1) rather than actually converted would have its
515
+ # non-ASCII bytes silently corrupted once the runtime re-interprets them as UTF-8.
516
+ #
517
+ # String#encode is a no-op — it does NOT validate — when the source encoding already
518
+ # equals the destination (UTF-8). charset_from_content_type returns UTF-8 for three of
519
+ # the four cases this method exists to handle (no Content-Type header, no charset
520
+ # parameter, and an explicit charset=utf-8), so `.encode(Encoding::UTF_8)` alone would
521
+ # silently let invalid bytes through unchanged on those three paths — only a genuine
522
+ # cross-encoding transcode (e.g. a declared ISO-8859-1 body) actually validates via
523
+ # `encode`. valid_encoding? is therefore checked explicitly afterward so every path
524
+ # fails fast, not just the cross-encoding one.
525
+ #
526
+ # An undecodable byte sequence (a charset that doesn't actually match the bytes) is
527
+ # deliberately raised here rather than silently replaced with U+FFFD: this is source
528
+ # code, and replacing corrupt bytes would produce a bundle that parses but behaves
529
+ # wrongly, which is harder to diagnose than a clear load failure naming the URL.
530
+ decoded = response.body.dup
531
+ .force_encoding(charset_from_content_type(response["content-type"]))
532
+ .encode(Encoding::UTF_8)
533
+ unless decoded.valid_encoding?
534
+ raise "response body is not valid UTF-8 after decoding from the declared charset " \
535
+ "(GET #{sanitized_renderer_url(url)})"
536
+ end
537
+
538
+ decoded
412
539
  rescue StandardError => e
413
- msg = "file_url_to_string #{url} failed\nError is: #{e}\n\n#{Utils.default_troubleshooting_section}"
540
+ # Sanitized here too: a non-2xx response (e.g. 401/403 from a bad-credentials config) is
541
+ # exactly the failure mode that would otherwise put a URL's embedded password into this
542
+ # message, which Rails.logger and error trackers can persist. e.message is scrubbed as
543
+ # well as url itself: a URL malformed enough that URI.parse raises embeds the raw,
544
+ # unsanitized URL verbatim in URI::InvalidURIError's own message.
545
+ sanitized_url = sanitized_renderer_url(url)
546
+ sanitized_error = sanitized_renderer_error_message(e.message, url, sanitized_url)
547
+ msg = "file_url_to_string #{sanitized_url} failed\nError is: " \
548
+ "#{sanitized_error}\n\n#{Utils.default_troubleshooting_section}"
414
549
  raise ReactOnRails::ServerBundleLoadError, msg
415
550
  end
416
551
 
552
+ # Extracts the charset from a Content-Type header value (e.g. "application/javascript;
553
+ # charset=utf-8"), tolerating a quoted charset value (`charset="UTF-8"`, valid per RFC
554
+ # 7231's quoted-string parameter syntax) and additional parameters after it (e.g.
555
+ # `charset=utf-8; boundary=...`). Falls back to UTF-8 — the standard default for
556
+ # JavaScript/JSON source — when the header is missing, has no charset parameter, or
557
+ # names an encoding Ruby does not recognize, rather than raising on an absent or
558
+ # malformed charset. See issue #4584.
559
+ def charset_from_content_type(content_type_header)
560
+ match = content_type_header.to_s.match(/;\s*charset=(?<encoding>[^;]+)/i)
561
+ return Encoding::UTF_8 unless match
562
+
563
+ charset = match[:encoding].strip.delete_prefix('"').delete_suffix('"')
564
+ Encoding.find(charset)
565
+ rescue ArgumentError
566
+ Encoding::UTF_8
567
+ end
568
+
417
569
  def parse_render_result(result_string, render_options)
418
570
  # Auto-detect format: length-prefixed (contains tab) or legacy JSON.
419
571
  # ExecJS with older bundles may return JSON; node renderer returns length-prefixed.
420
572
  result = if result_string.to_s.include?("\t")
421
573
  ReactOnRails::LengthPrefixedParser.parse_one_chunk_result(result_string)
422
574
  else
423
- JSON.parse(result_string.to_s)
575
+ # LenientJson repairs lone-surrogate escapes the JS renderer can emit (#4710).
576
+ ReactOnRails::LenientJson.parse(result_string.to_s)
424
577
  end
425
578
  replay_console_to_rails_logger(result, render_options)
426
579
  result
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ReactOnRails
4
- VERSION = "17.0.1"
4
+ VERSION = "17.1.0.rc.0"
5
5
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "react_on_rails/agent_guardrails"
4
+
5
+ namespace :react_on_rails do
6
+ desc "Install/update the RSC agent-guardrail skill + advisory hook into this app's .claude/ " \
7
+ "(steers AI coding agents away from React Server Components API footguns; idempotent)"
8
+ task :install_rsc_agent_guardrails do
9
+ root = ReactOnRails::AgentGuardrails.default_destination_root(ENV.fetch("DESTINATION", nil))
10
+ actions = ReactOnRails::AgentGuardrails.install(root)
11
+
12
+ puts "React on Rails: RSC agent guardrails -> #{File.join(root, '.claude')}"
13
+ actions.each { |action| puts " #{action}" }
14
+ puts ""
15
+ puts "The rsc-app-safety skill and its advisory hook are now active for Claude Code in this app."
16
+ puts "Re-run this task after upgrading React on Rails to pick up guardrail updates; managed files are replaced."
17
+ rescue ReactOnRails::AgentGuardrails::Error, SystemCallError => e
18
+ warn "React on Rails: #{e.message}"
19
+ exit 1
20
+ end
21
+ end
@@ -57,6 +57,20 @@ namespace :run_rspec do
57
57
  env_vars: rbs_runtime_env_vars)
58
58
  end
59
59
 
60
+ desc "Run gem unit specs without generator specs"
61
+ task :gem_unit do
62
+ run_tests_in("",
63
+ rspec_args: "#{File.join('spec', 'react_on_rails')} --exclude-pattern '**/generators/**'",
64
+ env_vars: rbs_runtime_env_vars)
65
+ end
66
+
67
+ desc "Run gem generator specs"
68
+ task :gem_generators do
69
+ run_tests_in("",
70
+ rspec_args: File.join("spec", "react_on_rails", "generators"),
71
+ env_vars: rbs_runtime_env_vars)
72
+ end
73
+
60
74
  desc "Runs dummy rspec with turbolinks"
61
75
  task dummy: ["dummy_apps:dummy_app"] do
62
76
  run_tests_in(spec_dummy_dir,
@@ -1,6 +1,7 @@
1
1
  module ReactOnRails
2
2
  class PrerenderError < ::ReactOnRails::Error
3
- MAX_ERROR_SNIPPET_TO_LOG: Integer
3
+ REDACTED_VALUE: String
4
+ SENSITIVE_CONTEXT_KEYS: Array[String]
4
5
 
5
6
  attr_reader component_name: String?
6
7
  attr_reader err: StandardError?
@@ -0,0 +1,26 @@
1
+ ---
2
+ name: doctor-fix-loop
3
+ description: >
4
+ Use when diagnosing a React on Rails app with the stable doctor JSON contract and
5
+ iterating through actionable remediation until no error-level checks remain.
6
+ ---
7
+
8
+ # Run the doctor fix loop
9
+
10
+ Read the [version-matched package guide](../../docs/agent/doctor-fix-loop.md), then run:
11
+
12
+ ```bash
13
+ bin/rails react_on_rails:doctor FORMAT=json
14
+ ```
15
+
16
+ 1. Parse standard output as one JSON document; treat incidental standard-error output as diagnostics.
17
+ 2. Process non-passing checks in array order using their stable `id`, `severity`, `message`,
18
+ `remediation.prompt`, `remediation.files`, and `remediation.expected_end_state` fields.
19
+ 3. Inspect the named files and verify the diagnosis before editing. Do not invent a broad automatic
20
+ fix from a check ID, and do not treat a nullable `fix_command` as required.
21
+ 4. Make the smallest correction, review its diff, and rerun the doctor.
22
+ 5. Continue until the report has no error-level checks. Warnings are advisory but should be reported.
23
+ 6. Run the focused build or test that exercises each repaired subsystem.
24
+
25
+ The hosted JSON contract is a secondary reference:
26
+ https://reactonrails.com/docs/api-reference/doctor.
@@ -0,0 +1,24 @@
1
+ ---
2
+ name: install-and-upgrade
3
+ description: >
4
+ Use when installing React on Rails into an existing Rails app or upgrading its
5
+ gem, npm package, generated configuration, and lockfiles without version skew.
6
+ ---
7
+
8
+ # Install or upgrade React on Rails
9
+
10
+ Read the [version-matched package guide](../../docs/agent/install-and-upgrade.md)
11
+ before editing dependencies or generated files.
12
+
13
+ 1. Inspect the Rails, Ruby, Node, Shakapacker, gem, npm package, and lockfile state.
14
+ 2. Keep the `react_on_rails` gem and `react-on-rails` npm package on the same release.
15
+ For Pro apps, update the Pro gem and JavaScript packages as one coordinated set.
16
+ 3. Use the app's declared JavaScript package manager and update both Ruby and JavaScript lockfiles.
17
+ 4. Run the install generator for a new integration. During an upgrade, review its proposed changes
18
+ against app-owned configuration instead of overwriting custom files blindly.
19
+ 5. Run `bin/rails react_on_rails:doctor FORMAT=json`, resolve every error, and rerun it.
20
+ 6. Compile assets and run the app's focused tests before calling the change complete.
21
+
22
+ Use hosted docs only for additional detail after reading the installed guide:
23
+ https://reactonrails.com/docs/getting-started/existing-rails-app and
24
+ https://reactonrails.com/docs/upgrading/upgrading-react-on-rails.
@@ -0,0 +1,28 @@
1
+ ---
2
+ name: rsc-adoption
3
+ description: >
4
+ Use when adopting React Server Components in a React on Rails Pro app, including
5
+ prerequisites, generator setup, component boundaries, data flow, and safety checks.
6
+ ---
7
+
8
+ # Adopt React Server Components (Pro)
9
+
10
+ React Server Components are a React on Rails Pro workflow. Read the
11
+ [version-matched package guide](../../docs/agent/rsc-adoption.md) before changing setup.
12
+
13
+ 1. Confirm the app uses the Pro gem and matching Pro npm packages, the Node renderer,
14
+ and compatible React, React DOM, and RSC package releases.
15
+ 2. Preserve the app's language on the supported setup path: use
16
+ `bundle exec rails generate react_on_rails:rsc --typescript` for TypeScript apps and
17
+ `bundle exec rails generate react_on_rails:rsc` for JavaScript apps. Review every generated change.
18
+ 3. Treat the `'use client'` directive as the RSC boundary. Do not confuse it with
19
+ `.client.` and `.server.` filename suffixes, which control bundle placement.
20
+ 4. Keep Rails-owned data, authentication, authorization, and caching on the Rails side;
21
+ pass data through props or async props rather than assuming server components can
22
+ access Rails process state.
23
+ 5. Run `bundle exec rake react_on_rails:install_rsc_agent_guardrails` and review payload
24
+ route authorization, renderer isolation, secret handling, and serialized output.
25
+ 6. Run the JSON doctor, build all required bundles, and exercise the RSC route in a browser.
26
+
27
+ Hosted RSC docs are a secondary reference:
28
+ https://reactonrails.com/docs/pro/react-server-components.
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: streaming-debug
3
+ description: >
4
+ Use when a React on Rails Pro streaming SSR or async-props response buffers,
5
+ stalls, fails to reveal Suspense content, or behaves differently behind a proxy.
6
+ ---
7
+
8
+ # Debug streaming SSR (Pro)
9
+
10
+ Streaming SSR is a React on Rails Pro workflow. Read the
11
+ [version-matched package guide](../../docs/agent/streaming-debug.md) before changing code.
12
+
13
+ 1. Run `bin/rails react_on_rails:doctor FORMAT=json` and fix setup errors first.
14
+ 2. Identify the exact helper before applying streaming prerequisites.
15
+
16
+ ## Progressive helpers
17
+
18
+ For `stream_react_component`, `cached_stream_react_component`, and
19
+ `stream_react_component_with_async_props`:
20
+
21
+ 1. Confirm the controller includes `ReactOnRailsPro::Stream`, the action calls
22
+ `stream_view_containing_react_components`, and the Node renderer is healthy.
23
+ 2. When Suspense content should reveal progressively, confirm the boundary's child actually suspends.
24
+ 3. For `stream_react_component_with_async_props` only, confirm
25
+ `ReactOnRailsPro.configuration.enable_rsc_support` is enabled by setting
26
+ `config.enable_rsc_support = true` in the Pro initializer.
27
+ This is not a prerequisite for `stream_react_component` or `cached_stream_react_component`.
28
+ 4. For async props, keep slow work inside the streaming block, emit JSON-serializable values,
29
+ and ensure every requested prop is emitted or rejected.
30
+
31
+ ## Buffered helpers
32
+
33
+ For `buffered_stream_react_component`, `cached_buffered_stream_react_component`, and
34
+ `cached_static_rsc_component`:
35
+
36
+ 1. Confirm the Node renderer is healthy and inspect the complete buffered result.
37
+ 2. These helpers do not require the streaming controller wrapper: they do not require
38
+ `ReactOnRailsPro::Stream`, `stream_view_containing_react_components`, or a suspending boundary.
39
+ 3. For `cached_static_rsc_component`, set `config.enable_rsc_support = true`; disabling RSC support
40
+ omits the RSC payload generation that gives this helper its static-RSC behavior.
41
+ This setting is not a prerequisite for `buffered_stream_react_component` or
42
+ `cached_buffered_stream_react_component`.
43
+ 4. Do not expect early shell flush or progressive Suspense reveal from a buffered helper.
44
+
45
+ ## Debug either path
46
+
47
+ 1. Observe the response with an unbuffered client and compare origin behavior with the proxy/CDN path.
48
+ Check compression and buffering configuration before changing React code.
49
+ 2. Correlate Rails logs, Node renderer logs, response chunks, and browser timing marks; fix one
50
+ demonstrated boundary at a time and rerun the same probe.
51
+
52
+ Hosted streaming docs are a secondary reference:
53
+ https://reactonrails.com/docs/pro/streaming-ssr.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: react_on_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 17.0.1
4
+ version: 17.1.0.rc.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Gordon
@@ -122,6 +122,11 @@ files:
122
122
  - Rakefile
123
123
  - Steepfile
124
124
  - app/helpers/react_on_rails_helper.rb
125
+ - docs/agent/README.md
126
+ - docs/agent/doctor-fix-loop.md
127
+ - docs/agent/install-and-upgrade.md
128
+ - docs/agent/rsc-adoption.md
129
+ - docs/agent/streaming-debug.md
125
130
  - lib/generators/USAGE
126
131
  - lib/generators/react_on_rails/USAGE
127
132
  - lib/generators/react_on_rails/base_generator.rb
@@ -153,9 +158,9 @@ files:
153
158
  - lib/generators/react_on_rails/templates/base/base/.dev-services.yml.example
154
159
  - lib/generators/react_on_rails/templates/base/base/.env.example
155
160
  - lib/generators/react_on_rails/templates/base/base/.github/workflows/ci.yml.tt
156
- - lib/generators/react_on_rails/templates/base/base/Procfile.dev
157
161
  - lib/generators/react_on_rails/templates/base/base/Procfile.dev-prod-assets
158
- - lib/generators/react_on_rails/templates/base/base/Procfile.dev-static-assets
162
+ - lib/generators/react_on_rails/templates/base/base/Procfile.dev-static-assets.tt
163
+ - lib/generators/react_on_rails/templates/base/base/Procfile.dev.tt
159
164
  - lib/generators/react_on_rails/templates/base/base/app/controllers/hello_world_controller.rb
160
165
  - lib/generators/react_on_rails/templates/base/base/app/controllers/home_controller.rb
161
166
  - lib/generators/react_on_rails/templates/base/base/app/javascript/bundles/HelloWorld/components/HelloWorld.jsx
@@ -173,7 +178,6 @@ files:
173
178
  - lib/generators/react_on_rails/templates/base/base/babel.config.js.tt
174
179
  - lib/generators/react_on_rails/templates/base/base/bin/dev
175
180
  - lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook
176
- - lib/generators/react_on_rails/templates/base/base/bin/shakapacker-watch
177
181
  - lib/generators/react_on_rails/templates/base/base/bin/switch-bundler
178
182
  - lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt
179
183
  - lib/generators/react_on_rails/templates/base/base/config/shakapacker.yml.tt
@@ -231,6 +235,9 @@ files:
231
235
  - lib/generators/react_on_rails/templates/rsc/base/app/views/hello_server/index.html.erb.tt
232
236
  - lib/generators/react_on_rails/templates/rsc/base/config/webpack/rscWebpackConfig.js.tt
233
237
  - lib/react_on_rails.rb
238
+ - lib/react_on_rails/agent_guardrails.rb
239
+ - lib/react_on_rails/agent_guardrails/templates/rsc_app_safety_check.rb
240
+ - lib/react_on_rails/agent_guardrails/templates/rsc_app_safety_skill.md
234
241
  - lib/react_on_rails/config_path_resolver.rb
235
242
  - lib/react_on_rails/configuration.rb
236
243
  - lib/react_on_rails/controller.rb
@@ -245,6 +252,7 @@ files:
245
252
  - lib/react_on_rails/dev/server_mode.rb
246
253
  - lib/react_on_rails/dev/service_checker.rb
247
254
  - lib/react_on_rails/doctor.rb
255
+ - lib/react_on_rails/doctor_schema.rb
248
256
  - lib/react_on_rails/engine.rb
249
257
  - lib/react_on_rails/error.rb
250
258
  - lib/react_on_rails/font_helper.rb
@@ -253,6 +261,7 @@ files:
253
261
  - lib/react_on_rails/json_output.rb
254
262
  - lib/react_on_rails/json_parse_error.rb
255
263
  - lib/react_on_rails/length_prefixed_parser.rb
264
+ - lib/react_on_rails/lenient_json.rb
256
265
  - lib/react_on_rails/locales/base.rb
257
266
  - lib/react_on_rails/locales/to_js.rb
258
267
  - lib/react_on_rails/locales/to_json.rb
@@ -282,6 +291,7 @@ files:
282
291
  - lib/react_on_rails/version_checker.rb
283
292
  - lib/react_on_rails/version_synchronizer.rb
284
293
  - lib/react_on_rails/version_syntax_converter.rb
294
+ - lib/tasks/agent_guardrails.rake
285
295
  - lib/tasks/assets.rake
286
296
  - lib/tasks/doctor.rake
287
297
  - lib/tasks/generate_packs.rake
@@ -327,6 +337,10 @@ files:
327
337
  - sig/react_on_rails/typescript_response_types/generated_file_writer.rbs
328
338
  - sig/react_on_rails/utils.rbs
329
339
  - sig/react_on_rails/version_checker.rbs
340
+ - skills/doctor-fix-loop/SKILL.md
341
+ - skills/install-and-upgrade/SKILL.md
342
+ - skills/rsc-adoption/SKILL.md
343
+ - skills/streaming-debug/SKILL.md
330
344
  homepage: https://reactonrails.com/docs/
331
345
  licenses:
332
346
  - MIT
@@ -1,26 +0,0 @@
1
- #!/usr/bin/env sh
2
- set -eu
3
-
4
- child_pid=""
5
-
6
- cleanup() {
7
- if [ -n "${child_pid}" ] && kill -0 "$child_pid" 2>/dev/null; then
8
- kill -TERM "$child_pid" 2>/dev/null || true
9
- wait "$child_pid" 2>/dev/null || true
10
- fi
11
-
12
- exit 0
13
- }
14
-
15
- bin/shakapacker "$@" &
16
- child_pid=$!
17
- trap cleanup INT TERM
18
-
19
- if wait "$child_pid"; then
20
- status=0
21
- else
22
- status=$?
23
- fi
24
-
25
- child_pid=""
26
- exit "$status"