Customize the react:component generator
This commit is contained in:
parent
6dfaf27f2e
commit
5af6cfb865
2 changed files with 288 additions and 0 deletions
251
gems/bess/lib/generators/react/component_generator.rb
Normal file
251
gems/bess/lib/generators/react/component_generator.rb
Normal 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
|
37
gems/bess/lib/generators/templates/component.js.jsx.tsx
Normal file
37
gems/bess/lib/generators/templates/component.js.jsx.tsx
Normal 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 %>
|
Loading…
Reference in a new issue