From c1f4855c937884a58abec468c4b6764a4d423e1e Mon Sep 17 00:00:00 2001 From: Kaosisochukwu Uzokwe Date: Sun, 26 May 2019 22:26:31 -0400 Subject: [PATCH 1/3] Update component_generator.rb --- lib/generators/react/component_generator.rb | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/generators/react/component_generator.rb b/lib/generators/react/component_generator.rb index 1c90c38d..d1b3312f 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, @@ -92,6 +97,8 @@ class ComponentGenerator < ::Rails::Generators::NamedBase 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 +108,12 @@ 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' target_dir = webpack_configuration.source_path .join('components') .relative_path_from(::Rails.root) @@ -129,6 +141,8 @@ def component_name def file_header if webpacker? %|import React from "react"\nimport PropTypes from "prop-types"\n| + elsif options[:ts] + %|import * as React from "react"\n| else '' end From 7ecb7c957c2ab4f86386719896c70bd2683819ee Mon Sep 17 00:00:00 2001 From: Kaosisochukwu Uzokwe Date: Sun, 26 May 2019 23:02:41 -0400 Subject: [PATCH 2/3] Create typescript component template --- lib/generators/templates/component.js.jsx.tsx | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 lib/generators/templates/component.js.jsx.tsx diff --git a/lib/generators/templates/component.js.jsx.tsx b/lib/generators/templates/component.js.jsx.tsx new file mode 100644 index 00000000..31703823 --- /dev/null +++ b/lib/generators/templates/component.js.jsx.tsx @@ -0,0 +1,23 @@ +<%= file_header %> +interface I<%= component_name %>Props { +<% if attributes.size > 0 -%> + <% attributes.each_with_index do | attribute, idx | -%> + <%= attribute[:name].camelize(:lower) %>?: <%= attribute[:type] %>; + <% 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 %> From d6b59687aa6da577ff1625347bc9bf4ed4b89125 Mon Sep 17 00:00:00 2001 From: Kaosisochukwu Uzokwe Date: Mon, 27 May 2019 13:26:11 -0400 Subject: [PATCH 3/3] 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 --- lib/generators/react/component_generator.rb | 94 +++++++++++++++---- lib/generators/templates/component.js.jsx.tsx | 27 ++++-- 2 files changed, 98 insertions(+), 23 deletions(-) diff --git a/lib/generators/react/component_generator.rb b/lib/generators/react/component_generator.rb index d1b3312f..bc98fe36 100644 --- a/lib/generators/react/component_generator.rb +++ b/lib/generators/react/component_generator.rb @@ -94,6 +94,33 @@ 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' @@ -114,6 +141,7 @@ def create_component_file 'tsx' else 'js' + end target_dir = webpack_configuration.source_path .join('components') .relative_path_from(::Rails.root) @@ -140,9 +168,8 @@ 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| - elsif options[:ts] - %|import * as React from "react"\n| else '' end @@ -160,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(':') + name, type = attr.split(':') - if matchdata = options_regex.match(type) - options = matchdata[:options] - type = type.gsub(options_regex, '') - end + if matchdata = args_regex.match(type) + args = matchdata[:args] + type = type.gsub(args_regex, '') + end - { :name => name, :type => lookup(type, options) } - end - 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 + + 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 + + args = args.to_s.gsub(/[{}]/, '').split(',') + + 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 index 31703823..ffdb98a4 100644 --- a/lib/generators/templates/component.js.jsx.tsx +++ b/lib/generators/templates/component.js.jsx.tsx @@ -1,21 +1,34 @@ <%= 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_with_index do | attribute, idx | -%> - <%= attribute[:name].camelize(:lower) %>?: <%= attribute[:type] %>; - <% end -%> +<% 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 -%> - + <% attributes.each do |attribute| -%> + <%= attribute[:name].titleize %>: {this.props.<%= attribute[:name].camelize(:lower) %>} + <% end -%> + ); } }