react_on_rails 16.2.0.beta.10 → 16.2.0.beta.12
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/CHANGELOG.md +14 -21
- data/CLAUDE.md +180 -5
- data/CONTRIBUTING.md +3 -1
- data/Gemfile.lock +1 -1
- data/Steepfile +4 -0
- data/analysis/rake-task-duplicate-analysis.md +149 -0
- data/analysis/v8-crash-retry-solution.md +148 -0
- data/bin/ci-run-failed-specs +6 -4
- data/bin/ci-switch-config +4 -3
- data/lib/generators/react_on_rails/base_generator.rb +2 -1
- data/lib/generators/react_on_rails/generator_helper.rb +29 -0
- data/lib/generators/react_on_rails/js_dependency_manager.rb +29 -7
- data/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt +19 -0
- data/lib/generators/react_on_rails/templates/base/base/config/{shakapacker.yml → shakapacker.yml.tt} +9 -0
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt +38 -4
- data/lib/react_on_rails/configuration.rb +89 -10
- data/lib/react_on_rails/dev/pack_generator.rb +49 -11
- data/lib/react_on_rails/dev/server_manager.rb +1 -0
- data/lib/react_on_rails/doctor.rb +94 -4
- data/lib/react_on_rails/helper.rb +32 -5
- data/lib/react_on_rails/packs_generator.rb +47 -35
- data/lib/react_on_rails/system_checker.rb +7 -4
- data/lib/react_on_rails/utils.rb +54 -0
- data/lib/react_on_rails/version.rb +1 -1
- data/lib/tasks/generate_packs.rake +12 -6
- data/react_on_rails_pro/CHANGELOG.md +11 -4
- data/react_on_rails_pro/Gemfile.lock +3 -3
- data/react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb +2 -1
- data/react_on_rails_pro/lib/react_on_rails_pro/version.rb +1 -1
- data/react_on_rails_pro/package.json +1 -1
- data/react_on_rails_pro/spec/dummy/Gemfile.lock +3 -3
- data/react_on_rails_pro/spec/dummy/spec/helpers/react_on_rails_pro_helper_spec.rb +15 -7
- data/react_on_rails_pro/spec/dummy/spec/requests/renderer_console_logging_spec.rb +9 -4
- data/sig/react_on_rails/dev/file_manager.rbs +15 -0
- data/sig/react_on_rails/dev/pack_generator.rbs +19 -0
- data/sig/react_on_rails/dev/process_manager.rbs +22 -0
- data/sig/react_on_rails/dev/server_manager.rbs +39 -0
- metadata +9 -3
|
@@ -226,7 +226,7 @@ module ReactOnRails
|
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
consoleReplayScript = ReactOnRails.
|
|
229
|
+
consoleReplayScript = ReactOnRails.getConsoleReplayScript();
|
|
230
230
|
|
|
231
231
|
return JSON.stringify({
|
|
232
232
|
html: htmlResult,
|
|
@@ -242,8 +242,9 @@ module ReactOnRails
|
|
|
242
242
|
.server_render_js_with_console_logging(js_code, render_options)
|
|
243
243
|
|
|
244
244
|
html = result["html"]
|
|
245
|
-
|
|
246
|
-
|
|
245
|
+
console_script = result["consoleReplayScript"]
|
|
246
|
+
console_script_tag = wrap_console_script_with_nonce(console_script) if render_options.replay_console
|
|
247
|
+
raw("#{html}#{console_script_tag}")
|
|
247
248
|
rescue ExecJS::ProgramError => err
|
|
248
249
|
raise ReactOnRails::PrerenderError.new(component_name: "N/A (server_render_js called)",
|
|
249
250
|
err: err,
|
|
@@ -394,7 +395,7 @@ module ReactOnRails
|
|
|
394
395
|
server_rendered_html.html_safe,
|
|
395
396
|
content_tag_options)
|
|
396
397
|
|
|
397
|
-
result_console_script = render_options.replay_console ? console_script : ""
|
|
398
|
+
result_console_script = render_options.replay_console ? wrap_console_script_with_nonce(console_script) : ""
|
|
398
399
|
result = compose_react_component_html_with_spec_and_console(
|
|
399
400
|
component_specification_tag, rendered_output, result_console_script
|
|
400
401
|
)
|
|
@@ -419,7 +420,7 @@ module ReactOnRails
|
|
|
419
420
|
server_rendered_html[COMPONENT_HTML_KEY].html_safe,
|
|
420
421
|
content_tag_options)
|
|
421
422
|
|
|
422
|
-
result_console_script = render_options.replay_console ? console_script : ""
|
|
423
|
+
result_console_script = render_options.replay_console ? wrap_console_script_with_nonce(console_script) : ""
|
|
423
424
|
result = compose_react_component_html_with_spec_and_console(
|
|
424
425
|
component_specification_tag, rendered_output, result_console_script
|
|
425
426
|
)
|
|
@@ -436,6 +437,32 @@ module ReactOnRails
|
|
|
436
437
|
)
|
|
437
438
|
end
|
|
438
439
|
|
|
440
|
+
# Wraps console replay JavaScript code in a script tag with CSP nonce if available.
|
|
441
|
+
# The console_script_code is already sanitized by scriptSanitizedVal() in the JavaScript layer,
|
|
442
|
+
# so using html_safe here is secure.
|
|
443
|
+
def wrap_console_script_with_nonce(console_script_code)
|
|
444
|
+
return "" if console_script_code.blank?
|
|
445
|
+
|
|
446
|
+
# Get the CSP nonce if available (Rails 5.2+)
|
|
447
|
+
# Rails 5.2-6.0 use content_security_policy_nonce with no arguments
|
|
448
|
+
# Rails 6.1+ accept an optional directive argument
|
|
449
|
+
nonce = if respond_to?(:content_security_policy_nonce)
|
|
450
|
+
begin
|
|
451
|
+
content_security_policy_nonce(:script)
|
|
452
|
+
rescue ArgumentError
|
|
453
|
+
# Fallback for Rails versions that don't accept arguments
|
|
454
|
+
content_security_policy_nonce
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# Build the script tag with nonce if available
|
|
459
|
+
script_options = { id: "consoleReplayLog" }
|
|
460
|
+
script_options[:nonce] = nonce if nonce.present?
|
|
461
|
+
|
|
462
|
+
# Safe to use html_safe because content is pre-sanitized via scriptSanitizedVal()
|
|
463
|
+
content_tag(:script, console_script_code.html_safe, script_options)
|
|
464
|
+
end
|
|
465
|
+
|
|
439
466
|
def compose_react_component_html_with_spec_and_console(component_specification_tag, rendered_output,
|
|
440
467
|
console_script)
|
|
441
468
|
# IMPORTANT: Ensure that we mark string as html_safe to avoid escaping.
|
|
@@ -25,40 +25,42 @@ module ReactOnRails
|
|
|
25
25
|
def generate_packs_if_stale
|
|
26
26
|
return unless ReactOnRails.configuration.auto_load_bundle
|
|
27
27
|
|
|
28
|
+
verbose = ENV["REACT_ON_RAILS_VERBOSE"] == "true"
|
|
29
|
+
|
|
28
30
|
add_generated_pack_to_server_bundle
|
|
29
31
|
|
|
30
32
|
# Clean any non-generated files from directories
|
|
31
|
-
clean_non_generated_files_with_feedback
|
|
33
|
+
clean_non_generated_files_with_feedback(verbose: verbose)
|
|
32
34
|
|
|
33
35
|
are_generated_files_present_and_up_to_date = Dir.exist?(generated_packs_directory_path) &&
|
|
34
36
|
File.exist?(generated_server_bundle_file_path) &&
|
|
35
37
|
!stale_or_missing_packs?
|
|
36
38
|
|
|
37
39
|
if are_generated_files_present_and_up_to_date
|
|
38
|
-
puts Rainbow("✅ Generated packs are up to date, no regeneration needed").green
|
|
40
|
+
puts Rainbow("✅ Generated packs are up to date, no regeneration needed").green if verbose
|
|
39
41
|
return
|
|
40
42
|
end
|
|
41
43
|
|
|
42
|
-
clean_generated_directories_with_feedback
|
|
43
|
-
generate_packs
|
|
44
|
+
clean_generated_directories_with_feedback(verbose: verbose)
|
|
45
|
+
generate_packs(verbose: verbose)
|
|
44
46
|
end
|
|
45
47
|
|
|
46
48
|
private
|
|
47
49
|
|
|
48
|
-
def generate_packs
|
|
49
|
-
common_component_to_path.each_value { |component_path| create_pack(component_path) }
|
|
50
|
-
client_component_to_path.each_value { |component_path| create_pack(component_path) }
|
|
50
|
+
def generate_packs(verbose: false)
|
|
51
|
+
common_component_to_path.each_value { |component_path| create_pack(component_path, verbose: verbose) }
|
|
52
|
+
client_component_to_path.each_value { |component_path| create_pack(component_path, verbose: verbose) }
|
|
51
53
|
|
|
52
|
-
create_server_pack if ReactOnRails.configuration.server_bundle_js_file.present?
|
|
54
|
+
create_server_pack(verbose: verbose) if ReactOnRails.configuration.server_bundle_js_file.present?
|
|
53
55
|
end
|
|
54
56
|
|
|
55
|
-
def create_pack(file_path)
|
|
57
|
+
def create_pack(file_path, verbose: false)
|
|
56
58
|
output_path = generated_pack_path(file_path)
|
|
57
59
|
content = pack_file_contents(file_path)
|
|
58
60
|
|
|
59
61
|
File.write(output_path, content)
|
|
60
62
|
|
|
61
|
-
puts(Rainbow("Generated Packs: #{output_path}").yellow)
|
|
63
|
+
puts(Rainbow("Generated Packs: #{output_path}").yellow) if verbose
|
|
62
64
|
end
|
|
63
65
|
|
|
64
66
|
def first_js_statement_in_code(content) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
@@ -126,11 +128,11 @@ module ReactOnRails
|
|
|
126
128
|
FILE_CONTENT
|
|
127
129
|
end
|
|
128
130
|
|
|
129
|
-
def create_server_pack
|
|
131
|
+
def create_server_pack(verbose: false)
|
|
130
132
|
File.write(generated_server_bundle_file_path, generated_server_pack_file_content)
|
|
131
133
|
|
|
132
134
|
add_generated_pack_to_server_bundle
|
|
133
|
-
puts(Rainbow("Generated Server Bundle: #{generated_server_bundle_file_path}").orange)
|
|
135
|
+
puts(Rainbow("Generated Server Bundle: #{generated_server_bundle_file_path}").orange) if verbose
|
|
134
136
|
end
|
|
135
137
|
|
|
136
138
|
def build_server_pack_content(component_on_server_imports, server_components, client_components)
|
|
@@ -200,17 +202,17 @@ module ReactOnRails
|
|
|
200
202
|
"#{generated_nonentrypoints_path}/#{generated_server_bundle_file_name}.js"
|
|
201
203
|
end
|
|
202
204
|
|
|
203
|
-
def clean_non_generated_files_with_feedback
|
|
205
|
+
def clean_non_generated_files_with_feedback(verbose: false)
|
|
204
206
|
directories_to_clean = [generated_packs_directory_path, generated_server_bundle_directory_path].compact.uniq
|
|
205
207
|
expected_files = build_expected_files_set
|
|
206
208
|
|
|
207
|
-
puts Rainbow("🧹 Cleaning non-generated files...").yellow
|
|
209
|
+
puts Rainbow("🧹 Cleaning non-generated files...").yellow if verbose
|
|
208
210
|
|
|
209
211
|
total_deleted = directories_to_clean.sum do |dir_path|
|
|
210
|
-
clean_unexpected_files_from_directory(dir_path, expected_files)
|
|
212
|
+
clean_unexpected_files_from_directory(dir_path, expected_files, verbose: verbose)
|
|
211
213
|
end
|
|
212
214
|
|
|
213
|
-
display_cleanup_summary(total_deleted)
|
|
215
|
+
display_cleanup_summary(total_deleted, verbose: verbose) if verbose
|
|
214
216
|
end
|
|
215
217
|
|
|
216
218
|
def build_expected_files_set
|
|
@@ -225,17 +227,17 @@ module ReactOnRails
|
|
|
225
227
|
{ pack_files: expected_pack_files, server_bundle: expected_server_bundle }
|
|
226
228
|
end
|
|
227
229
|
|
|
228
|
-
def clean_unexpected_files_from_directory(dir_path, expected_files)
|
|
230
|
+
def clean_unexpected_files_from_directory(dir_path, expected_files, verbose: false)
|
|
229
231
|
return 0 unless Dir.exist?(dir_path)
|
|
230
232
|
|
|
231
233
|
existing_files = Dir.glob("#{dir_path}/**/*").select { |f| File.file?(f) }
|
|
232
234
|
unexpected_files = find_unexpected_files(existing_files, dir_path, expected_files)
|
|
233
235
|
|
|
234
236
|
if unexpected_files.any?
|
|
235
|
-
delete_unexpected_files(unexpected_files, dir_path)
|
|
237
|
+
delete_unexpected_files(unexpected_files, dir_path, verbose: verbose)
|
|
236
238
|
unexpected_files.length
|
|
237
239
|
else
|
|
238
|
-
puts Rainbow(" No unexpected files found in #{dir_path}").cyan
|
|
240
|
+
puts Rainbow(" No unexpected files found in #{dir_path}").cyan if verbose
|
|
239
241
|
0
|
|
240
242
|
end
|
|
241
243
|
end
|
|
@@ -250,15 +252,21 @@ module ReactOnRails
|
|
|
250
252
|
end
|
|
251
253
|
end
|
|
252
254
|
|
|
253
|
-
def delete_unexpected_files(unexpected_files, dir_path)
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
255
|
+
def delete_unexpected_files(unexpected_files, dir_path, verbose: false)
|
|
256
|
+
if verbose
|
|
257
|
+
puts Rainbow(" Deleting #{unexpected_files.length} unexpected files from #{dir_path}:").cyan
|
|
258
|
+
unexpected_files.each do |file|
|
|
259
|
+
puts Rainbow(" - #{File.basename(file)}").blue
|
|
260
|
+
File.delete(file)
|
|
261
|
+
end
|
|
262
|
+
else
|
|
263
|
+
unexpected_files.each { |file| File.delete(file) }
|
|
258
264
|
end
|
|
259
265
|
end
|
|
260
266
|
|
|
261
|
-
def display_cleanup_summary(total_deleted)
|
|
267
|
+
def display_cleanup_summary(total_deleted, verbose: false)
|
|
268
|
+
return unless verbose
|
|
269
|
+
|
|
262
270
|
if total_deleted.positive?
|
|
263
271
|
puts Rainbow("🗑️ Deleted #{total_deleted} unexpected files total").red
|
|
264
272
|
else
|
|
@@ -266,15 +274,17 @@ module ReactOnRails
|
|
|
266
274
|
end
|
|
267
275
|
end
|
|
268
276
|
|
|
269
|
-
def clean_generated_directories_with_feedback
|
|
277
|
+
def clean_generated_directories_with_feedback(verbose: false)
|
|
270
278
|
directories_to_clean = [
|
|
271
279
|
generated_packs_directory_path,
|
|
272
280
|
generated_server_bundle_directory_path
|
|
273
281
|
].compact.uniq
|
|
274
282
|
|
|
275
|
-
puts Rainbow("🧹 Cleaning generated directories...").yellow
|
|
283
|
+
puts Rainbow("🧹 Cleaning generated directories...").yellow if verbose
|
|
284
|
+
|
|
285
|
+
total_deleted = directories_to_clean.sum { |dir_path| clean_directory_with_feedback(dir_path, verbose: verbose) }
|
|
276
286
|
|
|
277
|
-
|
|
287
|
+
return unless verbose
|
|
278
288
|
|
|
279
289
|
if total_deleted.positive?
|
|
280
290
|
puts Rainbow("🗑️ Deleted #{total_deleted} generated files total").red
|
|
@@ -283,27 +293,29 @@ module ReactOnRails
|
|
|
283
293
|
end
|
|
284
294
|
end
|
|
285
295
|
|
|
286
|
-
def clean_directory_with_feedback(dir_path)
|
|
287
|
-
return create_directory_with_feedback(dir_path) unless Dir.exist?(dir_path)
|
|
296
|
+
def clean_directory_with_feedback(dir_path, verbose: false)
|
|
297
|
+
return create_directory_with_feedback(dir_path, verbose: verbose) unless Dir.exist?(dir_path)
|
|
288
298
|
|
|
289
299
|
files = Dir.glob("#{dir_path}/**/*").select { |f| File.file?(f) }
|
|
290
300
|
|
|
291
301
|
if files.any?
|
|
292
|
-
|
|
293
|
-
|
|
302
|
+
if verbose
|
|
303
|
+
puts Rainbow(" Deleting #{files.length} files from #{dir_path}:").cyan
|
|
304
|
+
files.each { |file| puts Rainbow(" - #{File.basename(file)}").blue }
|
|
305
|
+
end
|
|
294
306
|
FileUtils.rm_rf(dir_path)
|
|
295
307
|
FileUtils.mkdir_p(dir_path)
|
|
296
308
|
files.length
|
|
297
309
|
else
|
|
298
|
-
puts Rainbow(" Directory #{dir_path} is already empty").cyan
|
|
310
|
+
puts Rainbow(" Directory #{dir_path} is already empty").cyan if verbose
|
|
299
311
|
FileUtils.rm_rf(dir_path)
|
|
300
312
|
FileUtils.mkdir_p(dir_path)
|
|
301
313
|
0
|
|
302
314
|
end
|
|
303
315
|
end
|
|
304
316
|
|
|
305
|
-
def create_directory_with_feedback(dir_path)
|
|
306
|
-
puts Rainbow(" Directory #{dir_path} does not exist, creating...").cyan
|
|
317
|
+
def create_directory_with_feedback(dir_path, verbose: false)
|
|
318
|
+
puts Rainbow(" Directory #{dir_path} does not exist, creating...").cyan if verbose
|
|
307
319
|
FileUtils.mkdir_p(dir_path)
|
|
308
320
|
0
|
|
309
321
|
end
|
|
@@ -213,17 +213,20 @@ module ReactOnRails
|
|
|
213
213
|
|
|
214
214
|
return unless npm_version && defined?(ReactOnRails::VERSION)
|
|
215
215
|
|
|
216
|
-
#
|
|
217
|
-
|
|
216
|
+
# Normalize NPM version format to Ruby gem format for comparison
|
|
217
|
+
# Uses existing VersionSyntaxConverter to handle dash/dot differences
|
|
218
|
+
# (e.g., "16.2.0-beta.10" → "16.2.0.beta.10")
|
|
219
|
+
converter = ReactOnRails::VersionSyntaxConverter.new
|
|
220
|
+
normalized_npm_version = converter.npm_to_rubygem(npm_version)
|
|
218
221
|
gem_version = ReactOnRails::VERSION
|
|
219
222
|
|
|
220
|
-
if
|
|
223
|
+
if normalized_npm_version == gem_version
|
|
221
224
|
add_success("✅ React on Rails gem and NPM package versions match (#{gem_version})")
|
|
222
225
|
check_version_patterns(npm_version, gem_version)
|
|
223
226
|
else
|
|
224
227
|
# Check for major version differences
|
|
225
228
|
gem_major = gem_version.split(".")[0].to_i
|
|
226
|
-
npm_major =
|
|
229
|
+
npm_major = normalized_npm_version.split(".")[0].to_i
|
|
227
230
|
|
|
228
231
|
if gem_major != npm_major # rubocop:disable Style/NegatedIfElseCondition
|
|
229
232
|
add_error(<<~MSG.strip)
|
data/lib/react_on_rails/utils.rb
CHANGED
|
@@ -443,6 +443,60 @@ module ReactOnRails
|
|
|
443
443
|
end
|
|
444
444
|
end
|
|
445
445
|
|
|
446
|
+
# Converts an absolute path (String or Pathname) to a path relative to Rails.root.
|
|
447
|
+
# If the path is already relative or doesn't contain Rails.root, returns it as-is.
|
|
448
|
+
#
|
|
449
|
+
# This method is used to normalize paths from Shakapacker's privateOutputPath (which is
|
|
450
|
+
# absolute) to relative paths suitable for React on Rails configuration.
|
|
451
|
+
#
|
|
452
|
+
# Note: Absolute paths that don't start with Rails.root are intentionally passed through
|
|
453
|
+
# unchanged. While there's no known use case for server bundles outside Rails.root,
|
|
454
|
+
# this behavior preserves the original path for debugging and error messages.
|
|
455
|
+
#
|
|
456
|
+
# @param path [String, Pathname] The path to normalize
|
|
457
|
+
# @return [String, nil] The relative path as a string, or nil if path is nil
|
|
458
|
+
#
|
|
459
|
+
# @example Converting absolute paths within Rails.root
|
|
460
|
+
# # Assuming Rails.root is "/app"
|
|
461
|
+
# normalize_to_relative_path("/app/ssr-generated") # => "ssr-generated"
|
|
462
|
+
# normalize_to_relative_path("/app/foo/bar") # => "foo/bar"
|
|
463
|
+
#
|
|
464
|
+
# @example Already relative paths pass through
|
|
465
|
+
# normalize_to_relative_path("ssr-generated") # => "ssr-generated"
|
|
466
|
+
# normalize_to_relative_path("./ssr-generated") # => "./ssr-generated"
|
|
467
|
+
#
|
|
468
|
+
# @example Absolute paths outside Rails.root (edge case)
|
|
469
|
+
# normalize_to_relative_path("/other/path/bundles") # => "/other/path/bundles"
|
|
470
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
471
|
+
def self.normalize_to_relative_path(path)
|
|
472
|
+
return nil if path.nil?
|
|
473
|
+
|
|
474
|
+
path_str = path.to_s
|
|
475
|
+
rails_root_str = Rails.root.to_s.chomp("/")
|
|
476
|
+
|
|
477
|
+
# Treat as "inside Rails.root" only for exact match or a subdirectory
|
|
478
|
+
inside_rails_root = rails_root_str.present? &&
|
|
479
|
+
(path_str == rails_root_str || path_str.start_with?("#{rails_root_str}/"))
|
|
480
|
+
|
|
481
|
+
# If path is within Rails.root, remove that prefix
|
|
482
|
+
if inside_rails_root
|
|
483
|
+
# Remove Rails.root and any leading slash
|
|
484
|
+
path_str.sub(%r{^#{Regexp.escape(rails_root_str)}/?}, "")
|
|
485
|
+
else
|
|
486
|
+
# Path is already relative or outside Rails.root
|
|
487
|
+
# Warn if it's an absolute path outside Rails.root (edge case)
|
|
488
|
+
if path_str.start_with?("/") && !inside_rails_root
|
|
489
|
+
Rails.logger&.warn(
|
|
490
|
+
"ReactOnRails: Detected absolute path outside Rails.root: '#{path_str}'. " \
|
|
491
|
+
"Server bundles are typically stored within Rails.root. " \
|
|
492
|
+
"Verify this is intentional."
|
|
493
|
+
)
|
|
494
|
+
end
|
|
495
|
+
path_str
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
499
|
+
|
|
446
500
|
def self.default_troubleshooting_section
|
|
447
501
|
<<~DEFAULT
|
|
448
502
|
📞 Get Help & Support:
|
|
@@ -17,18 +17,24 @@ namespace :react_on_rails do
|
|
|
17
17
|
DESC
|
|
18
18
|
|
|
19
19
|
task generate_packs: :environment do
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
verbose = ENV["REACT_ON_RAILS_VERBOSE"] == "true"
|
|
21
|
+
|
|
22
|
+
if verbose
|
|
23
|
+
puts Rainbow("🚀 Starting React on Rails pack generation...").bold
|
|
24
|
+
puts Rainbow("📁 Auto-load bundle: #{ReactOnRails.configuration.auto_load_bundle}").cyan
|
|
25
|
+
puts Rainbow("📂 Components subdirectory: #{ReactOnRails.configuration.components_subdirectory}").cyan
|
|
26
|
+
puts ""
|
|
27
|
+
end
|
|
24
28
|
|
|
25
29
|
begin
|
|
26
30
|
start_time = Time.now
|
|
27
31
|
ReactOnRails::PacksGenerator.instance.generate_packs_if_stale
|
|
28
32
|
end_time = Time.now
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
if verbose
|
|
35
|
+
puts ""
|
|
36
|
+
puts Rainbow("✨ Pack generation completed in #{((end_time - start_time) * 1000).round(1)}ms").green
|
|
37
|
+
end
|
|
32
38
|
rescue ReactOnRails::Error => e
|
|
33
39
|
handle_react_on_rails_error(e)
|
|
34
40
|
exit 1
|
|
@@ -20,13 +20,12 @@ You can find the **package** version numbers from this repo's tags and below in
|
|
|
20
20
|
_Add changes in master not yet tagged._
|
|
21
21
|
|
|
22
22
|
### Improved
|
|
23
|
-
- Significantly improved streaming performance by processing React components concurrently instead of sequentially. This reduces latency and improves responsiveness when using `stream_view_containing_react_components`.
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
- Added `config.concurrent_component_streaming_buffer_size` configuration option to control the memory buffer size for concurrent component streaming (defaults to 64). This allows fine-tuning of memory usage vs. performance for streaming applications.
|
|
24
|
+
- **Concurrent Streaming Performance**: Implemented concurrent draining of streamed React components using the async gem. Instead of processing components sequentially, the system now uses a producer-consumer pattern with bounded buffering to allow multiple components to stream simultaneously while maintaining per-component chunk ordering. This significantly reduces latency and improves responsiveness when using `stream_view_containing_react_components`. [PR 2015](https://github.com/shakacode/react_on_rails/pull/2015) by [ihabadham](https://github.com/ihabadham).
|
|
27
25
|
|
|
28
26
|
### Added
|
|
29
27
|
|
|
28
|
+
- Added `config.concurrent_component_streaming_buffer_size` configuration option to control the memory buffer size for concurrent component streaming (defaults to 64). This allows fine-tuning of memory usage vs. performance for streaming applications.
|
|
30
29
|
- Added `cached_stream_react_component` helper method, similar to `cached_react_component` but for streamed components.
|
|
31
30
|
- **License Validation System**: Implemented comprehensive JWT-based license validation with offline verification using RSA-256 signatures. License validation occurs at startup in both Ruby and Node.js environments. Supports required fields (`sub`, `iat`, `exp`) and optional fields (`plan`, `organization`, `iss`). FREE evaluation licenses are available for 3 months at [shakacode.com/react-on-rails-pro](https://shakacode.com/react-on-rails-pro). [PR #1857](https://github.com/shakacode/react_on_rails/pull/1857) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
|
|
32
31
|
- **Pro-Specific Configurations Moved from Open-Source**: The following React Server Components (RSC) configurations are now exclusively in the Pro gem and should be configured in `ReactOnRailsPro.configure`:
|
|
@@ -46,9 +45,17 @@ _Add changes in master not yet tagged._
|
|
|
46
45
|
|
|
47
46
|
- **Node Renderer Gem Version Validation**: The node renderer now validates that the Ruby gem version (`react_on_rails_pro`) matches the node renderer package version (`@shakacode-tools/react-on-rails-pro-node-renderer`) on every render request. Environment-aware: strict enforcement in development (returns 412 Precondition Failed on mismatch), permissive in production (allows with warning). Includes version normalization to handle Ruby gem vs NPM format differences (e.g., `4.0.0.rc.1` vs `4.0.0-rc.1`). [PR #1881](https://github.com/shakacode/react_on_rails/pull/1881) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
|
|
48
47
|
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- **Node Renderer Worker Restart**: Fixed "descriptor closed" error that occurred when the node renderer restarts while handling an in-progress request (especially streaming requests). Workers now perform graceful shutdowns: they disconnect from the cluster to stop receiving new requests, wait for active requests to complete, then shut down cleanly. A configurable `gracefulWorkerRestartTimeout` ensures workers are forcibly killed if they don't shut down in time. [PR 1970](https://github.com/shakacode/react_on_rails/pull/1970) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
|
|
51
|
+
|
|
52
|
+
- **Body Duplication Bug On Streaming**: Fixed a bug that happens while streaming if the node renderer connection closed after streaming some chunks to the client. [PR 1995](https://github.com/shakacode/react_on_rails/pull/1995) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
|
|
53
|
+
|
|
49
54
|
### Changed
|
|
50
55
|
|
|
51
|
-
|
|
56
|
+
### Deprecated
|
|
57
|
+
|
|
58
|
+
- **Node Renderer Configuration**: Renamed `bundlePath` configuration option to `serverBundleCachePath` in the node renderer to better describe its purpose and avoid confusion with Shakapacker's public bundle path. The old `bundlePath` option continues to work with deprecation warnings. Both `RENDERER_SERVER_BUNDLE_CACHE_PATH` (new) and `RENDERER_BUNDLE_PATH` (deprecated) environment variables are supported. [PR 2008](https://github.com/shakacode/react_on_rails/pull/2008) by [justin808](https://github.com/justin808).
|
|
52
59
|
|
|
53
60
|
### Changed (Breaking)
|
|
54
61
|
|
|
@@ -9,7 +9,7 @@ GIT
|
|
|
9
9
|
PATH
|
|
10
10
|
remote: ..
|
|
11
11
|
specs:
|
|
12
|
-
react_on_rails (16.2.0.beta.
|
|
12
|
+
react_on_rails (16.2.0.beta.12)
|
|
13
13
|
addressable
|
|
14
14
|
connection_pool
|
|
15
15
|
execjs (~> 2.5)
|
|
@@ -20,7 +20,7 @@ PATH
|
|
|
20
20
|
PATH
|
|
21
21
|
remote: .
|
|
22
22
|
specs:
|
|
23
|
-
react_on_rails_pro (16.2.0.beta.
|
|
23
|
+
react_on_rails_pro (16.2.0.beta.12)
|
|
24
24
|
addressable
|
|
25
25
|
async (>= 2.6)
|
|
26
26
|
connection_pool
|
|
@@ -28,7 +28,7 @@ PATH
|
|
|
28
28
|
httpx (~> 1.5)
|
|
29
29
|
jwt (~> 2.7)
|
|
30
30
|
rainbow
|
|
31
|
-
react_on_rails (= 16.2.0.beta.
|
|
31
|
+
react_on_rails (= 16.2.0.beta.12)
|
|
32
32
|
|
|
33
33
|
GEM
|
|
34
34
|
remote: https://rubygems.org/
|
|
@@ -347,7 +347,8 @@ module ReactOnRailsProHelper
|
|
|
347
347
|
render_options: render_options
|
|
348
348
|
)
|
|
349
349
|
else
|
|
350
|
-
|
|
350
|
+
console_script = chunk_json_result["consoleReplayScript"]
|
|
351
|
+
result_console_script = render_options.replay_console ? wrap_console_script_with_nonce(console_script) : ""
|
|
351
352
|
# No need to prepend component_specification_tag or add rails context again
|
|
352
353
|
# as they're already included in the first chunk
|
|
353
354
|
compose_react_component_html_with_spec_and_console(
|
|
@@ -9,7 +9,7 @@ GIT
|
|
|
9
9
|
PATH
|
|
10
10
|
remote: ../../..
|
|
11
11
|
specs:
|
|
12
|
-
react_on_rails (16.2.0.beta.
|
|
12
|
+
react_on_rails (16.2.0.beta.12)
|
|
13
13
|
addressable
|
|
14
14
|
connection_pool
|
|
15
15
|
execjs (~> 2.5)
|
|
@@ -20,7 +20,7 @@ PATH
|
|
|
20
20
|
PATH
|
|
21
21
|
remote: ../..
|
|
22
22
|
specs:
|
|
23
|
-
react_on_rails_pro (16.2.0.beta.
|
|
23
|
+
react_on_rails_pro (16.2.0.beta.12)
|
|
24
24
|
addressable
|
|
25
25
|
async (>= 2.6)
|
|
26
26
|
connection_pool
|
|
@@ -28,7 +28,7 @@ PATH
|
|
|
28
28
|
httpx (~> 1.5)
|
|
29
29
|
jwt (~> 2.7)
|
|
30
30
|
rainbow
|
|
31
|
-
react_on_rails (= 16.2.0.beta.
|
|
31
|
+
react_on_rails (= 16.2.0.beta.12)
|
|
32
32
|
|
|
33
33
|
GEM
|
|
34
34
|
remote: https://rubygems.org/
|
|
@@ -295,13 +295,13 @@ describe ReactOnRailsProHelper do
|
|
|
295
295
|
let(:chunks) do
|
|
296
296
|
[
|
|
297
297
|
{ html: "<div>Chunk 1: Stream React Server Components</div>",
|
|
298
|
-
consoleReplayScript: "
|
|
299
|
-
"['Chunk 1: Console Message'])
|
|
298
|
+
consoleReplayScript: "console.log.apply(console, " \
|
|
299
|
+
"['Chunk 1: Console Message'])" },
|
|
300
300
|
{ html: "<div>Chunk 2: More content</div>",
|
|
301
|
-
consoleReplayScript: "
|
|
301
|
+
consoleReplayScript: "console.log.apply(console, " \
|
|
302
302
|
"['Chunk 2: Console Message']);\n" \
|
|
303
303
|
"console.error.apply(console, " \
|
|
304
|
-
"['Chunk 2: Console Error'])
|
|
304
|
+
"['Chunk 2: Console Error']);" },
|
|
305
305
|
{ html: "<div>Chunk 3: Final content</div>", consoleReplayScript: "" }
|
|
306
306
|
]
|
|
307
307
|
end
|
|
@@ -373,7 +373,12 @@ describe ReactOnRailsProHelper do
|
|
|
373
373
|
mock_request_and_response
|
|
374
374
|
initial_result = stream_react_component(component_name, props: props, **component_options)
|
|
375
375
|
expect(initial_result).to include(react_component_div_with_initial_chunk)
|
|
376
|
-
|
|
376
|
+
# consoleReplayScript is now wrapped in a script tag with id="consoleReplayLog"
|
|
377
|
+
if chunks.first[:consoleReplayScript].present?
|
|
378
|
+
script = chunks.first[:consoleReplayScript]
|
|
379
|
+
wrapped = "<script id=\"consoleReplayLog\">#{script}</script>"
|
|
380
|
+
expect(initial_result).to include(wrapped)
|
|
381
|
+
end
|
|
377
382
|
expect(initial_result).not_to include("More content", "Final content")
|
|
378
383
|
expect(chunks_read.count).to eq(1)
|
|
379
384
|
end
|
|
@@ -386,9 +391,12 @@ describe ReactOnRailsProHelper do
|
|
|
386
391
|
expect(fiber).to be_alive
|
|
387
392
|
|
|
388
393
|
second_result = fiber.resume
|
|
389
|
-
# regex that matches the html and consoleReplayScript
|
|
394
|
+
# regex that matches the html and wrapped consoleReplayScript
|
|
395
|
+
# Note: consoleReplayScript is now wrapped in a script tag with id="consoleReplayLog"
|
|
396
|
+
script = chunks[1][:consoleReplayScript]
|
|
397
|
+
wrapped = script.present? ? "<script id=\"consoleReplayLog\">#{script}</script>" : ""
|
|
390
398
|
expect(second_result).to match(
|
|
391
|
-
/#{Regexp.escape(chunks[1][:html])}\s+#{Regexp.escape(
|
|
399
|
+
/#{Regexp.escape(chunks[1][:html])}\s+#{Regexp.escape(wrapped)}/
|
|
392
400
|
)
|
|
393
401
|
expect(second_result).not_to include("Stream React Server Components", "Final content")
|
|
394
402
|
expect(chunks_read.count).to eq(2)
|
|
@@ -21,15 +21,20 @@ describe "Console logging from server" do
|
|
|
21
21
|
console.log.apply(console, ["[SERVER] Script4\\"</div>\\"(/script <script>alert('WTF4')(/script>"]);
|
|
22
22
|
console.log.apply(console, ["[SERVER] Script5:\\"</div>\\"(/script> <script>alert('WTF5')(/script>"]);
|
|
23
23
|
console.log.apply(console, ["[SERVER] railsContext.serverSide is ","true"]);
|
|
24
|
+
console.log.apply(console, ["[SERVER] RENDERED ReduxSharedStoreApp to dom node with id: ReduxSharedStoreApp-react-component-1"]);
|
|
24
25
|
JS
|
|
25
26
|
|
|
26
27
|
expected_lines = expected.split("\n")
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
# When multiple components with replay_console are rendered, each creates its own script tag
|
|
30
|
+
# with id="consoleReplayLog". Nokogiri's .text concatenates them without separators, which
|
|
31
|
+
# breaks parsing. Instead, we explicitly join them with newlines.
|
|
32
|
+
script_nodes = html_nodes.css("script#consoleReplayLog")
|
|
33
|
+
script_text = script_nodes.map(&:text).join("\n")
|
|
34
|
+
script_lines = script_text.split("\n")
|
|
30
35
|
|
|
31
|
-
#
|
|
32
|
-
script_lines.shift
|
|
36
|
+
# Remove leading blank line if present (old format had it, new format doesn't)
|
|
37
|
+
script_lines.shift if script_lines.first && script_lines.first.empty?
|
|
33
38
|
|
|
34
39
|
# Create external iterators for expected and found console replay script lines:
|
|
35
40
|
expected_lines_iterator = expected_lines.to_enum
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module ReactOnRails
|
|
2
|
+
module Dev
|
|
3
|
+
class FileManager
|
|
4
|
+
def self.cleanup_stale_files: () -> bool
|
|
5
|
+
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def self.cleanup_overmind_sockets: () -> bool
|
|
9
|
+
def self.cleanup_rails_pid_file: () -> bool
|
|
10
|
+
def self.overmind_running?: () -> bool
|
|
11
|
+
def self.process_running?: (Integer) -> bool
|
|
12
|
+
def self.remove_file_if_exists: (String, String) -> bool
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module ReactOnRails
|
|
2
|
+
module Dev
|
|
3
|
+
class PackGenerator
|
|
4
|
+
def self.generate: (?verbose: bool) -> void
|
|
5
|
+
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def self.run_pack_generation: (?silent: bool, ?verbose: bool) -> bool
|
|
9
|
+
def self.should_run_directly?: () -> bool
|
|
10
|
+
def self.rails_available?: () -> bool
|
|
11
|
+
def self.run_rake_task_directly: (?silent: bool) -> bool
|
|
12
|
+
def self.load_rake_tasks: () -> void
|
|
13
|
+
def self.prepare_rake_task: () -> untyped
|
|
14
|
+
def self.capture_output: (bool) { () -> bool } -> bool
|
|
15
|
+
def self.handle_rake_error: (Exception, bool) -> void
|
|
16
|
+
def self.run_via_bundle_exec: (?silent: bool, ?verbose: bool) -> (bool | nil)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module ReactOnRails
|
|
2
|
+
module Dev
|
|
3
|
+
class ProcessManager
|
|
4
|
+
VERSION_CHECK_TIMEOUT: Integer
|
|
5
|
+
|
|
6
|
+
def self.installed?: (String) -> bool
|
|
7
|
+
def self.ensure_procfile: (String) -> void
|
|
8
|
+
def self.run_with_process_manager: (String) -> void
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def self.installed_in_current_context?: (String) -> bool
|
|
13
|
+
def self.version_flags_for: (String) -> Array[String]
|
|
14
|
+
def self.run_process_if_available: (String, Array[String]) -> bool
|
|
15
|
+
def self.run_process_outside_bundle: (String, Array[String]) -> bool
|
|
16
|
+
def self.process_available_in_system?: (String) -> bool
|
|
17
|
+
def self.with_unbundled_context: () { () -> untyped } -> untyped
|
|
18
|
+
def self.show_process_manager_installation_help: () -> void
|
|
19
|
+
def self.valid_procfile_path?: (String) -> bool
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|