react_on_rails 16.6.0 → 16.7.0.rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -0
  3. data/Gemfile.development_dependencies +2 -2
  4. data/Gemfile.lock +2 -14
  5. data/Rakefile +0 -6
  6. data/Steepfile +4 -0
  7. data/lib/generators/react_on_rails/base_generator.rb +4 -4
  8. data/lib/generators/react_on_rails/demo_page_config.rb +3 -3
  9. data/lib/generators/react_on_rails/dev_tests_generator.rb +1 -1
  10. data/lib/generators/react_on_rails/generator_helper.rb +6 -65
  11. data/lib/generators/react_on_rails/generator_messages/ci_section.rb +42 -0
  12. data/lib/generators/react_on_rails/generator_messages/package_manager_detection.rb +194 -0
  13. data/lib/generators/react_on_rails/generator_messages/shakapacker_status_section.rb +61 -0
  14. data/lib/generators/react_on_rails/generator_messages.rb +22 -79
  15. data/lib/generators/react_on_rails/install_generator.rb +243 -28
  16. data/lib/generators/react_on_rails/js_dependency_manager.rb +7 -4
  17. data/lib/generators/react_on_rails/pro/USAGE +1 -1
  18. data/lib/generators/react_on_rails/pro_generator.rb +206 -183
  19. data/lib/generators/react_on_rails/pro_setup.rb +102 -26
  20. data/lib/generators/react_on_rails/react_with_redux_generator.rb +3 -2
  21. data/lib/generators/react_on_rails/templates/base/base/.env.example +25 -0
  22. data/lib/generators/react_on_rails/templates/base/base/.github/workflows/ci.yml.tt +86 -0
  23. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev +4 -3
  24. data/lib/generators/react_on_rails/templates/base/base/babel.config.js.tt +1 -1
  25. data/lib/generators/react_on_rails/templates/base/base/bin/switch-bundler +2 -2
  26. data/lib/generators/react_on_rails/templates/base/base/config/webpack/ServerClientOrBoth.js.tt +1 -1
  27. data/lib/generators/react_on_rails/templates/base/base/config/webpack/clientWebpackConfig.js.tt +1 -1
  28. data/lib/generators/react_on_rails/templates/base/base/config/webpack/commonWebpackConfig.js.tt +2 -2
  29. data/lib/generators/react_on_rails/templates/base/base/config/webpack/development.js.tt +1 -1
  30. data/lib/generators/react_on_rails/templates/base/base/config/webpack/production.js.tt +1 -1
  31. data/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt +6 -5
  32. data/lib/generators/react_on_rails/templates/base/base/config/webpack/test.js.tt +1 -1
  33. data/lib/generators/react_on_rails/templates/pro/base/config/initializers/react_on_rails_pro.rb.tt +1 -1
  34. data/lib/generators/react_on_rails/templates/pro/base/{client → renderer}/node-renderer.js +1 -0
  35. data/lib/react_on_rails/config_path_resolver.rb +101 -4
  36. data/lib/react_on_rails/configuration.rb +22 -0
  37. data/lib/react_on_rails/dev/file_manager.rb +135 -8
  38. data/lib/react_on_rails/dev/port_selector.rb +259 -7
  39. data/lib/react_on_rails/dev/process_manager.rb +29 -2
  40. data/lib/react_on_rails/dev/server_manager.rb +607 -39
  41. data/lib/react_on_rails/doctor.rb +513 -45
  42. data/lib/react_on_rails/helper.rb +3 -11
  43. data/lib/react_on_rails/js_code_builder.rb +66 -0
  44. data/lib/react_on_rails/length_prefixed_parser.rb +142 -0
  45. data/lib/react_on_rails/packs_generator.rb +65 -12
  46. data/lib/react_on_rails/pro_migration.rb +175 -0
  47. data/lib/react_on_rails/render_request.rb +74 -0
  48. data/lib/react_on_rails/rendering_strategy/exec_js_strategy.rb +29 -0
  49. data/lib/react_on_rails/rendering_strategy.rb +44 -0
  50. data/lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb +33 -22
  51. data/lib/react_on_rails/system_checker.rb +44 -23
  52. data/lib/react_on_rails/utils.rb +5 -0
  53. data/lib/react_on_rails/version.rb +1 -1
  54. data/lib/react_on_rails.rb +3 -0
  55. data/rakelib/run_rspec.rake +0 -5
  56. data/rakelib/shakapacker_examples.rake +66 -23
  57. data/react_on_rails.gemspec +18 -8
  58. data/sig/react_on_rails/js_code_builder.rbs +11 -0
  59. data/sig/react_on_rails/render_request.rbs +28 -0
  60. data/sig/react_on_rails/rendering_strategy/exec_js_strategy.rbs +11 -0
  61. data/sig/react_on_rails/rendering_strategy.rbs +7 -0
  62. data/sig/react_on_rails.rbs +6 -0
  63. metadata +31 -10
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "generator_messages"
4
+ require "react_on_rails/pro_migration"
4
5
 
