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.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -0
- data/Gemfile.lock +1 -1
- data/lib/generators/react_on_rails/base_generator.rb +30 -10
- data/lib/generators/react_on_rails/dev_tests_generator.rb +9 -1
- data/lib/generators/react_on_rails/generator_helper.rb +101 -0
- data/lib/generators/react_on_rails/generator_messages.rb +2 -2
- data/lib/generators/react_on_rails/install_generator.rb +55 -19
- data/lib/generators/react_on_rails/js_dependency_manager.rb +115 -8
- data/lib/generators/react_on_rails/pro/USAGE +21 -0
- data/lib/generators/react_on_rails/pro_generator.rb +97 -0
- data/lib/generators/react_on_rails/pro_setup.rb +314 -0
- data/lib/generators/react_on_rails/rsc/USAGE +23 -0
- data/lib/generators/react_on_rails/rsc_generator.rb +100 -0
- data/lib/generators/react_on_rails/rsc_setup.rb +471 -0
- data/lib/generators/react_on_rails/templates/base/base/Procfile.dev +3 -3
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/{generateWebpackConfigs.js.tt → ServerClientOrBoth.js.tt} +28 -3
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/clientWebpackConfig.js.tt +8 -0
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/development.js.tt +2 -2
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/production.js.tt +2 -2
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt +54 -0
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/test.js.tt +2 -2
- data/lib/generators/react_on_rails/templates/dev_tests/spec/system/hello_server_spec.rb +17 -0
- data/lib/generators/react_on_rails/templates/pro/base/client/node-renderer.js +41 -0
- data/lib/generators/react_on_rails/templates/pro/base/config/initializers/react_on_rails_pro.rb.tt +25 -0
- data/lib/generators/react_on_rails/templates/rsc/base/app/controllers/hello_server_controller.rb +25 -0
- data/lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/components/HelloServer.jsx +80 -0
- data/lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/components/HelloServer.tsx +90 -0
- data/lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/components/LikeButton.jsx +54 -0
- data/lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/components/LikeButton.tsx +54 -0
- data/lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/ror_components/HelloServer.jsx +10 -0
- data/lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/ror_components/HelloServer.tsx +10 -0
- data/lib/generators/react_on_rails/templates/rsc/base/app/views/hello_server/index.html.erb +37 -0
- data/lib/generators/react_on_rails/templates/rsc/base/config/webpack/rscWebpackConfig.js.tt +64 -0
- data/lib/react_on_rails/engine.rb +5 -1
- data/lib/react_on_rails/version.rb +1 -1
- data/lib/react_on_rails/version_checker.rb +6 -1
- 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
|
|
1
|
+
# Procfile for development
|
|
2
2
|
# You can run these commands in separate shells
|
|
3
3
|
rails: bundle exec rails s -p 3000
|
|
4
|
-
|
|
5
|
-
|
|
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/
|
|
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
|
|
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 =
|
|
61
|
+
module.exports = serverClientOrBoth;
|
data/lib/generators/react_on_rails/templates/base/base/config/webpack/clientWebpackConfig.js.tt
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
|
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 =
|
|
9
|
+
module.exports = serverClientOrBoth(productionEnvOnly);
|
data/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt
CHANGED
|
@@ -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 -%>
|