module React module Generators class ComponentGenerator < ::Rails::Generators::NamedBase source_root File.expand_path '../../templates', __FILE__ desc <<-DESC.strip_heredoc Description: Scaffold a React component into `components/` of your Webpacker source or asset pipeline. The generated component will include a basic render function and a PropTypes hash to help with development. Available field types: Basic prop types do not take any additional arguments. If you do not specify a prop type, the generic node will be used. The basic types available are: any array bool element func number object node shape string Special PropTypes take additional arguments in {}, and must be enclosed in single quotes to keep bash from expanding the arguments in {}. instanceOf takes an optional class name in the form of {className} oneOf behaves like an enum, and takes an optional list of strings that will be allowed in the form of 'name:oneOf{one,two,three}'. oneOfType. oneOfType takes an optional list of react and custom types in the form of 'model:oneOfType{string,number,OtherType}' Examples: rails g react:component person name rails g react:component restaurant name:string rating:number owner:instanceOf{Person} rails g react:component food 'kind:oneOf{meat,cheese,vegetable}' rails g react:component events 'location:oneOfType{string,Restaurant}' DESC argument :attributes, :type => :array, :default => [], :banner => 'field[:type] field[:type] ...' class_option :es6, type: :boolean, 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, desc: 'Output coffeescript based component' REACT_PROP_TYPES = { 'node' => 'PropTypes.node', 'bool' => 'PropTypes.bool', 'boolean' => 'PropTypes.bool', 'string' => 'PropTypes.string', 'number' => 'PropTypes.number', 'object' => 'PropTypes.object', 'array' => 'PropTypes.array', 'shape' => 'PropTypes.shape({})', 'element' => 'PropTypes.element', 'func' => 'PropTypes.func', 'function' => 'PropTypes.func', 'any' => 'PropTypes.any', 'instanceOf' => ->(type) { 'PropTypes.instanceOf(%s)' % type.to_s.camelize }, 'oneOf' => ->(*options) { enums = options.map{ |k| "'#{k.to_s}'" }.join(',') 'PropTypes.oneOf([%s])' % enums }, 'oneOfType' => ->(*options) { types = options.map{ |k| "#{lookup(k.to_s, k.to_s)}" }.join(',') 'PropTypes.oneOfType([%s])' % types } } 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 'js.jsx' end # Prefer webpacker to sprockets: if webpacker? new_file_name = file_name.camelize 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) .to_s else new_file_name = file_name extension = template_extension target_dir = 'app/assets/javascripts/components' end file_path = File.join(target_dir, class_path, "#{new_file_name}.#{extension}") template("component.#{template_extension}", file_path) end private def webpack_configuration Webpacker.respond_to?(:config) ? Webpacker.config : Webpacker::Configuration end def component_name file_name.camelize end 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 '' end end def file_footer if webpacker? %|export default #{component_name}| else '' end end def webpacker? defined?(Webpacker) end 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 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] if react_prop_type.blank? if type =~ /^[[:upper:]]/ react_prop_type = REACT_PROP_TYPES['instanceOf'] else react_prop_type = REACT_PROP_TYPES['node'] end end options = options.to_s.gsub(/[{}]/, '').split(',') react_prop_type = react_prop_type.call(*options) if react_prop_type.respond_to? :call react_prop_type end def lookup(type = 'node', options = '') self.class.lookup(type, options) end end end end