react_on_rails 16.0.0 → 16.0.1.rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +124 -77
  3. data/CLAUDE.md +46 -2
  4. data/CONTRIBUTING.md +12 -6
  5. data/Gemfile.development_dependencies +1 -0
  6. data/Gemfile.lock +3 -1
  7. data/LICENSE.md +15 -1
  8. data/README.md +68 -18
  9. data/bin/lefthook/check-trailing-newlines +38 -0
  10. data/bin/lefthook/get-changed-files +26 -0
  11. data/bin/lefthook/prettier-format +26 -0
  12. data/bin/lefthook/ruby-autofix +26 -0
  13. data/bin/lefthook/ruby-lint +27 -0
  14. data/eslint.config.ts +10 -0
  15. data/knip.ts +20 -9
  16. data/lib/generators/react_on_rails/USAGE +65 -0
  17. data/lib/generators/react_on_rails/base_generator.rb +7 -7
  18. data/lib/generators/react_on_rails/generator_helper.rb +4 -0
  19. data/lib/generators/react_on_rails/generator_messages.rb +2 -2
  20. data/lib/generators/react_on_rails/install_generator.rb +115 -7
  21. data/lib/generators/react_on_rails/react_no_redux_generator.rb +16 -4
  22. data/lib/generators/react_on_rails/react_with_redux_generator.rb +83 -14
  23. data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.client.tsx +25 -0
  24. data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.server.tsx +5 -0
  25. data/lib/generators/react_on_rails/templates/base/base/bin/dev +12 -24
  26. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/actions/helloWorldActionCreators.ts +18 -0
  27. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/components/HelloWorld.tsx +24 -0
  28. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/constants/helloWorldConstants.ts +6 -0
  29. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/containers/HelloWorldContainer.ts +20 -0
  30. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/reducers/helloWorldReducer.ts +22 -0
  31. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.client.tsx +23 -0
  32. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.server.tsx +5 -0
  33. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/store/helloWorldStore.ts +18 -0
  34. data/lib/react_on_rails/configuration.rb +10 -6
  35. data/lib/react_on_rails/dev/server_manager.rb +185 -28
  36. data/lib/react_on_rails/doctor.rb +1149 -0
  37. data/lib/react_on_rails/helper.rb +9 -78
  38. data/lib/react_on_rails/pro/NOTICE +21 -0
  39. data/lib/react_on_rails/pro/helper.rb +122 -0
  40. data/lib/react_on_rails/pro/utils.rb +53 -0
  41. data/lib/react_on_rails/react_component/render_options.rb +6 -2
  42. data/lib/react_on_rails/system_checker.rb +659 -0
  43. data/lib/react_on_rails/version.rb +1 -1
  44. data/lib/tasks/doctor.rake +48 -0
  45. data/lib/tasks/generate_packs.rake +127 -4
  46. data/package-lock.json +11984 -0
  47. metadata +26 -6
  48. data/lib/generators/react_on_rails/bin/dev +0 -46
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env bash
2
+ # Auto-fix Ruby files using rake autofix
3
+ set -euo pipefail
4
+
5
+ CONTEXT="${1:-staged}"
6
+ files="$(bin/lefthook/get-changed-files "$CONTEXT" '\.(rb|rake|ru)$')"
7
+
8
+ if [ -z "$files" ]; then
9
+ echo "✅ No Ruby files to autofix"
10
+ exit 0
11
+ fi
12
+
13
+ if [ "$CONTEXT" = "all-changed" ]; then
14
+ echo "🎨 Autofix on all changed Ruby files:"
15
+ else
16
+ echo "🎨 Autofix on $CONTEXT Ruby files:"
17
+ fi
18
+ printf " %s\n" $files
19
+
20
+ bundle exec rake autofix
21
+
22
+ # Re-stage files if running on staged or all-changed context
23
+ if [ "$CONTEXT" = "staged" ] || [ "$CONTEXT" = "all-changed" ]; then
24
+ echo $files | xargs -r git add
25
+ echo "✅ Re-staged formatted files"
26
+ fi
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env bash
2
+ # Lint Ruby files with RuboCop
3
+ set -euo pipefail
4
+
5
+ CONTEXT="${1:-staged}"
6
+ files="$(bin/lefthook/get-changed-files "$CONTEXT" '\.(rb|rake|ru)$')"
7
+
8
+ if [ -z "$files" ]; then
9
+ echo "✅ No Ruby files to lint"
10
+ exit 0
11
+ fi
12
+
13
+ if [ "$CONTEXT" = "all-changed" ]; then
14
+ echo "🔍 RuboCop on all changed Ruby files:"
15
+ else
16
+ echo "🔍 RuboCop on $CONTEXT Ruby files:"
17
+ fi
18
+ printf " %s\n" $files
19
+
20
+ if ! bundle exec rubocop --force-exclusion --display-cop-names -- $files; then
21
+ echo ""
22
+ echo "❌ RuboCop check failed!"
23
+ echo "💡 Auto-fix: bundle exec rubocop --auto-correct --force-exclusion -- $files"
24
+ echo "🚫 Skip hook: git commit --no-verify"
25
+ exit 1
26
+ fi
27
+ echo "✅ RuboCop checks passed for Ruby files"
data/eslint.config.ts CHANGED
@@ -44,6 +44,9 @@ const config = tsEslint.config([
44
44
  // fixtures
45
45
  '**/fixtures/',
46
46
  '**/.yalc/**/*',
47
+ // generator templates - exclude TypeScript templates that need tsconfig.json
48
+ '**/templates/**/*.tsx',
49
+ '**/templates/**/*.ts',
47
50
  ]),