5
6
  module ReactOnRails
6
7
  module Generators
@@ -33,7 +34,7 @@ module ReactOnRails
33
34
  #
34
35
  # Creates:
35
36
  # - config/initializers/react_on_rails_pro.rb
36
- # - client/node-renderer.js
37
+ # - renderer/node-renderer.js
37
38
  # - Procfile.dev entry for node-renderer
38
39
  #
39
40
  # @note NPM dependencies are handled separately by JsDependencyManager
@@ -43,8 +44,8 @@ module ReactOnRails
43
44
  say set_color("=" * 80, :cyan)
44
45
 
45
46
  create_pro_initializer
46
- create_node_renderer
47
- add_pro_to_procfile
47
+ legacy_renderer_detected = create_node_renderer
48
+ add_pro_to_procfile unless legacy_renderer_detected
48
49
  update_webpack_config_for_pro
49
50
 
50
51
  say set_color("=" * 80, :cyan)
@@ -52,22 +53,26 @@ module ReactOnRails
52
53
  say set_color("=" * 80, :cyan)
53
54
  end
54
55
 
55
- # Check if Pro gem is missing. Attempts auto-install via bundle add.
56
+ # Check if the Pro gem is missing. When the base react_on_rails gem is in
57
+ # the Gemfile, installation is deferred to the later Gemfile swap (which
58
+ # preserves the user's version pin); otherwise auto-install via `bundle
59
+ # add` is attempted.
56
60
  # @param force [Boolean] When true, always checks (default: only if use_pro?).
57
- # @return [Boolean] true if Pro gem is missing and could not be installed
61
+ # @return [Boolean] true only if the Pro gem is missing and could not be
62
+ # installed; false if it is present, was auto-installed, or the install
63
+ # is deferred to the Gemfile swap.
58
64
  def missing_pro_gem?(force: false)
59
65
  return false unless force || use_pro?
60
66
  return false if pro_gem_installed?
67
+ return false if defer_pro_gem_install_to_gemfile_swap
61
68
  return false if attempt_pro_gem_auto_install
62
69
 
63
- context_line = pro_gem_requirement_context_line
64
- prerelease_note = rsc_pro_prerelease_note
70
+ optional_prerelease_line = prerelease_note.empty? ? "" : "\n#{prerelease_note}"
65
71
 
66
72
  GeneratorMessages.add_error(<<~MSG.strip)
67
73
  🚫 Failed to auto-install #{PRO_GEM_NAME} gem.
68
74
 
69
- #{context_line}
70
- #{prerelease_note}
75
+ #{pro_gem_requirement_context_line}#{optional_prerelease_line}
71
76
 
72
77
  Please add manually to your Gemfile:
73
78
  gem '#{PRO_GEM_NAME}', '#{pro_gem_version_requirement}'
@@ -94,20 +99,16 @@ module ReactOnRails
94
99
  end
95
100
 
96
101
  def pro_requirement_flag
97
- return "--rsc-pro" if use_rsc_pro_mode?
98
102
  return "--rsc" if options[:rsc]
99
103
 
100
104
  "--pro"
101
105
  end
102
106
 
103
- def rsc_pro_prerelease_note
104
- return "" unless use_rsc_pro_mode?
105
- return "" unless Gem::Version.new(ReactOnRails::VERSION).prerelease?
107
+ def prerelease_note
108
+ return "" unless prerelease_ror_version?
106
109
 
107
110
  "Note: #{PRO_GEM_NAME} #{ReactOnRails::VERSION} may not be published yet. " \
108
111
  "If you are testing from source, use a local Gemfile `path:` option."
109
- rescue ArgumentError
110
- ""
111
112
  end
112
113
 
113
114
  # Attempt to auto-install the Pro gem via bundle add.
@@ -210,23 +211,65 @@ module ReactOnRails
210
211
  say "✅ Created #{initializer_path}", :green
211
212
  end
212
213
 
214
+ # Matches active (uncommented) Procfile.dev node-renderer lines, tolerating
215
+ # an optional `./` prefix that a user may have added by hand
216
+ # (e.g. `node ./renderer/node-renderer.js`).
217
+ NEW_RENDERER_COMMAND_REGEX = %r{^[ \t]*node-renderer:[^\n]*\bnode\s+\.?/?renderer/node-renderer\.js\b}
218
+ LEGACY_RENDERER_COMMAND_REGEX = %r{^[ \t]*node-renderer:[^\n]*\bnode\s+\.?/?client/node-renderer\.js\b}
219
+
220
+ # Creates renderer/node-renderer.js unless either the new path or the legacy
221
+ # client/node-renderer.js already exists.
222
+ #
223
+ # @return [Boolean] true when a legacy client/node-renderer.js was detected
224
+ # (caller should skip add_pro_to_procfile to avoid pointing Procfile.dev
225
+ # at a file that wasn't created); false otherwise.
213
226
  def create_node_renderer
