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
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ReactOnRails
|
|
4
|
+
# Public contract for the machine-readable doctor report.
|
|
5
|
+
#
|
|
6
|
+
# Check IDs are stable identifiers for automation. Never rename or reuse one;
|
|
7
|
+
# add a new ID for a new check. Additive fields may be introduced without a
|
|
8
|
+
# schema version bump, but removing or changing a documented field requires a
|
|
9
|
+
# new version.
|
|
10
|
+
module DoctorSchema
|
|
11
|
+
VERSION = 1
|
|
12
|
+
STATUS_SEVERITIES = { "pass" => "info", "warn" => "warning", "fail" => "error" }.freeze
|
|
13
|
+
STATUSES = STATUS_SEVERITIES.keys.freeze
|
|
14
|
+
SEVERITIES = STATUS_SEVERITIES.values.uniq.freeze
|
|
15
|
+
DETAIL_LEVELS = %w[success warning error info].freeze
|
|
16
|
+
EXIT_CODES = { pass: 0, warn: 0, fail: 1 }.freeze
|
|
17
|
+
DOCS_URL = "https://reactonrails.com/docs/api-reference/doctor"
|
|
18
|
+
|
|
19
|
+
CHECK_METADATA = {
|
|
20
|
+
"environment_prerequisites" => {
|
|
21
|
+
files: ["package.json", "package manager lockfile"],
|
|
22
|
+
expected_end_state: "Node.js and the app's JavaScript package manager are installed and detected."
|
|
23
|
+
},
|
|
24
|
+
"react_on_rails_versions" => {
|
|
25
|
+
files: ["Gemfile", "package.json", "package manager lockfile"],
|
|
26
|
+
expected_end_state: "The React on Rails gem and npm package versions are compatible and pinned safely."
|
|
27
|
+
},
|
|
28
|
+
"react_on_rails_packages" => {
|
|
29
|
+
files: ["Gemfile", "config/shakapacker.yml", "config/webpack", "config/rspack"],
|
|
30
|
+
expected_end_state: "Shakapacker is installed and its configured bundler files exist."
|
|
31
|
+
},
|
|
32
|
+
"javascript_package_dependencies" => {
|
|
33
|
+
files: ["package.json", "package manager lockfile"],
|
|
34
|
+
expected_end_state: "Required React and React on Rails JavaScript dependencies are installed and compatible."
|
|
35
|
+
},
|
|
36
|
+
"key_configuration_files" => {
|
|
37
|
+
files: ["config/initializers/react_on_rails.rb", "config/shakapacker.yml", "app/javascript"],
|
|
38
|
+
expected_end_state: "The generated React on Rails configuration files exist and match the app setup."
|
|
39
|
+
},
|
|
40
|
+
"configuration_analysis" => {
|
|
41
|
+
files: ["config/initializers/react_on_rails.rb", "config/shakapacker.yml", "app/views/layouts"],
|
|
42
|
+
expected_end_state: "React on Rails, Shakapacker, and server bundle settings agree."
|
|
43
|
+
},
|
|
44
|
+
"bin_dev_launcher_setup" => {
|
|
45
|
+
files: ["bin/dev", "Procfile.dev", "Procfile.dev-static-assets"],
|
|
46
|
+
expected_end_state: "The development launcher starts Rails and the configured asset bundler correctly."
|
|
47
|
+
},
|
|
48
|
+
"rails_integration" => {
|
|
49
|
+
files: ["config/initializers/react_on_rails.rb", "config/application.rb"],
|
|
50
|
+
expected_end_state: "Rails loads the React on Rails initializer successfully."
|
|
51
|
+
},
|
|
52
|
+
"bundler_configuration" => {
|
|
53
|
+
files: ["config/shakapacker.yml", "config/webpack", "config/rspack"],
|
|
54
|
+
expected_end_state: "The configured webpack or Rspack entry point is present and valid."
|
|
55
|
+
},
|
|
56
|
+
"testing_setup" => {
|
|
57
|
+
files: ["spec/rails_helper.rb", "spec/spec_helper.rb", "test/test_helper.rb"],
|
|
58
|
+
expected_end_state: "The test suite builds React assets before tests that need them."
|
|
59
|
+
},
|
|
60
|
+
"development_environment" => {
|
|
61
|
+
files: ["config/environments/development.rb", "config/shakapacker.yml", "package.json"],
|
|
62
|
+
expected_end_state: "The development server, HMR, and generated bundle settings are consistent."
|
|
63
|
+
},
|
|
64
|
+
"react_on_rails_pro_setup" => {
|
|
65
|
+
files: ["Gemfile", "package.json", "config/initializers/react_on_rails.rb"],
|
|
66
|
+
expected_end_state: "React on Rails Pro packages and configuration are mutually compatible."
|
|
67
|
+
},
|
|
68
|
+
"node_renderer_rollout_capacity" => {
|
|
69
|
+
files: ["renderer/node-renderer.js", "client/node-renderer.js", "deployment environment"],
|
|
70
|
+
expected_end_state:
|
|
71
|
+
"The NodeRenderer declares and prewarms its current server/RSC set, and its VM hard cap can hold " \
|
|
72
|
+
"rollout overlap."
|
|
73
|
+
},
|
|
74
|
+
"react_server_components" => {
|
|
75
|
+
files: ["config/initializers/react_on_rails.rb", "config/shakapacker.yml", "app/javascript"],
|
|
76
|
+
expected_end_state: "RSC packages, generated artifacts, and renderer configuration are consistent."
|
|
77
|
+
}
|
|
78
|
+
}.freeze
|
|
79
|
+
|
|
80
|
+
# A check section can contain several distinct diagnoses, so no section-wide
|
|
81
|
+
# repair command is currently safe. Keep fix_command nullable until a future
|
|
82
|
+
# diagnosis-specific contract can prove that one command repairs the exact finding.
|
|
83
|
+
|
|
84
|
+
module_function
|
|
85
|
+
|
|
86
|
+
def metadata(check_id)
|
|
87
|
+
CHECK_METADATA.fetch(check_id) do
|
|
88
|
+
raise ArgumentError, "Missing DoctorSchema::CHECK_METADATA entry for check id #{check_id.inspect}"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def docs_url(check_id)
|
|
93
|
+
"#{DOCS_URL}#check-id-#{check_id.tr('_', '-')}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def summarize_statuses(statuses)
|
|
97
|
+
counts = statuses.tally
|
|
98
|
+
STATUSES.to_h { |status| [status.to_sym, counts.fetch(status, 0)] }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Runtime validation keeps the emitted payload aligned with the documented
|
|
102
|
+
# contract without adding a runtime JSON-schema dependency to the gem.
|
|
103
|
+
def validate!(report)
|
|
104
|
+
required_keys!(report, %i[schema_version ror_version status checks summary], "report")
|
|
105
|
+
assert(report[:schema_version] == VERSION, "unsupported schema_version")
|
|
106
|
+
assert(report[:ror_version].is_a?(String), "ror_version must be a string")
|
|
107
|
+
assert(STATUSES.include?(report[:status]), "invalid report status")
|
|
108
|
+
assert(report[:checks].is_a?(Array), "checks must be an array")
|
|
109
|
+
report[:checks].each { |check| validate_check!(check) }
|
|
110
|
+
validate_unique_check_ids!(report[:checks])
|
|
111
|
+
validate_summary!(report)
|
|
112
|
+
report
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def validate_check!(check)
|
|
116
|
+
required_keys!(check, %i[id title status severity message fix_command docs_url remediation details], "check")
|
|
117
|
+
assert(CHECK_METADATA.key?(check[:id]), "unknown check id #{check[:id].inspect}")
|
|
118
|
+
assert(STATUSES.include?(check[:status]), "invalid status for #{check[:id]}")
|
|
119
|
+
assert(SEVERITIES.include?(check[:severity]), "invalid severity for #{check[:id]}")
|
|
120
|
+
assert(check[:severity] == STATUS_SEVERITIES[check[:status]], "severity does not match status for #{check[:id]}")
|
|
121
|
+
validate_check_field_types!(check)
|
|
122
|
+
validate_details!(check)
|
|
123
|
+
validate_remediation!(check)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def validate_check_field_types!(check)
|
|
127
|
+
assert(check[:title].is_a?(String), "invalid title for #{check[:id]}")
|
|
128
|
+
assert(check[:message].nil? || check[:message].is_a?(String), "invalid message for #{check[:id]}")
|
|
129
|
+
assert(check[:fix_command].nil? || check[:fix_command].is_a?(String), "invalid fix_command for #{check[:id]}")
|
|
130
|
+
assert(check[:docs_url].is_a?(String), "invalid docs_url for #{check[:id]}")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def validate_details!(check)
|
|
134
|
+
assert(check[:details].is_a?(Array), "details must be an array for #{check[:id]}")
|
|
135
|
+
check[:details].each { |detail| validate_detail!(detail, check[:id]) }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def validate_detail!(detail, check_id)
|
|
139
|
+
required_keys!(detail, %i[level content], "detail for #{check_id}")
|
|
140
|
+
assert(DETAIL_LEVELS.include?(detail[:level]), "invalid detail level for #{check_id}")
|
|
141
|
+
assert(detail[:content].is_a?(String), "invalid detail content for #{check_id}")
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def validate_remediation!(check)
|
|
145
|
+
remediation = check[:remediation]
|
|
146
|
+
if check[:status] == "pass"
|
|
147
|
+
assert(remediation.nil?, "passing check #{check[:id]} must not have remediation")
|
|
148
|
+
return
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
assert(remediation.is_a?(Hash), "non-passing check #{check[:id]} must have remediation")
|
|
152
|
+
required_keys!(remediation, %i[prompt files expected_end_state], "remediation")
|
|
153
|
+
assert(remediation[:prompt].is_a?(String) && !remediation[:prompt].empty?, "invalid remediation prompt")
|
|
154
|
+
assert(remediation[:files].is_a?(Array) && remediation[:files].all?(String), "invalid remediation files")
|
|
155
|
+
assert(remediation[:expected_end_state].is_a?(String), "invalid remediation expected_end_state")
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def validate_unique_check_ids!(checks)
|
|
159
|
+
ids = checks.map { |check| check[:id] }
|
|
160
|
+
assert(ids.uniq.length == ids.length, "check ids must be unique")
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def validate_summary!(report)
|
|
164
|
+
summary = report[:summary]
|
|
165
|
+
required_keys!(summary, %i[pass warn fail], "summary")
|
|
166
|
+
assert(summary.values.all? { |count| count.is_a?(Integer) && count >= 0 },
|
|
167
|
+
"summary values must be non-negative integers")
|
|
168
|
+
|
|
169
|
+
statuses = report[:checks].map { |check| check[:status] }
|
|
170
|
+
expected_summary = summarize_statuses(statuses)
|
|
171
|
+
assert(summary == expected_summary, "summary does not match checks")
|
|
172
|
+
assert(report[:status] == overall_status(statuses), "report status does not match checks")
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def overall_status(statuses)
|
|
176
|
+
return "fail" if statuses.include?("fail")
|
|
177
|
+
return "warn" if statuses.include?("warn")
|
|
178
|
+
|
|
179
|
+
"pass"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def required_keys!(value, keys, label)
|
|
183
|
+
assert(value.is_a?(Hash), "#{label} must be an object")
|
|
184
|
+
missing = keys - value.keys
|
|
185
|
+
assert(missing.empty?, "#{label} missing keys: #{missing.join(', ')}")
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def assert(condition, message)
|
|
189
|
+
raise ArgumentError, "Invalid doctor JSON report: #{message}" unless condition
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -30,9 +30,9 @@ module ReactOnRails
|
|
|
30
30
|
config.after_initialize do
|
|
31
31
|
next if Engine.skip_version_validation?
|
|
32
32
|
|
|
33
|
-
Rails.logger.
|
|
33
|
+
Rails.logger.debug "[React on Rails] Validating package version and compatibility..."
|
|
34
34
|
VersionChecker.build.validate_version_and_package_compatibility!
|
|
35
|
-
Rails.logger.
|
|
35
|
+
Rails.logger.debug "[React on Rails] Package validation successful"
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
# 1. The white spacing in this file matters!
|
|
8
8
|
# 2. Keep all #{some_var} fully to the left so that all indentation is done evenly in that var
|
|
9
9
|
require "react_on_rails/prerender_error"
|
|
10
|
+
require "react_on_rails/json_parse_error"
|
|
10
11
|
require "react_on_rails/smart_error"
|
|
11
12
|
require "addressable/uri"
|
|
12
13
|
require "react_on_rails/utils"
|
|
@@ -904,6 +905,20 @@ module ReactOnRails
|
|
|
904
905
|
)
|
|
905
906
|
end
|
|
906
907
|
|
|
908
|
+
def raise_renderer_prerender_error(err, react_component_name, props, js_code)
|
|
909
|
+
prerender_error = ReactOnRails::PrerenderError.new(component_name: react_component_name,
|
|
910
|
+
# Sanitize as this might be browser logged
|
|
911
|
+
props: sanitized_props_string(props),
|
|
912
|
+
err:,
|
|
913
|
+
js_code:)
|
|
914
|
+
renderer_parse_error = err.is_a?(ReactOnRails::JsonParseError) ||
|
|
915
|
+
(defined?(ReactOnRails::LengthPrefixedParser::ParseError) &&
|
|
916
|
+
err.is_a?(ReactOnRails::LengthPrefixedParser::ParseError))
|
|
917
|
+
raise prerender_error, cause: nil if renderer_parse_error
|
|
918
|
+
|
|
919
|
+
raise prerender_error
|
|
920
|
+
end
|
|
921
|
+
|
|
907
922
|
def rendering_error_from_result(json_result)
|
|
908
923
|
rendering_error = json_result["renderingError"]
|
|
909
924
|
return unless rendering_error.is_a?(Hash)
|
|
@@ -987,11 +1002,7 @@ module ReactOnRails
|
|
|
987
1002
|
result = ReactOnRails::ServerRenderingPool.server_render_js_with_console_logging(js_code, render_options)
|
|
988
1003
|
rescue StandardError => err
|
|
989
1004
|
# This error came from the renderer
|
|
990
|
-
|
|
991
|
-
# Sanitize as this might be browser logged
|
|
992
|
-
props: sanitized_props_string(props),
|
|
993
|
-
err:,
|
|
994
|
-
js_code:)
|
|
1005
|
+
raise_renderer_prerender_error(err, react_component_name, props, js_code)
|
|
995
1006
|
end
|
|
996
1007
|
|
|
997
1008
|
if render_options.streaming?
|
|
@@ -1005,11 +1016,7 @@ module ReactOnRails
|
|
|
1005
1016
|
|
|
1006
1017
|
result.rescue do |err|
|
|
1007
1018
|
# This error came from the renderer
|
|
1008
|
-
|
|
1009
|
-
# Sanitize as this might be browser logged
|
|
1010
|
-
props: sanitized_props_string(props),
|
|
1011
|
-
err:,
|
|
1012
|
-
js_code:)
|
|
1019
|
+
raise_renderer_prerender_error(err, react_component_name, props, js_code)
|
|
1013
1020
|
end
|
|
1014
1021
|
elsif result["hasErrors"] && render_options.raise_on_prerender_error
|
|
1015
1022
|
raise_prerender_error(result, react_component_name, props, js_code)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require "react_on_rails/lenient_json"
|
|
4
5
|
|
|
5
6
|
module ReactOnRails
|
|
6
7
|
# Parses the length-prefixed wire format used between Node renderer and Ruby.
|
|
@@ -11,6 +12,8 @@ module ReactOnRails
|
|
|
11
12
|
# Used by both streaming (Pro) and non-streaming (OSS) paths.
|
|
12
13
|
# Strict protocol parser — any format violation raises an error.
|
|
13
14
|
class LengthPrefixedParser
|
|
15
|
+
class ParseError < ReactOnRails::Error; end
|
|
16
|
+
|
|
14
17
|
# Keep aligned with ReactOnRailsPro::StreamRequest::CONTROL_MESSAGE_TYPES,
|
|
15
18
|
# which routes these same control frames during bidirectional streaming.
|
|
16
19
|
# MIRROR VALUES OF: react_on_rails_pro/lib/react_on_rails_pro/stream_request.rb
|
|
@@ -28,11 +31,11 @@ module ReactOnRails
|
|
|
28
31
|
results = []
|
|
29
32
|
parser.feed(str.to_s.b) { |chunk| results << chunk }
|
|
30
33
|
if results.empty?
|
|
31
|
-
raise
|
|
34
|
+
raise ParseError,
|
|
32
35
|
"Malformed render result: expected exactly one length-prefixed chunk but found none"
|
|
33
36
|
end
|
|
34
37
|
if results.size > 1
|
|
35
|
-
raise
|
|
38
|
+
raise ParseError,
|
|
36
39
|
"Malformed render result: expected exactly one length-prefixed chunk but found #{results.size}"
|
|
37
40
|
end
|
|
38
41
|
results.first
|
|
@@ -98,11 +101,7 @@ module ReactOnRails
|
|
|
98
101
|
@buf = @buf.byteslice(idx + 1, @buf.bytesize - idx - 1)
|
|
99
102
|
|
|
100
103
|
tab_idx = header.byteindex("\t".b)
|
|
101
|
-
unless tab_idx
|
|
102
|
-
header_str = header.force_encoding(Encoding::UTF_8).inspect
|
|
103
|
-
raise ReactOnRails::Error,
|
|
104
|
-
"Malformed length-prefixed header: missing tab separator in: #{header_str}"
|
|
105
|
-
end
|
|
104
|
+
raise ParseError, "Malformed length-prefixed header: missing tab separator" unless tab_idx
|
|
106
105
|
|
|
107
106
|
parse_length_prefixed_header(header, tab_idx)
|
|
108
107
|
true
|
|
@@ -115,16 +114,18 @@ module ReactOnRails
|
|
|
115
114
|
begin
|
|
116
115
|
@content_len = Integer(len_hex, 16)
|
|
117
116
|
rescue ArgumentError
|
|
118
|
-
raise
|
|
117
|
+
raise ParseError, "Malformed length-prefixed header: invalid content length hex", cause: nil
|
|
119
118
|
end
|
|
119
|
+
raise ParseError, "Malformed length-prefixed header: negative content length" if @content_len.negative?
|
|
120
120
|
|
|
121
121
|
begin
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
122
|
+
# LenientJson tolerates lone UTF-16 surrogates that the JS renderer can emit
|
|
123
|
+
# (e.g. from truncating a string mid-emoji) which stock JSON.parse rejects. See #4710.
|
|
124
|
+
@metadata = LenientJson.parse(meta_json.force_encoding(Encoding::UTF_8))
|
|
125
|
+
rescue JSON::ParserError
|
|
126
|
+
raise ParseError, "Malformed length-prefixed header: invalid metadata JSON", cause: nil
|
|
127
127
|
end
|
|
128
|
+
raise ParseError, "Malformed length-prefixed header: metadata JSON must be an object" unless @metadata.is_a?(Hash)
|
|
128
129
|
|
|
129
130
|
@state = :content
|
|
130
131
|
end
|
|
@@ -153,7 +154,7 @@ module ReactOnRails
|
|
|
153
154
|
# "object" → JSON-serialized value (ServerRenderHash or null), needs JSON.parse
|
|
154
155
|
# "string" → raw HTML string, used as-is
|
|
155
156
|
payload_type = @metadata.delete("payloadType")
|
|
156
|
-
@metadata["html"] = payload_type == "object" ?
|
|
157
|
+
@metadata["html"] = payload_type == "object" ? parse_object_payload(raw_content) : raw_content
|
|
157
158
|
|
|
158
159
|
result = @metadata
|
|
159
160
|
@metadata = nil
|
|
@@ -161,5 +162,12 @@ module ReactOnRails
|
|
|
161
162
|
yield result
|
|
162
163
|
true
|
|
163
164
|
end
|
|
165
|
+
|
|
166
|
+
def parse_object_payload(raw_content)
|
|
167
|
+
# See the metadata parse above: LenientJson repairs lone-surrogate escapes (#4710).
|
|
168
|
+
LenientJson.parse(raw_content)
|
|
169
|
+
rescue JSON::ParserError
|
|
170
|
+
raise ParseError, "Malformed length-prefixed object payload JSON", cause: nil
|
|
171
|
+
end
|
|
164
172
|
end
|
|
165
173
|
end
|
|
Binary file
|
|
@@ -5,7 +5,8 @@ require "rainbow"
|
|
|
5
5
|
# rubocop:disable: Layout/IndentHeredoc
|
|
6
6
|
module ReactOnRails
|
|
7
7
|
class PrerenderError < ::ReactOnRails::Error
|
|
8
|
-
|
|
8
|
+
REDACTED_VALUE = "[REDACTED]"
|
|
9
|
+
SENSITIVE_CONTEXT_KEYS = %w[props js_code json].freeze
|
|
9
10
|
# TODO: Consider remove providing original `err` as already have access to `self.cause`
|
|
10
11
|
# http://blog.honeybadger.io/nested-errors-in-ruby-with-exception-cause/
|
|
11
12
|
attr_reader :component_name, :err, :props, :js_code, :console_messages
|
|
@@ -15,8 +16,8 @@ module ReactOnRails
|
|
|
15
16
|
js_code: nil, console_messages: nil)
|
|
16
17
|
@component_name = component_name
|
|
17
18
|
@err = err
|
|
18
|
-
@props = props
|
|
19
|
-
@js_code = js_code
|
|
19
|
+
@props = redacted_value(props)
|
|
20
|
+
@js_code = redacted_value(js_code)
|
|
20
21
|
@console_messages = console_messages
|
|
21
22
|
|
|
22
23
|
backtrace, message = calc_message(component_name, console_messages, err, js_code, props)
|
|
@@ -35,18 +36,43 @@ module ReactOnRails
|
|
|
35
36
|
def to_error_context
|
|
36
37
|
result = {
|
|
37
38
|
component_name:,
|
|
38
|
-
err
|
|
39
|
+
err: error_context_value(err),
|
|
39
40
|
props:,
|
|
40
41
|
js_code:,
|
|
41
42
|
console_messages:
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
if err.respond_to?(:to_error_context)
|
|
46
|
+
nested_context = err.to_error_context.reject { |key, _value| sensitive_nested_context_key?(key) }
|
|
47
|
+
result.merge!(nested_context)
|
|
48
|
+
end
|
|
45
49
|
result
|
|
46
50
|
end
|
|
47
51
|
|
|
48
52
|
private
|
|
49
53
|
|
|
54
|
+
def renderer_parse_error?(error)
|
|
55
|
+
json_parse_error = defined?(JsonParseError) && error.is_a?(JsonParseError)
|
|
56
|
+
length_prefixed_parse_error = defined?(LengthPrefixedParser::ParseError) &&
|
|
57
|
+
error.is_a?(LengthPrefixedParser::ParseError)
|
|
58
|
+
json_parse_error || length_prefixed_parse_error
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def safe_error_details(error)
|
|
62
|
+
return error.inspect unless renderer_parse_error?(error)
|
|
63
|
+
|
|
64
|
+
"#{error.class}: Renderer response could not be parsed"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def error_context_value(error)
|
|
68
|
+
renderer_parse_error?(error) ? safe_error_details(error) : error
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def sensitive_nested_context_key?(key)
|
|
72
|
+
SENSITIVE_CONTEXT_KEYS.include?(key.to_s) ||
|
|
73
|
+
(renderer_parse_error?(err) && key.to_s == "original_error")
|
|
74
|
+
end
|
|
75
|
+
|
|
50
76
|
# rubocop:disable Metrics/AbcSize
|
|
51
77
|
def calc_message(component_name, console_messages, err, js_code, props)
|
|
52
78
|
header = Rainbow("❌ React on Rails Server Rendering Error").red.bright
|
|
@@ -57,7 +83,7 @@ module ReactOnRails
|
|
|
57
83
|
if err
|
|
58
84
|
message << Rainbow("Error Details:").red.bright << "\n"
|
|
59
85
|
message << <<~MSG
|
|
60
|
-
#{err
|
|
86
|
+
#{safe_error_details(err)}
|
|
61
87
|
|
|
62
88
|
MSG
|
|
63
89
|
|
|
@@ -68,11 +94,11 @@ module ReactOnRails
|
|
|
68
94
|
|
|
69
95
|
# Add props information
|
|
70
96
|
message << Rainbow("Props:").blue.bright << "\n"
|
|
71
|
-
message << "#{
|
|
97
|
+
message << "#{redacted_value(props)}\n\n"
|
|
72
98
|
|
|
73
99
|
# Add code snippet
|
|
74
100
|
message << Rainbow("JavaScript Code:").blue.bright << "\n"
|
|
75
|
-
message << "#{
|
|
101
|
+
message << "#{redacted_value(js_code)}\n\n"
|
|
76
102
|
|
|
77
103
|
if console_messages && console_messages.strip.present?
|
|
78
104
|
message << Rainbow("Console Output:").magenta.bright << "\n"
|
|
@@ -90,6 +116,10 @@ module ReactOnRails
|
|
|
90
116
|
# rubocop:enable Metrics/AbcSize
|
|
91
117
|
end
|
|
92
118
|
|
|
119
|
+
def redacted_value(value)
|
|
120
|
+
value.nil? ? nil : REDACTED_VALUE
|
|
121
|
+
end
|
|
122
|
+
|
|
93
123
|
# Formats `err.backtrace` for display, or returns nil when there are no frames.
|
|
94
124
|
def formatted_backtrace(err)
|
|
95
125
|
error_backtrace = err.backtrace
|