react_on_rails 14.2.1 → 16.1.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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/AI_AGENT_INSTRUCTIONS.md +63 -0
  3. data/CHANGELOG.md +564 -85
  4. data/CLAUDE.md +135 -0
  5. data/CODING_AGENTS.md +313 -0
  6. data/CONTRIBUTING.md +448 -37
  7. data/Gemfile.development_dependencies +6 -1
  8. data/Gemfile.lock +13 -4
  9. data/KUDOS.md +22 -1
  10. data/LICENSE.md +30 -4
  11. data/LICENSES/README.md +14 -0
  12. data/NEWS.md +48 -48
  13. data/PROJECTS.md +45 -40
  14. data/REACT-ON-RAILS-PRO-LICENSE.md +129 -0
  15. data/README.md +113 -42
  16. data/SUMMARY.md +62 -52
  17. data/TODO.md +135 -0
  18. data/bin/lefthook/check-trailing-newlines +38 -0
  19. data/bin/lefthook/get-changed-files +26 -0
  20. data/bin/lefthook/prettier-format +26 -0
  21. data/bin/lefthook/ruby-autofix +26 -0
  22. data/bin/lefthook/ruby-lint +27 -0
  23. data/eslint.config.ts +232 -0
  24. data/knip.ts +40 -6
  25. data/lib/generators/USAGE +4 -5
  26. data/lib/generators/react_on_rails/USAGE +65 -0
  27. data/lib/generators/react_on_rails/base_generator.rb +276 -62
  28. data/lib/generators/react_on_rails/dev_tests_generator.rb +1 -0
  29. data/lib/generators/react_on_rails/generator_helper.rb +35 -1
  30. data/lib/generators/react_on_rails/generator_messages.rb +138 -17
  31. data/lib/generators/react_on_rails/install_generator.rb +474 -26
  32. data/lib/generators/react_on_rails/react_no_redux_generator.rb +19 -6
  33. data/lib/generators/react_on_rails/react_with_redux_generator.rb +110 -18
  34. data/lib/generators/react_on_rails/templates/.eslintrc +1 -1
  35. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev +5 -0
  36. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev-prod-assets +8 -0
  37. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev-static-assets +2 -0
  38. data/lib/generators/react_on_rails/templates/base/base/app/javascript/bundles/HelloWorld/components/HelloWorld.jsx +0 -5
  39. data/lib/generators/react_on_rails/templates/base/base/app/javascript/bundles/HelloWorld/components/HelloWorld.module.css +2 -2
  40. data/lib/generators/react_on_rails/templates/base/base/app/javascript/bundles/HelloWorld/components/HelloWorldServer.js +1 -1
  41. data/lib/generators/react_on_rails/templates/base/base/app/javascript/packs/server-bundle.js +1 -8
  42. data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.client.jsx +21 -0
  43. data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.client.tsx +25 -0
  44. data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.module.css +4 -0
  45. data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.server.jsx +5 -0
  46. data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.server.tsx +5 -0
  47. data/lib/generators/react_on_rails/templates/base/base/app/views/hello_world/index.html.erb.tt +1 -1
  48. data/lib/generators/react_on_rails/templates/base/base/app/views/layouts/hello_world.html.erb +4 -2
  49. data/lib/generators/react_on_rails/templates/base/base/babel.config.js.tt +5 -2
  50. data/lib/generators/react_on_rails/templates/base/base/bin/dev +34 -0
  51. data/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt +14 -5
  52. data/lib/generators/react_on_rails/templates/base/base/config/shakapacker.yml +76 -7
  53. data/lib/generators/react_on_rails/templates/base/base/config/webpack/commonWebpackConfig.js.tt +1 -1
  54. data/lib/generators/react_on_rails/templates/base/base/config/webpack/development.js.tt +6 -10
  55. data/lib/generators/react_on_rails/templates/base/base/config/webpack/production.js.tt +2 -2
  56. data/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt +3 -2
  57. data/lib/generators/react_on_rails/templates/base/base/config/webpack/test.js.tt +2 -2
  58. data/lib/generators/react_on_rails/templates/dev_tests/spec/system/hello_world_spec.rb +0 -2
  59. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/actions/helloWorldActionCreators.ts +18 -0
  60. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/components/HelloWorld.jsx +0 -6
  61. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/components/HelloWorld.module.css +4 -0
  62. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/components/HelloWorld.tsx +24 -0
  63. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/constants/helloWorldConstants.ts +6 -0
  64. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/containers/HelloWorldContainer.ts +20 -0
  65. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/reducers/helloWorldReducer.js +1 -1
  66. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/reducers/helloWorldReducer.ts +22 -0
  67. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.client.tsx +23 -0
  68. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.server.jsx +5 -0
  69. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.server.tsx +5 -0
  70. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/store/helloWorldStore.ts +18 -0
  71. data/lib/react_on_rails/configuration.rb +141 -57
  72. data/lib/react_on_rails/controller.rb +6 -2
  73. data/lib/react_on_rails/dev/file_manager.rb +78 -0
  74. data/lib/react_on_rails/dev/pack_generator.rb +27 -0
  75. data/lib/react_on_rails/dev/process_manager.rb +61 -0
  76. data/lib/react_on_rails/dev/server_manager.rb +487 -0
  77. data/lib/react_on_rails/dev.rb +20 -0
  78. data/lib/react_on_rails/doctor.rb +1149 -0
  79. data/lib/react_on_rails/engine.rb +6 -0
  80. data/lib/react_on_rails/git_utils.rb +12 -2
  81. data/lib/react_on_rails/helper.rb +176 -74
  82. data/lib/react_on_rails/json_parse_error.rb +6 -1
  83. data/lib/react_on_rails/packer_utils.rb +61 -71
  84. data/lib/react_on_rails/packs_generator.rb +221 -19
  85. data/lib/react_on_rails/prerender_error.rb +4 -0
  86. data/lib/react_on_rails/pro/NOTICE +21 -0
  87. data/lib/react_on_rails/pro/helper.rb +122 -0
  88. data/lib/react_on_rails/pro/utils.rb +53 -0
  89. data/lib/react_on_rails/react_component/render_options.rb +38 -6
  90. data/lib/react_on_rails/server_rendering_js_code.rb +0 -1
  91. data/lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb +12 -5
  92. data/lib/react_on_rails/system_checker.rb +659 -0
  93. data/lib/react_on_rails/test_helper/webpack_assets_compiler.rb +1 -1
  94. data/lib/react_on_rails/test_helper/webpack_assets_status_checker.rb +6 -4
  95. data/lib/react_on_rails/test_helper.rb +2 -3
  96. data/lib/react_on_rails/utils.rb +139 -43
  97. data/lib/react_on_rails/version.rb +1 -1
  98. data/lib/react_on_rails/version_checker.rb +14 -20
  99. data/lib/react_on_rails/version_syntax_converter.rb +1 -1
  100. data/lib/react_on_rails.rb +1 -0
  101. data/lib/tasks/assets.rake +1 -1
  102. data/lib/tasks/doctor.rake +48 -0
  103. data/lib/tasks/generate_packs.rake +158 -1
  104. data/react_on_rails.gemspec +1 -0
  105. data/tsconfig.eslint.json +6 -0
  106. data/tsconfig.json +5 -3
  107. metadata +63 -14
  108. data/REACT-ON-RAILS-PRO-LICENSE +0 -95
  109. data/lib/generators/react_on_rails/adapt_for_older_shakapacker_generator.rb +0 -41
  110. data/lib/generators/react_on_rails/bin/dev +0 -30
  111. data/lib/generators/react_on_rails/bin/dev-static +0 -30
  112. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev-static.tt +0 -9
  113. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev.tt +0 -5
  114. data/lib/generators/react_on_rails/templates/base/base/app/javascript/packs/registration.js.tt +0 -8
  115. /data/lib/generators/react_on_rails/templates/base/base/config/webpack/{webpackConfig.js.tt → generateWebpackConfigs.js.tt} +0 -0
  116. /data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/startup/{HelloWorldApp.jsx → HelloWorldApp.client.jsx} +0 -0
