Skip to content

Commit d5b313c

Browse files
justin808claude
andcommitted
Add TypeScript support to React on Rails install generator
This commit implements comprehensive TypeScript support for the React on Rails install generator, allowing developers to generate TypeScript-based React components with proper type safety and tooling support. ## Key Features Added ### Generator Options - Added `--typescript` or `-T` flag to the install generator - Enables TypeScript file generation and dependency installation - Maintains backward compatibility with JavaScript (default behavior) ### TypeScript Dependencies - Automatically installs required TypeScript packages: - `typescript` - TypeScript compiler - `@types/react` - React type definitions - `@types/react-dom` - React DOM type definitions - `@babel/preset-typescript` - Babel preset for TypeScript compilation - Supports all package managers: npm, yarn, pnpm, and bun ### TypeScript Templates - Created `.tsx` versions of all React component templates - Proper TypeScript interfaces for component props - Full type annotations for state and event handlers - Server and client component templates with type safety ### Configuration - Generates `tsconfig.json` with React-optimized settings - Automatic Babel configuration through Shakapacker detection - Proper JSX handling with React 18+ automatic runtime ### Testing Integration - Updated generator specs to test TypeScript functionality - Validates proper file generation and dependency installation - Ensures TypeScript compilation works correctly ## Technical Details The implementation leverages Shakapacker's automatic TypeScript preset detection. When `@babel/preset-typescript` is installed, Shakapacker automatically includes it in the Babel configuration, enabling seamless TypeScript compilation without manual Babel configuration changes. ## Usage ```bash # Generate TypeScript React on Rails app rails generate react_on_rails:install --typescript # Short form rails generate react_on_rails:install -T ``` This addresses the growing demand for TypeScript support in React on Rails applications and provides a smooth migration path for developers wanting to adopt TypeScript. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 6415a58 commit d5b313c

File tree

12 files changed

+13393
-1038
lines changed

12 files changed

+13393
-1038
lines changed

lib/generators/react_on_rails/install_generator.rb

Lines changed: 132 additions & 3 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,12 @@ def print_generator_messages
5866

5967
def invoke_generators
6068
ensure_shakapacker_installed
61-
invoke "react_on_rails:base"
69+
install_typescript_dependencies if options.typescript?
70+
invoke "react_on_rails:base", [], { typescript: options.typescript? }
6271
if options.redux?
63-
invoke "react_on_rails:react_with_redux"
72+
invoke "react_on_rails:react_with_redux", [], { typescript: options.typescript? }
6473
else
65-
invoke "react_on_rails:react_no_redux"
74+
invoke "react_on_rails:react_no_redux", [], { typescript: options.typescript? }
6675
end
6776
end
6877

@@ -302,6 +311,126 @@ def missing_package_manager?
302311
false
303312
end
304313

