react_on_rails 16.7.0.rc.3 → 17.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a330ac2b76a6a565ea1f64291f54d2ff5a90b39e33bb7c1929f3caa11ed8358e
4
- data.tar.gz: 4d06374dfcb3c2004138c3386ed326634b03c91a27e96c4d442b0bcd5a9c39c9
3
+ metadata.gz: 02f0fa1befbed99b17f624249398f47b5a1395ed5407079a472ad39218d2cfc1
4
+ data.tar.gz: f8b4e5a3abf309b98f7b244d1a807acb29096ff7ae1ea115e780eefd35f2d609
5
5
  SHA512:
6
- metadata.gz: 55392f01cd6ccad3792eac088c3f59d354131e159d22bc15dc086b17abc75ca248da294269d3e73ee25cea846af246e0d59ab2b93558ab0cf9bd9558e3ef52bf
7
- data.tar.gz: c8fed4f8f74704860bff9a4784906ebcf5bab48cf7a50d9e7240a750d71c4d87dd228c55b3f0bad80edcc0c06565a20f606f8a2f1938ab8b3fa0108c52241bbf
6
+ metadata.gz: c594211d78c05817226dd5d4ff06e7bb4f377b57320cdb824de7a8902ec40d66473b28da64e0f6d2df5b445786a050d1c6b61779af3fea0c1c4793f3955efa63
7
+ data.tar.gz: e86d35866011de451f725c441539bc7f8400e83d9702a4a3b9058d385e483a926229b4e33e18685636d73700bc7a9a1106246d78174a726327b47b8b0f1645d9
@@ -2,18 +2,22 @@
2
2
 
3
3
  eval_gemfile File.expand_path("../Gemfile.shared_dev_dependencies", __dir__)
4
4
 
5
- gem "shakapacker", "9.6.1"
5
+ gem "shakapacker", "10.1.0"
6
6
  gem "bootsnap", require: false
7
7
  gem "rails", "~> 7.1.0"
8
8
 
9
- gem "sqlite3", "~> 1.6"
9
+ gem "sqlite3", "~> 2.0"
10
10
  gem "sass-rails", "~> 6.0"
11
11
  gem "uglifier"
12
12
  gem "jquery-rails"
13
13
  gem "puma", "~> 6.0"
14
14
 
15
15
  # Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks
16
- gem "turbolinks" if ENV["DISABLE_TURBOLINKS"].nil? || ENV["DISABLE_TURBOLINKS"].strip.empty?
16
+ # Declared unconditionally so the gemset always matches Gemfile.lock. DISABLE_TURBOLINKS
17
+ # toggles Turbolinks at the application layer (the require_asset in
18
+ # spec/dummy/app/assets/javascripts/application_non_webpack.js.erb), not gem inclusion.
19
+ # A conditional declaration broke `bundle install --frozen` in CI (see AGENTS.md).
20
+ gem "turbolinks"
17
21
 
18
22
  # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
19
23
  gem "jbuilder"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- react_on_rails (16.7.0.rc.3)
4
+ react_on_rails (17.0.0.rc.0)
5
5
  addressable
6
6
  connection_pool
7
7
  execjs (~> 2.5)
@@ -237,7 +237,7 @@ GEM
237
237
  nio4r (~> 2.0)
238
238
  racc (1.8.1)
239
239
  rack (3.2.5)
240
- rack-proxy (0.7.7)
240
+ rack-proxy (0.8.2)
241
241
  rack
242
242
  rack-session (2.1.1)
243
243
  base64 (>= 0.1.0)
@@ -360,7 +360,7 @@ GEM
360
360
  rubyzip (>= 1.2.2, < 3.0)
361
361
  websocket (~> 1.0)
362
362
  semantic_range (3.1.1)
363
- shakapacker (9.6.1)
363
+ shakapacker (10.1.0)
364
364
  activesupport (>= 5.2)
365
365
  package_json
366
366
  rack-proxy (>= 0.6.1)
@@ -379,10 +379,10 @@ GEM
379
379
  actionpack (>= 5.2)
380
380
  activesupport (>= 5.2)
381
381
  sprockets (>= 3.0.0)
382
- sqlite3 (1.7.3)
382
+ sqlite3 (2.9.4)
383
383
  mini_portile2 (~> 2.8.0)
384
- sqlite3 (1.7.3-arm64-darwin)
385
- sqlite3 (1.7.3-x86_64-linux)
384
+ sqlite3 (2.9.4-arm64-darwin)
385
+ sqlite3 (2.9.4-x86_64-linux-gnu)
386
386
  steep (1.9.4)
387
387
  activesupport (>= 5.1)
388
388
  concurrent-ruby (>= 1.1.10)
@@ -475,11 +475,11 @@ DEPENDENCIES
475
475
  sass-rails (~> 6.0)
