react_on_rails 16.4.0 → 16.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/lib/generators/USAGE +1 -1
  4. data/lib/generators/react_on_rails/generator_messages.rb +20 -18
  5. data/lib/generators/react_on_rails/install_generator.rb +128 -12
  6. data/lib/generators/react_on_rails/pro_generator.rb +1 -1
  7. data/lib/generators/react_on_rails/pro_setup.rb +3 -3
  8. data/lib/generators/react_on_rails/react_with_redux_generator.rb +2 -1
  9. data/lib/generators/react_on_rails/rsc_generator.rb +1 -1
  10. data/lib/generators/react_on_rails/templates/base/base/bin/dev +1 -1
  11. data/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt +3 -3
  12. data/lib/generators/react_on_rails/templates/pro/base/client/node-renderer.js +8 -4
  13. data/lib/generators/react_on_rails/templates/pro/base/config/initializers/react_on_rails_pro.rb.tt +1 -1
  14. data/lib/generators/react_on_rails/templates/rsc/base/app/controllers/hello_server_controller.rb.tt +1 -1
  15. data/lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/components/HelloServer.jsx +1 -1
  16. data/lib/generators/react_on_rails/templates/rsc/base/app/javascript/src/HelloServer/components/HelloServer.tsx +1 -1
  17. data/lib/generators/react_on_rails/templates/rsc/base/app/views/hello_server/index.html.erb +1 -1
  18. data/lib/generators/react_on_rails/templates/rsc/base/config/webpack/rscWebpackConfig.js.tt +1 -1
  19. data/lib/react_on_rails/config_path_resolver.rb +50 -0
  20. data/lib/react_on_rails/configuration.rb +1 -1
  21. data/lib/react_on_rails/dev/process_manager.rb +16 -1
  22. data/lib/react_on_rails/dev/server_manager.rb +2 -2
  23. data/lib/react_on_rails/doctor.rb +389 -25
  24. data/lib/react_on_rails/git_utils.rb +95 -23
  25. data/lib/react_on_rails/helper.rb +3 -3
  26. data/lib/react_on_rails/packer_utils.rb +1 -1
  27. data/lib/react_on_rails/packs_generator.rb +3 -3
  28. data/lib/react_on_rails/react_component/render_options.rb +48 -0
  29. data/lib/react_on_rails/server_rendering_js_code.rb +1 -1
  30. data/lib/react_on_rails/system_checker.rb +42 -19
  31. data/lib/react_on_rails/test_helper/webpack_assets_compiler.rb +1 -2
  32. data/lib/react_on_rails/utils.rb +1 -1
  33. data/lib/react_on_rails/version.rb +1 -1
  34. data/lib/react_on_rails/version_synchronizer.rb +250 -0
  35. data/lib/tasks/generate_packs.rake +4 -4
  36. data/lib/tasks/sync_versions.rake +23 -0
  37. data/rakelib/examples_config.yml +1 -1
  38. data/rakelib/update_changelog.rake +3 -3
  39. data/react_on_rails.gemspec +1 -1
  40. data/sig/react_on_rails/dev/process_manager.rbs +2 -0
  41. metadata +6 -3
