proscenium 0.16.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 +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
|