476
476
  sdoc
477
477
  selenium-webdriver (= 4.9.0)
478
- shakapacker (= 9.6.1)
478
+ shakapacker (= 10.1.0)
479
479
  simplecov (~> 0.16.1)
480
480
  spring (~> 4.0)
481
481
  sprockets (~> 4.0)
482
- sqlite3 (~> 1.6)
482
+ sqlite3 (~> 2.0)
483
483
  steep
484
484
  turbo-rails
485
485
  turbolinks
@@ -578,12 +578,19 @@ module ReactOnRails
578
578
  end
579
579
 
580
580
  File.write("package.json", "#{JSON.pretty_generate(content)}\n")
581
+ GeneratorMessages.add_warning(package_json_pin_fallback_warning(versioned_packages))
581
582
  true
582
583
  rescue StandardError => e
583
584
  GeneratorMessages.add_warning("⚠️ Could not write dependency pins to package.json: #{e.message}")
584
585
  false
585
586
  end
586
587
 
588
+ def package_json_pin_fallback_warning(versioned_packages)
589
+ pinned_list = versioned_packages.map { |name, version| "#{name}@#{version}" }.join(", ")
590
+ "⚠️ Package manager install failed. Wrote the following version pins to package.json " \
591
+ "so you can rerun your package manager manually: #{pinned_list}"
592
+ end
593
+
587
594
  def fallback_package_manager
588
595
  package_manager = GeneratorMessages.detect_package_manager(app_root: destination_root)
589
596
  return package_manager if GeneratorMessages.supported_package_manager?(package_manager)
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "securerandom"
3
4
  require_relative "generator_messages"
4
5
  require "react_on_rails/node_renderer_procfile"
5
6
  require "react_on_rails/pro_migration"
@@ -46,16 +47,58 @@ module ReactOnRails
46
47
  say set_color("🚀 REACT ON RAILS PRO SETUP", :cyan, :bold)
47
48
  say set_color("=" * 80, :cyan)
48
49
 
49
- create_pro_initializer
50
+ # The Rails initializer and Node renderer bootstrap must share the same
51
+ # password literal. Only mint a fresh random password when BOTH files will
52
+ # be created — otherwise nil so each template falls back to the env-only
53
+ # branch, avoiding a literal mismatch with any existing file.
54
+ # Always reassign so a stale value from a prior invocation on the same
55
+ # instance can't leak into a later partial-install run.
56
+ @generated_renderer_password = nil
57
+ if pro_initializer_will_be_created? && node_renderer_will_be_created?
58
+ @generated_renderer_password = SecureRandom.hex(32)
59
+ end
60
+
61
+ initializer_created = create_pro_initializer
50
62
  legacy_renderer_detected = create_node_renderer
51
63
  add_pro_to_procfiles unless legacy_renderer_detected
52
64
  update_webpack_config_for_pro
53
65
 
66
+ say_renderer_password_setup_summary(initializer_created)
67
+
54
68
  say set_color("=" * 80, :cyan)
55
69
  say "✅ React on Rails Pro setup complete!", :green
56
70
  say set_color("=" * 80, :cyan)
57
71
  end
58
72
 
73
+ def say_renderer_password_setup_summary(initializer_created)
74
+ if @generated_renderer_password
75
+ say ""
76
+ say set_color("🔐 A random renderer password was written into your config files.", :yellow, :bold)
77
+ say " For production, set RENDERER_PASSWORD as an env var instead and"
78
+ say " remove the literal value from version control."
79
+ say " See: https://www.shakacode.com/react-on-rails/docs/pro/node-renderer/"
80
+ say ""
81
+ elsif initializer_created
82
+ # Initializer was newly created but the Node renderer file already exists;
83
+ # the new initializer falls back to ENV["RENDERER_PASSWORD"] only so it doesn't
84
+ # disagree with whatever literal the existing renderer file contains.
85
+ say ""
86
+ say set_color("⚠️ Existing Node renderer detected — Rails initializer uses " \
87
+ "ENV[\"RENDERER_PASSWORD\"] only.", :yellow, :bold)
88
+ say " Set RENDERER_PASSWORD in your environment to match the password in your existing renderer."
89
+ say ""
90
+ end
91
+ end
92
+
93
+ def pro_initializer_will_be_created?
94
+ !File.exist?(File.join(destination_root, "config/initializers/react_on_rails_pro.rb"))
95
+ end
96
+
97
+ def node_renderer_will_be_created?
98
+ !File.exist?(File.join(destination_root, "renderer/node-renderer.js")) &&
99
+ !File.exist?(File.join(destination_root, "client/node-renderer.js"))
100
+ end
101
+
59
102
  # Check if the Pro gem is missing. When the base react_on_rails gem is in
