frails 0.1.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/.rubocop.yml +1 -3
- data/Gemfile +12 -1
- data/Gemfile.lock +101 -33
- data/README.md +166 -16
- data/bin/test +5 -0
- data/frails.gemspec +5 -8
- data/index.js +35 -0
- data/lib/frails.rb +17 -13
- data/lib/frails/component.rb +11 -0
- data/lib/frails/component/abstract_component.rb +58 -0
- data/lib/frails/component/component_renderer.rb +74 -0
- data/lib/frails/component/plain_component.rb +19 -0
- data/lib/frails/component/react_component.rb +17 -0
- data/lib/frails/component/react_component_renderer.rb +78 -0
- data/lib/frails/component/renderer_concerns.rb +36 -0
- data/lib/frails/dev_server.rb +2 -2
- data/lib/frails/dev_server_proxy.rb +3 -10
- data/lib/frails/helper.rb +73 -6
- data/lib/frails/log_subscriber.rb +35 -0
- data/lib/frails/manifest.rb +40 -3
- data/lib/frails/manifest_manager.rb +22 -0
- data/lib/frails/monkey/action_view/abstract_renderer.rb +54 -0
- data/lib/frails/monkey/action_view/partial_renderer.rb +50 -0
- data/lib/frails/monkey/action_view/renderer.rb +38 -0
- data/lib/frails/monkey/action_view/template_renderer.rb +24 -0
- data/lib/frails/railtie.rb +18 -14
- data/lib/frails/version.rb +1 -1
- data/lib/tasks/frails.rake +31 -0
- data/package.json +16 -0
- data/package/components.js +47 -0
- data/package/side_load.js +25 -0
- data/yarn.lock +80 -0
- metadata +34 -45
- data/lib/frails/instance.rb +0 -11
- data/lib/frails/server_manifest.rb +0 -7
data/bin/test
ADDED
data/frails.gemspec
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
$LOAD_PATH.push File.expand_path('lib', __dir__)
|
4
|
+
|
5
5
|
require 'frails/version'
|
6
6
|
|
7
7
|
Gem::Specification.new do |spec|
|
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
|
|
10
10
|
spec.authors = ['Joel Moss']
|
11
11
|
spec.email = ['joel@developwithstyle.com']
|
12
12
|
|
13
|
-
spec.summary = 'A Modern
|
13
|
+
spec.summary = 'A Modern [F]ront End on [Rails] and Webpack'
|
14
14
|
spec.homepage = 'https://github.com/joelmoss/frails'
|
15
15
|
|
16
16
|
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
@@ -34,10 +34,7 @@ Gem::Specification.new do |spec|
|
|
34
34
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
35
35
|
spec.require_paths = ['lib']
|
36
36
|
|
37
|
+
spec.add_dependency 'nokogiri', '>= 1.10.4'
|
37
38
|
spec.add_dependency 'rack-proxy', '>= 0.6.5'
|
38
|
-
spec.add_dependency '
|
39
|
-
|
40
|
-
spec.add_development_dependency 'bundler', '~> 2.0'
|
41
|
-
spec.add_development_dependency 'minitest', '~> 5.0'
|
42
|
-
spec.add_development_dependency 'rake', '~> 10.0'
|
39
|
+
spec.add_dependency 'rails', '>= 6.0'
|
43
40
|
end
|
data/index.js
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
const path = require('path')
|
2
|
+
|
3
|
+
const rootPath = path.resolve(__dirname, '../../../')
|
4
|
+
const publicOutputPath = process.env.FRAILS_PUBLIC_OUTPUT_PATH || 'assets'
|
5
|
+
|
6
|
+
// Ensure that the publicPath includes our asset host so dynamic imports
|
7
|
+
// (code-splitting chunks and static assets) load from the CDN instead of a relative path.
|
8
|
+
const getPublicPathWithAssetHost = () => {
|
9
|
+
const rootUrl = process.env.RAILS_ASSET_HOST || '/'
|
10
|
+
let packPath = `${publicOutputPath}/`
|
11
|
+
|
12
|
+
// Add relative root prefix to pack path.
|
13
|
+
if (process.env.RAILS_RELATIVE_URL_ROOT) {
|
14
|
+
let relativeRoot = process.env.RAILS_RELATIVE_URL_ROOT
|
15
|
+
relativeRoot = relativeRoot.startsWith('/') ? relativeRoot.substr(1) : relativeRoot
|
16
|
+
packPath = `${ensureTrailingSlash(relativeRoot)}${packPath}`
|
17
|
+
}
|
18
|
+
|
19
|
+
return (rootUrl.endsWith('/') ? rootUrl : `${rootUrl}/`) + packPath
|
20
|
+
}
|
21
|
+
|
22
|
+
module.exports = {
|
23
|
+
// Configuration
|
24
|
+
publicOutputPath,
|
25
|
+
manifestPath: process.env.FRAILS_MANIFEST_PATH || 'manifest.json',
|
26
|
+
devServerPort: process.env.FRAILS_DEV_SERVER_PORT || '8080',
|
27
|
+
devServerHost: process.env.FRAILS_DEV_SERVER_HOST || 'localhost',
|
28
|
+
|
29
|
+
// The local ident name required for loading component styles.
|
30
|
+
cssLocalIdentName: process.env.NODE_ENV == 'development' ? '[path][name]__[local]___[md5:hash:hex:6]' : '[local]-[md5:hash:hex:6]',
|
31
|
+
|
32
|
+
getPublicPathWithAssetHost,
|
33
|
+
sideLoadEntry: require('./package/side_load'),
|
34
|
+
components: require('./package/components')
|
35
|
+
}
|
data/lib/frails.rb
CHANGED
@@ -4,30 +4,34 @@ require 'frails/version'
|
|
4
4
|
require 'active_support/core_ext/module'
|
5
5
|
require 'active_support/core_ext/module/attribute_accessors'
|
6
6
|
|
7
|
+
# ENV['FRAILS_DEV_SERVER_PORT'] ||= '8080'
|
8
|
+
# ENV['FRAILS_DEV_SERVER_HOST'] ||= 'localhost'
|
9
|
+
# ENV['FRAILS_PUBLIC_OUTPUT_PATH'] ||= 'assets'
|
10
|
+
# ENV['FRAILS_MANIFEST_PATH'] ||= 'manifest.json'
|
11
|
+
|
7
12
|
module Frails
|
8
13
|
extend self
|
9
14
|
|
10
|
-
def
|
11
|
-
@
|
15
|
+
def dev_server
|
16
|
+
@dev_server ||= Frails::DevServer.new
|
12
17
|
end
|
13
18
|
|
14
|
-
def
|
15
|
-
@
|
19
|
+
def manifest
|
20
|
+
@manifest ||= Frails::ManifestManager.new
|
16
21
|
end
|
17
22
|
|
18
|
-
def
|
19
|
-
|
20
|
-
ENV['NODE_ENV'] = env
|
21
|
-
yield
|
22
|
-
ensure
|
23
|
-
ENV['NODE_ENV'] = original
|
23
|
+
def public_output_path
|
24
|
+
ENV['FRAILS_PUBLIC_OUTPUT_PATH'] || 'assets'
|
24
25
|
end
|
25
26
|
|
26
|
-
|
27
|
+
def manifest_path
|
28
|
+
ENV['FRAILS_MANIFEST_PATH'] || 'manifest.json'
|
29
|
+
end
|
27
30
|
end
|
28
31
|
|
29
|
-
require 'frails/
|
32
|
+
require 'frails/log_subscriber'
|
30
33
|
require 'frails/dev_server_proxy'
|
31
|
-
require 'frails/
|
34
|
+
require 'frails/manifest_manager'
|
32
35
|
require 'frails/dev_server'
|
36
|
+
require 'frails/component'
|
33
37
|
require 'frails/railtie'
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Frails::Component
|
4
|
+
end
|
5
|
+
|
6
|
+
require 'frails/component/renderer_concerns'
|
7
|
+
require 'frails/component/component_renderer'
|
8
|
+
require 'frails/component/react_component_renderer'
|
9
|
+
require 'frails/component/abstract_component'
|
10
|
+
require 'frails/component/plain_component'
|
11
|
+
require 'frails/component/react_component'
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Frails::Component::AbstractComponent
|
4
|
+
include ActiveSupport::Callbacks
|
5
|
+
|
6
|
+
define_callbacks :render
|
7
|
+
|
8
|
+
def initialize(view, options)
|
9
|
+
@view, @options = view, options
|
10
|
+
|
11
|
+
expand_instance_vars
|
12
|
+
end
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def before_render(*methods, &block)
|
16
|
+
set_callback :render, :before, *methods, &block
|
17
|
+
end
|
18
|
+
|
19
|
+
def after_render(*methods, &block)
|
20
|
+
set_callback :render, :after, *methods, &block
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# rubocop:disable Lint/ShadowedException, Style/MissingRespondToMissing
|
25
|
+
def method_missing(method, *args, &block)
|
26
|
+
super
|
27
|
+
rescue NoMethodError, NameError => e1
|
28
|
+
# the error is not mine, so just releases it as is.
|
29
|
+
raise e1 if e1.name != method
|
30
|
+
|
31
|
+
begin
|
32
|
+
@view.send method, *args, &block
|
33
|
+
rescue NoMethodError => e2
|
34
|
+
raise e2 if e2.name != method
|
35
|
+
|
36
|
+
raise NoMethodError.new("undefined method `#{method}' for either #{self} or #{@view}",
|
37
|
+
method)
|
38
|
+
rescue NameError => e2
|
39
|
+
raise e2 if e2.name != method
|
40
|
+
|
41
|
+
raise NameError.new("undefined local variable `#{method}' for either #{self} or #{@view}",
|
42
|
+
method)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
# rubocop:enable Style/MissingRespondToMissing, Lint/ShadowedException
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
# Define instance variables for those that are already defined in the @view_context. Excludes
|
50
|
+
# variables starting with an underscore.
|
51
|
+
def expand_instance_vars
|
52
|
+
@view.instance_variables.each do |var|
|
53
|
+
next if var.to_s.start_with?('@_')
|
54
|
+
|
55
|
+
instance_variable_set var, @view.instance_variable_get(var)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Frails::Component::ComponentRenderer < ActionView::PartialRenderer
|
4
|
+
include Frails::Component::RendererConcerns
|
5
|
+
|
6
|
+
def render(context, options, &block)
|
7
|
+
@view = context
|
8
|
+
@component = options.delete(:component).to_s
|
9
|
+
@presenter = presenter_class.new(@view, options)
|
10
|
+
|
11
|
+
result = @presenter.run_callbacks :render do
|
12
|
+
if @presenter.respond_to?(:render)
|
13
|
+
@presenter.render(&block)
|
14
|
+
else
|
15
|
+
options[:locals] = @presenter.locals
|
16
|
+
super context, options, block
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
apply_styles((result.respond_to?(:body) ? result.body : result) || nil)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def presenter_class
|
26
|
+
super || Frails::Component::PlainComponent
|
27
|
+
end
|
28
|
+
|
29
|
+
def apply_styles(content)
|
30
|
+
return nil if content.nil?
|
31
|
+
|
32
|
+
render_inline_styles
|
33
|
+
replace_css_module_attribute(content).html_safe
|
34
|
+
end
|
35
|
+
|
36
|
+
def stylesheet_entry_file
|
37
|
+
"components/#{@component}/index"
|
38
|
+
end
|
39
|
+
|
40
|
+
def replace_css_module_attribute(content)
|
41
|
+
doc = Nokogiri::HTML::DocumentFragment.parse(content)
|
42
|
+
|
43
|
+
return content if (modules = doc.css('[css_module]')).empty?
|
44
|
+
|
45
|
+
modules.each do |ele|
|
46
|
+
classes = class_name_for_style(ele.delete('css_module'))
|
47
|
+
ele['class'] = (ele['class'].nil? ? classes : classes << ele['class']).join(' ')
|
48
|
+
end
|
49
|
+
|
50
|
+
doc.to_html
|
51
|
+
end
|
52
|
+
|
53
|
+
def class_name_for_style(class_names)
|
54
|
+
class_names.to_s.split.map { |class_name| build_ident class_name }
|
55
|
+
end
|
56
|
+
|
57
|
+
def build_ident(local_name)
|
58
|
+
hash_digest = Digest::MD5.hexdigest("#{stylesheet_path}+#{local_name}")[0, 6]
|
59
|
+
|
60
|
+
return "#{local_name}-#{hash_digest}" unless Frails.dev_server.running?
|
61
|
+
|
62
|
+
name = stylesheet_path.basename.sub(stylesheet_path.extname, '').sub('.', '-')
|
63
|
+
ident = +"#{name}__#{local_name}___#{hash_digest}"
|
64
|
+
ident.prepend("#{stylesheet_path.dirname.to_s.tr('/', '-')}-")
|
65
|
+
ident
|
66
|
+
end
|
67
|
+
|
68
|
+
def stylesheet_path
|
69
|
+
@stylesheet_path ||= begin
|
70
|
+
style_file = "#{@component}/index.css"
|
71
|
+
Rails.root.join('app', 'components', style_file).relative_path_from(Rails.root)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Frails::Component::PlainComponent < Frails::Component::AbstractComponent
|
4
|
+
PRIVATE_METHODS = %i[render method_missing locals].freeze
|
5
|
+
|
6
|
+
def initialize(view, options)
|
7
|
+
super
|
8
|
+
|
9
|
+
@locals = @options.fetch(:locals, @options)
|
10
|
+
end
|
11
|
+
|
12
|
+
def locals
|
13
|
+
hash = {}
|
14
|
+
public_methods(false).each do |method|
|
15
|
+
hash[method] = send(method) unless PRIVATE_METHODS.include?(method)
|
16
|
+
end
|
17
|
+
hash.merge @locals
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Frails::Component::ReactComponent < Frails::Component::AbstractComponent
|
4
|
+
attr_accessor :class_name, :props, :tag, :prerender, :content_loader
|
5
|
+
|
6
|
+
def initialize(view, options)
|
7
|
+
@view, @options = view, options
|
8
|
+
|
9
|
+
@class_name = @options.fetch(:class, nil)
|
10
|
+
@props = @options.fetch(:props, {})
|
11
|
+
@tag = @options.fetch(:tag, :div)
|
12
|
+
@prerender = @options.fetch(:prerender, false)
|
13
|
+
@content_loader = @options.fetch(:content_loader, false)
|
14
|
+
|
15
|
+
expand_instance_vars
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Frails::Component::ReactComponentRenderer
|
4
|
+
include Frails::Component::RendererConcerns
|
5
|
+
|
6
|
+
def render(context, options, &block)
|
7
|
+
@view = context
|
8
|
+
@component = options.delete(:component).to_s
|
9
|
+
@presenter = presenter_class.new(@view, options)
|
10
|
+
|
11
|
+
@children = @view.capture(&block) if block_given?
|
12
|
+
|
13
|
+
render_with_callbacks || nil
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
# rubocop:disable Rails/OutputSafety
|
19
|
+
def render_with_callbacks
|
20
|
+
@presenter.run_callbacks :render do
|
21
|
+
@prerender = @presenter.prerender
|
22
|
+
@content_loader = @presenter.content_loader
|
23
|
+
@props = camelize_keys(@presenter.props)
|
24
|
+
@props[:children] = @children if @children
|
25
|
+
|
26
|
+
@prerender && render_inline_styles
|
27
|
+
|
28
|
+
rendered_tag = content_tag do
|
29
|
+
@prerender ? React::Renderer.new.render(@component, @props).html_safe : loader
|
30
|
+
end
|
31
|
+
|
32
|
+
@prerender ? move_console_replay_script(rendered_tag) : rendered_tag
|
33
|
+
end
|
34
|
+
end
|
35
|
+
# rubocop:enable Rails/OutputSafety
|
36
|
+
|
37
|
+
def presenter_class
|
38
|
+
super || Frails::Component::ReactComponent
|
39
|
+
end
|
40
|
+
|
41
|
+
def data_for_content_tag
|
42
|
+
{
|
43
|
+
componentPath: @component,
|
44
|
+
componentName: @component.to_s.tr('/', '_').camelize,
|
45
|
+
props: @props,
|
46
|
+
contentLoader: @content_loader,
|
47
|
+
renderMethod: @prerender ? 'hydrate' : 'render'
|
48
|
+
}.to_json
|
49
|
+
end
|
50
|
+
|
51
|
+
def content_tag(&block)
|
52
|
+
classes = "js__reactComponent #{@presenter.class_name}"
|
53
|
+
@view.content_tag @presenter.tag, class: classes, data: data_for_content_tag, &block
|
54
|
+
end
|
55
|
+
|
56
|
+
def camelize_keys(data)
|
57
|
+
data.deep_transform_keys { |key| key.to_s.camelize :lower }
|
58
|
+
end
|
59
|
+
|
60
|
+
def loader
|
61
|
+
return unless @content_loader
|
62
|
+
|
63
|
+
@view.render "shared/content_loaders/#{@content_loader == true ? 'code' : @content_loader}"
|
64
|
+
end
|
65
|
+
|
66
|
+
# Grab the server-rendered console replay script and move it outside the container div.
|
67
|
+
#
|
68
|
+
# rubocop:disable Rails/OutputSafety, Style/RegexpLiteral
|
69
|
+
def move_console_replay_script(rendered_tag)
|
70
|
+
regex = /\n(<script class="js__reactServerConsoleReplay">.*<\/script>)<\/(\w+)>$/m
|
71
|
+
rendered_tag.sub(regex, '</\2>\1').html_safe
|
72
|
+
end
|
73
|
+
# rubocop:enable Rails/OutputSafety, Style/RegexpLiteral
|
74
|
+
|
75
|
+
def stylesheet_entry_file
|
76
|
+
"#{@component.tr('/', '-')}-index-entry-jsx"
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Frails::Component::RendererConcerns
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
private
|
7
|
+
|
8
|
+
def presenter_class
|
9
|
+
klass_file = Rails.root.join('app', 'components', "#{@component}_component.rb")
|
10
|
+
klass_file.exist? && "#{@component.to_s.camelcase}Component".constantize
|
11
|
+
end
|
12
|
+
|
13
|
+
def render_inline_styles
|
14
|
+
# TODO: We don't yet have support for compiling Webpack for tests.
|
15
|
+
return if Rails.env.test?
|
16
|
+
|
17
|
+
# Don't inline the styles if already inlined.
|
18
|
+
return if inlined_stylesheets.include?(@component)
|
19
|
+
|
20
|
+
Frails.manifest.read!(stylesheet_entry_file, :stylesheet) do |path, src|
|
21
|
+
@view.content_for :component_styles do
|
22
|
+
@view.content_tag(:style, src, { data: { href: path } }, false)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
inlined_stylesheets << @component
|
27
|
+
end
|
28
|
+
|
29
|
+
def inlined_stylesheets
|
30
|
+
if @view.instance_variable_defined?(:@inlined_stylesheets)
|
31
|
+
@view.instance_variable_get :@inlined_stylesheets
|
32
|
+
else
|
33
|
+
@view.instance_variable_set :@inlined_stylesheets, []
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/frails/dev_server.rb
CHANGED
@@ -15,10 +15,10 @@ class Frails::DevServer
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def host
|
18
|
-
|
18
|
+
ENV['FRAILS_DEV_SERVER_HOST'] || 'localhost'
|
19
19
|
end
|
20
20
|
|
21
21
|
def port
|
22
|
-
|
22
|
+
ENV['FRAILS_DEV_SERVER_PORT'] || 8080
|
23
23
|
end
|
24
24
|
end
|
@@ -3,13 +3,6 @@
|
|
3
3
|
require 'rack/proxy'
|
4
4
|
|
5
5
|
class Frails::DevServerProxy < Rack::Proxy
|
6
|
-
delegate :dev_server, to: :@frails
|
7
|
-
|
8
|
-
def initialize(app = nil, opts = {})
|
9
|
-
@frails = Frails.instance
|
10
|
-
super
|
11
|
-
end
|
12
|
-
|
13
6
|
def rewrite_response(response)
|
14
7
|
_status, headers, _body = response
|
15
8
|
headers.delete 'transfer-encoding'
|
@@ -17,8 +10,8 @@ class Frails::DevServerProxy < Rack::Proxy
|
|
17
10
|
end
|
18
11
|
|
19
12
|
def perform_request(env)
|
20
|
-
if env['PATH_INFO'].start_with?(public_output_path) && dev_server.running?
|
21
|
-
host = dev_server.host_with_port
|
13
|
+
if env['PATH_INFO'].start_with?(public_output_path) && Frails.dev_server.running?
|
14
|
+
host = Frails.dev_server.host_with_port
|
22
15
|
env['HTTP_HOST'] = env['HTTP_X_FORWARDED_HOST'] = env['HTTP_X_FORWARDED_SERVER'] = host
|
23
16
|
env['HTTP_X_FORWARDED_PROTO'] = env['HTTP_X_FORWARDED_SCHEME'] = 'http'
|
24
17
|
env['SCRIPT_NAME'] = ''
|
@@ -32,6 +25,6 @@ class Frails::DevServerProxy < Rack::Proxy
|
|
32
25
|
private
|
33
26
|
|
34
27
|
def public_output_path
|
35
|
-
|
28
|
+
"/#{Frails.public_output_path}"
|
36
29
|
end
|
37
30
|
end
|