214
- node_renderer_path = "client/node-renderer.js"
227
+ node_renderer_path = "renderer/node-renderer.js"
228
+ legacy_node_renderer_path = "client/node-renderer.js"
215
229
 
216
230
  if File.exist?(File.join(destination_root, node_renderer_path))
217
231
  say "ℹ️ #{node_renderer_path} already exists, skipping", :yellow
218
- return
232
+ return false
233
+ end
234
+
235
+ if File.exist?(File.join(destination_root, legacy_node_renderer_path))
236
+ say "ℹ️ #{legacy_node_renderer_path} detected, keeping existing renderer; " \
237
+ "to migrate, move it to #{node_renderer_path} and update any references " \
238
+ "(e.g. Procfile.dev, Procfile.prod, Docker CMD / command):", :yellow
239
+ say " node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=${RENDERER_PORT:-3800} " \
240
+ "node #{node_renderer_path}", :yellow
241
+ warn_on_stale_legacy_procfile_entry
242
+ return true
219
243
  end
220
244
 
221
245
  say "📝 Creating Node Renderer bootstrap...", :yellow
222
246
 
223
- # Ensure client directory exists
224
- FileUtils.mkdir_p(File.join(destination_root, "client"))
247
+ empty_directory("renderer")
225
248
 
226
- template_path = "templates/pro/base/client/node-renderer.js"
249
+ template_path = "templates/pro/base/renderer/node-renderer.js"
227
250
  copy_file(template_path, node_renderer_path)
228
251
 
229
252
  say "✅ Created #{node_renderer_path}", :green
253
+ false
254
+ end
255
+
256
+ # When a legacy client/node-renderer.js is detected, add_pro_to_procfile is
257
+ # skipped, so surface a pointed warning if Procfile.dev still launches the
258
+ # legacy entry. This nudges the user to update the exact line they need to
259
+ # touch rather than leaving them to diff the generic migration hint against
260
+ # their Procfile themselves.
261
+ def warn_on_stale_legacy_procfile_entry
262
+ procfile_path = File.join(destination_root, "Procfile.dev")
263
+ return unless File.exist?(procfile_path)
264
+
265
+ procfile_content = File.read(procfile_path)
266
+ return unless procfile_content.match?(LEGACY_RENDERER_COMMAND_REGEX)
267
+
268
+ GeneratorMessages.add_warning(<<~MSG.strip)
269
+ ⚠️ Procfile.dev still launches the legacy client/node-renderer.js.
270
+ After migrating the renderer file, update that line to:
271
+ node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=${RENDERER_PORT:-3800} node renderer/node-renderer.js
272
+ MSG
230
273
  end
231
274
 
232
275
  def add_pro_to_procfile
@@ -237,22 +280,32 @@ module ReactOnRails
237
280
  ⚠️ Procfile.dev not found. Skipping Node Renderer process addition.
238
281
 
239
282
  You'll need to add the Node Renderer to your process manager manually:
240
- node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=3800 node client/node-renderer.js
283
+ node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=${RENDERER_PORT:-3800} node renderer/node-renderer.js
241
284
  MSG
242
285
  return
243
286
  end
244
287
 
245
- if File.read(procfile_path).include?("node-renderer:")
288
+ procfile_content = File.read(procfile_path)
289
+
290
+ if procfile_content.match?(NEW_RENDERER_COMMAND_REGEX)
246
291
  say "ℹ️ Node Renderer already in Procfile.dev, skipping", :yellow
247
292
  return
248
293
  end
249
294
 
295
+ if procfile_content.match?(/^[ \t]*node-renderer:/)
296
+ say "⚠️ Procfile.dev has a node-renderer: entry that doesn't reference " \
297
+ "renderer/node-renderer.js. Update it manually to:", :yellow
298
+ say " node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=${RENDERER_PORT:-3800} " \
299
+ "node renderer/node-renderer.js", :yellow
300
+ return
301
+ end
302
+
250
303
  say "📝 Adding Node Renderer to Procfile.dev...", :yellow
251
304
 
252
305
  node_renderer_line = <<~PROCFILE
253
306
 
254
307
  # React on Rails Pro - Node Renderer for SSR
255
- node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=3800 node client/node-renderer.js
308
+ node-renderer: RENDERER_LOG_LEVEL=debug RENDERER_PORT=${RENDERER_PORT:-3800} node renderer/node-renderer.js
256
309
  PROCFILE
257
310
 
