frails 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ $: << File.expand_path("../test", __dir__)
3
+
4
+ require "bundler/setup"
5
+ require "rails/plugin/test"
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- lib = File.expand_path('lib', __dir__)
4
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
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 Front End on Rails and Webpack'
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 'railties', '>= 5.2'
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
@@ -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
+ }
@@ -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 instance=(instance)
11
- @instance = instance
15
+ def dev_server
16
+ @dev_server ||= Frails::DevServer.new
12
17
  end
13
18
 
14
- def instance
15
- @instance ||= Frails::Instance.new
19
+ def manifest
20
+ @manifest ||= Frails::ManifestManager.new
16
21
  end
17
22
 
18
- def with_node_env(env)
19
- original = ENV['NODE_ENV']
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
- delegate :manifest, :dev_server, to: :instance
27
+ def manifest_path
28
+ ENV['FRAILS_MANIFEST_PATH'] || 'manifest.json'
29
+ end
27
30
  end
28
31
 
29
- require 'frails/instance'
32
+ require 'frails/log_subscriber'
30
33
  require 'frails/dev_server_proxy'
31
- require 'frails/manifest'
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
@@ -15,10 +15,10 @@ class Frails::DevServer
15
15
  end
16
16
 
17
17
  def host
18
- Rails.configuration.frails.dev_server_host
18
+ ENV['FRAILS_DEV_SERVER_HOST'] || 'localhost'
19
19
  end
20
20
 
21
21
  def port
22
- Rails.configuration.frails.dev_server_port
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
- Rails.configuration.frails.public_output_path
28
+ "/#{Frails.public_output_path}"
36
29
  end
37
30
  end