314+
def install_typescript_dependencies
315+
puts Rainbow("📝 Installing TypeScript dependencies...").yellow
316+
317+
# Determine the package manager to use
318+
package_manager = detect_package_manager
319+
return unless package_manager
320+
321+
# Install TypeScript and React type definitions
322+
typescript_packages = %w[
323+
typescript
324+
@types/react
325+
@types/react-dom
326+
@babel/preset-typescript
327+
]
328+
329+
install_command = case package_manager
330+
when "npm"
331+
"npm install --save-dev #{typescript_packages.join(' ')}"
332+
when "yarn"
333+
"yarn add --dev #{typescript_packages.join(' ')}"
334+
when "pnpm"
335+
"pnpm add --save-dev #{typescript_packages.join(' ')}"
336+
when "bun"
337+
"bun add --dev #{typescript_packages.join(' ')}"
338+
end
339+
340+
success = system(install_command)
341+
unless success
342+
warning = <<~MSG.strip
343+
⚠️ Failed to install TypeScript dependencies automatically.
344+
345+
Please run manually:
346+
#{install_command}
347+
348+
TypeScript files will still be generated.
349+
MSG
350+
GeneratorMessages.add_warning(warning)
351+
end
352+
353+
# Generate tsconfig.json
354+
create_typescript_config
355+
356+
puts Rainbow("✅ TypeScript support configured").green
357+
puts Rainbow(" Note: Shakapacker automatically detects @babel/preset-typescript").blue
358+
end
359+
360+
def detect_package_manager
361+
return "yarn" if File.exist?("yarn.lock")
362+
return "pnpm" if File.exist?("pnpm-lock.yaml")
363+
return "bun" if File.exist?("bun.lockb")
364+
return "npm" if File.exist?("package-lock.json") || cli_exists?("npm")
365+
366+
nil
367+
end
368+
369+
def create_typescript_config
370+
tsconfig_content = {
371+
"compilerOptions" => {
372+
"target" => "es2018",
373+
"lib" => ["dom", "dom.iterable", "es6"],
374+
"allowJs" => true,
375+
"skipLibCheck" => true,
376+
"esModuleInterop" => true,
377+
"allowSyntheticDefaultImports" => true,
378+
"strict" => true,
379+
"forceConsistentCasingInFileNames" => true,
380+
"noFallthroughCasesInSwitch" => true,
381+
"module" => "esnext",
382+
"moduleResolution" => "node",
383+
"resolveJsonModule" => true,
384+
"isolatedModules" => true,
385+
"noEmit" => true,
386+
"jsx" => "react-jsx"
387+
},
388+
"include" => [
389+
"app/javascript/**/*"
390+
],
391+
"exclude" => [
392+
"node_modules"
393+
]
394+
}
395+
396+
File.write("tsconfig.json", JSON.pretty_generate(tsconfig_content))
397+
puts Rainbow("✅ Created tsconfig.json").green
398+
end
399+
400+
def configure_babel_for_typescript
401+
# Install Babel TypeScript preset
402+
package_manager = detect_package_manager
403+
return unless package_manager
404+
405+
babel_typescript_package = "@babel/preset-typescript"
406+
407+
install_command = case package_manager
408+
when "npm"
409+
"npm install --save-dev #{babel_typescript_package}"
410+
when "yarn"
411+
"yarn add --dev #{babel_typescript_package}"
412+
when "pnpm"
413+
"pnpm add --save-dev #{babel_typescript_package}"
414+
when "bun"
415+
"bun add --dev #{babel_typescript_package}"
416+
end
417+
418+
puts Rainbow("📝 Installing Babel TypeScript preset...").yellow
419+
success = system(install_command)
420+
unless success
421+
warning = <<~MSG.strip
422+
⚠️ Failed to install Babel TypeScript preset automatically.
423+
424+
Please run manually:
425+
#{install_command}
426+
427+
TypeScript compilation may not work without this preset.
428+
MSG
429+
GeneratorMessages.add_warning(warning)
430+
return
431+
end
432+
end
433+
305434
# Removed: Shakapacker auto-installation logic (now explicit dependency)
306435

307436
# Removed: Shakapacker 8+ is now required as explicit dependency

lib/generators/react_on_rails/react_no_redux_generator.rb

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,25 @@ 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+
extension = options.typescript? ? "tsx" : "jsx"
22+
23+
# Determine which component files to copy based on TypeScript option
24+
component_files = [
25+
"app/javascript/src/HelloWorld/ror_components/HelloWorld.client.#{extension}",
26+
"app/javascript/src/HelloWorld/ror_components/HelloWorld.server.#{extension}",
27+
"app/javascript/src/HelloWorld/ror_components/HelloWorld.module.css"
28+
]
29+
30+
component_files.each do |file|
31+
copy_file("#{base_js_path}/#{file}", file)
32+
end
2033
end
2134

2235
def create_appropriate_templates

lib/generators/react_on_rails/react_with_redux_generator.rb

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ class ReactWithReduxGenerator < Rails::Generators::Base
88
Rails::Generators.hide_namespace(namespace)
99
source_root(File.expand_path("templates", __dir__))
1010

11+
class_option :typescript,
12+
type: :boolean,
13+
default: false,
14+
desc: "Generate TypeScript files"
15+
1116
def create_redux_directories
1217
# Create auto-registration directory structure for Redux
1318
empty_directory("app/javascript/src/HelloWorldApp/ror_components")
@@ -19,33 +24,40 @@ def create_redux_directories
1924

2025
def copy_base_files
2126
base_js_path = "redux/base"
27+
extension = options.typescript? ? "tsx" : "jsx"
2228