@@ -0,0 +1,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "securerandom"
5
+ require_relative "version_syntax_converter"
6
+ require_relative "version_checker"
7
+
8
+ module ReactOnRails
9
+ # rubocop:disable Metrics/ClassLength
10
+ class VersionSynchronizer
11
+ PACKAGE_SECTIONS = %w[dependencies devDependencies optionalDependencies peerDependencies].freeze
12
+ NPM_ALIAS_PREFIX = "npm:"
13
+ # Matches exact npm versions and rubygem-style prerelease notation (e.g. "1.2.3.rc.4").
14
+ # Prerelease/build segments are intentionally bounded to avoid matching arbitrarily long dotted suffixes.
15
+ EXACT_VERSION_REGEX = /\A\d+\.\d+\.\d+(?:[-.][0-9A-Za-z]+(?:\.[0-9A-Za-z-]+){0,4})?\z/
16
+ PACKAGE_VERSION_SOURCES = {
17
+ "react-on-rails" => :react_on_rails,
18
+ "react-on-rails-pro" => :react_on_rails_pro,
19
+ "react-on-rails-pro-node-renderer" => :react_on_rails_pro
20
+ }.freeze
21
+
22
+ Result = Struct.new(:changes, :changed_files, :unsupported_specs, :missing_source_specs, keyword_init: true)
23
+
24
+ def initialize(package_json_path: VersionChecker::NodePackageVersion.package_json_path, io: $stdout)
25
+ @package_json_path = package_json_path.to_s
26
+ @io = io
27
+ @converter = VersionSyntaxConverter.new
28
+ end
29
+
30
+ def sync(write: false)
31
+ package_json_data, original_content = parse_package_json
32
+ changes, unsupported_specs, missing_source_specs = detect_changes(package_json_data)
33
+
34
+ apply_changes!(package_json_data, changes, original_content) if write && changes.any?
35
+ print_summary(changes,
36
+ unsupported_specs: unsupported_specs,
37
+ missing_source_specs: missing_source_specs,
38
+ write: write)
39
+
40
+ changed_files = write && changes.any? ? [package_json_path] : []
41
+ Result.new(changes: changes,
42
+ changed_files: changed_files,
43
+ unsupported_specs: unsupported_specs,
44
+ missing_source_specs: missing_source_specs)
45
+ end
46
+
47
+ private
48
+
49
+ attr_reader :package_json_path, :io, :converter
50
+
51
+ def parse_package_json
52
+ raise ReactOnRails::Error, "package.json not found at #{package_json_path}" unless File.file?(package_json_path)
53
+
54
+ content = File.read(package_json_path)
55
+ [JSON.parse(content), content]
56
+ rescue JSON::ParserError => e
57
+ raise ReactOnRails::Error, "Invalid JSON in #{package_json_path}: #{e.message}"
58
+ rescue SystemCallError => e
59
+ raise ReactOnRails::Error, "Unable to read #{package_json_path}: #{e.message}"
60
+ end
61
+
62
+ def detect_changes(package_json_data)
63
+ expected_versions = expected_package_versions
64
+ changes = []
65
+ unsupported_specs = []
66
+ missing_source_specs = []
67
+
68
+ PACKAGE_SECTIONS.each do |section|
69
+ dependencies = package_json_data[section]
70
+ next unless dependencies.is_a?(Hash)
71
+
72
+ PACKAGE_VERSION_SOURCES.each do |package_name, source_key|
73
+ next unless dependencies.key?(package_name)
74
+
75
+ current_version = dependencies[package_name]
76
+ parsed_spec = parse_supported_spec(current_version)
77
+ unless parsed_spec
78
+ unsupported_specs << { section: section, package: package_name, version: current_version }
79
+ next
80
+ end
81
+
82
+ expected_version = expected_versions[source_key]
83
+ if expected_version.nil?
84
+ missing_source_specs << { section: section, package: package_name, source: source_key }
85
+ next
86
+ end
87
+ normalized_current_version = converter.rubygem_to_npm(parsed_spec[:version])
88
+ next if normalized_current_version == expected_version
89
+
90
+ changes << {
91
+ section: section,
92
+ package: package_name,
93
+ from: current_version,
94
+ to: rewritten_spec(parsed_spec, expected_version)
95
+ }
96
+ end
97
+ end
98
+
99
+ [changes, unsupported_specs, missing_source_specs]
100
+ end
101
+
102
+ def expected_package_versions
103
+ versions = {
104
+ react_on_rails: converter.rubygem_to_npm(ReactOnRails::VERSION)
105
+ }
106
+
107
+ pro_version = ReactOnRails::Utils.react_on_rails_pro_version
108
+ return versions if pro_version.empty?
109
+
110
+ versions[:react_on_rails_pro] = converter.rubygem_to_npm(pro_version)
111
+ versions
112
+ end
113
+
114
+ def apply_changes!(package_json_data, changes, original_content)
115
+ changes.each do |change|
116
+ package_json_data[change[:section]][change[:package]] = change[:to]
117
+ end
118
+
119
+ indentation = detect_indentation(original_content)
120
+ generated_json = JSON.generate(package_json_data,
121
+ ascii_only: false,
122
+ indent: indentation,
123
+ object_nl: "\n",
124
+ array_nl: "\n",
125
+ space: " ")
126
+ write_atomically("#{generated_json}\n")
127
+ end
128
+
129
+ def print_summary(changes, unsupported_specs:, missing_source_specs:, write:)
130
+ if changes.empty?
131
+ print_no_changes_summary
132
+ else
133
+ print_changes_summary(changes, write: write)
134
+ end
135
+
136
+ print_unsupported_specs(unsupported_specs)
137
+ return if missing_source_specs.empty?
138
+
139
+ io.puts "Skipped packages whose source gem is not loaded:"
140
+ missing_source_specs.each do |spec|
141
+ io.puts " - #{spec[:section]}.#{spec[:package]} (missing #{spec[:source]} gem)"
142
+ end
143
+ end
144
+
145
+ def exact_version?(version)
146
+ version.is_a?(String) && version.match?(EXACT_VERSION_REGEX)
147
+ end
148
+
149
+ def parse_supported_spec(version_spec)
150
+ return { version: version_spec, prefix: nil } if exact_version?(version_spec)
151
+ return unless version_spec.is_a?(String) && version_spec.start_with?(NPM_ALIAS_PREFIX)
152
+
153
+ at_index = version_spec.rindex("@")
154
+ # Ensure at least one package-name character appears after "npm:" and before the version separator.
155
+ return unless at_index && at_index > NPM_ALIAS_PREFIX.length
156
+
157
+ alias_version = version_spec[(at_index + 1)..]
158
+ return unless exact_version?(alias_version)
159
+
160
+ { version: alias_version, prefix: version_spec[0..at_index] }
161
+ end
162
+
163
+ def rewritten_spec(parsed_spec, expected_version)
164
+ return expected_version unless parsed_spec[:prefix]
165
+
166
+ "#{parsed_spec[:prefix]}#{expected_version}"
167
+ end
168
+
169
+ def write_atomically(content)
170
+ tmp_path = "#{package_json_path}.tmp-#{Process.pid}-#{Thread.current.object_id}-#{SecureRandom.hex(4)}"
171
+ File.write(tmp_path, content)
172
+ File.rename(tmp_path, package_json_path)
173
+ rescue StandardError => e
174
+ raise ReactOnRails::Error, "Unable to write #{package_json_path}: #{e.message}"
175
+ ensure
176
+ # On success tmp_path no longer exists (it was renamed), so this is a no-op.
177
+ # On failure, remove any partially-written temp file while preserving package_json_path.
178
+ cleanup_tmp_file(tmp_path)
179
+ end
180
+
181
+ def lockfile_present?
182
+ package_dir = File.dirname(package_json_path)
183
+ %w[yarn.lock package-lock.json pnpm-lock.yaml bun.lock bun.lockb].any? do |lockfile_name|
184
+ File.exist?(File.join(package_dir, lockfile_name))
185
+ end
186
+ end
187
+
188
+ def print_no_changes_summary
189
+ io.puts "No package.json version mismatches found in #{package_json_path}."
190
+ io.puts "Lockfiles may still pin different versions than package.json." if lockfile_present?
191
+ end
192
+
193
+ def print_changes_summary(changes, write:)
194
+ io.puts "Version mismatches detected in #{package_json_path}:"
195
+ changes.each do |change|
196
+ io.puts " - #{change[:section]}.#{change[:package]}: #{change[:from]} -> #{change[:to]}"
197
+ end
198
+
199
+ if write
200
+ io.puts "Updated file:"
201
+ io.puts " - #{package_json_path}"
202
+ io.puts "Run your package manager install command to apply package.json updates."
203
+ io.puts "Lockfiles may still pin previous versions until install completes." if lockfile_present?
204
+ io.puts "Write mode reformats package.json and may normalize whitespace/newline layout."
205
+ io.puts "For minified package.json files, indentation falls back to two spaces."
206
+ else
207
+ io.puts "Dry run only. Re-run with REACT_ON_RAILS_WRITE=true to apply changes."
208
+ end
209
+ end
210
+
211
+ def print_unsupported_specs(unsupported_specs)
212
+ return if unsupported_specs.empty?
213
+
214
+ io.puts "Skipped non-exact version specs (not auto-updated):"
215
+ unsupported_specs.each do |spec|
216
+ io.puts " - #{spec[:section]}.#{spec[:package]}: #{spec[:version]}"
217
+ end
218
+ end
219
+
220
+ def cleanup_tmp_file(tmp_path)
221
+ return unless tmp_path && File.exist?(tmp_path)
222
+
223
+ File.delete(tmp_path)
224
+ rescue SystemCallError => e
225
+ warn "react_on_rails: could not remove temp file #{tmp_path}: #{e.message}"
226
+ end
227
+
228
+ def detect_indentation(content)
229
+ normalized_content = content.gsub("\r\n", "\n")
230
+ indentations = normalized_content.each_line.filter_map { |line| line.slice(/^[ \t]+(?="[^"\n]+":)/) }
231
+ return " " if indentations.empty?
232
+
233
+ indentation = indentation_for_majority_char(indentations)
234
+ indentation.nil? || indentation.empty? ? " " : indentation
235
+ end
236
+
237
+ def indentation_for_majority_char(indentations)
238
+ # Compare indentation widths within the dominant whitespace character class to avoid tabs
239
+ # (length 1) always winning against space indentation.
240
+ char = predominant_indent_char(indentations)
241
+ same_char = indentations.select { |indentation| indentation.chars.uniq == [char] }
242
+ (same_char.empty? ? indentations : same_char).min_by(&:length)
243
+ end
244
+
245
+ def predominant_indent_char(indentations)
246
+ indentations.map { |indentation| indentation[0] }.tally.max_by { |_value, count| count }&.first || " "
247
+ end
248
+ end
249
+ # rubocop:enable Metrics/ClassLength
250
+ end
@@ -92,8 +92,8 @@ namespace :react_on_rails do
92
92
  puts Rainbow(" • Delete the common component file (e.g., Component.jsx)").white
93
93
  puts Rainbow(" • Keep only the client/server specific files " \
94
94
  "(Component.client.jsx, Component.server.jsx)").white
95
- puts Rainbow(" • See: https://www.shakacode.com/react-on-rails/docs/guides/" \
96
- "auto-bundling-file-system-based-automated-bundle-generation.md").cyan
95
+ puts Rainbow(" • See: https://reactonrails.com/docs/core-concepts/" \
96
+ "auto-bundling-file-system-based-automated-bundle-generation/").cyan
97
97
 
98
98
  when /Cannot find component/
99
99
  puts Rainbow(" • Check that your component file exists in the expected location").white
@@ -127,9 +127,9 @@ namespace :react_on_rails do
127
127
  def show_documentation_links
128
128
  puts ""
129
129
  puts Rainbow("📚 DOCUMENTATION:").magenta.bold
130
- puts Rainbow(" • File-system based components: https://www.shakacode.com/react-on-rails/docs/" \
130
+ puts Rainbow(" • File-system based components: https://reactonrails.com/docs/" \
131
131
  "guides/auto-bundling-file-system-based-automated-bundle-generation.md").cyan
132
- puts Rainbow(" • Component registration: https://www.shakacode.com/react-on-rails/docs/").cyan
132
+ puts Rainbow(" • Component registration: https://reactonrails.com/docs/").cyan
133
133
  puts Rainbow("=" * 80).red
134
134
  end
135
135
 
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../react_on_rails"
4
+ require_relative "../react_on_rails/version_synchronizer"
5
+
6
+ namespace :react_on_rails do
7
+ task :prepare_sync_versions do
8
+ ENV["REACT_ON_RAILS_SKIP_VALIDATION"] = "true"
9
+ end
10
+
11
+ desc "Sync package.json versions with gem versions (dry-run by default; " \
12
+ "REACT_ON_RAILS_WRITE=true (or WRITE=true) writes and may reformat package.json; " \
13
+ "REACT_ON_RAILS_DRY_RUN=true (or DRY_RUN=true) forces dry-run mode)"
14
+ task sync_versions: %i[prepare_sync_versions environment] do
15
+ write = ENV.fetch("REACT_ON_RAILS_WRITE", ENV.fetch("WRITE", "false")) == "true"
16
+ # Also accept shorter WRITE/DRY_RUN aliases.
17
+ dry_run = ENV.fetch("REACT_ON_RAILS_DRY_RUN", ENV.fetch("DRY_RUN", "false")) == "true"
18
+
19
+ raise ReactOnRails::Error, "WRITE and DRY_RUN cannot both be true" if write && dry_run
20
+
21
+ ReactOnRails::VersionSynchronizer.new.sync(write: write)
22
+ end
23
+ end
@@ -5,7 +5,7 @@
5
5
  # - Latest CI (all PRs): Runs shakapacker_examples_latest (React 19, Shakapacker 9.x)
6
6
  # Examples: basic, basic-server-rendering, redux, redux-server-rendering
7
7
  #
8
- # - Pinned CI (master): Runs shakapacker_examples_pinned (React 16, 17, 18 with Shakapacker 8.2.0)
8
+ # - Pinned CI (main): Runs shakapacker_examples_pinned (React 16, 17, 18 with Shakapacker 8.2.0)
9
9
  # Examples: basic-react16, basic-server-rendering-react16,
10
10
  # basic-react17, basic-server-rendering-react17,
11
11
  # basic-react18, basic-server-rendering-react18
@@ -213,7 +213,7 @@ def cleanup_collapsed_prerelease_links(changelog, base_version)
213
213
  if stable_from
214
214
  # Update [unreleased] link to compare from the stable version instead of the old prerelease
215
215
  changelog = changelog.sub(
216
- /^(\[unreleased\]:\s*#{compare_prefix})\S+(\.\.\.master)/i,
216
+ /^(\[unreleased\]:\s*#{compare_prefix})\S+(\.\.\.main)/i,
217
217
  "\\1#{stable_from}\\2"
218
218
  )
219
219
  end
@@ -426,11 +426,11 @@ end
426
426
  # anchor: markdown anchor (e.g., "[16.2.0.beta.20]")
427
427
  def update_changelog_links(changelog, version, anchor)
428
428
  compare_link_prefix = "https://github.com/shakacode/react_on_rails/compare"
429
- match_data = %r{#{compare_link_prefix}/(?<prev_version>.*)\.\.\.master}.match(changelog)
429
+ match_data = %r{#{compare_link_prefix}/(?<prev_version>.*)\.\.\.main}.match(changelog)
430
430
  return unless match_data
431
431
 
432
432
  prev_version = match_data[:prev_version]
433
- new_unreleased_link = "#{compare_link_prefix}/v#{version}...master"
433
+ new_unreleased_link = "#{compare_link_prefix}/v#{version}...main"
434
434
  new_version_link = "#{anchor}: #{compare_link_prefix}/#{prev_version}...v#{version}"
435
435
  changelog.sub!(match_data[0], "#{new_unreleased_link}\n#{new_version_link}")
436
436
  end
@@ -39,7 +39,7 @@ Gem::Specification.new do |s|
39
39
  s.add_development_dependency "gem-release"
40
40
  s.post_install_message = '
41
41
  --------------------------------------------------------------------------------
42
- Checkout https://www.shakacode.com/react-on-rails-pro for information about
42
+ Checkout https://pro.reactonrails.com for information about
43
43
  "React on Rails Pro" which includes a gem for better performance, via caching helpers, and our
44
44
  node rendering server, support for React 19, and much more.
45
45
  --------------------------------------------------------------------------------
@@ -2,6 +2,7 @@ module ReactOnRails
2
2
  module Dev
3
3
  class ProcessManager
4
4
  VERSION_CHECK_TIMEOUT: Integer
5
+ ENV_KEYS_TO_PRESERVE: Array[String]
5
6
 
6
7
  def self.installed?: (String) -> bool
7
8
  def self.ensure_procfile: (String) -> void
@@ -16,6 +17,7 @@ module ReactOnRails
16
17
  def self.process_available_in_system?: (String) -> bool
17
18
  def self.with_unbundled_context: () { () -> untyped } -> untyped
18
19
  def self.show_process_manager_installation_help: () -> void
20
+ def self.preserve_runtime_env_vars: () -> Hash[String, String]
19
21
  def self.valid_procfile_path?: (String) -> bool
20
22
  end
21
23
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: react_on_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 16.4.0
4
+ version: 16.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Gordon
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-03-17 00:00:00.000000000 Z
11
+ date: 2026-03-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -210,6 +210,7 @@ files:
210
210
  - lib/generators/react_on_rails/templates/rsc/base/app/views/hello_server/index.html.erb
211
211
  - lib/generators/react_on_rails/templates/rsc/base/config/webpack/rscWebpackConfig.js.tt
212
212
  - lib/react_on_rails.rb
213
+ - lib/react_on_rails/config_path_resolver.rb
213
214
  - lib/react_on_rails/configuration.rb
214
215
  - lib/react_on_rails/controller.rb
215
216
  - lib/react_on_rails/dev.rb
@@ -248,11 +249,13 @@ files:
248
249
  - lib/react_on_rails/utils.rb
249
250
  - lib/react_on_rails/version.rb
250
251
  - lib/react_on_rails/version_checker.rb
252
+ - lib/react_on_rails/version_synchronizer.rb
251
253
  - lib/react_on_rails/version_syntax_converter.rb
252
254
  - lib/tasks/assets.rake
253
255
  - lib/tasks/doctor.rake
254
256
  - lib/tasks/generate_packs.rake
255
257
  - lib/tasks/locale.rake
258
+ - lib/tasks/sync_versions.rake
256
259
  - rakelib/docker.rake
257
260
  - rakelib/dummy_apps.rake
258
261
  - rakelib/example_type.rb
@@ -296,7 +299,7 @@ metadata: {}
296
299
  post_install_message: |2
297
300
 
298
301
  --------------------------------------------------------------------------------
299
- Checkout https://www.shakacode.com/react-on-rails-pro for information about
302
+ Checkout https://pro.reactonrails.com for information about
300
303
  "React on Rails Pro" which includes a gem for better performance, via caching helpers, and our
301
304
  node rendering server, support for React 19, and much more.
302
305
  --------------------------------------------------------------------------------