Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0966fc9
Add TypeScript support to React on Rails install generator
justin808 Sep 17, 2025
e505bba
Fix linting issues and exclude templates from ESLint
justin808 Sep 17, 2025
31e9f3e
Fix Prettier formatting issues in TypeScript templates
justin808 Sep 17, 2025
e470dda
Complete Redux TypeScript implementation for generators
justin808 Sep 17, 2025
0a3c0fb
Fix Redux generator tests and include missing GeneratorHelper
justin808 Sep 17, 2025
c5e03f1
Update lib/generators/react_on_rails/install_generator.rb
justin808 Sep 17, 2025
edb82bd
Address PR feedback: Fix type imports, security, and add CSS modules
justin808 Sep 17, 2025
1df67f5
Fix TypeScript generator tests and lint issues
justin808 Sep 18, 2025
68e601f
Trigger CI rebuild
justin808 Sep 18, 2025
9ea4952
Fix generator method scoping and indentation after rebase
justin808 Sep 18, 2025
535face
Fix Prettier formatting issues in documentation files
justin808 Sep 18, 2025
abb3e72
Update lib/generators/react_on_rails/install_generator.rb
justin808 Sep 18, 2025
79a41e9
Update lib/generators/react_on_rails/install_generator.rb
justin808 Sep 18, 2025
9eb5110
Update lib/generators/react_on_rails/install_generator.rb
justin808 Sep 18, 2025
bc5c1d7
Update lib/generators/react_on_rails/install_generator.rb
justin808 Sep 18, 2025
1f61210
Update lib/generators/react_on_rails/templates/base/base/app/javascri…
justin808 Sep 18, 2025
81f8da8
Apply suggestions from code review
justin808 Sep 18, 2025
0283efd
Address alexeyr-ci2 PR review suggestions
justin808 Sep 18, 2025
4ed7a0f
Fix require order and improve React component performance
justin808 Sep 18, 2025
054dff3
Fix Prettier formatting issues
justin808 Sep 18, 2025
b037c6a
Fix TypeScript test expectations to match improved code
justin808 Sep 18, 2025
622eecc
SECURITY: Fix command injection vulnerabilities in generators
justin808 Sep 18, 2025
cc1135e
Improve bin/dev generated script with hello_world default route
justin808 Sep 18, 2025
2ca96fd
Fix RuboCop violations in bin/dev template
justin808 Sep 18, 2025
ec7ffd3
Fix bin/dev test to handle default route functionality
justin808 Sep 18, 2025
1708714
Add CHANGELOG entry for TypeScript generator improvements
justin808 Sep 18, 2025
b597275
Add PR link and author attribution to CHANGELOG
justin808 Sep 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions eslint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ const config = tsEslint.config([
// fixtures
'**/fixtures/',
'**/.yalc/**/*',
// generator templates - exclude TypeScript templates that need tsconfig.json
'**/templates/**/*.tsx',
'**/templates/**/*.ts',
]),
{
files: ['**/*.[jt]s', '**/*.[jt]sx', '**/*.[cm][jt]s'],
Expand Down
14 changes: 7 additions & 7 deletions lib/generators/react_on_rails/base_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,13 @@ def add_js_dependencies
def install_js_dependencies
# Detect which package manager to use
success = if File.exist?(File.join(destination_root, "yarn.lock"))
run "yarn install"
system("yarn", "install")
elsif File.exist?(File.join(destination_root, "pnpm-lock.yaml"))
run "pnpm install"
system("pnpm", "install")
elsif File.exist?(File.join(destination_root, "package-lock.json")) ||
File.exist?(File.join(destination_root, "package.json"))
# Use npm for package-lock.json or as default fallback
run "npm install"
system("npm", "install")
else
true # No package manager detected, skip
end
Expand Down Expand Up @@ -173,7 +173,7 @@ def add_react_on_rails_package
return if add_npm_dependencies(react_on_rails_pkg)

puts "Using direct npm commands as fallback"
success = run "npm install #{react_on_rails_pkg.join(' ')}"
success = system("npm", "install", *react_on_rails_pkg)
handle_npm_failure("react-on-rails package", react_on_rails_pkg) unless success
end

Expand All @@ -189,7 +189,7 @@ def add_react_dependencies
]
return if add_npm_dependencies(react_deps)