2329
# 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")
30+
copy_file("#{base_js_path}/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.client.#{extension}",
31+
"app/javascript/src/HelloWorldApp/ror_components/HelloWorldApp.client.#{extension}")
32+
copy_file("#{base_js_path}/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.server.#{extension}",
33+
"app/javascript/src/HelloWorldApp/ror_components/HelloWorldApp.server.#{extension}")
2834
copy_file("#{base_js_path}/app/javascript/bundles/HelloWorld/components/HelloWorld.module.css",
2935
"app/javascript/src/HelloWorldApp/components/HelloWorld.module.css")
3036

3137
# Update import paths in client component
32-
ror_client_file = "app/javascript/src/HelloWorldApp/ror_components/HelloWorldApp.client.jsx"
38+
ror_client_file = "app/javascript/src/HelloWorldApp/ror_components/HelloWorldApp.client.#{extension}"
3339
gsub_file(ror_client_file, "../store/helloWorldStore", "../store/helloWorldStore")
3440
gsub_file(ror_client_file, "../containers/HelloWorldContainer",
3541
"../containers/HelloWorldContainer")
3642
end
3743

3844
def copy_base_redux_files
3945
base_hello_world_path = "redux/base/app/javascript/bundles/HelloWorld"
46+
component_extension = options.typescript? ? "tsx" : "jsx"
47+
48+
# Copy non-component files (keep as .js for now)
4049
%w[actions/helloWorldActionCreators.js
4150
containers/HelloWorldContainer.js
4251
constants/helloWorldConstants.js
4352
reducers/helloWorldReducer.js
44-
store/helloWorldStore.js
45-
components/HelloWorld.jsx].each do |file|
53+
store/helloWorldStore.js].each do |file|
4654
copy_file("#{base_hello_world_path}/#{file}",
4755
"app/javascript/src/HelloWorldApp/#{file}")
4856
end
57+
58+
# Copy component file with appropriate extension
59+
copy_file("#{base_hello_world_path}/components/HelloWorld.#{component_extension}",
60+
"app/javascript/src/HelloWorldApp/components/HelloWorld.#{component_extension}")
4961
end
5062

5163
def create_appropriate_templates
@@ -60,7 +72,11 @@ def create_appropriate_templates
6072
end
6173

6274
def add_redux_npm_dependencies
63-
run "npm install redux react-redux"
75+
if options.typescript?
76+
run "npm install redux react-redux @types/react-redux"
77+
else
78+
run "npm install redux react-redux"
79+
end
6480
end
6581

6682
def add_redux_specific_messages
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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<string>(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
19+
id="name"
20+
type="text"
21+
value={name}
22+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setName(e.target.value)}
23+
/>
24+
</label>
25+
</form>
26+
</div>
27+
);
28+
};
29+
30+
export default HelloWorld;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import React from 'react';
2+
import * as style from './HelloWorld.module.css';
3+
4+
interface HelloWorldProps {
5+
name: string;
6+
updateName: (name: string) => void;
7+
}
8+
9+
const HelloWorld: React.FC<HelloWorldProps> = ({ name, updateName }) => (
10+
<div>
11+
<h3>
12+
Hello,
13+
{name}!
14+
</h3>
15+
<hr />
16+
<form>
17+
<label className={style.bright} htmlFor="name">
18+
Say hello to:
19+
<input
20+
id="name"
21+
type="text"
22+
value={name}
23+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => updateName(e.target.value)}
24+
/>
25+
</label>
26+
</form>
27+
</div>
28+
);
29+
30+
export default HelloWorld;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import React from 'react';
2+
import { Provider } from 'react-redux';
3+
4+
import configureStore from '../store/helloWorldStore';
5+
import HelloWorldContainer from '../containers/HelloWorldContainer';
6+
7+
interface HelloWorldAppProps {
8+
name: string;
9+
}
10+
11+
// See documentation for https://github.com/reactjs/react-redux.
12+
// This is how you get props from the Rails view into the redux store.
13+
// This code here binds your smart component to the redux store.
14+
const HelloWorldApp: React.FC<HelloWorldAppProps> = (props) => (
15+
<Provider store={configureStore(props)}>
16+
<HelloWorldContainer />
17+
</Provider>
18+
);
19+
20+
export default HelloWorldApp;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import HelloWorldApp from './HelloWorldApp.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 HelloWorldApp;

0 commit comments

Comments
 (0)