frails 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|