@@ -0,0 +1,659 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module ReactOnRails
6
+ # SystemChecker provides validation methods for React on Rails setup
7
+ # Used by install generator and doctor rake task
8
+ # rubocop:disable Metrics/ClassLength
9
+ class SystemChecker
10
+ attr_reader :messages
11
+
12
+ def initialize
13
+ @messages = []
14
+ end
15
+
16
+ def add_error(message)
17
+ @messages << { type: :error, content: message }
18
+ end
19
+
20
+ def add_warning(message)
21
+ @messages << { type: :warning, content: message }
22
+ end
23
+
24
+ def add_success(message)
25
+ @messages << { type: :success, content: message }
26
+ end
27
+
28
+ def add_info(message)
29
+ @messages << { type: :info, content: message }
30
+ end
31
+
32
+ def errors?
33
+ @messages.any? { |msg| msg[:type] == :error }
34
+ end
35
+
36
+ def warnings?
37
+ @messages.any? { |msg| msg[:type] == :warning }
38
+ end
39
+
40
+ # Node.js validation
41
+ def check_node_installation
42
+ if node_missing?
43
+ add_error(<<~MSG.strip)
44
+ 🚫 Node.js is required but not found on your system.
45
+
46
+ Please install Node.js before continuing:
47
+ • Download from: https://nodejs.org/en/
48
+ • Recommended: Use a version manager like nvm, fnm, or volta
49
+ • Minimum required version: Node.js 18+
50
+
51
+ After installation, restart your terminal and try again.
52
+ MSG
53
+ return false
54
+ end
55
+
56
+ check_node_version
57
+ true
58
+ end
59
+
60
+ def check_node_version
61
+ stdout, stderr, status = Open3.capture3("node", "--version")
62
+
63
+ # Use stdout if available, fallback to stderr if stdout is empty
64
+ node_version = stdout.strip
65
+ node_version = stderr.strip if node_version.empty?
66
+
67
+ # Return early if node is not found (non-zero status) or no output
68
+ return if !status.success? || node_version.empty?
69
+
70
+ # Extract major version number (e.g., "v18.17.0" -> 18)
71
+ major_version = node_version[/v(\d+)/, 1]&.to_i
72
+ return unless major_version
73
+
74
+ if major_version < 18
75
+ add_warning(<<~MSG.strip)
76
+ ⚠️ Node.js version #{node_version} detected.
77
+
78
+ React on Rails recommends Node.js 18+ for best compatibility.
79
+ You may experience issues with older versions.
80
+
81
+ Consider upgrading: https://nodejs.org/en/
82
+ MSG
83
+ else
84
+ add_success("✅ Node.js #{node_version} is installed and compatible")
85
+ end
86
+ end
87
+
88
+ # Package manager validation
89
+ def check_package_manager
90
+ package_managers = %w[npm pnpm yarn bun]
91
+ available_managers = package_managers.select { |pm| cli_exists?(pm) }
92
+
93
+ if available_managers.empty?
94
+ add_error(<<~MSG.strip)
95
+ 🚫 No JavaScript package manager found on your system.
96
+
97
+ React on Rails requires a JavaScript package manager to install dependencies.
98
+ Please install one of the following:
99
+
100
+ • npm: Usually comes with Node.js (https://nodejs.org/en/)
101
+ • yarn: npm install -g yarn (https://yarnpkg.com/)
102
+ • pnpm: npm install -g pnpm (https://pnpm.io/)
103
+ • bun: Install from https://bun.sh/
104
+
105
+ After installation, restart your terminal and try again.
106
+ MSG
107
+ return false
108
+ end
109
+
110
+ # Detect which package manager is actually being used
111
+ used_manager = detect_used_package_manager
112
+ if used_manager
113
+ version_info = get_package_manager_version(used_manager)
114
+ deprecation_note = get_deprecation_note(used_manager, version_info)
115
+ message = "✅ Package manager in use: #{used_manager} #{version_info}"
116
+ message += deprecation_note if deprecation_note
117
+ add_success(message)
118
+ else
119
+ add_success("✅ Package managers available: #{available_managers.join(', ')}")
120
+ add_info("ℹ️ No lock file detected - run npm/yarn/pnpm install to establish which manager is used")
121
+ end
122
+ true
123
+ end
124
+
125
+ # Shakapacker validation
126
+ def check_shakapacker_configuration
127
+ unless shakapacker_configured?
128
+ add_error(<<~MSG.strip)
129
+ 🚫 Shakapacker is not properly configured.
130
+
131
+ Missing one or more required files:
132
+ • bin/shakapacker
133
+ • bin/shakapacker-dev-server
134
+ • config/shakapacker.yml
135
+ • config/webpack/webpack.config.js
136
+
137
+ Run: bundle exec rails shakapacker:install
138
+ MSG
139
+ return false
140
+ end
141
+
142
+ report_shakapacker_version_with_threshold
143
+ check_shakapacker_in_gemfile
144
+ true
145
+ end
146
+
147
+ def check_shakapacker_in_gemfile
148
+ if shakapacker_in_gemfile?
149
+ add_success("✅ Shakapacker is declared in Gemfile")
150
+ else
151
+ add_warning(<<~MSG.strip)
152
+ ⚠️ Shakapacker not found in Gemfile.
153
+
154
+ While Shakapacker might be available as a dependency,
155
+ it's recommended to add it explicitly to your Gemfile:
156
+
157
+ bundle add shakapacker --strict
158
+ MSG
159
+ end
160
+ end
161
+
162
+ # React on Rails package validation
163
+ def check_react_on_rails_packages
164
+ check_react_on_rails_gem
165
+ check_react_on_rails_npm_package
166
+ check_package_version_sync
167
+ check_gemfile_version_patterns
168
+ end
169
+
170
+ def check_react_on_rails_gem
171
+ require "react_on_rails"
172
+ add_success("✅ React on Rails gem #{ReactOnRails::VERSION} is loaded")
173
+ rescue LoadError
174
+ add_error(<<~MSG.strip)
175
+ 🚫 React on Rails gem is not available.
176
+
177
+ Add to your Gemfile:
178
+ gem 'react_on_rails'
179
+
180
+ Then run: bundle install
181
+ MSG
182
+ end
183
+
184
+ def check_react_on_rails_npm_package
185
+ package_json_path = "package.json"
186
+ return unless File.exist?(package_json_path)
187
+
188
+ package_json = JSON.parse(File.read(package_json_path))
189
+ npm_version = package_json.dig("dependencies", "react-on-rails") ||
190
+ package_json.dig("devDependencies", "react-on-rails")
191
+
192
+ if npm_version
193
+ add_success("✅ react-on-rails NPM package #{npm_version} is declared")
194
+ else
195
+ add_warning(<<~MSG.strip)
196
+ ⚠️ react-on-rails NPM package not found in package.json.
197
+
198
+ Install it with:
199
+ npm install react-on-rails
200
+ MSG
201
+ end
202
+ rescue JSON::ParserError
203
+ add_warning("⚠️ Could not parse package.json")
204
+ end
205
+
206
+ def check_package_version_sync # rubocop:disable Metrics/CyclomaticComplexity
207
+ return unless File.exist?("package.json")
208
+
209
+ begin
210
+ package_json = JSON.parse(File.read("package.json"))
211
+ npm_version = package_json.dig("dependencies", "react-on-rails") ||
212
+ package_json.dig("devDependencies", "react-on-rails")
213
+
214
+ return unless npm_version && defined?(ReactOnRails::VERSION)
215
+
216
+ # Clean version strings for comparison (remove ^, ~, =, etc.)
217
+ clean_npm_version = npm_version.gsub(/[^0-9.]/, "")
218
+ gem_version = ReactOnRails::VERSION
219
+
220
+ if clean_npm_version == gem_version
221
+ add_success("✅ React on Rails gem and NPM package versions match (#{gem_version})")
222
+ check_version_patterns(npm_version, gem_version)
223
+ else
224
+ # Check for major version differences
225
+ gem_major = gem_version.split(".")[0].to_i
226
+ npm_major = clean_npm_version.split(".")[0].to_i
227
+
228
+ if gem_major != npm_major # rubocop:disable Style/NegatedIfElseCondition
229
+ add_error(<<~MSG.strip)
230
+ 🚫 Major version mismatch detected:
231
+ • Gem version: #{gem_version} (major: #{gem_major})
232
+ • NPM version: #{npm_version} (major: #{npm_major})
233
+
234
+ Major version differences can cause serious compatibility issues.
235
+ Update both packages to use the same major version immediately.
236
+ MSG
237
+ else
238
+ add_warning(<<~MSG.strip)
239
+ ⚠️ Version mismatch detected:
240
+ • Gem version: #{gem_version}
241
+ • NPM version: #{npm_version}
242
+
243
+ Consider updating to exact, fixed matching versions of gem and npm package for best compatibility.
244
+ MSG
245
+ end
246
+ end
247
+ rescue JSON::ParserError
248
+ # Ignore parsing errors, already handled elsewhere
249
+ rescue StandardError
250
+ # Handle other errors gracefully
251
+ end
252
+ end
253
+
254
+ # React dependencies validation
255
+ def check_react_dependencies
256
+ return unless File.exist?("package.json")
257
+
258
+ package_json = parse_package_json
259
+ return unless package_json
260
+
261
+ # Check core React dependencies
262
+ required_deps = required_react_dependencies
263
+ missing_deps = find_missing_dependencies(package_json, required_deps)
264
+ report_dependency_status(required_deps, missing_deps, package_json)
265
+
266
+ # Check additional build dependencies (informational)
267
+ check_build_dependencies(package_json)
268
+
269
+ # Report versions
270
+ report_dependency_versions(package_json)
271
+ end
272
+
273
+ # Rails integration validation
274
+
275
+ def check_react_on_rails_initializer
276
+ initializer_path = "config/initializers/react_on_rails.rb"
277
+ if File.exist?(initializer_path)
278
+ add_success("✅ React on Rails initializer exists")
279
+ else
280
+ add_warning(<<~MSG.strip)
281
+ ⚠️ React on Rails initializer not found.
282
+
283
+ Create: config/initializers/react_on_rails.rb
284
+ Or run: rails generate react_on_rails:install
285
+ MSG
286
+ end
287
+ end
288
+
289
+ # Webpack configuration validation
290
+ def check_webpack_configuration
291
+ webpack_config_path = "config/webpack/webpack.config.js"
292
+ if File.exist?(webpack_config_path)
293
+ add_success("✅ Webpack configuration exists")
294
+ check_webpack_config_content
295
+ suggest_webpack_inspection
296
+ else
297
+ add_error(<<~MSG.strip)
298
+ 🚫 Webpack configuration not found.
299
+
300
+ Expected: config/webpack/webpack.config.js
301
+ Run: rails generate react_on_rails:install
302
+ MSG
303
+ end
304
+ end
305
+
306
+ def suggest_webpack_inspection
307
+ add_info("💡 To debug webpack builds:")
308
+ add_info(" bin/shakapacker --mode=development --progress")
309
+ add_info(" bin/shakapacker --mode=production --progress")
310
+ add_info(" bin/shakapacker --debug-shakapacker # Debug Shakapacker configuration")
311
+
312
+ add_info("💡 Advanced webpack debugging:")
313
+ add_info(" 1. Add 'debugger;' before 'module.exports' in config/webpack/webpack.config.js")
314
+ add_info(" 2. Run: ./bin/shakapacker --debug-shakapacker")
315
+ add_info(" 3. Open Chrome DevTools to inspect config object")
316
+ add_info(" 📖 See: https://github.com/shakacode/shakapacker/blob/main/docs/troubleshooting.md#debugging-your-webpack-config")
317
+
318
+ add_info("💡 To analyze bundle size:")
319
+ if bundle_analyzer_available?
320
+ add_info(" ANALYZE=true bin/shakapacker")
321
+ add_info(" This opens webpack-bundle-analyzer in your browser")
322
+ else
323
+ add_info(" 1. yarn add --dev webpack-bundle-analyzer")
324
+ add_info(" 2. Add to config/webpack/webpack.config.js:")
325
+ add_info(" const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');")
326
+ add_info(" // Add to plugins array when process.env.ANALYZE")
327
+ add_info(" 3. ANALYZE=true bin/shakapacker")
328
+ add_info(" Or use Shakapacker's built-in support if available")
329
+ end
330
+
331
+ add_info("💡 Generate webpack stats for analysis:")
332
+ add_info(" bin/shakapacker --json > webpack-stats.json")
333
+ add_info(" Upload to webpack.github.io/analyse or webpack-bundle-analyzer.com")
334
+ end
335
+
336
+ def bundle_analyzer_available?
337
+ return false unless File.exist?("package.json")
338
+
339
+ begin
340
+ package_json = JSON.parse(File.read("package.json"))
341
+ all_deps = package_json["dependencies"]&.merge(package_json["devDependencies"] || {}) || {}
342
+ all_deps["webpack-bundle-analyzer"]
343
+ rescue StandardError
344
+ false
345
+ end
346
+ end
347
+
348
+ def check_webpack_config_content
349
+ webpack_config_path = "config/webpack/webpack.config.js"
350
+ content = File.read(webpack_config_path)
351
+
352
+ if react_on_rails_config?(content)
353
+ add_success("✅ Webpack config includes React on Rails environment configuration")
354
+ add_info(" ℹ️ Environment-specific configs detected for optimal React on Rails integration")
355
+ elsif standard_shakapacker_config?(content)
356
+ add_warning(<<~MSG.strip)
357
+ ⚠️ Standard Shakapacker webpack config detected.
358
+
359
+ React on Rails works better with environment-specific configuration.
360
+ Consider running: rails generate react_on_rails:install --force
361
+ This adds client and server environment configs for better performance.
362
+ MSG
363
+ else
364
+ add_info("ℹ️ Custom webpack config detected")
365
+ add_info(" 💡 Ensure config supports both client and server rendering")
366
+ add_info(" 💡 Verify React JSX transformation is configured")
367
+ add_info(" 💡 Check that asset output paths match Rails expectations")
368
+ end
369
+ end
370
+
371
+ private
372
+
373
+ def node_missing?
374
+ command = ReactOnRails::Utils.running_on_windows? ? "where" : "which"
375
+ _stdout, _stderr, status = Open3.capture3(command, "node")
376
+ !status.success?
377
+ end
378
+
379
+ def cli_exists?(command)
380
+ which_command = ReactOnRails::Utils.running_on_windows? ? "where" : "which"
381
+ _stdout, _stderr, status = Open3.capture3(which_command, command)
382
+ status.success?
383
+ end
384
+
385
+ def detect_used_package_manager
386
+ # Check for lock files to determine which package manager is being used
387
+ if File.exist?("yarn.lock")
388
+ "yarn"
389
+ elsif File.exist?("pnpm-lock.yaml")
390
+ "pnpm"
391
+ elsif File.exist?("bun.lockb")
392
+ "bun"
393
+ elsif File.exist?("package-lock.json")
394
+ "npm"
395
+ end
396
+ end
397
+
398
+ def get_package_manager_version(manager)
399
+ begin
400
+ stdout, _stderr, status = Open3.capture3(manager, "--version")
401
+ return stdout.strip if status.success? && !stdout.strip.empty?
402
+ rescue StandardError
403
+ # Ignore errors
404
+ end
405
+ "(version unknown)"
406
+ end
407
+
408
+ def get_deprecation_note(manager, version)
409
+ case manager
410
+ when "yarn"
411
+ " (Classic Yarn v1 - consider upgrading to Yarn Modern)" if /^1\./.match?(version)
412
+ end
413
+ end
414
+
415
+ def shakapacker_configured?
416
+ File.exist?("bin/shakapacker") &&
417
+ File.exist?("bin/shakapacker-dev-server") &&
418
+ File.exist?("config/shakapacker.yml") &&
419
+ File.exist?("config/webpack/webpack.config.js")
420
+ end
421
+
422
+ def shakapacker_in_gemfile?
423
+ gemfile = ENV["BUNDLE_GEMFILE"] || "Gemfile"
424
+ File.file?(gemfile) &&
425
+ File.foreach(gemfile).any? { |l| l.match?(/^\s*gem\s+['"]shakapacker['"]/) }
426
+ end
427
+
428
+ def react_on_rails_config?(content)
429
+ content.include?("envSpecificConfig") || content.include?("env.nodeEnv")
430
+ end
431
+
432
+ def standard_shakapacker_config?(content)
433
+ normalized = normalize_config_content(content)
434
+ shakapacker_patterns = [
435
+ /generateWebpackConfig.*require.*shakapacker/,
436
+ /webpackConfig.*require.*shakapacker/
437
+ ]
438
+ shakapacker_patterns.any? { |pattern| normalized.match?(pattern) }
439
+ end
440
+
441
+ def normalize_config_content(content)
442
+ content.gsub(%r{//.*$}, "") # Remove single-line comments
443
+ .gsub(%r{/\*.*?\*/}m, "") # Remove multi-line comments
444
+ .gsub(/\s+/, " ") # Normalize whitespace
445
+ .strip
446
+ end
447
+
448
+ def required_react_dependencies
449
+ {
450
+ "react" => "React library",
451
+ "react-dom" => "React DOM library",
452
+ "@babel/preset-react" => "Babel React preset"
453
+ }
454
+ end
455
+
456
+ def additional_build_dependencies
457
+ {
458
+ "webpack" => "Webpack bundler",
459
+ "@babel/core" => "Babel compiler core",
460
+ "@babel/preset-env" => "Babel environment preset",
461
+ "css-loader" => "CSS loader for Webpack",
462
+ "style-loader" => "Style loader for Webpack",
463
+ "mini-css-extract-plugin" => "CSS extraction plugin",
464
+ "webpack-dev-server" => "Webpack development server"
465
+ }
466
+ end
467
+
468
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
469
+ def check_build_dependencies(package_json)
470
+ build_deps = additional_build_dependencies
471
+ all_deps = package_json["dependencies"]&.merge(package_json["devDependencies"] || {}) || {}
472
+
473
+ present_deps = []
474
+ missing_deps = []
475
+
476
+ build_deps.each do |package, description|
477
+ if all_deps[package]
478
+ present_deps << "#{description} (#{package})"
479
+ else
480
+ missing_deps << "#{description} (#{package})"
481
+ end
482
+ end
483
+
484
+ unless present_deps.empty?
485
+ short_list = present_deps.take(3).join(", ")
486
+ suffix = present_deps.length > 3 ? "..." : ""
487
+ add_info("✅ Build dependencies found: #{short_list}#{suffix}")
488
+ end
489
+
490
+ return if missing_deps.empty?
491
+
492
+ short_list = missing_deps.take(3).join(", ")
493
+ suffix = missing_deps.length > 3 ? "..." : ""
494
+ add_info("ℹ️ Optional build dependencies: #{short_list}#{suffix}")
495
+ end
496
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
497
+
498
+ def parse_package_json
499
+ JSON.parse(File.read("package.json"))
500
+ rescue JSON::ParserError
501
+ add_warning("⚠️ Could not parse package.json to check React dependencies")
502
+ nil
503
+ end
504
+
505
+ def find_missing_dependencies(package_json, required_deps)
506
+ all_deps = package_json["dependencies"]&.merge(package_json["devDependencies"] || {}) || {}
507
+ required_deps.keys.reject { |dep| all_deps[dep] }
508
+ end
509
+
510
+ def report_dependency_status(required_deps, missing_deps, package_json)
511
+ all_deps = package_json["dependencies"]&.merge(package_json["devDependencies"] || {}) || {}
512
+
513
+ required_deps.each do |dep, description|
514
+ add_success("✅ #{description} (#{dep}) is installed") if all_deps[dep]
515
+ end
516
+
517
+ return unless missing_deps.any?
518
+
519
+ add_warning(<<~MSG.strip)
520
+ ⚠️ Missing React dependencies: #{missing_deps.join(', ')}
521
+
522
+ Install them with:
523
+ npm install #{missing_deps.join(' ')}
524
+ MSG
525
+ end
526
+
527
+ def check_version_patterns(npm_version, gem_version)
528
+ # Check for version range patterns in package.json
529
+ return unless /^[\^~]/.match?(npm_version)
530
+
531
+ pattern_type = npm_version[0] == "^" ? "caret (^)" : "tilde (~)"
532
+ add_warning(<<~MSG.strip)
533
+ ⚠️ NPM package uses #{pattern_type} version pattern: #{npm_version}
534
+
535
+ While versions match, consider using exact version "#{gem_version}" in package.json
536
+ for guaranteed compatibility with the React on Rails gem.
537
+ MSG
538
+ end
539
+
540
+ # rubocop:disable Metrics/CyclomaticComplexity
541
+ def check_gemfile_version_patterns
542
+ gemfile_path = ENV["BUNDLE_GEMFILE"] || "Gemfile"
543
+ return unless File.exist?(gemfile_path)
544
+
545
+ begin
546
+ gemfile_content = File.read(gemfile_path)
547
+ react_on_rails_line = gemfile_content.lines.find { |line| line.match(/^\s*gem\s+['"]react_on_rails['"]/) }
548
+
549
+ return unless react_on_rails_line
550
+
551
+ # Check for version patterns in Gemfile
552
+ if /['"][~]/.match?(react_on_rails_line)
553
+ add_warning(<<~MSG.strip)
554
+ ⚠️ Gemfile uses version pattern for react_on_rails gem.
555
+
556
+ Consider using exact version in Gemfile for guaranteed compatibility:
557
+ gem 'react_on_rails', '#{ReactOnRails::VERSION}'
558
+ MSG
559
+ elsif />=\s*/.match?(react_on_rails_line)
560
+ add_warning(<<~MSG.strip)
561
+ ⚠️ Gemfile uses version range (>=) for react_on_rails gem.
562
+
563
+ Consider using exact version in Gemfile for guaranteed compatibility:
564
+ gem 'react_on_rails', '#{ReactOnRails::VERSION}'
565
+ MSG
566
+ end
567
+ rescue StandardError
568
+ # Ignore errors reading Gemfile
569
+ end
570
+ end
571
+ # rubocop:enable Metrics/CyclomaticComplexity
572
+
573
+ # rubocop:disable Metrics/CyclomaticComplexity
574
+ def report_dependency_versions(package_json)
575
+ all_deps = package_json["dependencies"]&.merge(package_json["devDependencies"] || {}) || {}
576
+
577
+ react_version = all_deps["react"]
578
+ react_dom_version = all_deps["react-dom"]
579
+
580
+ if react_version && react_dom_version
581
+ add_success("✅ React #{react_version}, React DOM #{react_dom_version}")
582
+ elsif react_version
583
+ add_success("✅ React #{react_version}")
584
+ add_warning("⚠️ React DOM not found")
585
+ elsif react_dom_version
586
+ add_warning("⚠️ React not found")
587
+ add_success("✅ React DOM #{react_dom_version}")
588
+ end
589
+ end
590
+ # rubocop:enable Metrics/CyclomaticComplexity
591
+
592
+ def report_shakapacker_version
593
+ return unless File.exist?("Gemfile.lock")
594
+
595
+ begin
596
+ lockfile_content = File.read("Gemfile.lock")
597
+ # Parse exact installed version from Gemfile.lock GEM section
598
+ shakapacker_match = lockfile_content.match(/^\s{4}shakapacker \(([^)>=<~]+)\)/)
599
+ if shakapacker_match
600
+ version = shakapacker_match[1].strip
601
+ add_info("📦 Shakapacker version: #{version}")
602
+ end
603
+ rescue StandardError
604
+ # Ignore errors in parsing Gemfile.lock
605
+ end
606
+ end
607
+
608
+ def report_shakapacker_version_with_threshold
609
+ return unless File.exist?("Gemfile.lock")
610
+
611
+ begin
612
+ lockfile_content = File.read("Gemfile.lock")
613
+ # Look for the exact installed version in the GEM section, not the dependency requirement
614
+ # This matches " shakapacker (8.0.0)" but not " shakapacker (>= 6.0)"
615
+ shakapacker_match = lockfile_content.match(/^\s{4}shakapacker \(([^)>=<~]+)\)/)
616
+
617
+ if shakapacker_match
618
+ version = shakapacker_match[1].strip
619
+
620
+ begin
621
+ # Validate version string format
622
+ Gem::Version.new(version)
623
+
624
+ if ReactOnRails::PackerUtils.supports_autobundling?
625
+ add_success("✅ Shakapacker #{version} (supports React on Rails auto-bundling)")
626
+ else
627
+ add_warning("⚠️ Shakapacker #{version} - Version 7.0+ with nested_entries support needed " \
628
+ "for React on Rails auto-bundling")
629
+ end
630
+ rescue ArgumentError
631
+ # Fallback for invalid version strings
632
+ add_success("✅ Shakapacker #{version}")
633
+ end
634
+ else
635
+ add_success("✅ Shakapacker is configured")
636
+ end
637
+ rescue StandardError
638
+ add_success("✅ Shakapacker is configured")
639
+ end
640
+ end
641
+
642
+ def report_webpack_version
643
+ return unless File.exist?("package.json")
644
+
645
+ begin
646
+ package_json = JSON.parse(File.read("package.json"))
647
+ all_deps = package_json["dependencies"]&.merge(package_json["devDependencies"] || {}) || {}
648
+
649
+ webpack_version = all_deps["webpack"]
650
+ add_info("📦 Webpack version: #{webpack_version}") if webpack_version
651
+ rescue JSON::ParserError
652
+ # Handle JSON parsing errors
653
+ rescue StandardError
654
+ # Handle other file/access errors
655
+ end
656
+ end
657
+ end
658
+ # rubocop:enable Metrics/ClassLength
659
+ end
@@ -16,7 +16,7 @@ module ReactOnRails
16
16
 
17
17
  React on Rails is aborting your test run
18
18
 
19
- If you wish to use the config/#{ReactOnRails::PackerUtils.packer_type}.yml compile option for tests
19
+ If you wish to use the config/shakapacker.yml compile option for tests
20
20
  them remove your call to the ReactOnRails test helper.
21
21
  MSG
22
22
  puts Rainbow(msg).red
@@ -10,6 +10,7 @@ module ReactOnRails
10
10
  module TestHelper
11
11
  class WebpackAssetsStatusChecker
12
12
  include Utils::Required
13
+
13
14
  # source_path is typically configured in the (shaka/web)packer.yml file
14
15
  # for `source_path`
15
16
  # or for legacy React on Rails, it's /client, where all client files go
@@ -30,8 +31,7 @@ module ReactOnRails
30
31
  end
31
32
 
32
33
  def stale_generated_files(files)
33
- manifest_needed = ReactOnRails::PackerUtils.using_packer? &&
34
- !ReactOnRails::PackerUtils.manifest_exists?
34
+ manifest_needed = !ReactOnRails::PackerUtils.manifest_exists?
35
35
 
36
36
  return ["manifest.json"] if manifest_needed
37
37
 
@@ -50,8 +50,10 @@ module ReactOnRails
50
50
  def all_compiled_assets
51
51
  @all_compiled_assets ||= begin
52
52
  webpack_generated_files = @webpack_generated_files.map do |bundle_name|
53
- if bundle_name == ReactOnRails.configuration.server_bundle_js_file
54
- ReactOnRails::Utils.server_bundle_js_file_path
53
+ if bundle_name == ReactOnRails.configuration.react_client_manifest_file
54
+ ReactOnRails::Utils.react_client_manifest_file_path
55
+ elsif bundle_name == ReactOnRails.configuration.react_server_client_manifest_file
56
+ ReactOnRails::Utils.react_server_client_manifest_file_path
55
57
  else
56
58
  ReactOnRails::Utils.bundle_js_file_path(bundle_name)
57
59
  end