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.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/docs/agent/README.md +26 -0
- data/docs/agent/doctor-fix-loop.md +25 -0
- data/docs/agent/install-and-upgrade.md +162 -0
- data/docs/agent/rsc-adoption.md +31 -0
- data/docs/agent/streaming-debug.md +57 -0
- data/lib/generators/react_on_rails/base_generator.rb +4 -4
- data/lib/generators/react_on_rails/generator_helper.rb +12 -1
- data/lib/generators/react_on_rails/install_generator.rb +50 -5
- data/lib/generators/react_on_rails/js_dependency_manager.rb +1 -1
- data/lib/generators/react_on_rails/pro_generator.rb +5 -3
- data/lib/generators/react_on_rails/pro_setup.rb +51 -19
- data/lib/generators/react_on_rails/rsc_generator.rb +18 -1
- data/lib/generators/react_on_rails/rsc_setup/client_references.rb +63 -25
- data/lib/generators/react_on_rails/rsc_setup.rb +141 -39
- data/lib/generators/react_on_rails/templates/agent_files/AGENTS.md +31 -3
- data/lib/generators/react_on_rails/templates/base/base/{Procfile.dev-static-assets → Procfile.dev-static-assets.tt} +1 -1
- data/lib/generators/react_on_rails/templates/base/base/{Procfile.dev → Procfile.dev.tt} +1 -1
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt +18 -42
- data/lib/react_on_rails/agent_guardrails/templates/rsc_app_safety_check.rb +215 -0
- data/lib/react_on_rails/agent_guardrails/templates/rsc_app_safety_skill.md +71 -0
- data/lib/react_on_rails/agent_guardrails.rb +223 -0
- data/lib/react_on_rails/dev/service_checker.rb +1 -1
- data/lib/react_on_rails/doctor.rb +880 -48
- data/lib/react_on_rails/doctor_schema.rb +192 -0
- data/lib/react_on_rails/engine.rb +2 -2
- data/lib/react_on_rails/helper.rb +17 -10
- data/lib/react_on_rails/length_prefixed_parser.rb +22 -14
- data/lib/react_on_rails/lenient_json.rb +0 -0
- data/lib/react_on_rails/prerender_error.rb +38 -8
- data/lib/react_on_rails/react_component/render_options.rb +1 -1
- data/lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb +168 -15
- data/lib/react_on_rails/version.rb +1 -1
- data/lib/tasks/agent_guardrails.rake +21 -0
- data/rakelib/run_rspec.rake +14 -0
- data/sig/react_on_rails/prerender_error.rbs +2 -1
- data/skills/doctor-fix-loop/SKILL.md +26 -0
- data/skills/install-and-upgrade/SKILL.md +24 -0
- data/skills/rsc-adoption/SKILL.md +28 -0
- data/skills/streaming-debug/SKILL.md +53 -0
- metadata +18 -4
- 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
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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.
|
|
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::
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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
|
data/rakelib/run_rspec.rake
CHANGED
|
@@ -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,
|
|
@@ -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.
|
|
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"
|