Customize the react:component generator

This commit is contained in:
Eric 2020-06-23 17:27:52 -05:00 committed by Eric Schultz
parent 6dfaf27f2e
commit 5af6cfb865
2 changed files with 288 additions and 0 deletions

View file

@ -0,0 +1,251 @@
# frozen_string_literal: true
# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later
# Full license explanation at https://github.com/houdiniproject/houdini/blob/master/LICENSE
# from: https://github.com/reactjs/react-rails/blob/master/lib/generators/react/component_generator.rb
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 :ts,
type: :boolean,
default: true,
desc: 'Output tsx class 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<any>',
'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 = /(?<args>{.*})/
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

View file

@ -0,0 +1,37 @@
<% # License: LGPL-3.0-or-later
# Full license explanation at https://github.com/houdiniproject/houdini/blob/master/LICENSE
# from: https://github.com/reactjs/react-rails/blob/master/lib/generators/templates/component.js.jsx.tsx
%>
// License: LGPL-3.0-or-later
<%= 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 -%>
}
function <%= component_name %>(props:I<%= component_name %>Props) {
return (
<React.Fragment>
<% attributes.each do |attribute| -%>
<%= attribute[:name].titleize %>: {props.<%= attribute[:name].camelize(:lower) %>}
<% end -%>
</React.Fragment>
);
}
<%= file_footer %>