Skip to content

Commit 2b14683

Browse files
authored
Enhanced TypeScript Generator Support with Security Fixes #1786
This PR modernizes the TypeScript generators with improved type patterns, security enhancements, and better developer experience. ## Key Improvements ### 🔧 Generator Enhancements - Modern TypeScript patterns using type inference over explicit annotations - Optimized tsconfig.json with "moduleResolution": "bundler" - Enhanced Redux TypeScript integration with modern React patterns - Smart bin/dev defaults that automatically navigate to /hello_world ### 🔐 Security Fixes - Fixed HIGH PRIORITY command injection vulnerabilities in package installation - Replaced unsafe string interpolation with secure array-based system calls - Enhanced input validation across all generators ### 🎯 Developer Experience - Cleaner component templates following TypeScript best practices - Improved helper methods for consistent file extension handling - Better test expectations matching improved code patterns - All linting and CI tests passing ## Breaking Changes None - all changes are backward compatible ## Migration No migration required - improvements are automatically applied to new projects Resolves issues with TypeScript generator code quality and eliminates security vulnerabilities while enhancing the developer experience.
1 parent ce30d89 commit 2b14683

File tree

23 files changed

+13568
-1062
lines changed

23 files changed

+13568
-1062
lines changed

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,26 @@ After a release, please make sure to run `bundle exec rake update_changelog`. Th
2323

2424
Changes since the last non-beta release.
2525

26+
#### Enhanced TypeScript Generator Support
27+
28+
**🔧 Generator Improvements**
29+
30+
- **Modern TypeScript patterns**: Generators now produce more idiomatic TypeScript code with improved type inference instead of explicit type annotations [PR 1786](https://github.com/shakacode/react_on_rails/pull/1786) by [justin808](https://github.com/justin808)
31+
- **Optimized tsconfig.json**: Updated compiler options to use `"moduleResolution": "bundler"` for better bundler compatibility
32+
- **Enhanced Redux TypeScript integration**: Improved type safety and modern React patterns (useMemo, type-only imports)
33+
- **Smart bin/dev defaults**: Generated `bin/dev` script now automatically navigates to `/hello_world` route for immediate component visibility
34+
35+
**🔐 Security Enhancements**
36+
37+
- **Fixed command injection vulnerabilities**: Replaced unsafe string interpolation in generator package installation commands with secure array-based system calls
38+
- **Improved input validation**: Enhanced package manager validation and argument sanitization across all generators
39+
40+
**🎯 Developer Experience**
41+
42+
- **Better component templates**: Removed unnecessary type annotations while maintaining type safety through TypeScript's inference
43+
- **Cleaner generated code**: Streamlined templates following modern React and TypeScript best practices
44+
- **Improved helper methods**: Added reusable `component_extension` helper for consistent file extension handling
45+
2646
### [16.0.0] - 2025-09-16
2747

2848
**React on Rails v16 is a major release that modernizes the library with ESM support, removes legacy Webpacker compatibility, and introduces significant performance improvements. This release builds on the foundation of v14 with enhanced RSC (React Server Components) support and streamlined configuration.**

eslint.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ const config = tsEslint.config([
4444
// fixtures
4545
'**/fixtures/',
4646
'**/.yalc/**/*',
47+
// generator templates - exclude TypeScript templates that need tsconfig.json
48+
'**/templates/**/*.tsx',
49+
'**/templates/**/*.ts',
4750
]),
4851
{
4952
files: ['**/*.[jt]s', '**/*.[jt]sx', '**/*.[cm][jt]s'],

lib/generators/react_on_rails/base_generator.rb

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -105,13 +105,13 @@ def add_js_dependencies
105105
def install_js_dependencies
106106
# Detect which package manager to use
107107
success = if File.exist?(File.join(destination_root, "yarn.lock"))
108-
run "yarn install"
108+
system("yarn", "install")
109109
elsif File.exist?(File.join(destination_root, "pnpm-lock.yaml"))
110-
run "pnpm install"
110+
system("pnpm", "install")
111111
elsif File.exist?(File.join(destination_root, "package-lock.json")) ||
112112
File.exist?(File.join(destination_root, "package.json"))
113113
# Use npm for package-lock.json or as default fallback
114-
run "npm install"
114+
system("npm", "install")
115115
else
116116
true # No package manager detected, skip
117117
end
@@ -173,7 +173,7 @@ def add_react_on_rails_package
173173
return if add_npm_dependencies(react_on_rails_pkg)
174174

175175
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)
177177
handle_npm_failure("react-on-rails package", react_on_rails_pkg) unless success
178178
end
179179

@@ -189,7 +189,7 @@ def add_react_dependencies
189189
]
190190
return if add_npm_dependencies(react_deps)
191191

192-
success = run "npm install #{react_deps.join(' ')}"
192+
success = system("npm", "install", *react_deps)
193193
handle_npm_failure("React dependencies", react_deps) unless success
194194
end
195195

@@ -203,7 +203,7 @@ def add_css_dependencies
203203
]
204204
return if add_npm_dependencies(css_deps)
205205

206-
success = run "npm install #{css_deps.join(' ')}"
206+
success = system("npm", "install", *css_deps)
207207
handle_npm_failure("CSS dependencies", css_deps) unless success
208208
end
209209

@@ -215,7 +215,7 @@ def add_dev_dependencies
215215
]
216216
return if add_npm_dependencies(dev_deps, dev: true)
217217

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

lib/generators/react_on_rails/generator_helper.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,8 @@ def copy_file_and_missing_parent_directories(source_file, destination_file = nil
9191
def add_documentation_reference(message, source)
9292
"#{message} \n#{source}"
9393
end
94+
95+
def component_extension(options)
96+
options.typescript? ? "tsx" : "jsx"
97+
end
9498
end

lib/generators/react_on_rails/install_generator.rb

Lines changed: 103 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require "rails/generators"
4+
require "json"
45
require_relative "generator_helper"
56
require_relative "generator_messages"
67

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

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+
2331
# --ignore-warnings
2432
class_option :ignore_warnings,
2533
type: :boolean,
@@ -58,11 +66,16 @@ def print_generator_messages
5866

5967
def invoke_generators
6068
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? }
6275
if options.redux?
63-
invoke "react_on_rails:react_with_redux"
76+
invoke "react_on_rails:react_with_redux", [], { typescript: options.typescript? }
6477
else
65-
invoke "react_on_rails:react_no_redux"
78+
invoke "react_on_rails:react_no_redux", [], { typescript: options.typescript? }
6679
end
6780
end
6881

@@ -311,10 +324,96 @@ def missing_package_manager?
311324
false
312325
end
313326

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+
314413
# Removed: Shakapacker auto-installation logic (now explicit dependency)
315414

316415
# Removed: Shakapacker 8+ is now required as explicit dependency
416+
# rubocop:enable Metrics/ClassLength
317417
end
318-
# rubocop:enable Metrics/ClassLength
319418
end
320419
end

lib/generators/react_on_rails/react_no_redux_generator.rb

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,24 @@ class ReactNoReduxGenerator < Rails::Generators::Base
1111
Rails::Generators.hide_namespace(namespace)
1212
source_root(File.expand_path("templates", __dir__))
1313

14+
class_option :typescript,
15+
type: :boolean,
16+
default: false,
17+
desc: "Generate TypeScript files"
18+
1419
def copy_base_files
1520
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
2032
end
2133

2234
def create_appropriate_templates

0 commit comments

Comments
 (0)