258
311
  append_to_file("Procfile.dev", node_renderer_line)
@@ -486,14 +539,37 @@ module ReactOnRails
486
539
  "bundle add #{PRO_GEM_NAME} --version='#{pro_gem_version_requirement}' --strict"
487
540
  end
488
541
 
542
+ def defer_pro_gem_install_to_gemfile_swap
543
+ return false unless base_react_on_rails_gem_in_gemfile?
544
+
545
+ mark_pro_gem_installed!
546
+ true
547
+ end
548
+
549
+ def base_react_on_rails_gem_in_gemfile?
550
+ gemfile_path = File.join(destination_root, "Gemfile")
551
+ return false unless File.exist?(gemfile_path)
552
+
553
+ ReactOnRails::ProMigration.base_gem_entry?(File.read(gemfile_path))
554
+ rescue SystemCallError, IOError
555
+ false
556
+ end
557
+
489
558
  def pro_gem_version_requirement
490
- # RSC Pro uses exact pinning so the Pro gem version always matches the
491
- # paired RSC package version generated in the same run.
492
- return ReactOnRails::VERSION if use_rsc_pro_mode?
559
+ # Prerelease gem versions need an exact pin: Bundler's pessimistic operator
560
+ # (~>) does not match prerelease versions, so a stable range would fail to
561
+ # install during prerelease cycles.
562
+ return ReactOnRails::VERSION if prerelease_ror_version?
493
563
 
494
564
  "~> #{recommended_pro_gem_version}"
495
565
  end
496
566
 
567
+ def prerelease_ror_version?
568
+ Gem::Version.new(ReactOnRails::VERSION).prerelease?
569
+ rescue ArgumentError
570
+ false
571
+ end
572
+
497
573
  # Keep manual fallback pinned to the latest stable release (drop pre-release suffixes like .rc.N).
498
574
  # react_on_rails_pro follows the same version number as react_on_rails by policy.
499
575
  # Both gems are released in lockstep; if this ever changes, replace with a dedicated constant.
@@ -104,7 +104,7 @@ module ReactOnRails
104
104
  # Fallback to package manager detection if GeneratorHelper fails
105
105
  return if success
106
106
 
107
- package_manager = GeneratorMessages.detect_package_manager
107
+ package_manager = GeneratorMessages.detect_package_manager(app_root: destination_root)
108
108
  return unless package_manager
109
109
 
110
110
  install_packages_with_fallback(regular_packages, dev: false, package_manager: package_manager)
@@ -116,7 +116,8 @@ module ReactOnRails
116
116
  # Append Redux-specific post-install instructions
117
117
  GeneratorMessages.add_info(
118
118
  GeneratorMessages.helpful_message_after_installation(component_name: "HelloWorldApp", route: "hello_world",
119
- pro: Gem.loaded_specs.key?("react_on_rails_pro"))
119
+ pro: Gem.loaded_specs.key?("react_on_rails_pro"),
120
+ app_root: destination_root)
120
121
  )
121
122
  end
122
123
 
@@ -7,12 +7,37 @@
7
7
  # Shakapacker reads SHAKAPACKER_DEV_SERVER_PORT on both the Ruby (proxy) and
8
8
  # JS (webpack-dev-server) sides, so no shakapacker.yml changes are needed.
9
9
  #
10
+ # === Coding Agent / CI Integration ===
11
+ # Set REACT_ON_RAILS_BASE_PORT to derive all ports from a single value.
12
+ # This works with any tool (Conductor.build, Codex, Quad Code, etc.).
13
+ # Ports are assigned as: Rails = base+0, webpack = base+1, renderer = base+2.
14
+ # When set, no manual port configuration is needed.
15
+ #
16
+ # Conductor.build sets CONDUCTOR_PORT automatically, which is recognized
17
+ # as a fallback when REACT_ON_RAILS_BASE_PORT is not set.
18
+ #
19
+ # === Manual Worktree Setup ===
10
20
  # Example for a second worktree:
11
21
  # PORT=3001
12
22
  # SHAKAPACKER_DEV_SERVER_PORT=3036
23
+ # RENDERER_PORT=3801 # React on Rails Pro only
24
+ # REACT_RENDERER_URL=http://localhost:3801 # Must match RENDERER_PORT
25
+
26
+ # Base port for coding agent tools (derives all other ports automatically)
27
+ # REACT_ON_RAILS_BASE_PORT=
13
28
 
14
29
  # Rails server port (default: 3000 for Procfile.dev / 3001 for Procfile.dev-prod-assets)
15
30
  # PORT=3000
16
31
 
17
32
  # Webpack dev server port (default: 3035, used by shakapacker)
18
33
  # SHAKAPACKER_DEV_SERVER_PORT=3035
