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.
@@ -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