251 lines
8 KiB
Ruby
251 lines
8 KiB
Ruby
|
# 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
|