48
51
  {
49
52
  files: ['**/*.[jt]s', '**/*.[jt]sx', '**/*.[cm][jt]s'],
@@ -156,6 +159,13 @@ const config = tsEslint.config([
156
159
  'react/prop-types': 'off',
157
160
  },
158
161
  },
162
+ {
163
+ files: ['spec/dummy/**/*'],
164
+ rules: {
165
+ // The dummy app dependencies are managed separately and may not be installed
166
+ 'import/no-unresolved': 'off',
167
+ },
168
+ },
159
169
  {
160
170
  files: ['**/*.ts{x,}', '**/*.[cm]ts'],
161
171
 
data/knip.ts CHANGED
@@ -6,21 +6,32 @@ const config: KnipConfig = {
6
6
  '.': {
7
7
  entry: [
8
8
  'node_package/src/ReactOnRails.node.ts!',
9
- 'node_package/src/ReactOnRailsRSC.ts!',
10
- 'node_package/src/registerServerComponent/client.tsx!',
11
- 'node_package/src/registerServerComponent/server.tsx!',
12
- 'node_package/src/registerServerComponent/server.rsc.ts!',
13
- 'node_package/src/wrapServerComponentRenderer/server.tsx!',
14
- 'node_package/src/wrapServerComponentRenderer/server.rsc.tsx!',
15
- 'node_package/src/RSCRoute.tsx!',
16
- 'node_package/src/ServerComponentFetchError.ts!',
9
+ 'node_package/src/pro/ReactOnRailsRSC.ts!',
10
+ 'node_package/src/pro/registerServerComponent/client.tsx!',
11
+ 'node_package/src/pro/registerServerComponent/server.tsx!',
12
+ 'node_package/src/pro/registerServerComponent/server.rsc.ts!',
13
+ 'node_package/src/pro/wrapServerComponentRenderer/server.tsx!',
14
+ 'node_package/src/pro/wrapServerComponentRenderer/server.rsc.tsx!',
15
+ 'node_package/src/pro/RSCRoute.tsx!',
16
+ 'node_package/src/pro/ServerComponentFetchError.ts!',
17
+ 'node_package/src/pro/getReactServerComponent.server.ts!',
18
+ 'node_package/src/pro/transformRSCNodeStream.ts!',
19
+ 'node_package/src/loadJsonFile.ts!',
17
20
  'eslint.config.ts',
18
21
  ],
19
22
  project: ['node_package/src/**/*.[jt]s{x,}!', 'node_package/tests/**/*.[jt]s{x,}'],
20
23
  babel: {
21
24
  config: ['node_package/babel.config.js'],
22
25
  },
23
- ignore: ['node_package/tests/emptyForTesting.js'],
26
+ ignore: [
27
+ 'node_package/tests/emptyForTesting.js',
28
+ // Pro features exported for external consumption
29
+ 'node_package/src/pro/streamServerRenderedReactComponent.ts:transformRenderStreamChunksToResultObject',
30
+ 'node_package/src/pro/streamServerRenderedReactComponent.ts:streamServerRenderedComponent',
31
+ 'node_package/src/pro/ServerComponentFetchError.ts:isServerComponentFetchError',
32
+ 'node_package/src/pro/RSCRoute.tsx:RSCRouteProps',
33
+ 'node_package/src/pro/streamServerRenderedReactComponent.ts:StreamingTrackers',
34
+ ],
24
35
  ignoreBinaries: [
25
36
  // Knip fails to detect it's declared in devDependencies
26
37
  'nps',
@@ -0,0 +1,65 @@
1
+ Description:
2
+ The `react_on_rails:doctor` generator diagnoses your React on Rails setup
3
+ and identifies potential configuration issues. It performs comprehensive
4
+ checks on your environment, dependencies, and configuration files.
5
+
6
+ This command is especially useful for:
7
+ • Troubleshooting setup issues
8
+ • Verifying installation after running react_on_rails:install
9
+ • Ensuring compatibility after upgrades
10
+ • Getting help with configuration problems
11
+
12
+ Example:
13
+ # Basic diagnosis
14
+ rails generate react_on_rails:doctor
15
+
16
+ # Verbose output showing all checks
17
+ rails generate react_on_rails:doctor --verbose
18
+
19
+ # Show help
20
+ rails generate react_on_rails:doctor --help
21
+
22
+ Checks performed:
23
+ Environment Prerequisites:
24
+ • Node.js installation and version compatibility
25
+ • JavaScript package manager availability (npm, yarn, pnpm, bun)
26
+ • Git working directory status
27
+
28
+ React on Rails Packages:
29
+ • React on Rails gem installation
30
+ • react-on-rails NPM package installation
31
+ • Version synchronization between gem and NPM package
32
+ • Shakapacker configuration and installation
33
+
34
+ Dependencies:
35
+ • React and React DOM installation
36
+ • Babel preset configuration
37
+ • Required development dependencies
38
+
39
+ Rails Integration:
40
+ • React on Rails initializer configuration
41
+ • Route and controller setup (Hello World example)
42
+ • View helper integration
43
+
44
+ Webpack Configuration:
45
+ • Webpack config file existence and structure
46
+ • React on Rails compatibility checks
47
+ • Environment-specific configuration validation
48
+
49
+ Development Environment:
50
+ • JavaScript bundle files
51
+ • Procfile.dev for development workflow
52
+ • .gitignore configuration for generated files
53
+
54
+ Options:
55
+ --verbose, -v: Show detailed output for all checks, including successful ones
56
+ --fix, -f: Attempt to fix simple issues automatically (planned feature)
57
+
58
+ Exit codes:
59
+ 0: All checks passed or only warnings found
60
+ 1: Critical errors found that prevent React on Rails from working
61
+
62
+ For more help:
63
+ • Documentation: https://github.com/shakacode/react_on_rails
64
+ • Issues: https://github.com/shakacode/react_on_rails/issues
65
+ • Discord: https://discord.gg/reactrails
@@ -105,13 +105,13 @@ module ReactOnRails
105
105
  def install_js_dependencies
106
106
  # Detect which package manager to use
107
107
  success = if File.exist?(File.join(destination_root, "yarn.lock"))
108
- run "yarn install"
108
+ system("yarn", "install")
109
109
  elsif File.exist?(File.join(destination_root, "pnpm-lock.yaml"))
110
- run "pnpm install"
110
+ system("pnpm", "install")
111
111
  elsif File.exist?(File.join(destination_root, "package-lock.json")) ||
112
112
  File.exist?(File.join(destination_root, "package.json"))
113
113
  # Use npm for package-lock.json or as default fallback
114
- run "npm install"
114
+ system("npm", "install")
115
115
  else
116
116
  true # No package manager detected, skip
117
117
  end
@@ -173,7 +173,7 @@ module ReactOnRails
173
173
  return if add_npm_dependencies(react_on_rails_pkg)
174
174
 
175
175
  puts "Using direct npm commands as fallback"
176
- success = run "npm install #{react_on_rails_pkg.join(' ')}"
176
+ success = system("npm", "install", *react_on_rails_pkg)
177
177
  handle_npm_failure("react-on-rails package", react_on_rails_pkg) unless success
178
178
  end
179
179
 
@@ -189,7 +189,7 @@ module ReactOnRails
189
189
  ]
190
190
  return if add_npm_dependencies(react_deps)
191
191
 
192
- success = run "npm install #{react_deps.join(' ')}"
192
+ success = system("npm", "install", *react_deps)
193
193
  handle_npm_failure("React dependencies", react_deps) unless success
194
194
  end
195
195
 
@@ -203,7 +203,7 @@ module ReactOnRails
203
203
  ]
204
204
  return if add_npm_dependencies(css_deps)
205
205
 
206
- success = run "npm install #{css_deps.join(' ')}"
206
+ success = system("npm", "install", *css_deps)
207
207
  handle_npm_failure("CSS dependencies", css_deps) unless success
208
208
  end
209
209
 
@@ -215,7 +215,7 @@ module ReactOnRails
215
215
  ]
216
216
  return if add_npm_dependencies(dev_deps, dev: true)
217
217
 
218
- success = run "npm install --save-dev #{dev_deps.join(' ')}"
218
+ success = system("npm", "install", "--save-dev", *dev_deps)
219
219
  handle_npm_failure("development dependencies", dev_deps, dev: true) unless success
220
220
  end
221
221
 
@@ -91,4 +91,8 @@ module GeneratorHelper
91
91
  def add_documentation_reference(message, source)
92
92
  "#{message} \n#{source}"
93
93
  end
94
+
95
+ def component_extension(options)
96
+ options.typescript? ? "tsx" : "jsx"
97
+ end
94
98
  end
@@ -38,7 +38,7 @@ module GeneratorMessages
38
38
  @output = []
39
39
  end
40
40
 
41
- def helpful_message_after_installation(component_name: "HelloWorld")
41
+ def helpful_message_after_installation(component_name: "HelloWorld", route: "hello_world")
42
42
  process_manager_section = build_process_manager_section
43
43
  testing_section = build_testing_section
44
44
  package_manager = detect_package_manager
@@ -62,7 +62,7 @@ module GeneratorMessages
62
62
  ./bin/dev prod # Production-like mode for testing
63
63
  ./bin/dev help # See all available options
64
64
 
65
- 3. Visit: #{Rainbow('http://localhost:3000/hello_world').cyan.underline}
65
+ 3. Visit: #{Rainbow(route ? "http://localhost:3000/#{route}" : 'http://localhost:3000').cyan.underline}
66
66
  ✨ KEY FEATURES:
67
67
  ─────────────────────────────────────────────────────────────────────────
68
68
  • Auto-registration enabled - Your layout only needs:
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rails/generators"
4
+ require "json"
4
5
  require_relative "generator_helper"
5
6
  require_relative "generator_messages"
6
7
 
@@ -20,6 +21,13 @@ module ReactOnRails
20
21
  desc: "Install Redux package and Redux version of Hello World Example. Default: false",
21
22
  aliases: "-R"
22
23
 
24
+ # --typescript
25
+ class_option :typescript,
26
+ type: :boolean,
27
+ default: false,
28
+ desc: "Generate TypeScript files and install TypeScript dependencies. Default: false",
29
+ aliases: "-T"
30
+
23
31
  # --ignore-warnings
24
32
  class_option :ignore_warnings,
25
33
  type: :boolean,
@@ -58,11 +66,16 @@ module ReactOnRails
58
66
 
59
67
  def invoke_generators
60
68
  ensure_shakapacker_installed
61
- invoke "react_on_rails:base"
69
+ if options.typescript?
70
+ install_typescript_dependencies
71
+ create_css_module_types
72
+ create_typescript_config
73
+ end
74
+ invoke "react_on_rails:base", [], { typescript: options.typescript? }
62
75
  if options.redux?
63
- invoke "react_on_rails:react_with_redux"
76
+ invoke "react_on_rails:react_with_redux", [], { typescript: options.typescript? }
64
77
  else
65
- invoke "react_on_rails:react_no_redux"
78
+ invoke "react_on_rails:react_no_redux", [], { typescript: options.typescript? }
66
79
  end
67
80
  end
68
81
 
@@ -136,11 +149,13 @@ module ReactOnRails
136
149
  end
137
150
 
138
151
  def add_bin_scripts
139
- directory "#{__dir__}/bin", "bin"
152
+ # Copy bin scripts from templates
153
+ template_bin_path = "#{__dir__}/templates/base/base/bin"
154
+ directory template_bin_path, "bin"
140
155
 
141
156
  # Make these and only these files executable
142
157
  files_to_copy = []
143
- Dir.chdir("#{__dir__}/bin") do
158
+ Dir.chdir(template_bin_path) do
144
159
  files_to_copy.concat(Dir.glob("*"))
145
160
  end
146
161
  files_to_become_executable = files_to_copy.map { |filename| "bin/#{filename}" }
@@ -149,7 +164,14 @@ module ReactOnRails
149
164
  end
150
165
 
151
166
  def add_post_install_message
152
- GeneratorMessages.add_info(GeneratorMessages.helpful_message_after_installation)
167
+ # Determine what route will be created by the generator
168
+ route = "hello_world" # This is the hardcoded route from base_generator.rb
169
+ component_name = options.redux? ? "HelloWorldApp" : "HelloWorld"
170
+
171
+ GeneratorMessages.add_info(GeneratorMessages.helpful_message_after_installation(
172
+ component_name: component_name,
173
+ route: route
174
+ ))
153
175
  end
154
176
 
155
177
  def shakapacker_loaded_in_process?(gem_name)
@@ -302,10 +324,96 @@ module ReactOnRails
302
324
  false
303
325
  end
304
326
 
327
+ def install_typescript_dependencies
328
+ puts Rainbow("📝 Installing TypeScript dependencies...").yellow
329
+
330
+ # Install TypeScript and React type definitions
331
+ typescript_packages = %w[
332
+ typescript
333
+ @types/react
334
+ @types/react-dom
335
+ @babel/preset-typescript
336
+ ]
337
+
338
+ # Try using GeneratorHelper first (package manager agnostic)
339
+ return if add_npm_dependencies(typescript_packages, dev: true)
340
+
341
+ # Fallback to npm if GeneratorHelper fails
342
+ success = system("npm", "install", "--save-dev", *typescript_packages)
343
+ return if success
344
+
345
+ warning = <<~MSG.strip
346
+ ⚠️ Failed to install TypeScript dependencies automatically.
347
+
348
+ Please run manually:
349
+ npm install --save-dev #{typescript_packages.join(' ')}
350
+ MSG
351
+ GeneratorMessages.add_warning(warning)
352
+ end
353
+
354
+ def create_css_module_types
355
+ puts Rainbow("📝 Creating CSS module type definitions...").yellow
356
+
357
+ # Ensure the types directory exists
358
+ FileUtils.mkdir_p("app/javascript/types")
359
+
360
+ css_module_types_content = <<~TS.strip
361
+ // TypeScript definitions for CSS modules
362
+ declare module "*.module.css" {
363
+ const classes: { [key: string]: string };
364
+ export default classes;
365
+ }
366
+
367
+ declare module "*.module.scss" {
368
+ const classes: { [key: string]: string };
369
+ export default classes;
370
+ }
371
+
372
+ declare module "*.module.sass" {
373
+ const classes: { [key: string]: string };
374
+ export default classes;
375
+ }
376
+ TS
377
+
378
+ File.write("app/javascript/types/css-modules.d.ts", css_module_types_content)
379
+ puts Rainbow("✅ Created CSS module type definitions").green
380
+ end
381
+
382
+ def create_typescript_config
383
+ if File.exist?("tsconfig.json")
384
+ puts Rainbow("⚠️ tsconfig.json already exists, skipping creation").yellow
385
+ return
386
+ end
387
+
388
+ tsconfig_content = {
389
+ "compilerOptions" => {
390
+ "target" => "es2018",
391
+ "allowJs" => true,
392
+ "skipLibCheck" => true,
393
+ "strict" => true,
394
+ "noUncheckedIndexedAccess" => true,
395
+ "forceConsistentCasingInFileNames" => true,
396
+ "noFallthroughCasesInSwitch" => true,
397
+ "module" => "esnext",
398
+ "moduleResolution" => "bundler",
399
+ "resolveJsonModule" => true,
400
+ "isolatedModules" => true,
401
+ "noEmit" => true,
402
+ "jsx" => "react-jsx"
403
+ },
404
+ "include" => [
405
+ "app/javascript/**/*"
406
+ ]
407
+ }
408
+
409
+ File.write("tsconfig.json", JSON.pretty_generate(tsconfig_content))
410
+ puts Rainbow("✅ Created tsconfig.json").green
411
+ end
412
+
305
413
  # Removed: Shakapacker auto-installation logic (now explicit dependency)
306
414
 
307
415
  # Removed: Shakapacker 8+ is now required as explicit dependency
416
+ # rubocop:enable Metrics/ClassLength
308
417
  end
309
- # rubocop:enable Metrics/ClassLength
310
418
  end
311
419
  end
@@ -11,12 +11,24 @@ module ReactOnRails
11
11
  Rails::Generators.hide_namespace(namespace)
12
12
  source_root(File.expand_path("templates", __dir__))
13
13
 
14
+ class_option :typescript,
15
+ type: :boolean,
16
+ default: false,
17
+ desc: "Generate TypeScript files"
18
+
14
19
  def copy_base_files
15
20
  base_js_path = "base/base"
16
- base_files = %w[app/javascript/src/HelloWorld/ror_components/HelloWorld.client.jsx
17
- app/javascript/src/HelloWorld/ror_components/HelloWorld.server.jsx
18
- app/javascript/src/HelloWorld/ror_components/HelloWorld.module.css]
19
- base_files.each { |file| copy_file("#{base_js_path}/#{file}", file) }
21
+
22
+ # Determine which component files to copy based on TypeScript option
23
+ component_files = [
24
+ "app/javascript/src/HelloWorld/ror_components/HelloWorld.client.#{component_extension(options)}",
25
+ "app/javascript/src/HelloWorld/ror_components/HelloWorld.server.#{component_extension(options)}",
26
+ "app/javascript/src/HelloWorld/ror_components/HelloWorld.module.css"
27
+ ]
28
+
29
+ component_files.each do |file|
30
+ copy_file("#{base_js_path}/#{file}", file)
31
+ end
20
32
  end
21
33
 
22
34
  def create_appropriate_templates
@@ -1,13 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rails/generators"
4
+ require_relative "generator_helper"
5
+ require_relative "generator_messages"
4
6
 
5
7
  module ReactOnRails
6
8
  module Generators
7
9
  class ReactWithReduxGenerator < Rails::Generators::Base
10
+ include GeneratorHelper
11
+
8
12
  Rails::Generators.hide_namespace(namespace)
9
13
  source_root(File.expand_path("templates", __dir__))
10
14
 
15
+ class_option :typescript,
16
+ type: :boolean,
17
+ default: false,
18
+ desc: "Generate TypeScript files",
19
+ aliases: "-T"
20
+
11
21
  def create_redux_directories
12
22
  # Create auto-registration directory structure for Redux
13
23
  empty_directory("app/javascript/src/HelloWorldApp/ror_components")
@@ -19,17 +29,18 @@ module ReactOnRails
19
29
 
20
30
  def copy_base_files
21
31
  base_js_path = "redux/base"
32
+ ext = component_extension(options)
22
33
 
23
34
  # Copy Redux-connected component to auto-registration structure
24
- copy_file("#{base_js_path}/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.client.jsx",
25
- "app/javascript/src/HelloWorldApp/ror_components/HelloWorldApp.client.jsx")
26
- copy_file("#{base_js_path}/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.server.jsx",
27
- "app/javascript/src/HelloWorldApp/ror_components/HelloWorldApp.server.jsx")
35
+ copy_file("#{base_js_path}/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.client.#{ext}",
36
+ "app/javascript/src/HelloWorldApp/ror_components/HelloWorldApp.client.#{ext}")
37
+ copy_file("#{base_js_path}/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.server.#{ext}",
38
+ "app/javascript/src/HelloWorldApp/ror_components/HelloWorldApp.server.#{ext}")
28
39
  copy_file("#{base_js_path}/app/javascript/bundles/HelloWorld/components/HelloWorld.module.css",
29
40
  "app/javascript/src/HelloWorldApp/components/HelloWorld.module.css")
30
41
 
31
42
  # Update import paths in client component
32
- ror_client_file = "app/javascript/src/HelloWorldApp/ror_components/HelloWorldApp.client.jsx"
43
+ ror_client_file = "app/javascript/src/HelloWorldApp/ror_components/HelloWorldApp.client.#{ext}"
33
44
  gsub_file(ror_client_file, "../store/helloWorldStore", "../store/helloWorldStore")
34
45
  gsub_file(ror_client_file, "../containers/HelloWorldContainer",
35
46
  "../containers/HelloWorldContainer")
@@ -37,12 +48,15 @@ module ReactOnRails
37
48
 
38
49
  def copy_base_redux_files
39
50
  base_hello_world_path = "redux/base/app/javascript/bundles/HelloWorld"
40
- %w[actions/helloWorldActionCreators.js
41
- containers/HelloWorldContainer.js
42
- constants/helloWorldConstants.js
43
- reducers/helloWorldReducer.js
44
- store/helloWorldStore.js
45
- components/HelloWorld.jsx].each do |file|
51
+ redux_extension = options.typescript? ? "ts" : "js"
52
+
53
+ # Copy Redux infrastructure files with appropriate extension
54
+ %W[actions/helloWorldActionCreators.#{redux_extension}
55
+ containers/HelloWorldContainer.#{redux_extension}
56
+ constants/helloWorldConstants.#{redux_extension}
57
+ reducers/helloWorldReducer.#{redux_extension}
58
+ store/helloWorldStore.#{redux_extension}
59
+ components/HelloWorld.#{component_extension(options)}].each do |file|
46
60
  copy_file("#{base_hello_world_path}/#{file}",
47
61
  "app/javascript/src/HelloWorldApp/#{file}")
48
62
  end
@@ -60,15 +74,70 @@ module ReactOnRails
60
74
  end
61
75
 
62
76
  def add_redux_npm_dependencies
63
- run "npm install redux react-redux"
77
+ # Add Redux dependencies as regular dependencies
78
+ regular_packages = %w[redux react-redux]
79
+
80
+ # Try using GeneratorHelper first (package manager agnostic)
81
+ success = add_npm_dependencies(regular_packages)
82
+
83
+ # Fallback to package manager detection if GeneratorHelper fails
84
+ return if success
85
+
86
+ package_manager = GeneratorMessages.detect_package_manager
87
+ return unless package_manager
88
+
89
+ install_packages_with_fallback(regular_packages, dev: false, package_manager: package_manager)
90
+ end
91
+
92
+ private
93
+
94
+ def install_packages_with_fallback(packages, dev:, package_manager:)
95
+ install_args = build_install_args(package_manager, dev, packages)
96
+
97
+ success = system(*install_args)
98
+ return if success
99
+
100
+ install_command = install_args.join(" ")
101
+ warning = <<~MSG.strip
102
+ ⚠️ Failed to install Redux dependencies automatically.
103
+
104
+ Please run manually:
105
+ #{install_command}
106
+ MSG
107
+ GeneratorMessages.add_warning(warning)
108
+ end
109
+
110
+ def build_install_args(package_manager, dev, packages)
111
+ # Security: Validate package manager to prevent command injection
112
+ allowed_package_managers = %w[npm yarn pnpm bun].freeze
113
+ unless allowed_package_managers.include?(package_manager)
114
+ raise ArgumentError, "Invalid package manager: #{package_manager}"
115
+ end
116
+
117
+ base_commands = {
118
+ "npm" => %w[npm install],
119
+ "yarn" => %w[yarn add],
120
+ "pnpm" => %w[pnpm add],
121
+ "bun" => %w[bun add]
122
+ }
123
+
124
+ base_args = base_commands[package_manager].dup
125
+ base_args << dev_flag_for(package_manager) if dev
126
+ base_args + packages
127
+ end
128
+
129
+ def dev_flag_for(package_manager)
130
+ case package_manager
131
+ when "npm", "pnpm" then "--save-dev"
132
+ when "yarn", "bun" then "--dev"
133
+ end
64
134
  end
65
135
 
66
136
  def add_redux_specific_messages
67
137
  # Override the generic messages with Redux-specific instructions
68
- require_relative "generator_messages"
69
138
  GeneratorMessages.output.clear
70
139
  GeneratorMessages.add_info(
71
- GeneratorMessages.helpful_message_after_installation(component_name: "HelloWorldApp")
140
+ GeneratorMessages.helpful_message_after_installation(component_name: "HelloWorldApp", route: "hello_world")
72
141
  )
73
142
  end
74
143
  end
@@ -0,0 +1,25 @@
1
+ import React, { useState } from 'react';
2
+ import * as style from './HelloWorld.module.css';
3
+
4
+ interface HelloWorldProps {
5
+ name: string;
6
+ }
7
+
8
+ const HelloWorld: React.FC<HelloWorldProps> = (props) => {
9
+ const [name, setName] = useState(props.name);
10
+
11
+ return (
12
+ <div>
13
+ <h3>Hello, {name}!</h3>
14
+ <hr />
15
+ <form>
16
+ <label className={style.bright} htmlFor="name">
17
+ Say hello to:
18
+ <input id="name" type="text" value={name} onChange={(e) => setName(e.target.value)} />
19
+ </label>
20
+ </form>
21
+ </div>
22
+ );
23
+ };
24
+
25
+ export default HelloWorld;
@@ -0,0 +1,5 @@
1
+ import HelloWorld from './HelloWorld.client';
2
+ // This could be specialized for server rendering
3
+ // For example, if using React Router, we'd have the SSR setup here.
4
+
5
+ export default HelloWorld;