react_on_rails 16.4.0.rc.1 → 16.4.0.rc.2

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/Gemfile.lock +1 -1
  4. data/lib/generators/react_on_rails/base_generator.rb +30 -10
  5. data/lib/generators/react_on_rails/dev_tests_generator.rb +9 -1
  6. data/lib/generators/react_on_rails/generator_helper.rb +101 -0
  7. data/lib/generators/react_on_rails/generator_messages.rb +2 -2
  8. data/lib/generators/react_on_rails/install_generator.rb +55 -19
  9. data/lib/generators/react_on_rails/js_dependency_manager.rb +115 -8
  10. data/lib/generators/react_on_rails/pro/USAGE +21 -0
  11. data/lib/generators/react_on_rails/pro_generator.rb +97 -0
  12. data/lib/generators/react_on_rails/pro_setup.rb +314 -0
  13. data/lib/generators/react_on_rails/rsc/USAGE +23 -0
  14. data/lib/generators/react_on_rails/rsc_generator.rb +100 -0
  15. data/lib/generators/react_on_rails/rsc_setup.rb +471 -0
  16. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev +3 -3
  17. data/lib/generators/react_on_rails/templates/base/base/config/webpack/{generateWebpackConfigs.js.tt → ServerClientOrBoth.js.tt} +28 -3
  18. data/lib/generators/react_on_rails/templates/base/base/config/webpack/clientWebpackConfig.js.tt +8 -0
  19. data/lib/generators/react_on_rails/templates/base/base/config/webpack/development.js.tt +2 -2
  20. data/lib/generators/react_on_rails/templates/base/base/config/webpack/production.js.tt +2 -2
  21. data/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt +54 -0
  22. data/lib/generators/react_on_rails/templates/base/base/config/webpack/test.js.tt +2 -2
  23. data/lib/generators/react_on_rails/templates/dev_tests/spec/system/hello_server_spec.rb +17 -0
  24. data/lib/generators/react_on_rails/templates/pro/base/client/node-renderer.js +41 -0
  25. data/lib/generators/react_on_rails/templates/pro/base/config/initializers/react_on_rails_pro.rb.tt +25 -0
  26. data/lib/generators/react_on_rails/templates/rsc/base/app/controllers/hello_server_controller.rb +25 -0
  27. data/lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/components/HelloServer.jsx +80 -0
  28. data/lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/components/HelloServer.tsx +90 -0
  29. data/lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/components/LikeButton.jsx +54 -0
  30. data/lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/components/LikeButton.tsx +54 -0
  31. data/lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/ror_components/HelloServer.jsx +10 -0
  32. data/lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/ror_components/HelloServer.tsx +10 -0
  33. data/lib/generators/react_on_rails/templates/rsc/base/app/views/hello_server/index.html.erb +37 -0
  34. data/lib/generators/react_on_rails/templates/rsc/base/config/webpack/rscWebpackConfig.js.tt +64 -0
  35. data/lib/react_on_rails/engine.rb +5 -1
  36. data/lib/react_on_rails/version.rb +1 -1
  37. data/lib/react_on_rails/version_checker.rb +6 -1
  38. metadata +21 -3