60
103
  # the Gemfile, installation is deferred to the later Gemfile swap (which
61
104
  # preserves the user's version pin); otherwise auto-install via `bundle
@@ -205,14 +248,18 @@ module ReactOnRails
205
248
 
206
249
  if File.exist?(File.join(destination_root, initializer_path))
207
250
  say "ℹ️ #{initializer_path} already exists, skipping", :yellow
208
- return
251
+ return false
209
252
  end
210
253
 
211
254
  say "📝 Creating React on Rails Pro initializer...", :yellow
212
255
 
256
+ # @generated_renderer_password is set by setup_pro only when both this
257
+ # file and the Node renderer bootstrap will be created together; nil here
258
+ # means the template emits the env-only fallback (no literal password).
213
259
  template("templates/pro/base/config/initializers/react_on_rails_pro.rb.tt", initializer_path)
214
260
 
215
261
  say "✅ Created #{initializer_path}", :green
262
+ true
216
263
  end
217
264
 
218
265
  # Matches active (uncommented) Procfile.dev node-renderer lines that
@@ -267,6 +314,11 @@ module ReactOnRails
267
314
  "to migrate, move it to #{node_renderer_path} and update any references " \
268
315
  "(e.g. Procfile.dev, Procfile.prod, Docker CMD / command):", :yellow
269
316
  say " #{node_renderer_procfile_command('Procfile.dev')}", :yellow
317
+ say set_color(
318
+ "⚠️ Ensure the password in #{legacy_node_renderer_path} matches " \
319
+ "config/initializers/react_on_rails_pro.rb. Both must use the same RENDERER_PASSWORD.",
320
+ :yellow
321
+ )
270
322
  warn_on_stale_legacy_procfile_entry
271
323
  return true
272
324
  end
@@ -275,8 +327,8 @@ module ReactOnRails
275
327
 
276
328
  empty_directory("renderer")
277
329
 
278
- template_path = "templates/pro/base/renderer/node-renderer.js"
279
- copy_file(template_path, node_renderer_path)
330
+ template_path = "templates/pro/base/renderer/node-renderer.js.tt"
331
+ template(template_path, node_renderer_path)
280
332
 
281
333
  say "✅ Created #{node_renderer_path}", :green
282
334
  false
@@ -123,7 +123,7 @@ module ReactOnRails
123
123
  def update_existing_rsc_webpack_config(config_path, content, is_server:)
124
124
  return unless rsc_plugin_sections_safe_to_rewrite?(config_path, content, is_server: is_server)
125
125
  return if rsc_plugin_uses_scoped_client_references?(content, is_server: is_server)
126
- return unless rsc_client_references_rewrite_needed?(config_path, content, is_server: is_server)
126
+ return unless prepare_rsc_client_references_rewrite!(config_path, content, is_server: is_server)
127
127
 
128
128
  return if rewrite_rsc_plugin_client_references(config_path, is_server: is_server)
129
129
 
@@ -131,25 +131,31 @@ module ReactOnRails
131
131
  warn_missing_rsc_plugin_target(config_path, is_server: is_server)
132
132
  end
133
133
 
134
- def rsc_client_references_rewrite_needed?(config_path, content, is_server:)
135
- # This predicate prepares the rewrite too: when it returns true, the scoped helper
136
- # may already have been injected on disk so `rewrite_rsc_plugin_client_references`
137
- # can re-read fresh content with valid offsets.
134
+ def prepare_rsc_client_references_rewrite!(config_path, content, is_server:)
135
+ # This prepares the rewrite and returns whether it should continue. When it returns true,
136
+ # the scoped helper may already have been injected on disk so
137
+ # `rewrite_rsc_plugin_client_references` can re-read fresh content with valid offsets.
138
138
  if rsc_plugin_references_any_scoped_client_references?(content, is_server: is_server)
139
139
  return false unless ensure_rsc_client_references_setup(config_path, content, is_server: is_server)
140
140
 
141
- return rsc_plugin_without_client_references?(content, is_server: is_server)
141
+ return rsc_plugin_needs_client_references_rewrite?(content, is_server: is_server)
142
142
  end
143
143
 
144
144
  rewritable_rsc_plugin?(config_path, content, is_server: is_server) &&
145
145
  ensure_rsc_client_references_setup(config_path, content, is_server: is_server)
146
146
  end
147
147
 
148
+ def rsc_plugin_needs_client_references_rewrite?(content, is_server:)
149
+ any_rsc_plugin_section_without_client_references?(content, is_server: is_server)
150
+ end
151
+
148
152
  # Detects RSCWebpackPlugin option blocks that the lightweight JS scanner could not parse
149
153
  # cleanly (most often a regex literal with an unmatched `{` / `}` that walks the depth