34
+
35
+ # Node renderer port (React on Rails Pro only, default: 3800)
36
+ # RENDERER_PORT=3800
37
+
38
+ # Node renderer URL (React on Rails Pro only, must match RENDERER_PORT).
39
+ # If you set only RENDERER_PORT, bin/dev auto-derives
40
+ # REACT_RENDERER_URL=http://localhost:RENDERER_PORT. For a remote or
41
+ # non-localhost renderer (Docker service name, remote host), set
42
+ # REACT_RENDERER_URL explicitly so it is not replaced with the localhost default.
43
+ # REACT_RENDERER_URL=http://localhost:3800
@@ -0,0 +1,86 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main, master]
6
+ pull_request:
7
+ branches: [main, master]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - name: Set up Ruby
17
+ uses: ruby/setup-ruby@v1
18
+ with:
19
+ # ruby-version: "3.3" # Uncomment and set if you don't have a .ruby-version file
20
+ bundler-cache: true
21
+
22
+ <%- if config[:package_manager] == 'pnpm' -%>
23
+ - name: Set up pnpm
24
+ uses: pnpm/action-setup@v4
25
+ <%- unless config[:pnpm_version_declared] -%>
26
+ with:
27
+ # ⚠️ Verify this matches the pnpm major that wrote your pnpm-lock.yaml —
28
+ # major mismatches can silently change dependency resolution. To manage
29
+ # the version via Corepack instead, add `"packageManager": "pnpm@<exact-version>"`
30
+ # to package.json and remove this `with:` block. Range/tag specs (e.g. `pnpm@^10`,
31
+ # `pnpm@latest`) are non-reproducible and some Corepack versions reject them.
32
+ version: "<%= config[:pnpm_fallback_version] %>"
33
+ <%- end -%>
34
+ <%- end -%>
35
+ <%- if config[:package_manager] == 'bun' -%>
36
+ - name: Set up Bun
37
+ uses: oven-sh/setup-bun@v2
38
+
39
+ <%- end -%>
40
+ <%- if config[:package_manager] == 'yarn' -%>
41
+ - name: Enable Corepack
42
+ # Must run before actions/setup-node so its `cache: "yarn"` step
43
+ # can resolve Yarn Berry's cache directory correctly.
44
+ run: corepack enable
45
+
46
+ <%- end -%>
47
+ - name: Set up Node
48
+ uses: actions/setup-node@v4
49
+ with:
50
+ # "lts/*" tracks the current Node LTS; consider pinning (e.g. "22") for reproducible builds.
51
+ node-version: "lts/*"
52
+ <%- if config[:has_lockfile] && %w[yarn pnpm npm].include?(config[:package_manager]) -%>
53
+ cache: "<%= config[:package_manager] %>"
54
+ <%- end -%>
55
+
56
+ - name: Install JS dependencies
57
+ <%- if !config[:has_lockfile] && config[:package_manager] == 'yarn' -%>
58
+ # Yarn Berry defaults to immutable installs; allow lockfile creation on first CI run.
59
+ run: yarn install --no-immutable
60
+ <%- elsif !config[:has_lockfile] && config[:package_manager] == 'pnpm' -%>
61
+ # pnpm defaults to frozen-lockfile in CI; allow lockfile creation on first CI run.
62
+ run: pnpm install --no-frozen-lockfile
63
+ <%- else -%>
64
+ run: <%= config[:package_manager] %> install
65
+ <%- end -%>
66
+
67
+ <%- if config[:has_active_record] -%>
68
+ - name: Set up database
69
+ # If using PostgreSQL or MySQL, add a services block above.
70
+ # See https://docs.github.com/en/actions/using-containerized-services
71
+ run: bin/rails db:prepare
72
+ env:
73
+ RAILS_ENV: test
74
+
75
+ <%- end -%>
76
+ - name: Build JavaScript bundles
77
+ # Shakapacker default. ViteRuby users: replace with `bin/vite build --mode test`.
78
+ run: bin/shakapacker
79
+ env:
80
+ RAILS_ENV: test
81
+ NODE_ENV: test
82
+
83
+ - name: Run tests
84
+ run: <%= config[:has_rspec] ? "bundle exec rspec" : "bin/rails test" %>
85
+ env:
86
+ RAILS_ENV: test
@@ -1,9 +1,10 @@
1
1
  # Procfile for development
2
2
  # You can run these commands in separate shells
3
3
  #
4
- # To run multiple worktrees simultaneously, set different ports in each worktree's .env:
5
- # PORT=3001
6
- # SHAKAPACKER_DEV_SERVER_PORT=3036
4
+ # PORT, SHAKAPACKER_DEV_SERVER_PORT, and renderer ports are set automatically by
5
+ # bin/dev when REACT_ON_RAILS_BASE_PORT (or CONDUCTOR_PORT) is present, so
6
+ # concurrent worktrees get distinct ports without manual .env edits.
7
+ # For manual worktree setup (no base-port tool), see .env.example.
7
8
  rails: bundle exec rails s -p ${PORT:-3000}