@@ -0,0 +1,471 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rainbow"
4
+ require_relative "generator_messages"
5
+
6
+ module ReactOnRails
7
+ module Generators
8
+ # Provides RSC (React Server Components) setup functionality for React on Rails generators.
9
+ #
10
+ # This module extracts RSC-specific setup methods that can be shared between:
11
+ # - InstallGenerator (when --rsc flag is used)
12
+ # - RscGenerator (standalone generator for upgrading existing Pro apps)
13
+ #
14
+ # == Required Dependencies
15
+ # Including classes must provide (typically via Rails::Generators::Base):
16
+ # - destination_root: Path to the target Rails application
17
+ # - template, copy_file, append_to_file, empty_directory, route: Thor file manipulation methods
18
+ # - options: Generator options hash (for options.typescript?)
19
+ #
20
+ # Including classes must also include GeneratorHelper which provides:
21
+ # - use_rsc?: Feature flag helper
22
+ # - component_extension: Returns 'jsx' or 'tsx' based on TypeScript option
23
+ # - detect_react_version: Detects installed React version
24
+ #
25
+ module RscSetup # rubocop:disable Metrics/ModuleLength
26
+ # Main entry point for RSC setup.
27
+ # Orchestrates creation of all RSC-related files and configuration.
28
+ #
29
+ # Creates:
30
+ # - config/webpack/rscWebpackConfig.js
31
+ # - Procfile.dev entry for RSC bundle watcher
32
+ # - HelloServer component (jsx or tsx based on --typescript flag)
33
+ # - HelloServerController
34
+ # - HelloServer view
35
+ # - RSC routes
36
+ #
37
+ # @note NPM dependencies are handled separately by JsDependencyManager
38
+ def setup_rsc
39
+ print_rsc_setup_banner
40
+
41
+ add_rsc_config_to_pro_initializer
42
+ create_rsc_webpack_config
43
+ update_webpack_configs_for_rsc
44
+ add_rsc_to_procfile
45
+ create_hello_server_component
46
+ create_hello_server_controller
47
+ create_hello_server_view
48
+ add_rsc_routes
49
+
50
+ print_rsc_complete_banner
51
+ end
52
+
53
+ # Warn if React version is not compatible with RSC.
54
+ # RSC requires React 19.0.x specifically (not 19.1.x or later).
55
+ #
56
+ # @param force [Boolean] When true, always performs the check.
57
+ # When false (default), only checks if RSC is enabled (use_rsc? returns true).
58
+ # Use force: true in standalone generators where RSC is always the purpose.
59
+ def warn_about_react_version_for_rsc(force: false)
60
+ return unless force || use_rsc?
61
+
62
+ react_version = detect_react_version
63
+ return if react_version.nil? # React not installed yet, will be installed by generator
64
+
65
+ major, minor, patch = react_version.split(".").map(&:to_i)
66
+
67
+ if major != 19 || minor != 0
68
+ GeneratorMessages.add_warning(<<~MSG.strip)
69
+ ⚠️ RSC requires React 19.0.x (detected: #{react_version})
70
+
71
+ React Server Components in React on Rails Pro currently only supports
72
+ React 19.0.x. React 19.1.x and later are not yet supported.
73
+
74
+ To install a compatible React version:
75
+ npm install react@~19.0.4 react-dom@~19.0.4
76
+ MSG
77
+ elsif patch < 4
78
+ GeneratorMessages.add_warning(<<~MSG.strip)
79
+ ⚠️ React #{react_version} is below the recommended minimum for RSC.
80
+
81
+ Please upgrade to at least React 19.0.4:
82
+ npm install react@19.0.4 react-dom@19.0.4
83
+
84
+ react-server-dom-webpack 19.0.0–19.0.3 has known vulnerabilities
85
+ (CVE-2025-55182, CVE-2025-67779, CVE-2026-23864) fixed in 19.0.4+.
86
+ MSG
87
+ end
88
+ end
89
+
90
+ private
91
+
92
+ def add_rsc_config_to_pro_initializer
93
+ initializer_path = "config/initializers/react_on_rails_pro.rb"
94
+ full_path = File.join(destination_root, initializer_path)
95
+
96
+ unless File.exist?(full_path)
97
+ GeneratorMessages.add_warning(<<~MSG.strip)
98
+ ⚠️ Pro initializer not found at #{initializer_path}. Skipping RSC config.
99
+
100
+ RSC requires React on Rails Pro. Run the Pro generator first:
101
+ rails g react_on_rails:pro
102
+ MSG
103
+ return
104
+ end
105
+
106
+ content = File.read(full_path)
107
+
108
+ if content.include?("enable_rsc_support")
109
+ puts Rainbow("ℹ️ RSC config already in Pro initializer, skipping").yellow
110
+ return
111
+ end
112
+
113
+ puts Rainbow("📝 Adding RSC config to Pro initializer...").yellow
114
+
115
+ rsc_config = <<-CONFIG
116
+
117
+ # React Server Components configuration
118
+ config.enable_rsc_support = true
119
+ config.rsc_bundle_js_file = "rsc-bundle.js"
120
+ config.rsc_payload_generation_url_path = "rsc_payload/"
121
+ CONFIG
122
+
123
+ # Insert before the final 'end'
124
+ gsub_file(initializer_path, /^end\s*\z/, "#{rsc_config}end")
125
+
126
+ puts Rainbow("✅ Added RSC config to #{initializer_path}").green
127
+ end
128
+
129
+ def print_rsc_setup_banner
130
+ puts Rainbow("\n#{'=' * 80}").magenta
131
+ puts Rainbow("🚀 REACT SERVER COMPONENTS SETUP").magenta.bold
132
+ puts Rainbow("=" * 80).magenta
133
+ end
134
+
135
+ def print_rsc_complete_banner
136
+ puts Rainbow("=" * 80).magenta
137
+ puts Rainbow("✅ React Server Components setup complete!").green
138
+ puts Rainbow("=" * 80).magenta
139
+ end
140
+
141
+ def create_rsc_webpack_config
142
+ webpack_config_path = "config/webpack/rscWebpackConfig.js"
143
+
144
+ if File.exist?(File.join(destination_root, webpack_config_path))
145
+ puts Rainbow("ℹ️ #{webpack_config_path} already exists, skipping").yellow
146
+ return
147
+ end
148
+
149
+ puts Rainbow("📝 Creating RSC webpack config...").yellow
150
+
151
+ rsc_template_path = "templates/rsc/base/config/webpack/rscWebpackConfig.js.tt"
152
+ template(rsc_template_path, webpack_config_path)
153
+
154
+ puts Rainbow("✅ Created #{webpack_config_path}").green
155
+ end
156
+
157
+ def add_rsc_to_procfile
158
+ procfile_path = File.join(destination_root, "Procfile.dev")
159
+
160
+ unless File.exist?(procfile_path)
161
+ GeneratorMessages.add_warning(<<~MSG.strip)
162
+ ⚠️ Procfile.dev not found. Skipping RSC bundle watcher addition.
163
+
164
+ You'll need to add the RSC bundle watcher to your process manager manually:
165
+ rsc-bundle: RSC_BUNDLE_ONLY=yes bin/shakapacker --watch
166
+ MSG
167
+ return
168
+ end
169
+
170
+ if File.read(procfile_path).include?("RSC_BUNDLE_ONLY")
171
+ puts Rainbow("ℹ️ RSC bundle watcher already in Procfile.dev, skipping").yellow
172
+ return
173
+ end
174
+
175
+ puts Rainbow("📝 Adding RSC bundle watcher to Procfile.dev...").yellow
176
+
177
+ rsc_watcher_line = <<~PROCFILE
178
+
179
+ # React on Rails Pro - RSC bundle watcher
180
+ rsc-bundle: RSC_BUNDLE_ONLY=yes bin/shakapacker --watch
181
+ PROCFILE
182
+
183
+ append_to_file("Procfile.dev", rsc_watcher_line)
184
+
185
+ puts Rainbow("✅ Added RSC bundle watcher to Procfile.dev").green
186
+ end
187
+
188
+ def create_hello_server_component
189
+ hello_server_dir = "app/javascript/src/HelloServer"
190
+ ror_components_dir = "#{hello_server_dir}/ror_components"
191
+ components_dir = "#{hello_server_dir}/components"
192
+ ext = component_extension(options)
193
+
194
+ # Check if HelloServer already exists (check both jsx and tsx)
195
+ if File.exist?(File.join(destination_root, "#{ror_components_dir}/HelloServer.jsx")) ||
196
+ File.exist?(File.join(destination_root, "#{ror_components_dir}/HelloServer.tsx"))
197
+ puts Rainbow("ℹ️ HelloServer component already exists, skipping").yellow
198
+ return
199
+ end
200
+
201
+ puts Rainbow("📝 Creating HelloServer component...").yellow
202
+
203
+ # Create directories
204
+ empty_directory(ror_components_dir)
205
+ empty_directory(components_dir)
206
+
207
+ # Copy component files (uses jsx or tsx based on --typescript flag)
208
+ copy_file("templates/rsc/base/app/javascript/src/HelloServer/ror_components/HelloServer.#{ext}",
209
+ "#{ror_components_dir}/HelloServer.#{ext}")
210
+ copy_file("templates/rsc/base/app/javascript/src/HelloServer/components/HelloServer.#{ext}",
211
+ "#{components_dir}/HelloServer.#{ext}")
212
+ copy_file("templates/rsc/base/app/javascript/src/HelloServer/components/LikeButton.#{ext}",
213
+ "#{components_dir}/LikeButton.#{ext}")
214
+
215
+ puts Rainbow("✅ Created HelloServer component").green
216
+ end
217
+
218
+ def create_hello_server_controller
219
+ controller_path = "app/controllers/hello_server_controller.rb"
220
+
221
+ if File.exist?(File.join(destination_root, controller_path))
222
+ puts Rainbow("ℹ️ HelloServerController already exists, skipping").yellow
223
+ return
224
+ end
225
+
226
+ puts Rainbow("📝 Creating HelloServerController...").yellow
227
+
228
+ copy_file("templates/rsc/base/app/controllers/hello_server_controller.rb", controller_path)
229
+
230
+ puts Rainbow("✅ Created #{controller_path}").green
231
+ end
232
+
233
+ def create_hello_server_view
234
+ view_path = "app/views/hello_server/index.html.erb"
235
+
236
+ if File.exist?(File.join(destination_root, view_path))
237
+ puts Rainbow("ℹ️ HelloServer view already exists, skipping").yellow
238
+ return
239
+ end
240
+
241
+ puts Rainbow("📝 Creating HelloServer view...").yellow
242
+
243
+ # Create views directory if needed
244
+ empty_directory("app/views/hello_server")
245
+
246
+ copy_file("templates/rsc/base/app/views/hello_server/index.html.erb", view_path)
247
+
248
+ puts Rainbow("✅ Created #{view_path}").green
249
+ end
250
+
251
+ def add_rsc_routes
252
+ routes_file = File.join(destination_root, "config/routes.rb")
253
+
254
+ unless File.exist?(routes_file)
255
+ GeneratorMessages.add_warning(<<~MSG.strip)
256
+ ⚠️ config/routes.rb not found. Skipping RSC routes.
257
+
258
+ You'll need to add the following routes manually:
259
+ rsc_payload_route
260
+ get 'hello_server', to: 'hello_server#index'
261
+ MSG
262
+ return
263
+ end
264
+
265
+ routes_content = File.read(routes_file)
266
+
267
+ if routes_content.include?("rsc_payload_route")
268
+ puts Rainbow("ℹ️ RSC routes already exist, skipping").yellow
269
+ return
270
+ end
271
+
272
+ puts Rainbow("📝 Adding RSC routes...").yellow
273
+
274
+ # Add rsc_payload_route (required for RSC payload requests)
275
+ route "rsc_payload_route"
276
+
277
+ # Add HelloServer route (RSC counterpart to hello_world)
278
+ route "get 'hello_server', to: 'hello_server#index'"
279
+
280
+ puts Rainbow("✅ Added RSC routes to config/routes.rb").green
281
+ end
282
+
283
+ # Update webpack configs to enable RSC support.
284
+ # This is needed for standalone RSC upgrades where the base install
285
+ # created webpack configs without RSC settings enabled.
286
+ #
287
+ # Updates:
288
+ # - ServerClientOrBoth.js: RSC imports, rscConfig, RSC_BUNDLE_ONLY handling
289
+ # - serverWebpackConfig.js: RSCWebpackPlugin import, rscBundle param, plugin
290
+ # - clientWebpackConfig.js: RSCWebpackPlugin import, plugin
291
+ def update_webpack_configs_for_rsc
292
+ puts Rainbow("📝 Updating webpack configs for RSC...").yellow
293
+
294
+ update_server_client_or_both_for_rsc
295
+ update_server_webpack_config_for_rsc
296
+ update_client_webpack_config_for_rsc
297
+
298
+ verify_rsc_webpack_transforms
299
+ puts Rainbow("✅ Updated webpack configs for RSC").green
300
+ end
301
+
302
+ def update_server_client_or_both_for_rsc
303
+ config_path = resolve_server_client_or_both_path
304
+ return unless config_path
305
+
306
+ content = File.read(File.join(destination_root, config_path))
307
+
308
+ # Skip if RSC is already configured
309
+ return if content.include?("rscWebpackConfig")
310
+
311
+ # Add RSC import after serverWebpackConfig import
312
+ gsub_file(
313
+ config_path,
314
+ %r{(const (?:\{ default: serverWebpackConfig \}|serverWebpackConfig) = require\('\./serverWebpackConfig'\);)},
315
+ "\\1\nconst rscWebpackConfig = require('./rscWebpackConfig');"
316
+ )
317
+
318
+ # Add rscConfig variable after serverConfig (with blank line separator)
319
+ gsub_file(
320
+ config_path,
321
+ /^(\s*const serverConfig = serverWebpackConfig\(\);)$/,
322
+ "\\1\n\n const rscConfig = rscWebpackConfig();"
323
+ )
324
+
325
+ # Update envSpecific call to include rscConfig
326
+ gsub_file(
327
+ config_path,
328
+ /envSpecific\(clientConfig, serverConfig\);/,
329
+ "envSpecific(clientConfig, serverConfig, rscConfig);"
330
+ )
331
+
332
+ # Add RSC_BUNDLE_ONLY handling before the else block
333
+ rsc_bundle_handling = <<~JS.chomp
334
+ } else if (process.env.RSC_BUNDLE_ONLY) {
335
+ // eslint-disable-next-line no-console
336
+ console.log('[React on Rails] Creating only the RSC bundle.');
337
+ result = rscConfig;
338
+ JS
339
+ gsub_file(
340
+ config_path,
341
+ %r{\n(\s*\} else \{\s*\n\s*// default is the standard client and server build)},
342
+ "\n #{rsc_bundle_handling}\n\\1"
343
+ )
344
+
345
+ # Update default multi-bundle output to include RSC
346
+ gsub_file(
347
+ config_path,
348
+ /console\.log\('\[React on Rails\] Creating both client and server bundles\.'\);/,
349
+ "console.log('[React on Rails] Creating client, server, and RSC bundles.');"
350
+ )
351
+ gsub_file(
352
+ config_path,
353
+ /result = \[clientConfig, serverConfig\];/,
354
+ "result = [clientConfig, serverConfig, rscConfig];"
355
+ )
356
+ end
357
+
358
+ def update_server_webpack_config_for_rsc
359
+ config_path = "config/webpack/serverWebpackConfig.js"
360
+ full_path = File.join(destination_root, config_path)
361
+
362
+ return unless File.exist?(full_path)
363
+
364
+ content = File.read(full_path)
365
+
366
+ # Skip if RSCWebpackPlugin is already configured
367
+ return if content.include?("RSCWebpackPlugin")
368
+
369
+ # Add RSCWebpackPlugin import after bundler require
370
+ gsub_file(
371
+ config_path,
372
+ %r{(const bundler = config\.assets_bundler.*\n.*require\('@rspack/core'\).*\n.*: require\('webpack'\);)},
373
+ "\\1\nconst { RSCWebpackPlugin } = require('react-on-rails-rsc/WebpackPlugin');"
374
+ )
375
+
376
+ # Add rscBundle parameter to configureServer function
377
+ gsub_file(
378
+ config_path,
379
+ /^const configureServer = \(\) => \{/,
380
+ "// rscBundle parameter: when true, skips RSCWebpackPlugin (RSC bundle doesn't need it)\n" \
381
+ "const configureServer = (rscBundle = false) => {"
382
+ )
383
+
384
+ # Add RSCWebpackPlugin to plugins before LimitChunkCountPlugin (matches template ordering)
385
+ rsc_plugin_code = "// Add RSC plugin for server bundle (handles client component references)\n " \
386
+ "// Skip for RSC bundle - it doesn't need RSCWebpackPlugin\n " \
387
+ "if (!rscBundle) {\n " \
388
+ "serverWebpackConfig.plugins.push(new RSCWebpackPlugin({ isServer: true }));\n " \
389
+ "}"
390
+ gsub_file(
391
+ config_path,
392
+ /(serverWebpackConfig\.plugins\.unshift\(new bundler\.optimize\.LimitChunkCountPlugin.*\);)/,
393
+ "#{rsc_plugin_code}\n \\1"
394
+ )
395
+ end
396
+
397
+ def update_client_webpack_config_for_rsc
398
+ config_path = "config/webpack/clientWebpackConfig.js"
399
+ full_path = File.join(destination_root, config_path)
400
+
401
+ return unless File.exist?(full_path)
402
+
403
+ content = File.read(full_path)
404
+
405
+ # Skip if RSCWebpackPlugin is already configured
406
+ return if content.include?("RSCWebpackPlugin")
407
+
408
+ # Add RSCWebpackPlugin import after commonWebpackConfig import
409
+ gsub_file(
410
+ config_path,
411
+ %r{(const commonWebpackConfig = require\('\./commonWebpackConfig'\);)},
412
+ "\\1\nconst { RSCWebpackPlugin } = require('react-on-rails-rsc/WebpackPlugin');"
413
+ )
414
+
415
+ # Add RSCWebpackPlugin to client config before return statement
416
+ rsc_plugin_code = " // Add React Server Components plugin for client bundle\n " \
417
+ "clientConfig.plugins.push(new RSCWebpackPlugin({ isServer: false }));"
418
+ gsub_file(
419
+ config_path,
420
+ /^( *return clientConfig;)$/,
421
+ "#{rsc_plugin_code}\n\n\\1"
422
+ )
423
+ end
424
+
425
+ def verify_rsc_webpack_transforms
426
+ missing = []
427
+ missing.concat(check_rsc_server_config)
428
+ missing.concat(check_rsc_client_config)
429
+ missing.concat(check_rsc_scob_config)
430
+ return if missing.empty?
431
+
432
+ GeneratorMessages.add_warning(<<~MSG.strip)
433
+ ⚠️ Some RSC webpack transforms may not have applied correctly.
434
+
435
+ Missing expected patterns:
436
+ #{missing.map { |m| " - #{m}" }.join("\n")}
437
+
438
+ This can happen if your webpack config has been customized.
439
+ Please verify your webpack configs manually.
440
+ MSG
441
+ end
442
+
443
+ def check_rsc_server_config
444
+ path = File.join(destination_root, "config/webpack/serverWebpackConfig.js")
445
+ return [] unless File.exist?(path)
446
+
447
+ content = File.read(path)
448
+ missing = []
449
+ missing << "RSCWebpackPlugin in serverWebpackConfig.js" unless content.include?("RSCWebpackPlugin")
450
+ missing << "rscBundle parameter in serverWebpackConfig.js" unless content.include?("rscBundle")
451
+ missing
452
+ end
453
+
454
+ def check_rsc_client_config
455
+ path = File.join(destination_root, "config/webpack/clientWebpackConfig.js")
456
+ return [] unless File.exist?(path)
457
+
458
+ content = File.read(path)
459
+ content.include?("RSCWebpackPlugin") ? [] : ["RSCWebpackPlugin in clientWebpackConfig.js"]
460
+ end
461
+
462
+ def check_rsc_scob_config
463
+ scob_path = resolve_server_client_or_both_path
464
+ return [] unless scob_path
465
+
466
+ content = File.read(File.join(destination_root, scob_path))
467
+ content.include?("rscWebpackConfig") ? [] : ["rscWebpackConfig in ServerClientOrBoth.js"]
468
+ end
469
+ end
470
+ end
471
+ end
@@ -1,5 +1,5 @@
1
- # Procfile for development using HMR
1
+ # Procfile for development
2
2
  # You can run these commands in separate shells
3
3
  rails: bundle exec rails s -p 3000
4
- wp-client: WEBPACK_SERVE=true bin/shakapacker-dev-server
5
- wp-server: SERVER_BUNDLE_ONLY=yes bin/shakapacker --watch
4
+ dev-server: bin/shakapacker-dev-server
5
+ server-bundle: SERVER_BUNDLE_ONLY=yes bin/shakapacker --watch
@@ -1,14 +1,28 @@
1
- <%= add_documentation_reference(config[:message], "// https://github.com/shakacode/react_on_rails_demo_ssr_hmr/blob/master/config/webpack/webpackConfig.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
+ <% if use_pro? -%>
5
+ const { default: serverWebpackConfig } = require('./serverWebpackConfig');
6
+ <% else -%>
4
7
  const serverWebpackConfig = require('./serverWebpackConfig');
8
+ <% end -%>
9
+ <% if use_rsc? -%>
10
+ const rscWebpackConfig = require('./rscWebpackConfig');
11
+ <% end -%>
5
12
 
6
- const webpackConfig = (envSpecific) => {
13
+ const serverClientOrBoth = (envSpecific) => {
7
14
  const clientConfig = clientWebpackConfig();
8
15
  const serverConfig = serverWebpackConfig();
16
+ <% if use_rsc? %>
17
+ const rscConfig = rscWebpackConfig();
18
+ <% end %>
9
19
 
10
20
  if (envSpecific) {
21
+ <% if use_rsc? %>
22
+ envSpecific(clientConfig, serverConfig, rscConfig);
23
+ <% else %>
11
24
  envSpecific(clientConfig, serverConfig);
25
+ <% end %>
12
26
  }
13
27
 
14
28
  let result;
@@ -21,11 +35,22 @@ const webpackConfig = (envSpecific) => {
21
35
  // eslint-disable-next-line no-console
22
36
  console.log('[React on Rails] Creating only the server bundle.');
23
37
  result = serverConfig;
38
+ <% if use_rsc? %>
39
+ } else if (process.env.RSC_BUNDLE_ONLY) {
40
+ // eslint-disable-next-line no-console
41
+ console.log('[React on Rails] Creating only the RSC bundle.');
42
+ result = rscConfig;
43
+ <% end %>
24
44
  } else {
25
45
  // default is the standard client and server build
26
46
  // eslint-disable-next-line no-console
47
+ <% if use_rsc? %>
48
+ console.log('[React on Rails] Creating client, server, and RSC bundles.');
49
+ result = [clientConfig, serverConfig, rscConfig];
50
+ <% else %>
27
51
  console.log('[React on Rails] Creating both client and server bundles.');
28
52
  result = [clientConfig, serverConfig];
53
+ <% end %>
29
54
  }
30
55
 
31
56
  // To debug, uncomment next line and inspect "result"
@@ -33,4 +58,4 @@ const webpackConfig = (envSpecific) => {
33
58
  return result;
34
59
  };
35
60
 
36
- module.exports = webpackConfig;
61
+ module.exports = serverClientOrBoth;
@@ -1,6 +1,9 @@
1
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
+ <% if use_rsc? -%>
5
+ const { RSCWebpackPlugin } = require('react-on-rails-rsc/WebpackPlugin');
6
+ <% end -%>
4
7
 
5
8
  const configureClient = () => {
6
9
  const clientConfig = commonWebpackConfig();
@@ -11,6 +14,11 @@ const configureClient = () => {
11
14
  // client config is going to try to load chunks.
12
15
  delete clientConfig.entry['server-bundle'];
13
16
 
17
+ <% if use_rsc? -%>
18
+ // Add React Server Components plugin for client bundle
19
+ clientConfig.plugins.push(new RSCWebpackPlugin({ isServer: false }));
20
+
21
+ <% end -%>
14
22
  return clientConfig;
15
23
  };
16
24
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { devServer, inliningCss, config } = require('shakapacker');
4
4
 
5
- const generateWebpackConfigs = require('./generateWebpackConfigs');
5
+ const serverClientOrBoth = require('./ServerClientOrBoth');
6
6
 
7
7
  const developmentEnvOnly = (clientWebpackConfig, _serverWebpackConfig) => {
8
8
  // React Refresh (Fast Refresh) setup - only when dev server is running (HMR mode)
@@ -24,4 +24,4 @@ const developmentEnvOnly = (clientWebpackConfig, _serverWebpackConfig) => {
24
24
  }
25
25
  };
26
26
 
27
- module.exports = generateWebpackConfigs(developmentEnvOnly);
27
+ module.exports = serverClientOrBoth(developmentEnvOnly);
@@ -1,9 +1,9 @@
1
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
- const generateWebpackConfigs = require('./generateWebpackConfigs');
3
+ const serverClientOrBoth = require('./ServerClientOrBoth');
4
4
 
5
5
  const productionEnvOnly = (_clientWebpackConfig, _serverWebpackConfig) => {
6
6
  // place any code here that is for production only
7
7
  };
8
8
 
9
- module.exports = generateWebpackConfigs(productionEnvOnly);
9
+ module.exports = serverClientOrBoth(productionEnvOnly);
@@ -6,8 +6,26 @@ const commonWebpackConfig = require('./commonWebpackConfig');
6
6
  const bundler = config.assets_bundler === 'rspack'
7
7
  ? require('@rspack/core')
8
8
  : require('webpack');
9
+ <% if use_rsc? -%>
10
+ const { RSCWebpackPlugin } = require('react-on-rails-rsc/WebpackPlugin');
11
+ <% end -%>
12
+
13
+ <% if use_pro? -%>
14
+ function extractLoader(rule, loaderName) {
15
+ if (!Array.isArray(rule.use)) return null;
16
+ return rule.use.find((item) => {
17
+ const testValue = typeof item === 'string' ? item : item.loader;
18
+ return testValue && testValue.includes(loaderName);
19
+ });
20
+ }
9
21
 
22
+ <% end -%>
23
+ <% if use_rsc? -%>
24
+ // rscBundle parameter: when true, skips RSCWebpackPlugin (RSC bundle doesn't need it)
25
+ const configureServer = (rscBundle = false) => {
26
+ <% else -%>
10
27
  const configureServer = () => {
28
+ <% end -%>
11
29
  // We need to use "merge" because the clientConfigObject, EVEN after running
12
30
  // toWebpackConfig() is a mutable GLOBAL. Thus any changes, like modifying the
13
31
  // entry value will result in changing the client config!
@@ -42,6 +60,13 @@ const configureServer = () => {
42
60
  serverWebpackConfig.optimization = {
43
61
  minimize: false,
44
62
  };
63
+ <% if use_rsc? -%>
64
+ // Add RSC plugin for server bundle (handles client component references)
65
+ // Skip for RSC bundle - it doesn't need RSCWebpackPlugin
66
+ if (!rscBundle) {
67
+ serverWebpackConfig.plugins.push(new RSCWebpackPlugin({ isServer: true }));
68
+ }
69
+ <% end -%>
45
70
  serverWebpackConfig.plugins.unshift(new bundler.optimize.LimitChunkCountPlugin({ maxChunks: 1 }));
46
71
 
47
72
  // Custom output for the server-bundle
@@ -61,8 +86,13 @@ const configureServer = () => {
61
86
  serverWebpackConfig.output = {
62
87
  filename: 'server-bundle.js',
63
88
  globalObject: 'this',
89
+ <% if use_pro? -%>
90
+ // Required for React on Rails Pro Node Renderer
91
+ libraryTarget: 'commonjs2',
92
+ <% else -%>
64
93
  // If using the React on Rails Pro node server renderer, uncomment the next line
65
94
  // libraryTarget: 'commonjs2',
95
+ <% end -%>
66
96
  path: serverBundleOutputPath,
67
97
  // No publicPath needed since server bundles are not served via web
68
98
  // https://webpack.js.org/configuration/output/#outputglobalobject
@@ -131,6 +161,14 @@ const configureServer = () => {
131
161
  if (cssLoader && cssLoader.options) {
132
162
  cssLoader.options.modules = { exportOnlyLocals: true };
133
163
  }
164
+ <% if use_pro? -%>
165
+
166
+ // Set SSR caller for Babel (if using Babel instead of SWC)
167
+ const babelLoader = extractLoader(rule, 'babel-loader');
168
+ if (babelLoader && babelLoader.options) {
169
+ babelLoader.options.caller = { ssr: true };
170
+ }
171
+ <% end -%>
134
172
 
135
173
  // Skip writing image files during SSR by setting emitFile to false
136
174
  } else if (rule.use && (rule.use.loader === 'url-loader' || rule.use.loader === 'file-loader')) {
@@ -143,12 +181,28 @@ const configureServer = () => {
143
181
  // The default of cheap-module-source-map is slow and provides poor info.
144
182
  serverWebpackConfig.devtool = 'eval';
145
183
 
184
+ <% if use_pro? -%>
185
+ // React on Rails Pro uses Node renderer, so target must be 'node'
186
+ // This fixes issues with libraries like Emotion and loadable-components
187
+ serverWebpackConfig.target = 'node';
188
+
189
+ // Disable Node.js polyfills - not needed when targeting Node
190
+ serverWebpackConfig.node = false;
191
+ <% else -%>
146
192
  // If using the default 'web', then libraries like Emotion and loadable-components
147
193
  // break with SSR. The fix is to use a node renderer and change the target.
148
194
  // If using the React on Rails Pro node server renderer, uncomment the next line
149
195
  // serverWebpackConfig.target = 'node'
196
+ <% end -%>
150
197
 
151
198
  return serverWebpackConfig;
152
199
  };
153
200
 
201
+ <% if use_pro? -%>
202
+ module.exports = {
203
+ default: configureServer,
204
+ extractLoader,
205
+ };
206
+ <% else -%>
154
207
  module.exports = configureServer;
208
+ <% end -%>