diff --git a/lib/generators/react/component_generator.rb b/lib/generators/react/component_generator.rb index 1c90c38d4..bc98fe360 100644 --- a/lib/generators/react/component_generator.rb +++ b/lib/generators/react/component_generator.rb @@ -55,6 +55,11 @@ class ComponentGenerator < ::Rails::Generators::NamedBase default: false, desc: 'Output es6 class based component' + class_option :ts, + type: :boolean, + default: false, + desc: 'Output tsx class based component' + class_option :coffee, type: :boolean, default: false, @@ -89,9 +94,38 @@ class ComponentGenerator < ::Rails::Generators::NamedBase } } + TYPESCRIPT_TYPES = { + 'node' => 'React.ReactNode', + 'bool' => 'boolean', + 'boolean' => 'boolean', + 'string' => 'string', + 'number' => 'number', + 'object' => 'object', + 'array' => 'Array', + 'shape' => 'object', + 'element' => 'object', + 'func' => 'object', + 'function' => 'object', + 'any' => 'any', + + 'instanceOf' => ->(type) { + type.to_s.camelize + }, + + 'oneOf' => ->(*opts) { + opts.map{ |k| "'#{k.to_s}'" }.join(" | ") + }, + + 'oneOfType' => ->(*opts) { + opts.map{ |k| "#{ts_lookup(k.to_s, k.to_s)}" }.join(" | ") + } + } + def create_component_file template_extension = if options[:coffee] 'js.jsx.coffee' + elsif options[:ts] + 'js.jsx.tsx' elsif options[:es6] || webpacker? 'es6.jsx' else @@ -101,7 +135,13 @@ def create_component_file # Prefer webpacker to sprockets: if webpacker? new_file_name = file_name.camelize - extension = options[:coffee] ? 'coffee' : 'js' + extension = if options[:coffee] + 'coffee' + elsif options[:ts] + 'tsx' + else + 'js' + end target_dir = webpack_configuration.source_path .join('components') .relative_path_from(::Rails.root) @@ -128,6 +168,7 @@ def component_name def file_header if webpacker? + return %|import * as React from "react"\n| if options[:ts] %|import React from "react"\nimport PropTypes from "prop-types"\n| else '' @@ -146,23 +187,58 @@ def webpacker? defined?(Webpacker) end - def parse_attributes! - self.attributes = (attributes || []).map do |attr| - name = '' - type = '' - options = '' - options_regex = /(?{.*})/ + def parse_attributes! + self.attributes = (attributes || []).map do |attr| + name = '' + type = '' + args = '' + args_regex = /(?{.*})/ + + name, type = attr.split(':') + + if matchdata = args_regex.match(type) + args = matchdata[:args] + type = type.gsub(args_regex, '') + end + + if options[:ts] + { :name => name, :type => ts_lookup(name, type, args), :union => union?(args) } + else + { :name => name, :type => lookup(type, args) } + end + end + end + + def union?(args = '') + return args.to_s.gsub(/[{}]/, '').split(',').count > 1 + end - name, type = attr.split(':') + def self.ts_lookup(name, type = 'node', args = '') + ts_type = TYPESCRIPT_TYPES[type] + if ts_type.blank? + if type =~ /^[[:upper:]]/ + ts_type = TYPESCRIPT_TYPES['instanceOf'] + else + ts_type = TYPESCRIPT_TYPES['node'] + end + end - if matchdata = options_regex.match(type) - options = matchdata[:options] - type = type.gsub(options_regex, '') - end + args = args.to_s.gsub(/[{}]/, '').split(',') - { :name => name, :type => lookup(type, options) } - end - end + if ts_type.respond_to? :call + if args.blank? + return ts_type.call(type) + end + + ts_type = ts_type.call(*args) + end + + ts_type + end + + def ts_lookup(name, type = 'node', args = '') + self.class.ts_lookup(name, type, args) + end def self.lookup(type = 'node', options = '') react_prop_type = REACT_PROP_TYPES[type] diff --git a/lib/generators/templates/component.js.jsx.tsx b/lib/generators/templates/component.js.jsx.tsx new file mode 100644 index 000000000..ffdb98a49 --- /dev/null +++ b/lib/generators/templates/component.js.jsx.tsx @@ -0,0 +1,36 @@ +<%= file_header %> +<% unions = attributes.select{ |a| a[:union] } -%> +<% if unions.size > 0 -%> +<% unions.each do |e| -%> +type <%= e[:name].titleize %> = <%= e[:type]%> +<% end -%> +<% end -%> + +interface I<%= component_name %>Props { +<% if attributes.size > 0 -%> +<% attributes.each do | attribute | -%> +<% if attribute[:union] -%> + <%= attribute[:name].camelize(:lower) %>: <%= attribute[:name].titleize %>; +<% else -%> + <%= attribute[:name].camelize(:lower) %>: <%= attribute[:type] %>; +<% end -%> +<% end -%> +<% end -%> +} + +interface I<%= component_name %>State { +} + +class <%= component_name %> extends React.Component Props, I<%= component_name %>State> { + render() { + return ( + + <% attributes.each do |attribute| -%> + <%= attribute[:name].titleize %>: {this.props.<%= attribute[:name].camelize(:lower) %>} + <% end -%> + + ); + } +} + +<%= file_footer %>