8
9
  dev-server: bin/shakapacker-dev-server
9
10
  server-bundle: SERVER_BUNDLE_ONLY=true bin/shakapacker-watch --watch
@@ -1,4 +1,4 @@
1
- <%= add_documentation_reference(config[:message], "// https://github.com/shakacode/react_on_rails_demo_ssr_hmr/blob/master/babel.config.js") %>
1
+ <%= add_documentation_reference(config[:message], "// https://github.com/shakacode/react-on-rails-demo-ssr-hmr/blob/master/babel.config.js") %>
2
2
 
3
3
  module.exports = function (api) {
4
4
  const defaultConfigFunc = require('shakapacker/package/babel/preset.js')
@@ -15,8 +15,8 @@ class BundlerSwitcher
15
15
  }.freeze
16
16
 
17
17
  RSPACK_DEPS = {
18
- dependencies: %w[@rspack/core@^1.0.0 rspack-manifest-plugin@^5.0.0],
19
- dev_dependencies: %w[@rspack/cli@^1.0.0 @rspack/plugin-react-refresh@^1.0.0]
18
+ dependencies: %w[@rspack/core@^2.0.0-0 rspack-manifest-plugin@^5.0.0],
19
+ dev_dependencies: %w[@rspack/cli@^2.0.0-0 @rspack/plugin-react-refresh@^2.0.0]
20
20
  }.freeze
21
21
 
22
22
  def initialize(target_bundler)
@@ -1,4 +1,4 @@
1
- <%= add_documentation_reference(config[:message], "// https://github.com/shakacode/react_on_rails_demo_ssr_hmr/blob/master/config/webpack/ServerClientOrBoth.js") %>
1
+ <%= add_documentation_reference(config[:message], "// https://github.com/shakacode/react-on-rails-demo-ssr-hmr/blob/master/config/webpack/ServerClientOrBoth.js") %>
2
2
 
3
3
  const clientWebpackConfig = require('./clientWebpackConfig');
4
4
  <% if use_pro? -%>
@@ -1,4 +1,4 @@
1
- <%= add_documentation_reference(config[:message], "// https://github.com/shakacode/react_on_rails_demo_ssr_hmr/blob/master/config/webpack/clientWebpackConfig.js") %>
1
+ <%= add_documentation_reference(config[:message], "// https://github.com/shakacode/react-on-rails-demo-ssr-hmr/blob/master/config/webpack/clientWebpackConfig.js") %>
2
2
 
3
3
  const commonWebpackConfig = require('./commonWebpackConfig');
4
4
  <% if use_rsc? -%>
@@ -1,4 +1,4 @@
1
- <%= add_documentation_reference(config[:message], "// https://github.com/shakacode/react_on_rails_demo_ssr_hmr/blob/master/config/webpack/commonWebpackConfig.js") %>
1
+ <%= add_documentation_reference(config[:message], "// https://github.com/shakacode/react-on-rails-demo-ssr-hmr/blob/master/config/webpack/commonWebpackConfig.js") %>
2
2
 
3
3
  // Common configuration applying to client and server configuration
4
4
  const { generateWebpackConfig, merge } = require('shakapacker');
@@ -14,4 +14,4 @@ const commonOptions = {
14
14
  // Copy the object using merge b/c the baseClientWebpackConfig and commonOptions are mutable globals
15
15
  const commonWebpackConfig = () => merge({}, baseClientWebpackConfig, commonOptions);
16
16
 
17
- module.exports = commonWebpackConfig;
17
+ module.exports = commonWebpackConfig;
@@ -1,4 +1,4 @@
1
- <%= add_documentation_reference(config[:message], "// https://github.com/shakacode/react_on_rails_demo_ssr_hmr/blob/master/config/webpack/development.js") %>
1
+ <%= add_documentation_reference(config[:message], "// https://github.com/shakacode/react-on-rails-demo-ssr-hmr/blob/master/config/webpack/development.js") %>
2
2
 
3
3
  const { devServer, inliningCss, config } = require('shakapacker');
4
4
 
@@ -1,4 +1,4 @@
1
- <%= add_documentation_reference(config[:message], "// https://github.com/shakacode/react_on_rails_demo_ssr_hmr/blob/master/config/webpack/production.js") %>
1
+ <%= add_documentation_reference(config[:message], "// https://github.com/shakacode/react-on-rails-demo-ssr-hmr/blob/master/config/webpack/production.js") %>
2
2
 
3
3
  const serverClientOrBoth = require('./ServerClientOrBoth');
4
4
 
@@ -1,4 +1,4 @@
1
- <%= add_documentation_reference(config[:message], "// https://github.com/shakacode/react_on_rails_demo_ssr_hmr/blob/master/config/webpack/serverWebpackConfig.js") %>
1
+ <%= add_documentation_reference(config[:message], "// https://github.com/shakacode/react-on-rails-demo-ssr-hmr/blob/master/config/webpack/serverWebpackConfig.js") %>
2
2
 
3
3
  const { merge, config } = require('shakapacker');
4
4
  const commonWebpackConfig = require('./commonWebpackConfig');
@@ -197,10 +197,11 @@ const configureServer = () => {
197
197
  }
198
198
  });