150
154
  # counter past the real closing brace). When found, we warn and refuse to rewrite anything
151
155
  # in the file so a sibling rewrite cannot accidentally splice into a wrong location.
152
156
  def rsc_plugin_sections_safe_to_rewrite?(config_path, content, is_server:)
157
+ # `:unparseable` is file-wide: the partitioner increments it before filtering parseable
158
+ # sections by the current `is_server` target.
153
159
  unparseable = rsc_plugin_option_sections_partition(content, is_server: is_server).fetch(:unparseable)
154
160
  return true if unparseable.zero?
155
161
 
@@ -160,7 +166,7 @@ module ReactOnRails
160
166
  def rewritable_rsc_plugin?(config_path, content, is_server:)
161
167
  # Mixed same-target plugins are still rewritable: the later rewrite only updates plugins
162
168
  # missing clientReferences and leaves sibling custom clientReferences untouched.
163
- return true if rsc_plugin_without_client_references?(content, is_server: is_server)
169
+ return true if any_rsc_plugin_section_without_client_references?(content, is_server: is_server)
164
170
 
165
171
  if rsc_plugin_defines_client_references?(content, is_server: is_server)
166
172
  GeneratorMessages.add_warning(
@@ -258,18 +264,24 @@ module ReactOnRails
258
264
  end
259
265
  end
260
266
 
261
- def rsc_plugin_without_client_references?(content, is_server:)
267
+ # Existential check: returns true when at least one matching plugin section is missing a
268
+ # top-level `clientReferences:` key. Pairs with `rsc_plugin_defines_client_references?`,
269
+ # which uses the same any-section semantics for the opposite condition. The two are not
270
+ # complements when multiple plugin sections exist — a file with one configured plugin and
271
+ # one unconfigured plugin returns true from both.
272
+ def any_rsc_plugin_section_without_client_references?(content, is_server:)
262
273
  rsc_plugin_option_sections(content, is_server: is_server).any? do |section|
263
274
  !rsc_plugin_body_has_top_level_key?(section.fetch(:body), "clientReferences")
264
275
  end
265
276
  end
266
277
 
267
278
  # Strips JavaScript line and block comments while preserving string-literal contents,
268
- # so `clientReferences:` / `isServer:` substrings inside strings are not mis-detected.
279
+ # including simple `${...}` template-literal interpolation, so `clientReferences:` /
280
+ # `isServer:` substrings inside strings are not mis-detected.
269
281
  # Shares the `advance_js_scan_state` family used by `js_top_level_position?` and
270
282
  # `matching_js_closing_brace` so all JS-aware passes follow the same comment/string rules.
271
- # Regex literals (e.g. `/a{2}/`) are still outside this scanner's supported surface
272
- # because brace quantifiers can confuse `matching_js_closing_brace`'s depth counter.
283
+ # See `advance_js_scan_state` for the scanner's supported surface (including the regex-
284
+ # literal and nested-template-literal limits that callers must be aware of).
273
285
  def rsc_plugin_options_without_comments(options)
274
286
  result = String.new(capacity: options.length)
275
287
  state = nil
@@ -429,15 +441,15 @@ module ReactOnRails
429
441
  end
430
442
 
431
443
  # Expects `content[open_index] == "{"`; callers pass the options-object opening brace.
432
- # This lightweight scanner treats template literals as opaque strings (backtick to backtick).
433
- # Simple `${...}` expressions are handled correctly: while in the backtick state every
434
- # character including `{` and `}` inside the expression is consumed as string content
435
- # and never reaches the depth counter. The real unsupported case is *nested* template
436
- # literals (e.g. `` `outer ${`inner`}` ``) where the inner backtick falsely closes the outer
437
- # string state, exposing later braces to the depth counter. Callers detect that via
438
- # `rsc_plugin_options_followed_by_close_paren?` and mark the section unparseable rather
439
- # than producing a corrupt rewrite. Regex literals are outside this scanner's supported
440
- # surface for the same reason.
444
+ # This lightweight scanner supports strings (including template literals for simple
445
+ # `${...}` interpolation) plus JS line/block comments. It does not classify regex
446
+ # literals, so braces inside constructs such as `/a{2}/` or `/[{]/` can be counted as
447
+ # object braces. Nested template literals (for example, `outer ${`inner`}`) are also
448
+ # unsupported: the inner backtick falsely closes the outer string state, exposing later
449
+ # braces to the depth counter. `rsc_plugin_options_without_comments` shares the same
450
+ # supported surface, and callers detect corrupted sections via
451
+ # `rsc_plugin_options_followed_by_close_paren?` so the migration warns instead of
452
+ # producing a corrupt rewrite.
441
453
  def matching_js_closing_brace(content, open_index)
442
454
  depth = 0
443
455
  index = open_index
@@ -470,8 +482,51 @@ module ReactOnRails
470
482
  nil
471
483
  end
472
484
 
473
- # Return index is the last consumed character. Line comments leave the newline
474
- # for the caller's normal index increment; block comments consume the closing slash.
485
+ # Central dispatcher for the lightweight JS scanner shared by every JS-aware pass in this
486
+ # generator (`matching_js_closing_brace`, `js_top_level_position?`, `js_code_position?`,
487
+ # `rsc_plugin_options_without_comments`, `first_significant_js_index`,
488
+ # `rsc_plugin_options_followed_by_close_paren?`, `last_js_code_char_index`,
489
+ # `last_js_code_line_start`). Return index is the last consumed character. Line comments
490
+ # leave the newline for the caller's normal index increment; block comments consume the
491
+ # closing slash.
492
+ #
493
+ # Supported lexical constructs:
494
+ # - Line comments (`// ...\n`) and block comments (`/* ... */`).
495
+ # - Single-quoted (`'...'`), double-quoted (`"..."`), and template-literal (`` `...` ``)
496
+ # strings, including escape sequences and the simple `${expr}` interpolation form
497
+ # (interpolation braces stay inside the string state and never reach the depth counter).
498
+ #
499
+ # Outside the supported surface for callers that do **not** run `js_regex_literal_start?`
500
+ # preprocessing (e.g. `matching_js_closing_brace`, `rsc_plugin_options_without_comments`),
501
+ # the scanner cannot distinguish these from the syntax they shadow, so `{`/`}` characters
502
+ # they contain can confuse the depth counter. `js_top_level_position?` and
503
+ # `js_code_position?` handle regex literals via their own pre-pass and are unaffected.
504
+ # - Regex literals (e.g. `/a{2}/`, `/\{/`, `/[{]/`): not recognized as a distinct state,
505
+ # so brace-containing patterns walk the depth counter past the real options close. The
506
+ # user-facing warning text in `warn_unparseable_rsc_plugin_sections` calls these out
507
+ # explicitly.
508
+ # - Nested template literals (`` `outer ${`inner`}` ``): the inner backtick falsely closes
509
+ # the outer string state, exposing later braces to the depth counter.
510
+ #
511
+ # The downstream `rsc_plugin_option_sections_partition` catches both failure modes by
512
+ # requiring the matched closing `}` to be followed by `)`. When it isn't, the section is
513
+ # marked unparseable and `warn_unparseable_rsc_plugin_sections` asks the user to add
514
+ # `clientReferences:` manually — the migration declines to rewrite rather than risk
515
+ # corrupting the config.
516
+ #
517
+ # Future expansion (only worth doing if a real-world RSC plugin options block needs it):
518
+ # 1. Add a `:regex_literal` state alongside the string and comment states. Track regex
519
+ # contexts by detecting `/` after a token that legally precedes a regex literal
520
+ # (`=`, `(`, `,`, `:`, `;`, `?`, `!`, `&&`, `||`, `return`, `typeof`, etc.) and consume
521
+ # until the unescaped closing `/` plus any flags. The token-context check is necessary
522
+ # because the same `/` character means division in expression position.
523
+ # 2. Add a stack-based template-literal state so nested `` `...${`inner`}...` `` pairs
524
+ # track depth instead of toggling a single boolean state.
525
+ # Regex literals require expanding `advance_js_default_scan_state`; nested template
526
+ # literals would also require replacing `advance_js_string_state` with stack-aware
527
+ # handling. Both changes need a new state-machine branch; the current callers were
528
+ # specifically designed around the simpler scanner and would need re-validation against
529
+ # the expanded state set.
475
530
  #
476
531
  # IMPORTANT CALLER CONTRACT — block-comment exit:
477
532
  # When this returns from a `*/` exit, `state` is cleared, but `char` is still the `*` and
@@ -909,13 +964,12 @@ module ReactOnRails
909
964
  # generator has already confirmed `RSCWebpackPlugin` is imported in the file. That's why
910
965
  # this helper deliberately omits the `RSCWebpackPlugin` import that `inject_rsc_*_imports`
911
966
  # adds on the from-scratch path — adding it here would produce a duplicate import.
967
+ # Must only be called via `ensure_rsc_client_references_setup`, which has already verified
968
+ # that no module-scope `rscClientReferences` declaration exists and that the import anchor
969
+ # is present and not yet consumed by a previous call. Bypassing that guard can write a
970
+ # duplicate `const rscClientReferences` declaration and leave the user's webpack config with
971
+ # a Node SyntaxError at load time.
912
972
  def add_rsc_client_references_setup(config_path, content, existing_imports_content, is_server:)
913
- # Belt-and-suspenders: the only caller, `ensure_rsc_client_references_setup`, already
914
- # checks both `scoped_rsc_client_references_defined?` and `rsc_client_references_defined?`
915
- # before delegating here. The guards are kept so the helper is safe to call directly.
916
- return false if scoped_rsc_client_references_defined?(content)
917
- return false if rsc_client_references_defined?(content)
918
-
919
973
  replace_rsc_client_references_setup_anchor(config_path, content, is_server: is_server) do |anchor|
920
974
  join_rsc_client_references_setup(
921
975
  content,
@@ -7,7 +7,12 @@ ReactOnRailsPro.configure do |config|
7
7
  config.renderer_url = ENV.fetch("REACT_RENDERER_URL", "http://localhost:3800")
8
8
 
9
9
  # See value in renderer/node-renderer.js
10
- config.renderer_password = ENV.fetch("RENDERER_PASSWORD", "devPassword")
10
+ # For production, set RENDERER_PASSWORD as an env var and remove the literal fallback.
11
+ <% if @generated_renderer_password -%>
12
+ config.renderer_password = ENV.fetch("RENDERER_PASSWORD", "<%= @generated_renderer_password %>")
13
+ <% else -%>
14
+ config.renderer_password = ENV["RENDERER_PASSWORD"]
15
+ <% end -%>
11
16
 
12
17
  config.ssr_timeout = 5
13
18
  config.renderer_request_retry_limit = 1
@@ -12,7 +12,12 @@ const config = {
12
12
  logLevel: env.RENDERER_LOG_LEVEL || 'info',
13
13
 
14
14
  // See value in /config/initializers/react_on_rails_pro.rb
15
- password: env.RENDERER_PASSWORD || 'devPassword',
15
+ // For production, set RENDERER_PASSWORD as an env var and remove the literal fallback.
16
+ <% if @generated_renderer_password -%>
17
+ password: env.RENDERER_PASSWORD ?? '<%= @generated_renderer_password %>',
18
+ <% else -%>
19
+ password: env.RENDERER_PASSWORD,
20
+ <% end -%>
16
21
 
17
22
  // Number of Node.js worker threads for SSR rendering
18
23
  // Set RENDERER_WORKERS_COUNT env var to override (e.g., for production tuning)
@@ -94,7 +94,8 @@ module ReactOnRails
94
94
  "scripts/deploy.sh",
95
95
  ".circleci/config.yml",
96
96
  ".gitlab-ci.yml",
97
- "bitbucket-pipelines.yml"
97
+ "bitbucket-pipelines.yml",
98
+ "Jenkinsfile"
98
99
  ].freeze
99
100
  # Bounded glob allowlist for deploy manifests that live in a known directory
100
101
  # but use per-environment or per-workflow filenames. Each pattern matches
@@ -2898,20 +2899,29 @@ module ReactOnRails
2898
2899
  checker.add_warning("⚠️ Could not complete scan for deprecated renderer-cache task references: #{e.message}")
2899
2900
  end
2900
2901
 
2901
- def deploy_script_references_deprecated_task?(full_path)
2902
- # Only `#` comments matter for the scanned file types: Procfile, Dockerfile*,
2903
- # and bin/* scripts all use `#`. None use `//`, so we don't filter it.
2904
- # The trailing-comment strip requires whitespace before `#`, so a fragment
2905
- # like `task#name` stays intact while `cmd # was: <deprecated>` is filtered.
2902
+ def deploy_script_references_deprecated_task?(full_path, path)
2903
+ comment_prefixes = renderer_cache_deploy_script_comment_prefixes(path)
2904
+
2905
+ # The trailing-comment strip requires whitespace before the comment marker,
2906
+ # so fragments like `task#name` stay intact while `cmd # was: <deprecated>`
2907
+ # and Jenkinsfile `cmd // was: <deprecated>` comments are filtered.
2906
2908
  full_path.binread.each_line.any? do |line|
2907
2909
  stripped = line.lstrip
2908
- next false if stripped.start_with?("#")
2910
+ next false if comment_prefixes.any? { |prefix| stripped.start_with?(prefix) }
2909
2911
 
2910
- without_inline_comment = stripped.sub(/ +#.*/, "")
2912
+ without_inline_comment = comment_prefixes.reduce(stripped) do |content, prefix|
2913
+ content.sub(/ +#{Regexp.escape(prefix)}.*/, "")
2914
+ end
2911
2915
  without_inline_comment.include?(DEPRECATED_RENDERER_CACHE_TASK)
2912
2916
  end
2913
2917
  end
2914
2918
 
2919
+ def renderer_cache_deploy_script_comment_prefixes(path)
2920
+ return ["#", "//"] if path == "Jenkinsfile"
2921
+
2922
+ ["#"]
2923
+ end
2924
+
2915
2925
  def deploy_script_path_references_deprecated_task?(path)
2916
2926
  full_path = Rails.root.join(path)
2917
2927
 
@@ -2920,7 +2930,7 @@ module ReactOnRails
2920
2930
  # deploy scripts and CI manifests should be tiny.
2921
2931
  return false if full_path.size > RENDERER_CACHE_DEPLOY_SCRIPT_MAX_BYTES
2922
2932
 
2923
- deploy_script_references_deprecated_task?(full_path)
2933
+ deploy_script_references_deprecated_task?(full_path, path)
2924
2934
  rescue StandardError => e
2925
2935
  checker.add_warning(
2926
2936
  "⚠️ Could not scan #{path} for deprecated renderer-cache task references: #{e.message}"
@@ -170,7 +170,8 @@ module ReactOnRails
170
170
  return if env_config_path.to_s.empty?
171
171
 
172
172
  Rails.logger&.warn(
173
- "[React on Rails] SHAKAPACKER_CONFIG is set to '#{env_config_path}' but the file " \
173
+ "[React on Rails] SHAKAPACKER_CONFIG is set to '#{env_config_path}' " \
174
+ "(resolved to '#{shakapacker_config_path}') but the file " \
174
175
  "does not exist. Bundler detection treated Shakapacker as not configured for this app; " \
175
176
  "fix the path or unset the variable if this is unintended."
176
177
  )
@@ -710,12 +710,46 @@ module ReactOnRails
710
710
  raise ReactOnRails::PrerenderError.new(
711
711
  component_name: react_component_name,
712
712
  props: sanitized_props_string(props),
713
- err: nil,
713
+ err: rendering_error_from_result(json_result),
714
714
  js_code: js_code,
715
715
  console_messages: json_result["consoleReplayScript"]
716
716
  )
717
717
  end
718
718
 
719
+ def rendering_error_from_result(json_result)
720
+ rendering_error = json_result["renderingError"]
721
+ return unless rendering_error.is_a?(Hash)
722
+
723
+ stack = rendering_error["stack"]
724
+ # Mirror the JS `buildRSCStreamDiagnosticError` fallback: a renderingError that carries only
725
+ # a stack (no message) should still surface a diagnostic rather than be silently dropped.
726
+ message = rendering_error["message"].presence ||
727
+ (stack.present? ? "RSC stream metadata reported a rendering error" : nil)
728
+ return if message.nil?
729
+
730
+ error = StandardError.new(message)
731
+ frames = normalize_js_stack_lines(stack)
732
+ error.set_backtrace(frames) unless frames.empty?
733
+ error
734
+ end
735
+
736
+ # V8 stack strings begin with a `"TypeError: ..."` header line — and chained-exception stacks
737
+ # add further non-frame headers mid-stack (e.g. `Caused by: ...`) — none of which match Ruby's
738
+ # `file:line:in 'method'` backtrace format. Keep only the `at <frame>` lines so backtrace
739
+ # cleaners and APM tools that ship the array raw don't choke on unparseable strings.
740
+ # Frames are also `.strip`-ed to drop V8's leading indentation, which Ruby backtraces never carry.
741
+ #
742
+ # When the stack has no `at <frame>` lines (e.g. a header-only or non-V8 string) this returns
743
+ # `[]`, leaving the backtrace nil rather than seeding it with unparseable header lines.
744
+ def normalize_js_stack_lines(stack)
745
+ # Only V8 string stacks are parseable here. A non-String stack (e.g. an array of frames
746
+ # from a non-V8 serializer) would otherwise be `to_s`-ed into Ruby array syntax and yield
747
+ # no usable frames — guard explicitly so the no-backtrace path is intentional, not garbage.
748
+ return [] unless stack.is_a?(String)
749
+
750
+ stack.lines.map { |line| line.chomp.strip }.select { |line| line.start_with?("at ") }
751
+ end
752
+
719
753
  def should_raise_streaming_prerender_error?(chunk_json_result, render_options)
720
754
  chunk_json_result["hasErrors"] &&
721
755
  (if chunk_json_result["isShellReady"]
@@ -61,12 +61,7 @@ module ReactOnRails
61
61
 
62
62
  MSG
63
63
 
64
- backtrace = if Utils.full_text_errors_enabled?
65
- err.backtrace.join("\n")
66
- else
67
- "#{Rails.backtrace_cleaner.clean(err.backtrace).join("\n")}\n" +
68
- Rainbow("💡 Tip: Set FULL_TEXT_ERRORS=true to see the full backtrace").yellow
69
- end
64
+ backtrace = formatted_backtrace(err)
70
65
  else
71
66
  backtrace = nil
72
67
  end
@@ -95,6 +90,19 @@ module ReactOnRails
95
90
  # rubocop:enable Metrics/AbcSize
96
91
  end
97
92
 
93
+ # Formats `err.backtrace` for display, or returns nil when there are no frames.
94
+ def formatted_backtrace(err)
95
+ error_backtrace = err.backtrace
96
+ # JS-originated errors (e.g. an RSC renderingError carrying a message but no parseable
97
+ # stack) have no Ruby backtrace. Skip it rather than crashing on `nil.join` /
98
+ # `Rails.backtrace_cleaner.clean(nil)`.
99
+ return nil if error_backtrace.nil? || error_backtrace.empty?
100
+ return error_backtrace.join("\n") if Utils.full_text_errors_enabled?
101
+
102
+ "#{Rails.backtrace_cleaner.clean(error_backtrace).join("\n")}\n" +
103
+ Rainbow("💡 Tip: Set FULL_TEXT_ERRORS=true to see the full backtrace").yellow
104
+ end
105
+
98
106
  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
99
107
  def build_troubleshooting_suggestions(component_name, err, console_messages)
100
108
  suggestions = []
@@ -13,7 +13,7 @@ module ReactOnRails
13
13
  exit!(1)
14
14
  end
15
15
 
16
- puts "\nBuilding Webpack assets..."
16
+ puts "\nBuilding assets..."
17
17
 
18
18
  cmd = ReactOnRails::Utils.prepend_cd_node_modules_directory(
19
19
  ReactOnRails.configuration.build_test_command
@@ -21,7 +21,7 @@ module ReactOnRails
21
21
 
22
22
  ReactOnRails::Utils.invoke_and_exit_if_failed(cmd, compilation_failed_message)
23
23
 
24
- puts "Completed building Webpack assets."
24
+ puts "Completed building assets."
25
25
  end
26
26
 
27
27
  private
@@ -49,14 +49,14 @@ module ReactOnRails
49
49
 
50
50
  def compilation_failed_message
51
51
  <<~MSG
52
- React on Rails: Error building webpack assets!
52
+ React on Rails: Error building test assets!
53
53
 
54
54
  The build_test_command failed. This means test assets could not be compiled.
55
55
 
56
56
  Quick alternatives to get unblocked:
57
57
  • Run 'bin/dev static' in another terminal (assets auto-reuse for tests)
58
58
  • Run 'bin/dev test-watch' to keep test assets fresh in the background
59
- • Run 'RAILS_ENV=test bin/shakapacker' manually to compile once
59
+ • Run '#{ReactOnRails.configuration.build_test_command}' manually to compile once
60
60
 
61
61
  For full details: #{TESTING_DOCS_URL}
62
62
  Run 'bin/dev --help' for development server modes.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ReactOnRails
4
- VERSION = "16.7.0.rc.3"
4
+ VERSION = "17.0.0.rc.0"
5
5
  end
data/rakelib/lint.rake CHANGED
@@ -37,7 +37,7 @@ namespace :lint do # rubocop:disable Metrics/BlockLength
37
37
  private
38
38
 
39
39
  def stylelint_command
40
- "pnpm run stylelint \"spec/dummy/app/assets/stylesheets/**/*.scss\" \"spec/dummy/client/**/*.scss\""
40
+ "pnpm run lint:scss"
41
41
  end
42
42
 
43
43
  def stylelint_fix_command
@@ -69,8 +69,6 @@ namespace :run_rspec do
69
69
  env_vars_array = []
70
70
  env_vars_array << rbs_runtime_env_vars unless rbs_runtime_env_vars.empty?
71
71
  env_vars_array << "DISABLE_TURBOLINKS=TRUE"
72
- # This task intentionally runs with a different Gemfile view where the turbolinks gem is excluded.
73
- env_vars_array << "BUNDLE_FROZEN=false"
74
72
  env_vars = env_vars_array.join(" ")
75
73
  run_tests_in(spec_dummy_dir,
76
74
  env_vars: env_vars,
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: react_on_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 16.7.0.rc.3
4
+ version: 17.0.0.rc.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Gordon
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-27 00:00:00.000000000 Z
11
+ date: 2026-05-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -192,7 +192,7 @@ files:
192
192
  - lib/generators/react_on_rails/templates/dev_tests/spec/system/hello_server_spec.rb
193
193
  - lib/generators/react_on_rails/templates/dev_tests/spec/system/hello_world_spec.rb
194
194
  - lib/generators/react_on_rails/templates/pro/base/config/initializers/react_on_rails_pro.rb.tt
195
- - lib/generators/react_on_rails/templates/pro/base/renderer/node-renderer.js
195
+ - lib/generators/react_on_rails/templates/pro/base/renderer/node-renderer.js.tt
196
196
  - lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/actions/helloWorldActionCreators.js
197
197
  - lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/actions/helloWorldActionCreators.ts
198
198
  - lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/components/HelloWorld.jsx