proscenium 0.16.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +908 -0
- data/lib/proscenium/builder.rb +189 -0
- data/lib/proscenium/core_ext/object/css_module_ivars.rb +19 -0
- data/lib/proscenium/css_module/path.rb +31 -0
- data/lib/proscenium/css_module/rewriter.rb +44 -0
- data/lib/proscenium/css_module/transformer.rb +84 -0
- data/lib/proscenium/css_module.rb +57 -0
- data/lib/proscenium/ensure_loaded.rb +27 -0
- data/lib/proscenium/ext/proscenium +0 -0
- data/lib/proscenium/ext/proscenium.h +131 -0
- data/lib/proscenium/helper.rb +70 -0
- data/lib/proscenium/importer.rb +134 -0
- data/lib/proscenium/libs/custom_element.js +54 -0
- data/lib/proscenium/libs/react-manager/index.jsx +121 -0
- data/lib/proscenium/libs/react-manager/react.js +2 -0
- data/lib/proscenium/libs/stimulus-loading.js +65 -0
- data/lib/proscenium/libs/test.js +1 -0
- data/lib/proscenium/libs/ujs/class.js +15 -0
- data/lib/proscenium/libs/ujs/data_confirm.js +23 -0
- data/lib/proscenium/libs/ujs/data_disable_with.js +68 -0
- data/lib/proscenium/libs/ujs/index.js +9 -0
- data/lib/proscenium/log_subscriber.rb +37 -0
- data/lib/proscenium/middleware/base.rb +103 -0
- data/lib/proscenium/middleware/engines.rb +45 -0
- data/lib/proscenium/middleware/esbuild.rb +30 -0
- data/lib/proscenium/middleware/runtime.rb +18 -0
- data/lib/proscenium/middleware/url.rb +16 -0
- data/lib/proscenium/middleware.rb +76 -0
- data/lib/proscenium/monkey.rb +95 -0
- data/lib/proscenium/phlex/asset_inclusions.rb +17 -0
- data/lib/proscenium/phlex/css_modules.rb +79 -0
- data/lib/proscenium/phlex/react_component.rb +32 -0
- data/lib/proscenium/phlex.rb +42 -0
- data/lib/proscenium/railtie.rb +106 -0
- data/lib/proscenium/react_componentable.rb +95 -0
- data/lib/proscenium/resolver.rb +39 -0
- data/lib/proscenium/side_load.rb +155 -0
- data/lib/proscenium/source_path.rb +15 -0
- data/lib/proscenium/templates/rescues/build_error.html.erb +30 -0
- data/lib/proscenium/ui/breadcrumbs/component.module.css +14 -0
- data/lib/proscenium/ui/breadcrumbs/component.rb +79 -0
- data/lib/proscenium/ui/breadcrumbs/computed_element.rb +69 -0
- data/lib/proscenium/ui/breadcrumbs/control.rb +95 -0
- data/lib/proscenium/ui/breadcrumbs/mixins.css +83 -0
- data/lib/proscenium/ui/breadcrumbs.rb +72 -0
- data/lib/proscenium/ui/component.rb +11 -0
- data/lib/proscenium/ui/test.js +1 -0
- data/lib/proscenium/ui.rb +14 -0
- data/lib/proscenium/utils.rb +13 -0
- data/lib/proscenium/version.rb +5 -0
- data/lib/proscenium/view_component/css_modules.rb +11 -0
- data/lib/proscenium/view_component/react_component.rb +22 -0
- data/lib/proscenium/view_component/sideload.rb +4 -0
- data/lib/proscenium/view_component.rb +38 -0
- data/lib/proscenium.rb +70 -0
- metadata +228 -0
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proscenium
|
4
|
+
class Middleware
|
5
|
+
class Esbuild < Base
|
6
|
+
class CompileError < Base::CompileError
|
7
|
+
def initialize(args)
|
8
|
+
detail = args[:detail]
|
9
|
+
detail = ActiveSupport::HashWithIndifferentAccess.new(Oj.load(detail, mode: :strict))
|
10
|
+
|
11
|
+
args[:detail] = if detail[:location]
|
12
|
+
"#{detail[:text]} in #{detail[:location][:file]}:" +
|
13
|
+
detail[:location][:line].to_s
|
14
|
+
else
|
15
|
+
detail[:text]
|
16
|
+
end
|
17
|
+
|
18
|
+
super
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def attempt
|
23
|
+
render_response Builder.build_to_string(path_to_build, root: Rails.root.to_s,
|
24
|
+
base_url: @request.base_url)
|
25
|
+
rescue Builder::CompileError => e
|
26
|
+
raise self.class::CompileError, { file: @request.fullpath, detail: e.message }, caller
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proscenium
|
4
|
+
class Middleware
|
5
|
+
class Runtime < Esbuild
|
6
|
+
private
|
7
|
+
|
8
|
+
def real_path
|
9
|
+
@real_path ||= Pathname.new(@request.path.sub(%r{^/@proscenium},
|
10
|
+
'/lib/proscenium/libs')).to_s
|
11
|
+
end
|
12
|
+
|
13
|
+
def root_for_readable
|
14
|
+
Proscenium::Railtie.root
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proscenium
|
4
|
+
class Middleware
|
5
|
+
# Handles requests for URL encoded URL's.
|
6
|
+
class Url < Esbuild
|
7
|
+
private
|
8
|
+
|
9
|
+
# @override [Esbuild] It's a URL, so always assume it is renderable (we won't actually know
|
10
|
+
# until it's downloaded).
|
11
|
+
def renderable?
|
12
|
+
true
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proscenium
|
4
|
+
class Middleware
|
5
|
+
extend ActiveSupport::Autoload
|
6
|
+
|
7
|
+
# Error when the build command fails.
|
8
|
+
class BuildError < StandardError; end
|
9
|
+
|
10
|
+
autoload :Base
|
11
|
+
autoload :Esbuild
|
12
|
+
autoload :Engines
|
13
|
+
autoload :Runtime
|
14
|
+
autoload :Url
|
15
|
+
|
16
|
+
def initialize(app)
|
17
|
+
@app = app
|
18
|
+
|
19
|
+
chunks_path = Rails.public_path.join('assets').to_s
|
20
|
+
headers = Rails.application.config.public_file_server.headers || {}
|
21
|
+
@chunk_handler = ::ActionDispatch::FileHandler.new(chunks_path, headers:)
|
22
|
+
end
|
23
|
+
|
24
|
+
def call(env)
|
25
|
+
request = Rack::Request.new(env)
|
26
|
+
|
27
|
+
return @app.call(env) if !request.get? && !request.head?
|
28
|
+
return @chunk_handler.attempt(request.env) if request.path.match?(%r{^/_asset_chunks/})
|
29
|
+
|
30
|
+
attempt(request) || @app.call(env)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def attempt(request)
|
36
|
+
return unless (type = find_type(request))
|
37
|
+
|
38
|
+
# file_handler.attempt(request.env) || type.attempt(request)
|
39
|
+
|
40
|
+
type.attempt request
|
41
|
+
end
|
42
|
+
|
43
|
+
def find_type(request)
|
44
|
+
return Url if request.path.match?(%r{^/https?%3A%2F%2F})
|
45
|
+
return Runtime if request.path.match?(%r{^/@proscenium/})
|
46
|
+
return Esbuild if Pathname.new(request.path).fnmatch?(app_path_glob, File::FNM_EXTGLOB)
|
47
|
+
|
48
|
+
pathname = Pathname.new(request.path)
|
49
|
+
Engines if pathname.fnmatch?(ui_path_glob, File::FNM_EXTGLOB) ||
|
50
|
+
pathname.fnmatch?(engines_path_glob, File::FNM_EXTGLOB)
|
51
|
+
end
|
52
|
+
|
53
|
+
def app_path_glob
|
54
|
+
"/{#{Proscenium::ALLOWED_DIRECTORIES}}/**.{#{file_extensions}}"
|
55
|
+
end
|
56
|
+
|
57
|
+
def engines_path_glob
|
58
|
+
names = Proscenium.config.engines.map(&:engine_name)
|
59
|
+
"/{#{names.join(',')}}/{#{Proscenium::ALLOWED_DIRECTORIES}}/**.{#{file_extensions}}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def ui_path_glob
|
63
|
+
"/proscenium/ui/**.{#{file_extensions}}"
|
64
|
+
end
|
65
|
+
|
66
|
+
def file_extensions
|
67
|
+
@file_extensions ||= FILE_EXTENSIONS.join(',')
|
68
|
+
end
|
69
|
+
|
70
|
+
# TODO: handle precompiled assets
|
71
|
+
# def file_handler
|
72
|
+
# ::ActionDispatch::FileHandler.new Rails.public_path.join('assets').to_s,
|
73
|
+
# headers: { 'X-Proscenium-Middleware' => 'precompiled' }
|
74
|
+
# end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proscenium
|
4
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
5
|
+
module Monkey
|
6
|
+
module TemplateRenderer
|
7
|
+
private
|
8
|
+
|
9
|
+
def render_template(view, template, layout_name, locals) # rubocop:disable Metrics/*
|
10
|
+
result = super
|
11
|
+
return result if !view.controller || !Proscenium.config.side_load
|
12
|
+
|
13
|
+
renderable = template.instance_variable_get(:@renderable)
|
14
|
+
|
15
|
+
to_sideload = if Object.const_defined?(:ViewComponent) &&
|
16
|
+
template.is_a?(ActionView::Template::Renderable) &&
|
17
|
+
renderable.class < ::ViewComponent::Base &&
|
18
|
+
renderable.class.format == :html
|
19
|
+
renderable
|
20
|
+
elsif template.respond_to?(:virtual_path) &&
|
21
|
+
template.respond_to?(:type) && template.type == :html
|
22
|
+
template
|
23
|
+
end
|
24
|
+
if to_sideload
|
25
|
+
options = view.controller.sideload_assets_options
|
26
|
+
layout = find_layout(layout_name, locals.keys, [formats.first])
|
27
|
+
sideload_template_assets layout, view.controller, options if layout
|
28
|
+
sideload_template_assets to_sideload, view.controller, options
|
29
|
+
end
|
30
|
+
|
31
|
+
result
|
32
|
+
end
|
33
|
+
|
34
|
+
def sideload_template_assets(tpl, controller, options)
|
35
|
+
options = {} if options.nil?
|
36
|
+
options = { js: options, css: options } unless options.is_a?(Hash)
|
37
|
+
|
38
|
+
if tpl.instance_variable_defined?(:@sideload_assets_options)
|
39
|
+
tpl_options = tpl.instance_variable_get(:@sideload_assets_options)
|
40
|
+
options = case tpl_options
|
41
|
+
when Hash then options.deep_merge(tpl_options)
|
42
|
+
else
|
43
|
+
{ js: tpl_options, css: tpl_options }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
%i[css js].each do |k|
|
48
|
+
options[k] = controller.instance_eval(&options[k]) if options[k].is_a?(Proc)
|
49
|
+
end
|
50
|
+
|
51
|
+
Importer.sideload "app/views/#{tpl.virtual_path}", **options
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
module PartialRenderer
|
56
|
+
private
|
57
|
+
|
58
|
+
def render_partial_template(view, locals, template, layout, block)
|
59
|
+
result = super
|
60
|
+
|
61
|
+
return result if !view.controller || !Proscenium.config.side_load
|
62
|
+
|
63
|
+
if template.respond_to?(:virtual_path) &&
|
64
|
+
template.respond_to?(:type) && template.type == :html
|
65
|
+
options = view.controller.sideload_assets_options
|
66
|
+
sideload_template_assets layout, options if layout
|
67
|
+
sideload_template_assets template, options
|
68
|
+
end
|
69
|
+
|
70
|
+
result
|
71
|
+
end
|
72
|
+
|
73
|
+
def sideload_template_assets(tpl, options)
|
74
|
+
options = {} if options.nil?
|
75
|
+
options = { js: options, css: options } unless options.is_a?(Hash)
|
76
|
+
|
77
|
+
if tpl.instance_variable_defined?(:@sideload_assets_options)
|
78
|
+
tpl_options = tpl.instance_variable_get(:@sideload_assets_options)
|
79
|
+
options = if tpl_options.is_a?(Hash)
|
80
|
+
options.deep_merge tpl_options
|
81
|
+
else
|
82
|
+
{ js: tpl_options, css: tpl_options }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
%i[css js].each do |k|
|
87
|
+
options[k] = controller.instance_eval(&options[k]) if options[k].is_a?(Proc)
|
88
|
+
end
|
89
|
+
|
90
|
+
Importer.sideload "app/views/#{tpl.virtual_path}", **options
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
95
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proscenium::Phlex::AssetInclusions
|
4
|
+
def include_stylesheets
|
5
|
+
comment { '[PROSCENIUM_STYLESHEETS]' }
|
6
|
+
end
|
7
|
+
|
8
|
+
def include_javascripts
|
9
|
+
comment { '[PROSCENIUM_LAZY_SCRIPTS]' }
|
10
|
+
comment { '[PROSCENIUM_JAVASCRIPTS]' }
|
11
|
+
end
|
12
|
+
|
13
|
+
def include_assets
|
14
|
+
include_stylesheets
|
15
|
+
include_javascripts
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proscenium
|
4
|
+
module Phlex::CssModules
|
5
|
+
include Proscenium::CssModule
|
6
|
+
|
7
|
+
def self.included(base)
|
8
|
+
base.extend CssModule::Path
|
9
|
+
base.extend ClassMethods
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
# Set of CSS module paths that have been resolved after being transformed from 'class' HTML
|
14
|
+
# attributes. See #process_attributes. This is here because Phlex caches attributes. Which
|
15
|
+
# means while the CSS class names will be transformed, any resolved paths will be lost in
|
16
|
+
# subsequent requests.
|
17
|
+
attr_accessor :resolved_css_module_paths
|
18
|
+
end
|
19
|
+
|
20
|
+
def before_template
|
21
|
+
self.class.resolved_css_module_paths ||= Concurrent::Set.new
|
22
|
+
super
|
23
|
+
end
|
24
|
+
|
25
|
+
def after_template
|
26
|
+
self.class.resolved_css_module_paths.each do |path|
|
27
|
+
Proscenium::Importer.import path
|
28
|
+
end
|
29
|
+
|
30
|
+
super
|
31
|
+
end
|
32
|
+
|
33
|
+
# Resolve and side load any CSS modules in the "class" attributes, where a CSS module is a class
|
34
|
+
# name beginning with a `@`. The class name is resolved to a CSS module name based on the file
|
35
|
+
# system path of the Phlex class, and any CSS file is side loaded.
|
36
|
+
#
|
37
|
+
# For example, the following will side load the CSS module file at
|
38
|
+
# app/components/user/component.module.css, and add the CSS Module name `user_name` to the
|
39
|
+
# <div>.
|
40
|
+
#
|
41
|
+
# # app/components/user/component.rb
|
42
|
+
# class User::Component < Proscenium::Phlex
|
43
|
+
# def view_template
|
44
|
+
# div class: :@user_name do
|
45
|
+
# 'Joel Moss'
|
46
|
+
# end
|
47
|
+
# end
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# Additionally, any class name containing a `/` is resolved as a CSS module path. Allowing you
|
51
|
+
# to use the same syntax as a CSS module, but without the need to manually import the CSS file.
|
52
|
+
#
|
53
|
+
# For example, the following will side load the CSS module file at /lib/users.module.css, and
|
54
|
+
# add the CSS Module name `name` to the <div>.
|
55
|
+
#
|
56
|
+
# class User::Component < Proscenium::Phlex
|
57
|
+
# def view_template
|
58
|
+
# div class: '/lib/users@name' do
|
59
|
+
# 'Joel Moss'
|
60
|
+
# end
|
61
|
+
# end
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
# @raise [Proscenium::CssModule::Resolver::NotFound] If a CSS module file is not found for the
|
65
|
+
# Phlex class file path.
|
66
|
+
def process_attributes(**attributes)
|
67
|
+
if attributes.key?(:class) && (attributes[:class] = tokens(attributes[:class])).include?('@')
|
68
|
+
names = attributes[:class].is_a?(Array) ? attributes[:class] : attributes[:class].split
|
69
|
+
|
70
|
+
attributes[:class] = cssm.class_names(*names).map do |name, path|
|
71
|
+
self.class.resolved_css_module_paths << path if path
|
72
|
+
name
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
attributes
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proscenium
|
4
|
+
# Renders a <div> for use with React components, with data attributes specifying the component
|
5
|
+
# path and props.
|
6
|
+
#
|
7
|
+
# If a block is given, it will be yielded within the div, allowing for a custom "loading" UI. If
|
8
|
+
# no block is given, then a "loading..." text will be rendered. It is intended that the component
|
9
|
+
# is mounted to this div, and the loading UI will then be replaced with the component's rendered
|
10
|
+
# output.
|
11
|
+
#
|
12
|
+
# You can pass props to the component in the `:props` keyword argument.
|
13
|
+
class Phlex::ReactComponent < Phlex
|
14
|
+
self.abstract_class = true
|
15
|
+
|
16
|
+
include ReactComponentable
|
17
|
+
|
18
|
+
# Override this to provide your own loading UI.
|
19
|
+
#
|
20
|
+
# @example
|
21
|
+
# def view_template(**attributes, &block)
|
22
|
+
# super do
|
23
|
+
# 'Look at me! I am loading now...'
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# @yield the given block to a `div` within the top level component div.
|
28
|
+
def view_template(**attributes, &)
|
29
|
+
send(root_tag, **{ data: data_attributes }.deep_merge(attributes), &)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'phlex-rails'
|
4
|
+
|
5
|
+
module Proscenium
|
6
|
+
class Phlex < ::Phlex::HTML
|
7
|
+
extend ActiveSupport::Autoload
|
8
|
+
|
9
|
+
autoload :CssModules
|
10
|
+
autoload :ReactComponent
|
11
|
+
autoload :AssetInclusions
|
12
|
+
|
13
|
+
include Proscenium::SourcePath
|
14
|
+
include CssModules
|
15
|
+
include AssetInclusions
|
16
|
+
|
17
|
+
module Sideload
|
18
|
+
def before_template
|
19
|
+
Proscenium::SideLoad.sideload_inheritance_chain self,
|
20
|
+
helpers.controller.sideload_assets_options
|
21
|
+
|
22
|
+
super
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class_attribute :sideload_assets_options
|
27
|
+
|
28
|
+
class << self
|
29
|
+
attr_accessor :abstract_class
|
30
|
+
|
31
|
+
def inherited(child)
|
32
|
+
child.prepend Sideload
|
33
|
+
|
34
|
+
super
|
35
|
+
end
|
36
|
+
|
37
|
+
def sideload_assets(value)
|
38
|
+
self.sideload_assets_options = value
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails'
|
4
|
+
require 'proscenium/log_subscriber'
|
5
|
+
|
6
|
+
ENV['RAILS_ENV'] = Rails.env
|
7
|
+
|
8
|
+
module Proscenium
|
9
|
+
class Railtie < ::Rails::Engine
|
10
|
+
isolate_namespace Proscenium
|
11
|
+
|
12
|
+
config.proscenium = ActiveSupport::OrderedOptions.new
|
13
|
+
config.proscenium.debug = false
|
14
|
+
config.proscenium.side_load = true
|
15
|
+
config.proscenium.code_splitting = true
|
16
|
+
|
17
|
+
# Cache asset paths when building to path. Enabled by default in production.
|
18
|
+
# @see Proscenium::Builder#build_to_path
|
19
|
+
config.proscenium.cache = ActiveSupport::Cache::MemoryStore.new if Rails.env.production?
|
20
|
+
|
21
|
+
# TODO: implement!
|
22
|
+
config.proscenium.cache_query_string = Rails.env.production? && ENV.fetch('REVISION', nil)
|
23
|
+
config.proscenium.cache_max_age = 2_592_000 # 30 days
|
24
|
+
|
25
|
+
# List of environment variable names that should be passed to the builder, which will then be
|
26
|
+
# passed to esbuild's `Define` option. Being explicit about which environment variables are
|
27
|
+
# defined means a faster build, as esbuild will have less to do.
|
28
|
+
config.proscenium.env_vars = Set.new
|
29
|
+
|
30
|
+
# Rails engines to expose and allow Proscenium to serve their assets.
|
31
|
+
#
|
32
|
+
# A Rails engine that has assets, can add Proscenium as a gem dependency, and then add itself
|
33
|
+
# to this list. Proscenium will then serve the engine's assets at the URL path beginning with
|
34
|
+
# the engine name.
|
35
|
+
#
|
36
|
+
# Example:
|
37
|
+
# class Gem1::Engine < ::Rails::Engine
|
38
|
+
# config.proscenium.engines << self
|
39
|
+
# end
|
40
|
+
config.proscenium.engines = Set.new
|
41
|
+
|
42
|
+
config.action_dispatch.rescue_templates = {
|
43
|
+
'Proscenium::Builder::BuildError' => 'build_error'
|
44
|
+
}
|
45
|
+
|
46
|
+
config.after_initialize do |_app|
|
47
|
+
ActiveSupport.on_load(:action_view) do
|
48
|
+
include Proscenium::Helper
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
initializer 'proscenium.ui' do
|
53
|
+
ActiveSupport::Inflector.inflections(:en) do |inflect|
|
54
|
+
inflect.acronym 'UI'
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
initializer 'proscenium.debugging' do
|
59
|
+
if Rails.gem_version >= Gem::Version.new('7.1.0')
|
60
|
+
tpl_path = root.join('lib', 'proscenium', 'templates').to_s
|
61
|
+
ActionDispatch::DebugView::RESCUES_TEMPLATE_PATHS << tpl_path
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
initializer 'proscenium.middleware' do |app|
|
66
|
+
app.middleware.insert_after ActionDispatch::Static, Middleware
|
67
|
+
app.middleware.insert_after ActionDispatch::Static, Rack::ETag, 'no-cache'
|
68
|
+
app.middleware.insert_after ActionDispatch::Static, Rack::ConditionalGet
|
69
|
+
end
|
70
|
+
|
71
|
+
initializer 'proscenium.sideloading' do
|
72
|
+
ActiveSupport.on_load(:action_controller) do
|
73
|
+
ActionController::Base.include EnsureLoaded
|
74
|
+
ActionController::Base.include SideLoad::Controller
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
initializer 'proscenium.monkey_patches' do
|
79
|
+
ActiveSupport.on_load(:action_view) do
|
80
|
+
ActionView::TemplateRenderer.prepend Monkey::TemplateRenderer
|
81
|
+
ActionView::PartialRenderer.prepend Monkey::PartialRenderer
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
initializer 'proscenium.public_path' do |app|
|
86
|
+
if app.config.public_file_server.enabled
|
87
|
+
headers = app.config.public_file_server.headers || {}
|
88
|
+
index = app.config.public_file_server.index_name || 'index'
|
89
|
+
|
90
|
+
app.middleware.insert_after(ActionDispatch::Static, ActionDispatch::Static,
|
91
|
+
root.join('public').to_s, index:, headers:)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
if Rails.gem_version < Gem::Version.new('7.1.0')
|
98
|
+
class ActionDispatch::DebugView
|
99
|
+
def initialize(assigns)
|
100
|
+
tpl_path = Proscenium::Railtie.root.join('lib', 'proscenium', 'templates').to_s
|
101
|
+
paths = [RESCUES_TEMPLATE_PATH, tpl_path]
|
102
|
+
lookup_context = ActionView::LookupContext.new(paths)
|
103
|
+
super(lookup_context, assigns, nil)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proscenium
|
4
|
+
module ReactComponentable
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
# @return [Hash] the props to pass to the React component.
|
9
|
+
attr_writer :props
|
10
|
+
|
11
|
+
# The HTML tag to use as the wrapping element for the component. You can reassign this in your
|
12
|
+
# component class to use a different tag:
|
13
|
+
#
|
14
|
+
# class MyComponent < Proscenium::ViewComponent::ReactComponent
|
15
|
+
# self.root_tag = :span
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# @return [Symbol]
|
19
|
+
class_attribute :root_tag, instance_predicate: false, default: :div
|
20
|
+
|
21
|
+
# By default, the template block (content) of the component will be server rendered as normal.
|
22
|
+
# However, when React hydrates and takes control of the component, it's content will be
|
23
|
+
# replaced by React with the JavaScript rendered content. Enabling this option will forward
|
24
|
+
# the server rendered content as the `children` prop passed to the React component.
|
25
|
+
#
|
26
|
+
# @example
|
27
|
+
#
|
28
|
+
# const Component = ({ children }) => {
|
29
|
+
# return <div dangerouslySetInnerHTML={{ __html: children }} />
|
30
|
+
# }
|
31
|
+
#
|
32
|
+
# @return [Boolean]
|
33
|
+
class_attribute :forward_children, default: false
|
34
|
+
|
35
|
+
# Lazy load the component using IntersectionObserver?
|
36
|
+
#
|
37
|
+
# @return [Boolean]
|
38
|
+
class_attribute :lazy, default: false
|
39
|
+
|
40
|
+
class_attribute :loader
|
41
|
+
|
42
|
+
# @return [String] the URL path to the component manager.
|
43
|
+
class_attribute :manager, default: '/@proscenium/react-manager/index.jsx'
|
44
|
+
end
|
45
|
+
|
46
|
+
class_methods do
|
47
|
+
def sideload(options)
|
48
|
+
Importer.import manager, **options, js: { type: 'module' }
|
49
|
+
Importer.sideload source_path, lazy: true, **options
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# @param props: [Hash]
|
54
|
+
def initialize(lazy: self.class.lazy, loader: self.class.loader, props: {})
|
55
|
+
self.lazy = lazy
|
56
|
+
self.loader = loader
|
57
|
+
@props = props
|
58
|
+
end
|
59
|
+
|
60
|
+
# The absolute URL path to the javascript component.
|
61
|
+
def virtual_path
|
62
|
+
@virtual_path ||= Resolver.resolve self.class.source_path.sub_ext('.jsx').to_s
|
63
|
+
end
|
64
|
+
|
65
|
+
def props
|
66
|
+
@props ||= {}
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def data_attributes
|
72
|
+
{
|
73
|
+
proscenium_component_path: Pathname.new(virtual_path).to_s,
|
74
|
+
proscenium_component_props: prepared_props,
|
75
|
+
proscenium_component_lazy: lazy
|
76
|
+
}.tap do |x|
|
77
|
+
x[:proscenium_component_forward_children] = true if forward_children?
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def prepared_props
|
82
|
+
props.deep_transform_keys do |term|
|
83
|
+
# This ensures that the first letter after a slash is not capitalized.
|
84
|
+
string = term.to_s.split('/').map { |str| str.camelize :lower }.join('/')
|
85
|
+
|
86
|
+
# Reverses the effect of ActiveSupport::Inflector.camelize converting slashes into `::`.
|
87
|
+
string.gsub '::', '/'
|
88
|
+
end.to_json
|
89
|
+
end
|
90
|
+
|
91
|
+
def loader_component
|
92
|
+
render Loader::Component.new(loader, @html_class, data_attributes, tag: @html_tag)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/current_attributes'
|
4
|
+
|
5
|
+
module Proscenium
|
6
|
+
class Resolver < ActiveSupport::CurrentAttributes
|
7
|
+
# TODO: cache this across requests in production.
|
8
|
+
attribute :resolved
|
9
|
+
|
10
|
+
# Resolve the given `path` to a URL path.
|
11
|
+
#
|
12
|
+
# @param path [String] Can be URL path, file system path, or bare specifier (ie. NPM package).
|
13
|
+
# @return [String] URL path.
|
14
|
+
#
|
15
|
+
# rubocop:disable Metrics/*
|
16
|
+
def self.resolve(path)
|
17
|
+
self.resolved ||= {}
|
18
|
+
|
19
|
+
self.resolved[path] ||= begin
|
20
|
+
if path.start_with?('./', '../')
|
21
|
+
raise ArgumentError, 'path must be an absolute file system or URL path'
|
22
|
+
end
|
23
|
+
|
24
|
+
if path.start_with?('@proscenium/')
|
25
|
+
"/#{path}"
|
26
|
+
elsif path.start_with?(Proscenium.ui_path.to_s)
|
27
|
+
path.delete_prefix Proscenium.root.join('lib').to_s
|
28
|
+
elsif (engine = Proscenium.config.engines.find { |e| path.start_with? "#{e.root}/" })
|
29
|
+
path.sub(/^#{engine.root}/, "/#{engine.engine_name}")
|
30
|
+
elsif path.start_with?("#{Rails.root}/")
|
31
|
+
path.delete_prefix Rails.root.to_s
|
32
|
+
else
|
33
|
+
Builder.resolve path
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
# rubocop:enable Metrics/*
|
38
|
+
end
|
39
|
+
end
|