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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 483be34de15d43cc71b1f39a384275d9f3af3b90f201c5777785d24e9377cceb
4
- data.tar.gz: 2fa0039ba762f2a0e26882c1729c914c252197f99aedfc498b344a341863657f
3
+ metadata.gz: 60603cec3dbd4b84deedfe50bc3abcee226363d53fb0250e175f4b9af3f88492
4
+ data.tar.gz: 2a72fffbf70d1b840aeedfed0f7eafa2e378684e8688bef5c1b0b750749a4654
5
5
  SHA512:
6
- metadata.gz: 2c8e998f636ef7aa1465d12587c7536150b83ec2a9b72a546610cbdc38cc29237cb9bb27e247637b60cf3b58385cca6d2c0dc06d267c1aa0adfc112e2d987abf
7
- data.tar.gz: c98e20ac4c306db1faaa0eb49d4155f103057b5fc24baa8f57be7ac7c0325bdd1a2ab8e03f43dbefd01572ef9fbfd53a7c372549d44cb2f15b5072cdb81d155d
6
+ metadata.gz: f0dfddc3d2db936834d1d03f2c69c491359b482d1849d53cb08c6cbc07a052a96a73619ac6deccee5263cb8a72d03770ff9da8217309447330fde064f586a5c6
7
+ data.tar.gz: 525a931c6f9999180a3b388cd09f0cafabad9864b64adf5bb3fd824757c8f1c0511be22b6b0214d3d4c79ba9ef0c4ea5302e58378e659b9b478aae53979040c4
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- react_on_rails (17.0.0.rc.5)
4
+ react_on_rails (17.0.0.rc.6)
5
5
  addressable
6
6
  connection_pool
7
7
  execjs (~> 2.5)
@@ -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("app/javascript/src/HelloWorld/ror_components")
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
- base_files = %w[app/javascript/packs/server-bundle.js]
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
- unless options.redux? || use_rsc? || use_tailwind?
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
- base_files.each { |file| copy_file("#{base_path}#{file}", file) }
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
- "app/javascript/stylesheets/application.css")
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 "app/javascript/src/HelloServer/" if use_rsc? && !options.redux?
690
- return "app/javascript/src/HelloWorldApp/" if options.redux?
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
- "app/javascript/src/HelloWorld/"
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: "app/javascript/src/HelloServer/",
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). An explicit --no-rspack still
927
- # selects Webpack. using_rspack? memoizes, so the rest of the run (e.g.
928
- # configure_rspack_in_shakapacker) stays consistent with this decision.
929
- shakapacker_install_env = using_rspack? ? { "SHAKAPACKER_ASSETS_BUNDLER" => "rspack" } : {}
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
- # Ensure the types directory exists
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
- File.write("app/javascript/types/css-modules.d.ts", css_module_types_content)
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
- "app/javascript/**/*"
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-rc.7 because the discovery plugin export, native Rspack plugin, and
164
- # RSC manifest CSS fixes all ship in that prerelease.
165
- # TODO(#3642): switch to a stable react-on-rails-rsc release after 19.0.5 stable ships.
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
- "React Server Components package pin: all --rsc installs temporarily use " \
494
- "react-on-rails-rsc@#{RSC_PACKAGE_VERSION_PIN}, including Webpack projects. " \
495
- "This prerelease keeps react-on-rails-rsc/WebpackPlugin compatible while adding " \
496
- "react-on-rails-rsc/RspackPlugin. Keep the pin until stable " \
497
- "react-on-rails-rsc@#{rsc_stable_package_version_target} " \
498
- "is published and tagged latest."
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
- "Warning: Could not install the pinned react-on-rails-rsc@#{RSC_PACKAGE_VERSION_PIN}. " \
503
- "All RSC projects are temporarily pinned to that version: the prerelease keeps " \
504
- "react-on-rails-rsc/WebpackPlugin compatible while adding react-on-rails-rsc/RspackPlugin, " \
505
- "and the unversioned `latest` tag may not include both until stable " \
506
- "#{rsc_stable_package_version_target} " \
507
- "is published, so the generator left the version pin in package.json rather than " \
508
- "install a potentially incompatible version."
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
- "app/javascript/src/HelloWorld/ror_components/HelloWorld.client.#{ext}"
39
+ "#{component_dir}/ror_components/HelloWorld.client.#{ext}"
39
40
  server_component =
40
- "app/javascript/src/HelloWorld/ror_components/HelloWorld.server.#{ext}"
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}/#{client_component}", client_component)
47
+ copy_file("#{tailwind_js_path}/#{client_component_template}", client_component)
44
48
  else
45
- copy_file("#{base_js_path}/#{client_component}", client_component)
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
- "app/javascript/src/HelloWorld/ror_components/HelloWorld.module.css")
51
+ "#{component_dir}/ror_components/HelloWorld.module.css")
48
52
  end
49
53
 
50
- copy_file("#{base_js_path}/#{server_component}", server_component)
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: "app/javascript/src/HelloWorld/",
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("app/javascript/src/HelloWorldApp/ror_components")
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("app/javascript/src/HelloWorldApp/#{name}") }
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
- "app/javascript/src/HelloWorldApp/ror_components/HelloWorldApp.client.#{ext}")
61
+ "#{component_dir}/ror_components/HelloWorldApp.client.#{ext}")
59
62
  copy_file("#{base_js_path}/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.server.#{ext}",
60
- "app/javascript/src/HelloWorldApp/ror_components/HelloWorldApp.server.#{ext}")
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
- "app/javascript/src/HelloWorldApp/components/HelloWorld.module.css")
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
- stylesheet_import = "import '../../../stylesheets/application.css';\n"
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
- "app/javascript/src/HelloWorldApp/#{file}")
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
- "app/javascript/src/HelloWorldApp/#{component_file}")
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: "app/javascript/src/HelloWorldApp/",
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 = "app/javascript/src/HelloServer"
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
- # Path is relative to app/javascript/src/HelloServer/components/.
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
@@ -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 {
@@ -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.
@@ -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
- config.source_entry_path,
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
- import { defineMessages } from 'react-intl';
192
-
193
- const defaultLocale = '#{default_locale}';
196
+ const defaultLocale = #{default_locale.to_json};
194
197
 
195
- const defaultMessages = defineMessages(#{@defaults});
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 template_translations
15
- <<~JS
16
- export const translations = #{@translations};
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
- export { defaultMessages, defaultLocale };
29
- JS
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ReactOnRails
4
- VERSION = "17.0.0.rc.5"
4
+ VERSION = "17.0.0.rc.6"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: react_on_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 17.0.0.rc.5
4
+ version: 17.0.0.rc.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Gordon