success = run "npm install #{react_deps.join(' ')}"
success = system("npm", "install", *react_deps)
handle_npm_failure("React dependencies", react_deps) unless success
end

Expand All @@ -203,7 +203,7 @@ def add_css_dependencies
]
return if add_npm_dependencies(css_deps)

success = run "npm install #{css_deps.join(' ')}"
success = system("npm", "install", *css_deps)
handle_npm_failure("CSS dependencies", css_deps) unless success
end

Expand All @@ -215,7 +215,7 @@ def add_dev_dependencies
]
return if add_npm_dependencies(dev_deps, dev: true)

success = run "npm install --save-dev #{dev_deps.join(' ')}"
success = system("npm", "install", "--save-dev", *dev_deps)
handle_npm_failure("development dependencies", dev_deps, dev: true) unless success
end

Expand Down
4 changes: 4 additions & 0 deletions lib/generators/react_on_rails/generator_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,8 @@ def copy_file_and_missing_parent_directories(source_file, destination_file = nil
def add_documentation_reference(message, source)
"#{message} \n#{source}"
end

def component_extension(options)
options.typescript? ? "tsx" : "jsx"
end
end
107 changes: 103 additions & 4 deletions lib/generators/react_on_rails/install_generator.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require "rails/generators"
require "json"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Require FileUtils before using it.

create_css_module_types calls FileUtils.mkdir_p but fileutils isn’t required in this file.

Apply this diff:

 require "rails/generators"
 require "json"
+require "fileutils"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
require "json"
require "rails/generators"
require "json"
require "fileutils"
🤖 Prompt for AI Agents
In lib/generators/react_on_rails/install_generator.rb around line 4, FileUtils
is used in create_css_module_types but not required; add require "fileutils"
(for example immediately after the existing require "json") so FileUtils.mkdir_p
calls succeed.

require_relative "generator_helper"
require_relative "generator_messages"

Expand All @@ -20,6 +21,13 @@ class InstallGenerator < Rails::Generators::Base
desc: "Install Redux package and Redux version of Hello World Example. Default: false",
aliases: "-R"

# --typescript
class_option :typescript,
type: :boolean,
default: false,
desc: "Generate TypeScript files and install TypeScript dependencies. Default: false",
aliases: "-T"

# --ignore-warnings
class_option :ignore_warnings,
type: :boolean,
Expand Down Expand Up @@ -58,11 +66,16 @@ def print_generator_messages

def invoke_generators
ensure_shakapacker_installed
invoke "react_on_rails:base"
if options.typescript?
install_typescript_dependencies
create_css_module_types
create_typescript_config
end
invoke "react_on_rails:base", [], { typescript: options.typescript? }
if options.redux?
invoke "react_on_rails:react_with_redux"
invoke "react_on_rails:react_with_redux", [], { typescript: options.typescript? }
else
invoke "react_on_rails:react_no_redux"
invoke "react_on_rails:react_no_redux", [], { typescript: options.typescript? }
end
end

Expand Down Expand Up @@ -311,10 +324,96 @@ def missing_package_manager?
false
end

def install_typescript_dependencies
puts Rainbow("📝 Installing TypeScript dependencies...").yellow

# Install TypeScript and React type definitions
typescript_packages = %w[
typescript
@types/react
@types/react-dom
@babel/preset-typescript
]

# Try using GeneratorHelper first (package manager agnostic)
return if add_npm_dependencies(typescript_packages, dev: true)

# Fallback to npm if GeneratorHelper fails
success = system("npm", "install", "--save-dev", *typescript_packages)
return if success

warning = <<~MSG.strip
⚠️ Failed to install TypeScript dependencies automatically.

Please run manually:
npm install --save-dev #{typescript_packages.join(' ')}
MSG
GeneratorMessages.add_warning(warning)
end

def create_css_module_types
puts Rainbow("📝 Creating CSS module type definitions...").yellow

# Ensure the types directory exists
FileUtils.mkdir_p("app/javascript/types")

css_module_types_content = <<~TS.strip
// TypeScript definitions for CSS modules
declare module "*.module.css" {
const classes: { [key: string]: string };
export default classes;
}

declare module "*.module.scss" {
const classes: { [key: string]: string };
export default classes;
}

declare module "*.module.sass" {
const classes: { [key: string]: string };
export default classes;
}
TS

File.write("app/javascript/types/css-modules.d.ts", css_module_types_content)
puts Rainbow("✅ Created CSS module type definitions").green
end
Comment on lines +355 to +380
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

CSS Modules d.ts mismatches namedExports=true (project convention).

Per project learning, CSS Modules use named exports with import * as style from './file.module.css'. The current typings export a default, which will mislead TS. Use export = classes to support namespace imports.

Apply this diff:

-        css_module_types_content = <<~TS.strip
-          // TypeScript definitions for CSS modules
-          declare module "*.module.css" {
-            const classes: { [key: string]: string };
-            export default classes;
-          }
-
-          declare module "*.module.scss" {
-            const classes: { [key: string]: string };
-            export default classes;
-          }
-
-          declare module "*.module.sass" {
-            const classes: { [key: string]: string };
-            export default classes;
-          }
-        TS
+        css_module_types_content = <<~TS.strip
+          // TypeScript definitions for CSS modules (supports `import * as styles from ...`)
+          declare module "*.module.css" {
+            const classes: { readonly [key: string]: string };
+            export = classes;
+          }
+
+          declare module "*.module.scss" {
+            const classes: { readonly [key: string]: string };
+            export = classes;
+          }
+
+          declare module "*.module.sass" {
+            const classes: { readonly [key: string]: string };
+            export = classes;
+          }
+        TS

Note: If you prefer strict per-file typings, consider adding a typed‑CSS‑modules generator later.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
puts Rainbow("📝 Creating CSS module type definitions...").yellow
# Ensure the types directory exists
FileUtils.mkdir_p("app/javascript/types")
css_module_types_content = <<~TS.strip
// TypeScript definitions for CSS modules
declare module "*.module.css" {
const classes: { [key: string]: string };
export default classes;
}
declare module "*.module.scss" {
const classes: { [key: string]: string };
export default classes;
}
declare module "*.module.sass" {
const classes: { [key: string]: string };
export default classes;
}
TS
File.write("app/javascript/types/css-modules.d.ts", css_module_types_content)
puts Rainbow("✅ Created CSS module type definitions").green
end
puts Rainbow("📝 Creating CSS module type definitions...").yellow
# Ensure the types directory exists
FileUtils.mkdir_p("app/javascript/types")
css_module_types_content = <<~TS.strip
// TypeScript definitions for CSS modules (supports `import * as styles from ...`)
declare module "*.module.css" {
const classes: { readonly [key: string]: string };
export = classes;
}
declare module "*.module.scss" {
const classes: { readonly [key: string]: string };
export = classes;
}
declare module "*.module.sass" {
const classes: { readonly [key: string]: string };
export = classes;
}
TS
File.write("app/javascript/types/css-modules.d.ts", css_module_types_content)
puts Rainbow("✅ Created CSS module type definitions").green
end
🤖 Prompt for AI Agents
In lib/generators/react_on_rails/install_generator.rb around lines 346 to 371,
the generated CSS module type file currently uses "export default classes" which
conflicts with the project's namedExports=true convention; update the generated
declarations to use CommonJS-style namespace exports so namespace imports work.
Replace each "export default classes;" with "export = classes;" (keep the const
classes: { [key: string]: string }; declaration) for all "*.module.css",
"*.module.scss", and "*.module.sass" blocks and write that updated content to
app/javascript/types/css-modules.d.ts.


def create_typescript_config
Copy link

Copilot AI Sep 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The create_typescript_config method is defined but never called. This means TypeScript projects won't get a tsconfig.json file, which is essential for TypeScript compilation.

Copilot uses AI. Check for mistakes.
if File.exist?("tsconfig.json")
puts Rainbow("⚠️ tsconfig.json already exists, skipping creation").yellow
return
end

tsconfig_content = {
"compilerOptions" => {
"target" => "es2018",
"allowJs" => true,
"skipLibCheck" => true,
"strict" => true,
"noUncheckedIndexedAccess" => true,
"forceConsistentCasingInFileNames" => true,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed.

"noFallthroughCasesInSwitch" => true,
"module" => "esnext",
"moduleResolution" => "bundler",
"resolveJsonModule" => true,
"isolatedModules" => true,
Copy link
Collaborator

@alexeyr-ci2 alexeyr-ci2 Sep 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not desirable in my opinion, it simply makes some TS code not compile which the user may want to compile.

"noEmit" => true,
"jsx" => "react-jsx"
},
"include" => [
"app/javascript/**/*"
]
}

File.write("tsconfig.json", JSON.pretty_generate(tsconfig_content))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we always running in an empty project or could the user could already have tsconfig.json?

puts Rainbow("✅ Created tsconfig.json").green
end

# Removed: Shakapacker auto-installation logic (now explicit dependency)

# Removed: Shakapacker 8+ is now required as explicit dependency
# rubocop:enable Metrics/ClassLength
end
# rubocop:enable Metrics/ClassLength
end
end
20 changes: 16 additions & 4 deletions lib/generators/react_on_rails/react_no_redux_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,24 @@ class ReactNoReduxGenerator < Rails::Generators::Base
Rails::Generators.hide_namespace(namespace)
source_root(File.expand_path("templates", __dir__))

class_option :typescript,
type: :boolean,
default: false,
desc: "Generate TypeScript files"

def copy_base_files
base_js_path = "base/base"
base_files = %w[app/javascript/src/HelloWorld/ror_components/HelloWorld.client.jsx
app/javascript/src/HelloWorld/ror_components/HelloWorld.server.jsx
app/javascript/src/HelloWorld/ror_components/HelloWorld.module.css]
base_files.each { |file| copy_file("#{base_js_path}/#{file}", file) }

# Determine which component files to copy based on TypeScript option
component_files = [
"app/javascript/src/HelloWorld/ror_components/HelloWorld.client.#{component_extension(options)}",
"app/javascript/src/HelloWorld/ror_components/HelloWorld.server.#{component_extension(options)}",
"app/javascript/src/HelloWorld/ror_components/HelloWorld.module.css"
]

component_files.each do |file|
copy_file("#{base_js_path}/#{file}", file)
end
end

def create_appropriate_templates
Expand Down
95 changes: 82 additions & 13 deletions lib/generators/react_on_rails/react_with_redux_generator.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
# frozen_string_literal: true

require "rails/generators"
require_relative "generator_helper"
require_relative "generator_messages"

module ReactOnRails
module Generators
class ReactWithReduxGenerator < Rails::Generators::Base
include GeneratorHelper

Rails::Generators.hide_namespace(namespace)
source_root(File.expand_path("templates", __dir__))

class_option :typescript,
type: :boolean,
default: false,
desc: "Generate TypeScript files",
aliases: "-T"

def create_redux_directories
# Create auto-registration directory structure for Redux
empty_directory("app/javascript/src/HelloWorldApp/ror_components")
Expand All @@ -19,30 +29,34 @@ def create_redux_directories

def copy_base_files
base_js_path = "redux/base"
ext = component_extension(options)

# Copy Redux-connected component to auto-registration structure
copy_file("#{base_js_path}/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.client.jsx",
"app/javascript/src/HelloWorldApp/ror_components/HelloWorldApp.client.jsx")
copy_file("#{base_js_path}/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.server.jsx",
"app/javascript/src/HelloWorldApp/ror_components/HelloWorldApp.server.jsx")
copy_file("#{base_js_path}/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.client.#{ext}",
"app/javascript/src/HelloWorldApp/ror_components/HelloWorldApp.client.#{ext}")
copy_file("#{base_js_path}/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.server.#{ext}",
"app/javascript/src/HelloWorldApp/ror_components/HelloWorldApp.server.#{ext}")
copy_file("#{base_js_path}/app/javascript/bundles/HelloWorld/components/HelloWorld.module.css",
"app/javascript/src/HelloWorldApp/components/HelloWorld.module.css")

# Update import paths in client component
ror_client_file = "app/javascript/src/HelloWorldApp/ror_components/HelloWorldApp.client.jsx"
ror_client_file = "app/javascript/src/HelloWorldApp/ror_components/HelloWorldApp.client.#{ext}"
gsub_file(ror_client_file, "../store/helloWorldStore", "../store/helloWorldStore")
gsub_file(ror_client_file, "../containers/HelloWorldContainer",
"../containers/HelloWorldContainer")
end

def copy_base_redux_files
base_hello_world_path = "redux/base/app/javascript/bundles/HelloWorld"
%w[actions/helloWorldActionCreators.js
containers/HelloWorldContainer.js
constants/helloWorldConstants.js
reducers/helloWorldReducer.js
store/helloWorldStore.js
components/HelloWorld.jsx].each do |file|
redux_extension = options.typescript? ? "ts" : "js"

# Copy Redux infrastructure files with appropriate extension
%W[actions/helloWorldActionCreators.#{redux_extension}
containers/HelloWorldContainer.#{redux_extension}
constants/helloWorldConstants.#{redux_extension}
reducers/helloWorldReducer.#{redux_extension}
store/helloWorldStore.#{redux_extension}
components/HelloWorld.#{component_extension(options)}].each do |file|
copy_file("#{base_hello_world_path}/#{file}",
"app/javascript/src/HelloWorldApp/#{file}")
end
Expand All @@ -60,12 +74,67 @@ def create_appropriate_templates
end

def add_redux_npm_dependencies
run "npm install redux react-redux"
# Add Redux dependencies as regular dependencies
regular_packages = %w[redux react-redux]

# Try using GeneratorHelper first (package manager agnostic)
success = add_npm_dependencies(regular_packages)

# Fallback to package manager detection if GeneratorHelper fails
return if success

package_manager = GeneratorMessages.detect_package_manager
return unless package_manager

install_packages_with_fallback(regular_packages, dev: false, package_manager: package_manager)
end

private

def install_packages_with_fallback(packages, dev:, package_manager:)
install_args = build_install_args(package_manager, dev, packages)

success = system(*install_args)
return if success

install_command = install_args.join(" ")
warning = <<~MSG.strip
⚠️ Failed to install Redux dependencies automatically.

Please run manually:
#{install_command}
MSG
GeneratorMessages.add_warning(warning)
end

def build_install_args(package_manager, dev, packages)
# Security: Validate package manager to prevent command injection
allowed_package_managers = %w[npm yarn pnpm bun].freeze
unless allowed_package_managers.include?(package_manager)
raise ArgumentError, "Invalid package manager: #{package_manager}"
end

base_commands = {
"npm" => %w[npm install],
"yarn" => %w[yarn add],
"pnpm" => %w[pnpm add],
"bun" => %w[bun add]
}

base_args = base_commands[package_manager].dup
base_args << dev_flag_for(package_manager) if dev
base_args + packages
end

def dev_flag_for(package_manager)
case package_manager
when "npm", "pnpm" then "--save-dev"
when "yarn", "bun" then "--dev"
end
end

def add_redux_specific_messages
# Override the generic messages with Redux-specific instructions
require_relative "generator_messages"
GeneratorMessages.output.clear
GeneratorMessages.add_info(
GeneratorMessages.helpful_message_after_installation(component_name: "HelloWorldApp", route: "hello_world")
Expand Down
Loading
Loading