199
199
 
200
- // eval works well for the SSR bundle because it's the fastest and shows
201
- // lines in the server bundle which is good for debugging SSR
202
- // The default of cheap-module-source-map is slow and provides poor info.
203
- serverWebpackConfig.devtool = 'eval';
200
+ // Avoid the webpack eval devtool, which triggers a webpack 5.106+ regression
201
+ // with ESM default exports (ReferenceError: __WEBPACK_DEFAULT_EXPORT__ is not defined).
202
+ // In development, cheap-module-source-map provides original line numbers in SSR error traces.
203
+ // In production, devtool is disabled to avoid generating .map files.
204
+ serverWebpackConfig.devtool = process.env.NODE_ENV === 'production' ? false : 'cheap-module-source-map';
204
205
 
205
206
  <% if use_pro? -%>
206
207
  // React on Rails Pro uses Node renderer, so target must be 'node'
@@ -1,4 +1,4 @@
1
- <%= add_documentation_reference(config[:message], "// https://github.com/shakacode/react_on_rails_demo_ssr_hmr/blob/master/config/webpack/test.js") %>
1
+ <%= add_documentation_reference(config[:message], "// https://github.com/shakacode/react-on-rails-demo-ssr-hmr/blob/master/config/webpack/test.js") %>
2
2
 
3
3
  const serverClientOrBoth = require('./ServerClientOrBoth')
4
4
 
@@ -6,7 +6,7 @@ ReactOnRailsPro.configure do |config|
6
6
  config.server_renderer = "NodeRenderer"
7
7
  config.renderer_url = ENV.fetch("REACT_RENDERER_URL", "http://localhost:3800")
8
8
 
9
- # See value in client/node-renderer.js
9
+ # See value in renderer/node-renderer.js
10
10
  config.renderer_password = ENV.fetch("RENDERER_PASSWORD", "devPassword")
11
11
 
12
12
  config.ssr_timeout = 5
@@ -6,6 +6,7 @@ const configuredWorkersCount =
6
6
  parseWorkersCount(env.RENDERER_WORKERS_COUNT) ?? parseWorkersCount(env.NODE_RENDERER_CONCURRENCY);
7
7
 
8
8
  const config = {
9
+ // Resolves to <project-root>/.node-renderer-bundles (one level up from renderer/).
9
10
  serverBundleCachePath: path.resolve(__dirname, '../.node-renderer-bundles'),
10
11
  port: Number(env.RENDERER_PORT) || 3800,
11
12
  logLevel: env.RENDERER_LOG_LEVEL || 'info',
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
4
+
3
5
  module ReactOnRails
4
6
  module ConfigPathResolver
5
7
  # Keep JS before TS to match generator defaults and to prefer the
@@ -14,13 +16,108 @@ module ReactOnRails
14
16
  ].freeze
15
17
  ALL_DEFAULT_CONFIG_CANDIDATES = (WEBPACK_DEFAULT_CONFIG_CANDIDATES + RSPACK_DEFAULT_CONFIG_CANDIDATES).freeze
16
18
 
19
+ protected
20
+
21
+ # Protected so including classes and overrides can share the same warning
22
+ # de-dupe registry across resolver callers without exposing it as API.
23
+ def config_path_warning_registry
24
+ @config_path_warning_registry ||= {
25
+ package_roots: Set.new,
26
+ package_json_paths: Set.new
27
+ }
28
+ end
29
+
17
30
  private
18
31
 
