Skip to content

Commit 17f0bfb

Browse files
delkopisoBookOfGreg
authored andcommitted
Typescript component generator (#990)
* Update component_generator.rb * Create typescript component template * Handle special prop types - Treat instanceOf prop types as reference to predefined types. - Treat oneOf prop types as union types, and define the union type. - Treat oneOfType prop types as union of primitives and custom types, and define both custom type and union type
1 parent bfc282c commit 17f0bfb

File tree

2 files changed

+127
-15
lines changed

2 files changed

+127
-15
lines changed

lib/generators/react/component_generator.rb

+91-15
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ class ComponentGenerator < ::Rails::Generators::NamedBase
5555
default: false,
5656
desc: 'Output es6 class based component'
5757

58+
class_option :ts,
59+
type: :boolean,
60+
default: false,
61+
desc: 'Output tsx class based component'
62+
5863
class_option :coffee,
5964
type: :boolean,
6065
default: false,
@@ -89,9 +94,38 @@ class ComponentGenerator < ::Rails::Generators::NamedBase
8994
}
9095
}
9196

97+
TYPESCRIPT_TYPES = {
98+
'node' => 'React.ReactNode',
99+
'bool' => 'boolean',
100+
'boolean' => 'boolean',
101+
'string' => 'string',
102+
'number' => 'number',
103+
'object' => 'object',
104+
'array' => 'Array<any>',
105+
'shape' => 'object',
106+
'element' => 'object',
107+
'func' => 'object',
108+
'function' => 'object',
109+
'any' => 'any',
110+
111+
'instanceOf' => ->(type) {
112+
type.to_s.camelize
113+
},
114+
115+
'oneOf' => ->(*opts) {
116+
opts.map{ |k| "'#{k.to_s}'" }.join(" | ")
117+
},
118+
119+
'oneOfType' => ->(*opts) {
120+
opts.map{ |k| "#{ts_lookup(k.to_s, k.to_s)}" }.join(" | ")
121+
}
122+
}
123+
92124
def create_component_file
93125
template_extension = if options[:coffee]
94126
'js.jsx.coffee'
127+
elsif options[:ts]
128+
'js.jsx.tsx'
95129
elsif options[:es6] || webpacker?
96130
'es6.jsx'
97131
else
@@ -101,7 +135,13 @@ def create_component_file
101135
# Prefer webpacker to sprockets:
102136
if webpacker?
103137
new_file_name = file_name.camelize
104-
extension = options[:coffee] ? 'coffee' : 'js'
138+
extension = if options[:coffee]
139+
'coffee'
140+
elsif options[:ts]
141+
'tsx'
142+
else
143+
'js'
144+
end
105145
target_dir = webpack_configuration.source_path
106146
.join('components')
107147
.relative_path_from(::Rails.root)
@@ -128,6 +168,7 @@ def component_name
128168

129169
def file_header
130170
if webpacker?
171+
return %|import * as React from "react"\n| if options[:ts]
131172
%|import React from "react"\nimport PropTypes from "prop-types"\n|
132173
else
133174
''
@@ -146,23 +187,58 @@ def webpacker?
146187
defined?(Webpacker)
147188
end
148189

149-
def parse_attributes!
150-
self.attributes = (attributes || []).map do |attr|
151-
name = ''
152-
type = ''
153-
options = ''
154-
options_regex = /(?<options>{.*})/
190+
def parse_attributes!
191+
self.attributes = (attributes || []).map do |attr|
192+
name = ''
193+
type = ''
194+
args = ''
195+
args_regex = /(?<args>{.*})/
196+
197+
name, type = attr.split(':')
198+
199+
if matchdata = args_regex.match(type)
200+
args = matchdata[:args]
201+
type = type.gsub(args_regex, '')
202+
end
203+
204+
if options[:ts]
205+
{ :name => name, :type => ts_lookup(name, type, args), :union => union?(args) }
206+
else
207+
{ :name => name, :type => lookup(type, args) }
208+
end
209+
end
210+
end
211+
212+
def union?(args = '')
213+
return args.to_s.gsub(/[{}]/, '').split(',').count > 1
214+
end
155215

156-
name, type = attr.split(':')
216+
def self.ts_lookup(name, type = 'node', args = '')
217+
ts_type = TYPESCRIPT_TYPES[type]
218+
if ts_type.blank?
219+
if type =~ /^[[:upper:]]/
220+
ts_type = TYPESCRIPT_TYPES['instanceOf']
221+
else
222+
ts_type = TYPESCRIPT_TYPES['node']
223+
end
224+
end
157225

158-
if matchdata = options_regex.match(type)
159-
options = matchdata[:options]
160-
type = type.gsub(options_regex, '')
161-
end
226+
args = args.to_s.gsub(/[{}]/, '').split(',')
162227

163-
{ :name => name, :type => lookup(type, options) }
164-
end
165-
end
228+
if ts_type.respond_to? :call
229+
if args.blank?
230+
return ts_type.call(type)
231+
end
232+
233+
ts_type = ts_type.call(*args)
234+
end
235+
236+
ts_type
237+
end
238+
239+
def ts_lookup(name, type = 'node', args = '')
240+
self.class.ts_lookup(name, type, args)
241+
end
166242

167243
def self.lookup(type = 'node', options = '')
168244
react_prop_type = REACT_PROP_TYPES[type]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<%= file_header %>
2+
<% unions = attributes.select{ |a| a[:union] } -%>
3+
<% if unions.size > 0 -%>
4+
<% unions.each do |e| -%>
5+
type <%= e[:name].titleize %> = <%= e[:type]%>
6+
<% end -%>
7+
<% end -%>
8+
9+
interface I<%= component_name %>Props {
10+
<% if attributes.size > 0 -%>
11+
<% attributes.each do | attribute | -%>
12+
<% if attribute[:union] -%>
13+
<%= attribute[:name].camelize(:lower) %>: <%= attribute[:name].titleize %>;
14+
<% else -%>
15+
<%= attribute[:name].camelize(:lower) %>: <%= attribute[:type] %>;
16+
<% end -%>
17+
<% end -%>
18+
<% end -%>
19+
}
20+
21+
interface I<%= component_name %>State {
22+
}
23+
24+
class <%= component_name %> extends React.Component <I<%= component_name %>Props, I<%= component_name %>State> {
25+
render() {
26+
return (
27+
<React.Fragment>
28+
<% attributes.each do |attribute| -%>
29+
<%= attribute[:name].titleize %>: {this.props.<%= attribute[:name].camelize(:lower) %>}
30+
<% end -%>
31+
</React.Fragment>
32+
);
33+
}
34+
}
35+
36+
<%= file_footer %>

0 commit comments

Comments
 (0)