react_on_rails 17.0.0.rc.5 → 17.0.0.rc.6
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/Gemfile.lock +1 -1
- data/lib/generators/react_on_rails/base_generator.rb +46 -36
- data/lib/generators/react_on_rails/demo_page_config.rb +4 -4
- data/lib/generators/react_on_rails/generator_helper.rb +107 -0
- data/lib/generators/react_on_rails/install_generator.rb +13 -14
- data/lib/generators/react_on_rails/js_dependency_manager.rb +35 -17
- data/lib/generators/react_on_rails/react_no_redux_generator.rb +12 -7
- data/lib/generators/react_on_rails/react_with_redux_generator.rb +16 -15
- data/lib/generators/react_on_rails/rsc_setup.rb +4 -4
- data/lib/generators/react_on_rails/templates/base/base/app/views/hello_world/index.html.erb.tt +10 -1
- data/lib/generators/react_on_rails/templates/base/base/app/views/home/index.html.erb.tt +5 -0
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt +16 -0
- data/lib/generators/react_on_rails/templates/rsc/base/app/views/hello_server/index.html.erb.tt +8 -1
- data/lib/generators/react_on_rails/templates/rsc/base/config/webpack/rscWebpackConfig.js.tt +4 -1
- data/lib/react_on_rails/length_prefixed_parser.rb +19 -0
- data/lib/react_on_rails/locales/base.rb +7 -4
- data/lib/react_on_rails/locales/to_js.rb +6 -15
- data/lib/react_on_rails/pro_helper.rb +8 -0
- data/lib/react_on_rails/react_component/render_options.rb +19 -1
- data/lib/react_on_rails/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 60603cec3dbd4b84deedfe50bc3abcee226363d53fb0250e175f4b9af3f88492
|
|
4
|
+
data.tar.gz: 2a72fffbf70d1b840aeedfed0f7eafa2e378684e8688bef5c1b0b750749a4654
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f0dfddc3d2db936834d1d03f2c69c491359b482d1849d53cb08c6cbc07a052a96a73619ac6deccee5263cb8a72d03770ff9da8217309447330fde064f586a5c6
|
|
7
|
+
data.tar.gz: 525a931c6f9999180a3b388cd09f0cafabad9864b64adf5bb3fd824757c8f1c0511be22b6b0214d3d4c79ba9ef0c4ea5302e58378e659b9b478aae53979040c4
|
data/Gemfile.lock
CHANGED
|
@@ -188,11 +188,46 @@ module ReactOnRails
|
|
|
188
188
|
route "get 'hello_world', to: 'hello_world#index'"
|
|
189
189
|
end
|
|
190
190
|
|
|
191
|
+
def copy_packer_config
|
|
192
|
+
# Rails generator actions run in method definition order.
|
|
193
|
+
# Keep this before actions that call shakapacker_source_path or
|
|
194
|
+
# shakapacker_source_entry_path; those helpers memoize on first read.
|
|
195
|
+
if instance_variable_defined?(:@shakapacker_source_path) ||
|
|
196
|
+
instance_variable_defined?(:@shakapacker_source_entry_path)
|
|
197
|
+
raise Thor::Error, "copy_packer_config must run before path-dependent generator actions"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
base_path = "base/base/"
|
|
201
|
+
config = "config/shakapacker.yml"
|
|
202
|
+
use_rspack = using_rspack?
|
|
203
|
+
|
|
204
|
+
if options.shakapacker_just_installed?
|
|
205
|
+
say "Replacing Shakapacker default config with React on Rails version"
|
|
206
|
+
# Shakapacker's installer just created this file from scratch (no pre-existing config).
|
|
207
|
+
# Safe to overwrite silently with RoR's version-aware template (e.g., private_output_path).
|
|
208
|
+
template("#{base_path}#{config}.tt", config, force: true)
|
|
209
|
+
else
|
|
210
|
+
say "Adding Shakapacker #{ReactOnRails::PackerUtils.shakapacker_version} config"
|
|
211
|
+
# Thor handles the conflict: prompts user interactively, or respects --force/--skip flags.
|
|
212
|
+
template("#{base_path}#{config}.tt", config)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Configure bundler-specific settings
|
|
216
|
+
configure_rspack_in_shakapacker if use_rspack
|
|
217
|
+
|
|
218
|
+
# Always ensure precompile_hook is configured (Shakapacker 9.0+ only)
|
|
219
|
+
configure_precompile_hook_in_shakapacker
|
|
220
|
+
|
|
221
|
+
# For SSR bundles, configure Shakapacker private_output_path (9.0+ only)
|
|
222
|
+
# This keeps Shakapacker and React on Rails server bundle paths in sync.
|
|
223
|
+
configure_private_output_path_in_shakapacker
|
|
224
|
+
end
|
|
225
|
+
|
|
191
226
|
def create_react_directories
|
|
192
227
|
# Skip HelloWorld directory for Redux (uses HelloWorldApp) or RSC (uses HelloServer)
|
|
193
228
|
return if options.redux? || use_rsc?
|
|
194
229
|
|
|
195
|
-
empty_directory("
|
|
230
|
+
empty_directory(File.join(example_component_source_directory("HelloWorld"), "ror_components"))
|
|
196
231
|
end
|
|
197
232
|
|
|
198
233
|
def copy_base_files
|
|
@@ -235,14 +270,15 @@ module ReactOnRails
|
|
|
235
270
|
|
|
236
271
|
def copy_js_bundle_files
|
|
237
272
|
base_path = "base/base/"
|
|
238
|
-
|
|
273
|
+
copy_file("#{base_path}app/javascript/packs/server-bundle.js",
|
|
274
|
+
shakapacker_entrypoint_path("server-bundle.js"))
|
|
239
275
|
|
|
240
276
|
# Skip HelloWorld CSS for Redux (uses HelloWorldApp) or RSC (uses HelloServer)
|
|
241
|
-
|
|
242
|
-
base_files << "app/javascript/src/HelloWorld/ror_components/HelloWorld.module.css"
|
|
243
|
-
end
|
|
277
|
+
return if options.redux? || use_rsc? || use_tailwind?
|
|
244
278
|
|
|
245
|
-
|
|
279
|
+
copy_file("#{base_path}app/javascript/src/HelloWorld/ror_components/HelloWorld.module.css",
|
|
280
|
+
File.join(example_component_source_directory("HelloWorld"),
|
|
281
|
+
"ror_components/HelloWorld.module.css"))
|
|
246
282
|
end
|
|
247
283
|
|
|
248
284
|
def copy_webpack_config
|
|
@@ -274,33 +310,7 @@ module ReactOnRails
|
|
|
274
310
|
|
|
275
311
|
base_path = "base/tailwind/"
|
|
276
312
|
copy_file("#{base_path}app/javascript/stylesheets/application.css",
|
|
277
|
-
"
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
def copy_packer_config
|
|
281
|
-
base_path = "base/base/"
|
|
282
|
-
config = "config/shakapacker.yml"
|
|
283
|
-
|
|
284
|
-
if options.shakapacker_just_installed?
|
|
285
|
-
say "Replacing Shakapacker default config with React on Rails version"
|
|
286
|
-
# Shakapacker's installer just created this file from scratch (no pre-existing config).
|
|
287
|
-
# Safe to overwrite silently with RoR's version-aware template (e.g., private_output_path).
|
|
288
|
-
template("#{base_path}#{config}.tt", config, force: true)
|
|
289
|
-
else
|
|
290
|
-
say "Adding Shakapacker #{ReactOnRails::PackerUtils.shakapacker_version} config"
|
|
291
|
-
# Thor handles the conflict: prompts user interactively, or respects --force/--skip flags.
|
|
292
|
-
template("#{base_path}#{config}.tt", config)
|
|
293
|
-
end
|
|
294
|
-
|
|
295
|
-
# Configure bundler-specific settings
|
|
296
|
-
configure_rspack_in_shakapacker if using_rspack?
|
|
297
|
-
|
|
298
|
-
# Always ensure precompile_hook is configured (Shakapacker 9.0+ only)
|
|
299
|
-
configure_precompile_hook_in_shakapacker
|
|
300
|
-
|
|
301
|
-
# For SSR bundles, configure Shakapacker private_output_path (9.0+ only)
|
|
302
|
-
# This keeps Shakapacker and React on Rails server bundle paths in sync.
|
|
303
|
-
configure_private_output_path_in_shakapacker
|
|
313
|
+
shakapacker_stylesheet_path("application.css"))
|
|
304
314
|
end
|
|
305
315
|
|
|
306
316
|
def add_base_gems_to_gemfile
|
|
@@ -686,10 +696,10 @@ module ReactOnRails
|
|
|
686
696
|
end
|
|
687
697
|
|
|
688
698
|
def example_source_path
|
|
689
|
-
return "
|
|
690
|
-
return "
|
|
699
|
+
return example_component_source_path("HelloServer") if use_rsc? && !options.redux?
|
|
700
|
+
return example_component_source_path("HelloWorldApp") if options.redux?
|
|
691
701
|
|
|
692
|
-
"
|
|
702
|
+
example_component_source_path("HelloWorld")
|
|
693
703
|
end
|
|
694
704
|
|
|
695
705
|
def example_view_path
|
|
@@ -15,13 +15,13 @@ module ReactOnRails
|
|
|
15
15
|
}
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
def build_hello_server_view_config(landing_page:, redux_demo:)
|
|
18
|
+
def build_hello_server_view_config(landing_page:, redux_demo:, source_path:)
|
|
19
19
|
{
|
|
20
20
|
title: "React Server Components Demo",
|
|
21
21
|
intro: "This route shows the Pro React Server Components flow: Rails streams an async server " \
|
|
22
22
|
"component response while only client islands ship JavaScript to the browser.",
|
|
23
23
|
highlights: hello_server_highlights,
|
|
24
|
-
file_hints: hello_server_file_hints,
|
|
24
|
+
file_hints: hello_server_file_hints(source_path:),
|
|
25
25
|
quick_links: hello_server_quick_links(landing_page:, redux_demo:),
|
|
26
26
|
learning_links: hello_server_learning_links
|
|
27
27
|
}
|
|
@@ -167,10 +167,10 @@ module ReactOnRails
|
|
|
167
167
|
]
|
|
168
168
|
end
|
|
169
169
|
|
|
170
|
-
def hello_server_file_hints
|
|
170
|
+
def hello_server_file_hints(source_path:)
|
|
171
171
|
[
|
|
172
172
|
{
|
|
173
|
-
path:
|
|
173
|
+
path: source_path,
|
|
174
174
|
description: "Source for the generated server component example and client island."
|
|
175
175
|
},
|
|
176
176
|
{
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require "pathname"
|
|
4
5
|
require_relative "shakapacker_precompile_hook_helper"
|
|
5
6
|
|
|
6
7
|
# rubocop:disable Metrics/ModuleLength
|
|
7
8
|
module GeneratorHelper
|
|
8
9
|
include ReactOnRails::Generators::ShakapackerPrecompileHookHelper
|
|
9
10
|
|
|
11
|
+
DEFAULT_SHAKAPACKER_SOURCE_PATH = "app/javascript"
|
|
12
|
+
DEFAULT_SHAKAPACKER_SOURCE_ENTRY_PATH = "packs"
|
|
13
|
+
private_constant :DEFAULT_SHAKAPACKER_SOURCE_PATH, :DEFAULT_SHAKAPACKER_SOURCE_ENTRY_PATH
|
|
14
|
+
|
|
10
15
|
def package_json
|
|
11
16
|
# Lazy load package_json gem only when actually needed for dependency management
|
|
12
17
|
|
|
@@ -75,6 +80,108 @@ module GeneratorHelper
|
|
|
75
80
|
options.typescript? ? "tsx" : "jsx"
|
|
76
81
|
end
|
|
77
82
|
|
|
83
|
+
def shakapacker_source_path
|
|
84
|
+
# These helpers memoize config-backed paths. Install generators must copy or
|
|
85
|
+
# overwrite config/shakapacker.yml before any path-dependent copy action runs.
|
|
86
|
+
@shakapacker_source_path ||= configured_shakapacker_relative_path("source_path", DEFAULT_SHAKAPACKER_SOURCE_PATH)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def shakapacker_source_entry_path
|
|
90
|
+
@shakapacker_source_entry_path ||= configured_shakapacker_relative_path(
|
|
91
|
+
"source_entry_path",
|
|
92
|
+
DEFAULT_SHAKAPACKER_SOURCE_ENTRY_PATH,
|
|
93
|
+
allow_root: true
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def shakapacker_entrypoint_path(filename)
|
|
98
|
+
filename = filename.to_s
|
|
99
|
+
raise ArgumentError, "filename must be present" if filename.empty?
|
|
100
|
+
|
|
101
|
+
entry_dir = shakapacker_source_entry_path # "" means entrypoints live directly under source_path.
|
|
102
|
+
File.join(*[shakapacker_source_path, entry_dir, filename].reject(&:empty?))
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def shakapacker_stylesheet_path(filename)
|
|
106
|
+
# "stylesheets" is a generated demo convention, not a Shakapacker config key.
|
|
107
|
+
File.join(shakapacker_source_path, "stylesheets", filename)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def relative_stylesheet_import_path(entry_path, filename: "application.css")
|
|
111
|
+
# InstallGenerator copies the final Shakapacker config before path-dependent demo files are generated.
|
|
112
|
+
safe_entry_path = safe_generator_destination_path(entry_path, default: nil)
|
|
113
|
+
raise ArgumentError, "entry_path must stay inside the generator destination" if safe_entry_path.nil?
|
|
114
|
+
|
|
115
|
+
entry_dir = Pathname.new(File.join(destination_root, safe_entry_path)).dirname
|
|
116
|
+
stylesheet = Pathname.new(File.join(destination_root, shakapacker_stylesheet_path(filename)))
|
|
117
|
+
|
|
118
|
+
stylesheet.relative_path_from(entry_dir).to_s
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def example_component_source_directory(component_name)
|
|
122
|
+
File.join(shakapacker_source_path, "src", component_name)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def example_component_source_path(component_name)
|
|
126
|
+
# Trailing slash is intentional: this value is only for generated demo file hints.
|
|
127
|
+
"#{example_component_source_directory(component_name)}/"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def configured_shakapacker_relative_path(config_key, default, allow_root: false)
|
|
131
|
+
config_path = File.join(destination_root, "config/shakapacker.yml")
|
|
132
|
+
return default unless File.exist?(config_path)
|
|
133
|
+
|
|
134
|
+
config = parse_shakapacker_yml(config_path)
|
|
135
|
+
configured_path = shakapacker_path_config_value(config, config_key)
|
|
136
|
+
|
|
137
|
+
safe_generator_destination_path(configured_path, default:, allow_root:)
|
|
138
|
+
rescue Psych::SyntaxError
|
|
139
|
+
default
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def shakapacker_path_config_value(config, config_key)
|
|
143
|
+
# Generators run in the development context, so prefer that section before falling back to shared defaults.
|
|
144
|
+
%w[development default].each do |section_name|
|
|
145
|
+
section = shakapacker_config_section(config, section_name)
|
|
146
|
+
value = shakapacker_config_value(section, config_key)
|
|
147
|
+
return value unless value.to_s.strip.empty?
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
nil
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def safe_generator_destination_path(path, default:, allow_root: false)
|
|
154
|
+
candidate = path.to_s.strip
|
|
155
|
+
return default if candidate.empty?
|
|
156
|
+
|
|
157
|
+
pathname = Pathname.new(candidate).cleanpath
|
|
158
|
+
# Shakapacker uses "/" to mean entrypoints live directly under source_path.
|
|
159
|
+
return "" if allow_root && pathname.to_s == "/"
|
|
160
|
+
|
|
161
|
+
relative_path = if pathname.absolute?
|
|
162
|
+
absolute_path_relative_to_destination(pathname)
|
|
163
|
+
else
|
|
164
|
+
pathname.to_s
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
return default if unsafe_generator_destination_path?(relative_path)
|
|
168
|
+
|
|
169
|
+
relative_path
|
|
170
|
+
rescue ArgumentError # Pathname.new raises on null bytes in path strings.
|
|
171
|
+
default
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def absolute_path_relative_to_destination(pathname)
|
|
175
|
+
destination = Pathname.new(destination_root).cleanpath
|
|
176
|
+
pathname.relative_path_from(destination).to_s
|
|
177
|
+
rescue ArgumentError
|
|
178
|
+
nil # Signals the caller to fall back to the default path.
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def unsafe_generator_destination_path?(path)
|
|
182
|
+
path.nil? || path == "." || path == ".." || path.start_with?("../")
|
|
183
|
+
end
|
|
184
|
+
|
|
78
185
|
# Check if a gem is present in Gemfile.lock
|
|
79
186
|
# Always checks the target app's Gemfile.lock, not inherited BUNDLE_GEMFILE
|
|
80
187
|
# See: https://github.com/shakacode/react_on_rails/issues/2287
|
|
@@ -234,11 +234,7 @@ module ReactOnRails
|
|
|
234
234
|
|
|
235
235
|
def invoke_generators
|
|
236
236
|
ensure_shakapacker_installed
|
|
237
|
-
if options.typescript?
|
|
238
|
-
install_typescript_dependencies
|
|
239
|
-
create_css_module_types
|
|
240
|
-
create_typescript_config
|
|
241
|
-
end
|
|
237
|
+
install_typescript_dependencies if options.typescript?
|
|
242
238
|
# `invoke` instantiates child generators with a fresh options hash, so
|
|
243
239
|
# --pretend/--force/--skip must be forwarded explicitly at each boundary.
|
|
244
240
|
invoke "react_on_rails:base", [],
|
|
@@ -247,6 +243,11 @@ module ReactOnRails
|
|
|
247
243
|
shakapacker_just_installed: shakapacker_just_installed?,
|
|
248
244
|
force: options[:force], skip: options[:skip], pretend: options[:pretend] }
|
|
249
245
|
|
|
246
|
+
if options.typescript?
|
|
247
|
+
create_css_module_types
|
|
248
|
+
create_typescript_config
|
|
249
|
+
end
|
|
250
|
+
|
|
250
251
|
# Component generator logic:
|
|
251
252
|
# - --rsc without --redux: Skip HelloWorld, HelloServer will be generated in setup_rsc
|
|
252
253
|
# - --rsc with --redux: Generate HelloWorldApp (user explicitly wants Redux) + HelloServer
|
|
@@ -923,10 +924,10 @@ module ReactOnRails
|
|
|
923
924
|
# Resolve the bundler via using_rspack?. shakapacker.yml doesn't exist yet at this point,
|
|
924
925
|
# so the fresh-install default applies: an unset --rspack flag resolves to Rspack when
|
|
925
926
|
# Shakapacker supports it (shakapacker_version_9_or_higher? is optimistically true on a
|
|
926
|
-
# brand-new install where Shakapacker isn't loaded yet).
|
|
927
|
-
#
|
|
928
|
-
|
|
929
|
-
shakapacker_install_env =
|
|
927
|
+
# brand-new install where Shakapacker isn't loaded yet). Pass the resolved choice explicitly
|
|
928
|
+
# so Shakapacker installs dependencies for the same bundler that React on Rails configures.
|
|
929
|
+
assets_bundler = using_rspack? ? "rspack" : "webpack"
|
|
930
|
+
shakapacker_install_env = { "SHAKAPACKER_ASSETS_BUNDLER" => assets_bundler }
|
|
930
931
|
success = Bundler.with_unbundled_env do
|
|
931
932
|
system(shakapacker_install_env, "bundle exec rails shakapacker:install")
|
|
932
933
|
end
|
|
@@ -1169,9 +1170,7 @@ module ReactOnRails
|
|
|
1169
1170
|
|
|
1170
1171
|
say "📝 Creating CSS module type definitions...", :yellow
|
|
1171
1172
|
|
|
1172
|
-
|
|
1173
|
-
FileUtils.mkdir_p("app/javascript/types")
|
|
1174
|
-
|
|
1173
|
+
css_module_types_path = File.join(shakapacker_source_path, "types", "css-modules.d.ts")
|
|
1175
1174
|
css_module_types_content = <<~TS.strip
|
|
1176
1175
|
// TypeScript definitions for CSS modules
|
|
1177
1176
|
declare module "*.module.css" {
|
|
@@ -1190,7 +1189,7 @@ module ReactOnRails
|
|
|
1190
1189
|
}
|
|
1191
1190
|
TS
|
|
1192
1191
|
|
|
1193
|
-
|
|
1192
|
+
create_file(css_module_types_path, css_module_types_content)
|
|
1194
1193
|
say "✅ Created CSS module type definitions", :green
|
|
1195
1194
|
end
|
|
1196
1195
|
|
|
@@ -1222,7 +1221,7 @@ module ReactOnRails
|
|
|
1222
1221
|
"jsx" => "react-jsx"
|
|
1223
1222
|
},
|
|
1224
1223
|
"include" => [
|
|
1225
|
-
"
|
|
1224
|
+
File.join(shakapacker_source_path, "**/*")
|
|
1226
1225
|
]
|
|
1227
1226
|
}
|
|
1228
1227
|
|
|
@@ -160,10 +160,9 @@ module ReactOnRails
|
|
|
160
160
|
# prerelease RSC package broadly enough to keep `npm ls` healthy, but generator behavior still
|
|
161
161
|
# installs the tested React 19.0.x range and exact RSC package pin until both policies advance.
|
|
162
162
|
RSC_REACT_VERSION_RANGE = "~19.0.4"
|
|
163
|
-
# Pinned to 19.0.5
|
|
164
|
-
# RSC manifest CSS fixes
|
|
165
|
-
|
|
166
|
-
RSC_PACKAGE_VERSION_PIN = "19.0.5-rc.7"
|
|
163
|
+
# Pinned to the stable 19.0.5 release, which carries the discovery plugin export, native
|
|
164
|
+
# Rspack plugin, and RSC manifest CSS fixes.
|
|
165
|
+
RSC_PACKAGE_VERSION_PIN = "19.0.5"
|
|
167
166
|
|
|
168
167
|
private
|
|
169
168
|
|
|
@@ -489,23 +488,42 @@ module ReactOnRails
|
|
|
489
488
|
RSC_PACKAGE_VERSION_PIN.split("-", 2).first
|
|
490
489
|
end
|
|
491
490
|
|
|
491
|
+
def rsc_package_version_prerelease?
|
|
492
|
+
RSC_PACKAGE_VERSION_PIN.include?("-")
|
|
493
|
+
end
|
|
494
|
+
|
|
492
495
|
def rsc_dependency_pin_info
|
|
493
|
-
|
|
494
|
-
"
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
496
|
+
if rsc_package_version_prerelease?
|
|
497
|
+
"React Server Components package pin: all --rsc installs temporarily use " \
|
|
498
|
+
"react-on-rails-rsc@#{RSC_PACKAGE_VERSION_PIN}, including Webpack projects. " \
|
|
499
|
+
"This prerelease keeps react-on-rails-rsc/WebpackPlugin compatible while adding " \
|
|
500
|
+
"react-on-rails-rsc/RspackPlugin. Keep the pin until stable " \
|
|
501
|
+
"react-on-rails-rsc@#{rsc_stable_package_version_target} " \
|
|
502
|
+
"is published and tagged latest."
|
|
503
|
+
else
|
|
504
|
+
"React Server Components package pin: all --rsc installs use " \
|
|
505
|
+
"react-on-rails-rsc@#{RSC_PACKAGE_VERSION_PIN}, including Webpack projects. " \
|
|
506
|
+
"This pin keeps react-on-rails-rsc/WebpackPlugin compatible while adding " \
|
|
507
|
+
"react-on-rails-rsc/RspackPlugin."
|
|
508
|
+
end
|
|
499
509
|
end
|
|
500
510
|
|
|
501
511
|
def rsc_dependency_pin_failed_warning
|
|
502
|
-
|
|
503
|
-
"
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
512
|
+
if rsc_package_version_prerelease?
|
|
513
|
+
"Warning: Could not install the pinned react-on-rails-rsc@#{RSC_PACKAGE_VERSION_PIN}. " \
|
|
514
|
+
"All RSC projects are temporarily pinned to that version: the prerelease keeps " \
|
|
515
|
+
"react-on-rails-rsc/WebpackPlugin compatible while adding react-on-rails-rsc/RspackPlugin, " \
|
|
516
|
+
"and the unversioned `latest` tag may not include both until stable " \
|
|
517
|
+
"#{rsc_stable_package_version_target} " \
|
|
518
|
+
"is published, so the generator left the version pin in package.json rather than " \
|
|
519
|
+
"install a potentially incompatible version."
|
|
520
|
+
else
|
|
521
|
+
"Warning: Could not install the pinned react-on-rails-rsc@#{RSC_PACKAGE_VERSION_PIN}. " \
|
|
522
|
+
"All RSC projects are pinned to that version: this pin keeps " \
|
|
523
|
+
"react-on-rails-rsc/WebpackPlugin compatible while adding react-on-rails-rsc/RspackPlugin, " \
|
|
524
|
+
"so the generator left the version pin in package.json rather than " \
|
|
525
|
+
"install a potentially incompatible version."
|
|
526
|
+
end
|
|
509
527
|
end
|
|
510
528
|
|
|
511
529
|
def rsc_dependency_pin_failure_details(used_version_pins)
|
|
@@ -32,22 +32,27 @@ module ReactOnRails
|
|
|
32
32
|
base_js_path = "base/base"
|
|
33
33
|
tailwind_js_path = "base/tailwind"
|
|
34
34
|
ext = component_extension(options)
|
|
35
|
+
component_dir = example_component_source_directory("HelloWorld")
|
|
35
36
|
|
|
36
37
|
# Determine which component files to copy based on TypeScript option
|
|
37
38
|
client_component =
|
|
38
|
-
"
|
|
39
|
+
"#{component_dir}/ror_components/HelloWorld.client.#{ext}"
|
|
39
40
|
server_component =
|
|
40
|
-
"
|
|
41
|
+
"#{component_dir}/ror_components/HelloWorld.server.#{ext}"
|
|
42
|
+
client_component_template = "app/javascript/src/HelloWorld/ror_components/HelloWorld.client.#{ext}"
|
|
41
43
|
|
|
44
|
+
# Source paths are relative to this generator's templates; only
|
|
45
|
+
# destinations vary with the app's Shakapacker config.
|
|
42
46
|
if use_tailwind?
|
|
43
|
-
copy_file("#{tailwind_js_path}/#{
|
|
47
|
+
copy_file("#{tailwind_js_path}/#{client_component_template}", client_component)
|
|
44
48
|
else
|
|
45
|
-
copy_file("#{base_js_path}/#{
|
|
49
|
+
copy_file("#{base_js_path}/#{client_component_template}", client_component)
|
|
46
50
|
copy_file("#{base_js_path}/app/javascript/src/HelloWorld/ror_components/HelloWorld.module.css",
|
|
47
|
-
"
|
|
51
|
+
"#{component_dir}/ror_components/HelloWorld.module.css")
|
|
48
52
|
end
|
|
49
53
|
|
|
50
|
-
copy_file("#{base_js_path}
|
|
54
|
+
copy_file("#{base_js_path}/app/javascript/src/HelloWorld/ror_components/HelloWorld.server.#{ext}",
|
|
55
|
+
server_component)
|
|
51
56
|
end
|
|
52
57
|
|
|
53
58
|
def create_appropriate_templates
|
|
@@ -58,7 +63,7 @@ module ReactOnRails
|
|
|
58
63
|
"app/views/hello_world/index.html.erb",
|
|
59
64
|
build_hello_world_view_config(
|
|
60
65
|
component_name: "HelloWorld",
|
|
61
|
-
source_path: "
|
|
66
|
+
source_path: example_component_source_path("HelloWorld"),
|
|
62
67
|
landing_page: new_app_landing_page_available?,
|
|
63
68
|
redux: false,
|
|
64
69
|
rsc_demo: false
|
|
@@ -41,42 +41,42 @@ module ReactOnRails
|
|
|
41
41
|
hide: true
|
|
42
42
|
|
|
43
43
|
def create_redux_directories
|
|
44
|
+
component_dir = example_component_source_directory("HelloWorldApp")
|
|
45
|
+
|
|
44
46
|
# Create auto-bundling directory structure for Redux
|
|
45
|
-
empty_directory("
|
|
47
|
+
empty_directory("#{component_dir}/ror_components")
|
|
46
48
|
|
|
47
49
|
# Create Redux support directories within the component directory
|
|
48
50
|
dirs = %w[actions constants containers reducers store components]
|
|
49
|
-
dirs.each { |name| empty_directory("
|
|
51
|
+
dirs.each { |name| empty_directory("#{component_dir}/#{name}") }
|
|
50
52
|
end
|
|
51
53
|
|
|
52
54
|
def copy_base_files
|
|
53
55
|
base_js_path = "redux/base"
|
|
54
56
|
ext = component_extension(options)
|
|
57
|
+
component_dir = example_component_source_directory("HelloWorldApp")
|
|
55
58
|
|
|
56
59
|
# Copy Redux-connected component to auto-bundling structure
|
|
57
60
|
copy_file("#{base_js_path}/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.client.#{ext}",
|
|
58
|
-
"
|
|
61
|
+
"#{component_dir}/ror_components/HelloWorldApp.client.#{ext}")
|
|
59
62
|
copy_file("#{base_js_path}/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.server.#{ext}",
|
|
60
|
-
"
|
|
63
|
+
"#{component_dir}/ror_components/HelloWorldApp.server.#{ext}")
|
|
61
64
|
|
|
62
65
|
unless use_tailwind?
|
|
63
66
|
copy_file("#{base_js_path}/app/javascript/bundles/HelloWorld/components/HelloWorld.module.css",
|
|
64
|
-
"
|
|
67
|
+
"#{component_dir}/components/HelloWorld.module.css")
|
|
65
68
|
end
|
|
66
69
|
|
|
67
|
-
# Update import paths in client component
|
|
68
|
-
ror_client_file = "app/javascript/src/HelloWorldApp/ror_components/HelloWorldApp.client.#{ext}"
|
|
69
|
-
gsub_file(ror_client_file, "../store/helloWorldStore", "../store/helloWorldStore")
|
|
70
|
-
gsub_file(ror_client_file, "../containers/HelloWorldContainer",
|
|
71
|
-
"../containers/HelloWorldContainer")
|
|
72
70
|
return unless use_tailwind?
|
|
73
71
|
|
|
74
|
-
|
|
75
|
-
ror_client_file_path = File.join(destination_root, ror_client_file)
|
|
72
|
+
ror_client_file = "#{component_dir}/ror_components/HelloWorldApp.client.#{ext}"
|
|
76
73
|
if options[:pretend]
|
|
77
74
|
say_status :pretend, "Would add Tailwind stylesheet import to #{ror_client_file}", :yellow
|
|
78
75
|
return
|
|
79
76
|
end
|
|
77
|
+
|
|
78
|
+
stylesheet_import = "import '#{relative_stylesheet_import_path(ror_client_file)}';\n"
|
|
79
|
+
ror_client_file_path = File.join(destination_root, ror_client_file)
|
|
80
80
|
return if File.read(ror_client_file_path).include?(stylesheet_import)
|
|
81
81
|
|
|
82
82
|
prepend_to_file(ror_client_file, stylesheet_import)
|
|
@@ -86,6 +86,7 @@ module ReactOnRails
|
|
|
86
86
|
base_hello_world_path = "redux/base/app/javascript/bundles/HelloWorld"
|
|
87
87
|
tailwind_hello_world_path = "redux/tailwind/app/javascript/bundles/HelloWorld"
|
|
88
88
|
redux_extension = options.typescript? ? "ts" : "js"
|
|
89
|
+
component_dir = example_component_source_directory("HelloWorldApp")
|
|
89
90
|
|
|
90
91
|
# Copy Redux infrastructure files with appropriate extension
|
|
91
92
|
%W[actions/helloWorldActionCreators.#{redux_extension}
|
|
@@ -94,13 +95,13 @@ module ReactOnRails
|
|
|
94
95
|
reducers/helloWorldReducer.#{redux_extension}
|
|
95
96
|
store/helloWorldStore.#{redux_extension}].each do |file|
|
|
96
97
|
copy_file("#{base_hello_world_path}/#{file}",
|
|
97
|
-
"
|
|
98
|
+
"#{component_dir}/#{file}")
|
|
98
99
|
end
|
|
99
100
|
|
|
100
101
|
component_file = "components/HelloWorld.#{component_extension(options)}"
|
|
101
102
|
component_source_path = use_tailwind? ? tailwind_hello_world_path : base_hello_world_path
|
|
102
103
|
copy_file("#{component_source_path}/#{component_file}",
|
|
103
|
-
"
|
|
104
|
+
"#{component_dir}/#{component_file}")
|
|
104
105
|
end
|
|
105
106
|
|
|
106
107
|
def create_appropriate_templates
|
|
@@ -111,7 +112,7 @@ module ReactOnRails
|
|
|
111
112
|
"app/views/hello_world/index.html.erb",
|
|
112
113
|
build_hello_world_view_config(
|
|
113
114
|
component_name: "HelloWorldApp",
|
|
114
|
-
source_path: "
|
|
115
|
+
source_path: example_component_source_path("HelloWorldApp"),
|
|
115
116
|
landing_page: new_app_landing_page_available?,
|
|
116
117
|
redux: true,
|
|
117
118
|
rsc_demo: options[:rsc]
|
|
@@ -198,7 +198,7 @@ module ReactOnRails
|
|
|
198
198
|
end
|
|
199
199
|
|
|
200
200
|
def create_hello_server_component
|
|
201
|
-
hello_server_dir = "
|
|
201
|
+
hello_server_dir = example_component_source_directory("HelloServer")
|
|
202
202
|
ror_components_dir = "#{hello_server_dir}/ror_components"
|
|
203
203
|
components_dir = "#{hello_server_dir}/components"
|
|
204
204
|
ext = component_extension(options)
|
|
@@ -246,8 +246,7 @@ module ReactOnRails
|
|
|
246
246
|
end
|
|
247
247
|
return false unless relative_entry_path
|
|
248
248
|
|
|
249
|
-
|
|
250
|
-
stylesheet_import = "import '../../../stylesheets/application.css';"
|
|
249
|
+
stylesheet_import = "import '#{relative_stylesheet_import_path(relative_entry_path)}';"
|
|
251
250
|
entry_path = File.join(destination_root, relative_entry_path)
|
|
252
251
|
entry_content = File.read(entry_path)
|
|
253
252
|
return false if entry_content.include?(stylesheet_import)
|
|
@@ -296,7 +295,8 @@ module ReactOnRails
|
|
|
296
295
|
view_path,
|
|
297
296
|
build_hello_server_view_config(
|
|
298
297
|
landing_page: new_app_landing_page_available?,
|
|
299
|
-
redux_demo: options[:redux]
|
|
298
|
+
redux_demo: options[:redux],
|
|
299
|
+
source_path: example_component_source_path("HelloServer")
|
|
300
300
|
))
|
|
301
301
|
|
|
302
302
|
say "✅ Created #{view_path}", :green
|
data/lib/generators/react_on_rails/templates/base/base/app/views/hello_world/index.html.erb.tt
CHANGED
|
@@ -129,6 +129,15 @@
|
|
|
129
129
|
background: rgba(17, 32, 49, 0.06);
|
|
130
130
|
color: var(--ink);
|
|
131
131
|
font-size: 0.95em;
|
|
132
|
+
overflow-wrap: anywhere;
|
|
133
|
+
word-break: break-word;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.path-hint {
|
|
137
|
+
display: inline-block;
|
|
138
|
+
max-width: 100%;
|
|
139
|
+
overflow-wrap: anywhere;
|
|
140
|
+
word-break: break-word;
|
|
132
141
|
}
|
|
133
142
|
|
|
134
143
|
@media (max-width: 900px) {
|
|
@@ -179,7 +188,7 @@
|
|
|
179
188
|
<ul>
|
|
180
189
|
<% config[:file_hints].each do |hint| %>
|
|
181
190
|
<li>
|
|
182
|
-
<code><%= hint[:path] %></code><br>
|
|
191
|
+
<code class="path-hint"><%= hint[:path] %></code><br>
|
|
183
192
|
<%= hint[:description] %>
|
|
184
193
|
</li>
|
|
185
194
|
<% end %>
|
|
@@ -259,13 +259,18 @@
|
|
|
259
259
|
letter-spacing: 0.08em;
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
+
/* Home-page code samples are all compact commands or path hints, so they share wrapping rules. */
|
|
262
263
|
.command-list code,
|
|
263
264
|
.hint-list code {
|
|
265
|
+
display: inline-block;
|
|
266
|
+
max-width: 100%;
|
|
264
267
|
padding: 2px 6px;
|
|
265
268
|
border-radius: 8px;
|
|
266
269
|
background: rgba(17, 32, 49, 0.06);
|
|
267
270
|
color: var(--ink);
|
|
268
271
|
font-size: 0.95em;
|
|
272
|
+
overflow-wrap: anywhere;
|
|
273
|
+
word-break: break-word;
|
|
269
274
|
}
|
|
270
275
|
|
|
271
276
|
.footer-note {
|
data/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt
CHANGED
|
@@ -219,6 +219,22 @@ const configureServer = () => {
|
|
|
219
219
|
|
|
220
220
|
// Disable Node.js polyfills - not needed when targeting Node
|
|
221
221
|
serverWebpackConfig.node = false;
|
|
222
|
+
|
|
223
|
+
// Source-mapped SSR stack traces in production:
|
|
224
|
+
// The Pro Node renderer can remap bundled stack frames back to your original
|
|
225
|
+
// source files (see docs/oss/building-features/node-renderer/debugging.md). This
|
|
226
|
+
// needs source maps in the *production* server bundle, which the default above
|
|
227
|
+
// disables (`devtool: false`). To opt in, replace only the `serverWebpackConfig.devtool`
|
|
228
|
+
// assignment above (keep the eval-devtool note) with a production-aware variant so
|
|
229
|
+
// development is unaffected. Both examples below use non-`eval` devtools, satisfying
|
|
230
|
+
// that constraint. E.g.:
|
|
231
|
+
// serverWebpackConfig.devtool = process.env.NODE_ENV === 'production' ? 'source-map' : 'cheap-module-source-map';
|
|
232
|
+
// // 'source-map' — external .map (smaller bundle; stage the .map next to the uploaded bundle)
|
|
233
|
+
// serverWebpackConfig.devtool = process.env.NODE_ENV === 'production' ? 'inline-source-map' : 'cheap-module-source-map';
|
|
234
|
+
// // 'inline-source-map' — simplest; map travels inside the bundle (larger file)
|
|
235
|
+
// The server bundle is never served to browsers; still, never expose
|
|
236
|
+
// server-bundle source maps publicly.
|
|
237
|
+
|
|
222
238
|
<% else -%>
|
|
223
239
|
// If using the default 'web', then libraries like Emotion and loadable-components
|
|
224
240
|
// break with SSR. The fix is to use a node renderer and change the target.
|
data/lib/generators/react_on_rails/templates/rsc/base/app/views/hello_server/index.html.erb.tt
CHANGED
|
@@ -131,6 +131,13 @@
|
|
|
131
131
|
font-size: 0.95em;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
.path-hint {
|
|
135
|
+
display: inline-block;
|
|
136
|
+
max-width: 100%;
|
|
137
|
+
overflow-wrap: anywhere;
|
|
138
|
+
word-break: break-word;
|
|
139
|
+
}
|
|
140
|
+
|
|
134
141
|
@media (max-width: 900px) {
|
|
135
142
|
.hero,
|
|
136
143
|
.card,
|
|
@@ -179,7 +186,7 @@
|
|
|
179
186
|
<ul>
|
|
180
187
|
<% config[:file_hints].each do |hint| %>
|
|
181
188
|
<li>
|
|
182
|
-
<code><%= hint[:path] %></code><br>
|
|
189
|
+
<code class="path-hint"><%= hint[:path] %></code><br>
|
|
183
190
|
<%= hint[:description] %>
|
|
184
191
|
</li>
|
|
185
192
|
<% end %>
|
|
@@ -50,10 +50,13 @@ const configureRsc = () => {
|
|
|
50
50
|
// Pass true to skip the RSC manifest plugin - RSC bundle doesn't need it
|
|
51
51
|
const rscConfig = serverWebpackConfig(true);
|
|
52
52
|
const discoveryBuild = process.env.RSC_REFERENCE_DISCOVERY_BUILD === 'true';
|
|
53
|
+
// Shakapacker uses "/" to mean entrypoints live directly under source_path.
|
|
54
|
+
// Keep that sentinel relative so path.resolve does not escape to the filesystem root.
|
|
55
|
+
const sourceEntryPath = config.source_entry_path === '/' ? '' : config.source_entry_path;
|
|
53
56
|
|
|
54
57
|
const defaultServerComponentRegistrationEntry = resolve(
|
|
55
58
|
config.source_path,
|
|
56
|
-
|
|
59
|
+
sourceEntryPath,
|
|
57
60
|
'../generated/server-component-registration-entry.js',
|
|
58
61
|
);
|
|
59
62
|
const expectedServerComponentRegistrationEntry = 'server-component-registration-entry.js';
|
|
@@ -11,6 +11,11 @@ module ReactOnRails
|
|
|
11
11
|
# Used by both streaming (Pro) and non-streaming (OSS) paths.
|
|
12
12
|
# Strict protocol parser — any format violation raises an error.
|
|
13
13
|
class LengthPrefixedParser
|
|
14
|
+
# Keep aligned with ReactOnRailsPro::StreamRequest::CONTROL_MESSAGE_TYPES,
|
|
15
|
+
# which routes these same control frames during bidirectional streaming.
|
|
16
|
+
CONTROL_MESSAGE_TYPES = %w[propRequest renderComplete].freeze
|
|
17
|
+
private_constant :CONTROL_MESSAGE_TYPES
|
|
18
|
+
|
|
14
19
|
# Parses a complete length-prefixed result string that must contain exactly one chunk.
|
|
15
20
|
# Used by the non-streaming rendering path where ExecJS/node renderer returns a single result.
|
|
16
21
|
# Returns a single Hash: { "html" => String|nil, "consoleReplayScript" => "...", ... }
|
|
@@ -127,6 +132,20 @@ module ReactOnRails
|
|
|
127
132
|
raw_content = @buf.byteslice(0, @content_len).force_encoding(Encoding::UTF_8)
|
|
128
133
|
@buf = @buf.byteslice(@content_len, @buf.bytesize - @content_len)
|
|
129
134
|
|
|
135
|
+
# Control messages (propRequest, renderComplete) have no HTML payload;
|
|
136
|
+
# raw_content is therefore empty and intentionally unused. Those two
|
|
137
|
+
# messageType values are reserved by the wire format. Other messageType
|
|
138
|
+
# metadata is treated as ordinary chunk metadata so future tracing or
|
|
139
|
+
# diagnostics annotations cannot be swallowed accidentally.
|
|
140
|
+
if CONTROL_MESSAGE_TYPES.include?(@metadata["messageType"])
|
|
141
|
+
@metadata.delete("payloadType")
|
|
142
|
+
result = @metadata
|
|
143
|
+
@metadata = nil
|
|
144
|
+
@state = :header
|
|
145
|
+
yield result
|
|
146
|
+
return true
|
|
147
|
+
end
|
|
148
|
+
|
|
130
149
|
# Reconstruct html type based on payloadType:
|
|
131
150
|
# "object" → JSON-serialized value (ServerRenderHash or null), needs JSON.parse
|
|
132
151
|
# "string" → raw HTML string, used as-is
|
|
@@ -77,10 +77,15 @@ module ReactOnRails
|
|
|
77
77
|
def obsolete?
|
|
78
78
|
return true if exist_files.length != files.length # Some files missing
|
|
79
79
|
return true if exist_files.empty?
|
|
80
|
+
return true if generated_files_obsolete?
|
|
80
81
|
|
|
81
82
|
files_are_outdated
|
|
82
83
|
end
|
|
83
84
|
|
|
85
|
+
def generated_files_obsolete?
|
|
86
|
+
false
|
|
87
|
+
end
|
|
88
|
+
|
|
84
89
|
def exist_files
|
|
85
90
|
@exist_files ||= files.select { |file| File.exist?(file) }
|
|
86
91
|
end
|
|
@@ -188,11 +193,9 @@ module ReactOnRails
|
|
|
188
193
|
|
|
189
194
|
def template_default
|
|
190
195
|
<<~JS
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const defaultLocale = '#{default_locale}';
|
|
196
|
+
const defaultLocale = #{default_locale.to_json};
|
|
194
197
|
|
|
195
|
-
const defaultMessages =
|
|
198
|
+
const defaultMessages = #{@defaults};
|
|
196
199
|
|
|
197
200
|
export { defaultMessages, defaultLocale };
|
|
198
201
|
JS
|
|
@@ -11,22 +11,13 @@ module ReactOnRails
|
|
|
11
11
|
"js"
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
JS
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def template_default
|
|
21
|
-
<<~JS
|
|
22
|
-
import { defineMessages } from 'react-intl';
|
|
23
|
-
|
|
24
|
-
const defaultLocale = '#{default_locale}';
|
|
25
|
-
|
|
26
|
-
const defaultMessages = defineMessages(#{@defaults});
|
|
14
|
+
def generated_files_obsolete?
|
|
15
|
+
# obsolete? only calls this after all output files exist; if the file disappears, regenerate.
|
|
16
|
+
default_source = File.read(file("default"))
|
|
27
17
|
|
|
28
|
-
|
|
29
|
-
|
|
18
|
+
default_source.match?(/^\s*import\s+\{\s*defineMessages\s*\}\s+from\s+["']react-intl["'];?/)
|
|
19
|
+
rescue Errno::ENOENT
|
|
20
|
+
true
|
|
30
21
|
end
|
|
31
22
|
end
|
|
32
23
|
end
|
|
@@ -15,6 +15,8 @@ module ReactOnRails
|
|
|
15
15
|
"data-component-name" => render_options.react_component_name,
|
|
16
16
|
"data-trace" => (render_options.trace ? true : nil),
|
|
17
17
|
"data-dom-id" => render_options.dom_id,
|
|
18
|
+
"data-hydrate-on" =>
|
|
19
|
+
hydrate_on_data_attribute_value(render_options),
|
|
18
20
|
"data-ssr-identifier-prefix" =>
|
|
19
21
|
(render_options.html_streaming? ? render_options.dom_id : nil),
|
|
20
22
|
"data-store-dependencies" =>
|
|
@@ -39,6 +41,12 @@ module ReactOnRails
|
|
|
39
41
|
spec_tag.html_safe
|
|
40
42
|
end
|
|
41
43
|
|
|
44
|
+
def hydrate_on_data_attribute_value(render_options)
|
|
45
|
+
return unless render_options.internal_option(:hydrate_on) || render_options.hydrate_on != :immediate
|
|
46
|
+
|
|
47
|
+
render_options.hydrate_on
|
|
48
|
+
end
|
|
49
|
+
|
|
42
50
|
def generated_stylesheet_hrefs_json(render_options)
|
|
43
51
|
return unless render_options.auto_load_bundle
|
|
44
52
|
|
|
@@ -4,6 +4,23 @@ require "react_on_rails/utils"
|
|
|
4
4
|
|
|
5
5
|
module ReactOnRails
|
|
6
6
|
module ReactComponent
|
|
7
|
+
HYDRATE_ON_MODES = %i[immediate visible idle].freeze
|
|
8
|
+
|
|
9
|
+
def self.normalize_hydrate_on(value)
|
|
10
|
+
normalized_value = value.is_a?(String) ? value.to_sym : value
|
|
11
|
+
unless HYDRATE_ON_MODES.include?(normalized_value)
|
|
12
|
+
raise ArgumentError,
|
|
13
|
+
"Invalid hydrate_on option: #{value.inspect}. " \
|
|
14
|
+
"Supported OSS modes are :immediate, :visible, and :idle."
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
return normalized_value if normalized_value == :immediate || !ReactOnRails::Utils.react_on_rails_pro?
|
|
18
|
+
|
|
19
|
+
raise ArgumentError,
|
|
20
|
+
"hydrate_on: #{value.inspect} is only supported by the open-source React on Rails client renderer. " \
|
|
21
|
+
"React on Rails Pro does not support hydrate_on scheduling yet; use :immediate."
|
|
22
|
+
end
|
|
23
|
+
|
|
7
24
|
class RenderOptions
|
|
8
25
|
include Utils::Required
|
|
9
26
|
|
|
@@ -54,9 +71,10 @@ module ReactOnRails
|
|
|
54
71
|
def initialize(react_component_name: required("react_component_name"), options: required("options"))
|
|
55
72
|
@react_component_name = react_component_name.camelize
|
|
56
73
|
@options = options
|
|
74
|
+
@hydrate_on = ReactComponent.normalize_hydrate_on(options.fetch(:hydrate_on, :immediate))
|
|
57
75
|
end
|
|
58
76
|
|
|
59
|
-
attr_reader :react_component_name
|
|
77
|
+
attr_reader :react_component_name, :hydrate_on
|
|
60
78
|
|
|
61
79
|
def throw_js_errors
|
|
62
80
|
options.fetch(:throw_js_errors, false)
|