19
- def resolved_package_json_path
20
- node_modules_location = ReactOnRails.configuration.node_modules_location.to_s
21
- return "package.json" if node_modules_location.empty? || node_modules_location == Rails.root.to_s
32
+ def resolved_package_json_path(package_root = resolved_package_root)
33
+ resolved_package_path("package.json", package_root)
34
+ end
35
+
36
+ # Memoized per instance: configuration is set once at boot in production.
37
+ # Tests must build a fresh resolver after stubbing a different
38
+ # node_modules_location, since reconfiguration on a reused instance is
39
+ # ignored after the first call.
40
+ def resolved_package_root
41
+ @resolved_package_root ||= begin
42
+ node_modules_location = ReactOnRails.configuration.node_modules_location.to_s
43
+
44
+ resolved_location = Pathname.new(node_modules_location).cleanpath
45
+ # cleanpath normalizes redundant separators and ".." without resolving symlinks;
46
+ # realpath is intentionally skipped to avoid filesystem I/O on every call.
47
+ # Relative paths like "../client" remain valid diagnostics targets and are
48
+ # not constrained to stay within Rails.root.
49
+ if resolved_location == Pathname.new(".")
50
+ Rails.root.to_s
51
+ elsif resolved_location.absolute?
52
+ resolved_location.to_s
53
+ else
54
+ Rails.root.join(resolved_location).to_s
55
+ end
56
+ end
57
+ end
58
+
59
+ def resolved_package_path(filename, package_root = resolved_package_root)
60
+ File.join(package_root, filename)
61
+ end
62
+
63
+ def package_root_missing?(package_root)
64
+ !Dir.exist?(package_root)
65
+ end
66
+
67
+ def package_json_path_for(detection_target, package_root = resolved_package_root)
68
+ package_json_path = resolved_package_json_path(package_root)
69
+ return package_json_path if File.exist?(package_json_path)
70
+
71
+ if package_root_missing?(package_root)
72
+ warn_missing_package_root(package_root)
73
+ else
74
+ warn_missing_package_json(package_json_path, detection_target)
75
+ end
76
+ nil
77
+ end
78
+
79
+ # Including classes must provide #add_warning(message). Classes that route
80
+ # warnings into another object's message list can override
81
+ # #config_path_warning_registry to share de-dupe state with that sink.
82
+ # Overrides must return a Hash with :package_roots and :package_json_paths
83
+ # keys whose values respond to #add?, such as Set instances.
84
+ def warn_missing_package_root(package_root)
85
+ return unless warned_package_roots.add?(package_root)
86
+
87
+ add_config_path_warning(missing_package_root_warning(package_root))
88
+ end
89
+
90
+ def missing_package_root_warning(package_root)
91
+ "⚠️ node_modules_location points to #{package_root}, but that directory does not exist; " \
92
+ "all diagnostics that read from it are skipped. Check config/initializers/react_on_rails.rb."
93
+ end
94
+
95
+ def warn_missing_package_json(package_json_path, detection_target)
96
+ return unless warned_package_json_paths.add?(package_json_path)
97
+
98
+ add_config_path_warning(missing_package_json_warning(package_json_path, detection_target))
99
+ end
100
+
101
+ def missing_package_json_warning(package_json_path, detection_target)
102
+ "⚠️ #{package_json_path} not found; cannot detect #{detection_target}. " \
103
+ "Check config/initializers/react_on_rails.rb."
104
+ end
105
+
106
+ def warned_package_roots
107
+ config_path_warning_registry[:package_roots]
108
+ end
109
+
110
+ def warned_package_json_paths
111
+ config_path_warning_registry[:package_json_paths]
112
+ end
113
+
114
+ def add_warning(_message)
115
+ raise NotImplementedError,
116
+ "#{self.class} must implement #add_warning(message) to include ReactOnRails::ConfigPathResolver"
117
+ end
22
118
 
23
- Rails.root.join(node_modules_location, "package.json").to_s
119
+ def add_config_path_warning(message)
120
+ add_warning(message)
24
121
  end
25
122
 
26
123
  def resolved_webpack_config_path
@@ -26,6 +26,28 @@ module ReactOnRails
26
26
  configuration.setup_config_values
27
27
  end
28
28
 
29
+ # Rendering strategy configured at boot time by engine initializers.
30
+ # Replaces runtime react_on_rails_pro? checks (see issue #2905).
31
+ # Not yet wired into the main rendering path — currently additive only.
32
+ def self.rendering_strategy
33
+ @rendering_strategy ||= ReactOnRails::RenderingStrategy::ExecJsStrategy.new
34
+ end
35
+
36
+ def self.rendering_strategy=(strategy)
37
+ @rendering_strategy = strategy
38
+ end
39
+
40
+ # JS code builder configured at boot time by engine initializers.
41
+ # Used by RenderRequest#to_js to generate SSR JavaScript code.
42
+ # Not yet wired into the main rendering path — currently additive only.
43
+ def self.js_code_builder
44
+ @js_code_builder ||= ReactOnRails::JsCodeBuilder.new
45
+ end
46
+
47
+ def self.js_code_builder=(builder)
48
+ @js_code_builder = builder
49
+ end
50
+
29
51
  DEFAULT_GENERATED_ASSETS_DIR = File.join(%w[public webpack], Rails.env).freeze
30
52
  DEFAULT_COMPONENT_REGISTRY_TIMEOUT = 5000
31
53
  DEFAULT_SERVER_BUNDLE_OUTPUT_PATH = "ssr-generated"