proscenium 0.9.1-x86_64-darwin → 0.11.0.pre.1-x86_64-darwin
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +423 -63
- data/lib/proscenium/builder.rb +126 -0
- data/lib/proscenium/css_module/path.rb +31 -0
- data/lib/proscenium/css_module/transformer.rb +76 -0
- data/lib/proscenium/css_module.rb +6 -28
- data/lib/proscenium/ensure_loaded.rb +27 -0
- data/lib/proscenium/ext/proscenium +0 -0
- data/lib/proscenium/ext/proscenium.h +19 -12
- data/lib/proscenium/helper.rb +62 -0
- data/lib/proscenium/importer.rb +110 -0
- data/lib/proscenium/libs/react-manager/index.jsx +88 -0
- data/lib/proscenium/libs/react-manager/react.js +2 -0
- data/lib/proscenium/libs/stimulus-loading.js +83 -0
- data/lib/proscenium/log_subscriber.rb +1 -2
- data/lib/proscenium/middleware/base.rb +1 -1
- data/lib/proscenium/middleware/esbuild.rb +3 -5
- data/lib/proscenium/middleware.rb +7 -1
- data/lib/proscenium/{side_load/monkey.rb → monkey.rb} +16 -12
- data/lib/proscenium/phlex/{resolve_css_modules.rb → css_modules.rb} +6 -20
- data/lib/proscenium/phlex/page.rb +2 -2
- data/lib/proscenium/phlex/react_component.rb +27 -64
- data/lib/proscenium/phlex.rb +10 -29
- data/lib/proscenium/railtie.rb +20 -22
- data/lib/proscenium/react_componentable.rb +94 -0
- data/lib/proscenium/resolver.rb +37 -0
- data/lib/proscenium/side_load.rb +13 -72
- data/lib/proscenium/source_path.rb +15 -0
- data/lib/proscenium/utils.rb +13 -0
- data/lib/proscenium/version.rb +1 -1
- data/lib/proscenium/view_component/css_modules.rb +11 -0
- data/lib/proscenium/view_component/react_component.rb +15 -28
- data/lib/proscenium/view_component/sideload.rb +4 -0
- data/lib/proscenium/view_component.rb +8 -31
- data/lib/proscenium.rb +24 -68
- metadata +21 -58
- data/lib/proscenium/css_module/class_names_resolver.rb +0 -66
- data/lib/proscenium/css_module/resolver.rb +0 -76
- data/lib/proscenium/current.rb +0 -9
- data/lib/proscenium/esbuild/golib.rb +0 -97
- data/lib/proscenium/esbuild.rb +0 -32
- data/lib/proscenium/phlex/component_concerns.rb +0 -27
- data/lib/proscenium/side_load/ensure_loaded.rb +0 -25
- data/lib/proscenium/side_load/helper.rb +0 -25
- data/lib/proscenium/view_component/tag_builder.rb +0 -23
@@ -0,0 +1,83 @@
|
|
1
|
+
const controllerAttribute = "data-controller";
|
2
|
+
const controllerFilenameExtension = ".js";
|
3
|
+
|
4
|
+
export function lazyLoadControllersFrom(
|
5
|
+
under,
|
6
|
+
application,
|
7
|
+
element = document
|
8
|
+
) {
|
9
|
+
lazyLoadExistingControllers(under, application, element);
|
10
|
+
lazyLoadNewControllers(under, application, element);
|
11
|
+
}
|
12
|
+
|
13
|
+
function lazyLoadExistingControllers(under, application, element) {
|
14
|
+
queryControllerNamesWithin(element).forEach((controllerName) =>
|
15
|
+
loadController(controllerName, under, application)
|
16
|
+
);
|
17
|
+
}
|
18
|
+
|
19
|
+
function lazyLoadNewControllers(under, application, element) {
|
20
|
+
new MutationObserver((mutationsList) => {
|
21
|
+
for (const { attributeName, target, type } of mutationsList) {
|
22
|
+
switch (type) {
|
23
|
+
case "attributes": {
|
24
|
+
if (
|
25
|
+
attributeName == controllerAttribute &&
|
26
|
+
target.getAttribute(controllerAttribute)
|
27
|
+
) {
|
28
|
+
extractControllerNamesFrom(target).forEach((controllerName) =>
|
29
|
+
loadController(controllerName, under, application)
|
30
|
+
);
|
31
|
+
}
|
32
|
+
}
|
33
|
+
|
34
|
+
case "childList": {
|
35
|
+
lazyLoadExistingControllers(under, application, target);
|
36
|
+
}
|
37
|
+
}
|
38
|
+
}
|
39
|
+
}).observe(element, {
|
40
|
+
attributeFilter: [controllerAttribute],
|
41
|
+
subtree: true,
|
42
|
+
childList: true,
|
43
|
+
});
|
44
|
+
}
|
45
|
+
|
46
|
+
function queryControllerNamesWithin(element) {
|
47
|
+
return Array.from(element.querySelectorAll(`[${controllerAttribute}]`))
|
48
|
+
.map(extractControllerNamesFrom)
|
49
|
+
.flat();
|
50
|
+
}
|
51
|
+
|
52
|
+
function extractControllerNamesFrom(element) {
|
53
|
+
return element
|
54
|
+
.getAttribute(controllerAttribute)
|
55
|
+
.split(/\s+/)
|
56
|
+
.filter((content) => content.length);
|
57
|
+
}
|
58
|
+
|
59
|
+
function loadController(name, under, application) {
|
60
|
+
if (canRegisterController(name, application)) {
|
61
|
+
import(controllerFilename(name, under))
|
62
|
+
.then((module) => registerController(name, module, application))
|
63
|
+
.catch((error) =>
|
64
|
+
console.error(`Failed to autoload controller: ${name}`, error)
|
65
|
+
);
|
66
|
+
}
|
67
|
+
}
|
68
|
+
|
69
|
+
function controllerFilename(name, under) {
|
70
|
+
return `${under}/${name
|
71
|
+
.replace(/--/g, "/")
|
72
|
+
.replace(/-/g, "_")}_controller${controllerFilenameExtension}`;
|
73
|
+
}
|
74
|
+
|
75
|
+
function registerController(name, module, application) {
|
76
|
+
if (canRegisterController(name, application)) {
|
77
|
+
application.register(name, module.default);
|
78
|
+
}
|
79
|
+
}
|
80
|
+
|
81
|
+
function canRegisterController(name, application) {
|
82
|
+
return !application.router.modulesByIdentifier.has(name);
|
83
|
+
}
|
@@ -12,12 +12,11 @@ module Proscenium
|
|
12
12
|
|
13
13
|
def build(event)
|
14
14
|
path = event.payload[:identifier]
|
15
|
-
path = path.start_with?(/https?%3A%2F%2F/)
|
15
|
+
path = CGI.unescape(path) if path.start_with?(/https?%3A%2F%2F/)
|
16
16
|
|
17
17
|
info do
|
18
18
|
message = +"[Proscenium] Building #{path}"
|
19
19
|
message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
|
20
|
-
message << "\n" if defined?(Rails.env) && Rails.env.development?
|
21
20
|
end
|
22
21
|
end
|
23
22
|
end
|
@@ -51,7 +51,7 @@ module Proscenium
|
|
51
51
|
end
|
52
52
|
|
53
53
|
def file_readable?
|
54
|
-
return unless (path = clean_path(sourcemap? ? real_path[0...-4] : real_path))
|
54
|
+
return false unless (path = clean_path(sourcemap? ? real_path[0...-4] : real_path))
|
55
55
|
|
56
56
|
file_stat = File.stat(Pathname(root).join(path.delete_prefix('/').b).to_s)
|
57
57
|
rescue SystemCallError
|
@@ -20,11 +20,9 @@ module Proscenium
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def attempt
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
end
|
27
|
-
rescue Proscenium::Esbuild::CompileError => e
|
23
|
+
render_response Proscenium::Builder.build(path_to_build, root: root,
|
24
|
+
base_url: @request.base_url)
|
25
|
+
rescue Proscenium::Builder::CompileError => e
|
28
26
|
raise self.class::CompileError, { file: @request.fullpath, detail: e.message }, caller
|
29
27
|
end
|
30
28
|
end
|
@@ -13,12 +13,17 @@ module Proscenium
|
|
13
13
|
|
14
14
|
def initialize(app)
|
15
15
|
@app = app
|
16
|
+
|
17
|
+
chunks_path = Rails.public_path.join('assets').to_s
|
18
|
+
headers = Rails.application.config.public_file_server.headers || {}
|
19
|
+
@chunk_handler = ::ActionDispatch::FileHandler.new(chunks_path, headers: headers)
|
16
20
|
end
|
17
21
|
|
18
22
|
def call(env)
|
19
23
|
request = Rack::Request.new(env)
|
20
24
|
|
21
25
|
return @app.call(env) if !request.get? && !request.head?
|
26
|
+
return @chunk_handler.attempt(request.env) if request.path.match?(%r{^/_asset_chunks/})
|
22
27
|
|
23
28
|
attempt(request) || @app.call(env)
|
24
29
|
end
|
@@ -35,7 +40,8 @@ module Proscenium
|
|
35
40
|
|
36
41
|
def find_type(request)
|
37
42
|
return Url if request.path.match?(%r{^/https?%3A%2F%2F})
|
38
|
-
|
43
|
+
|
44
|
+
Esbuild if Pathname.new(request.path).fnmatch?(path_glob, File::FNM_EXTGLOB)
|
39
45
|
end
|
40
46
|
|
41
47
|
def path_glob
|
@@ -1,12 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
module Proscenium
|
4
4
|
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
5
5
|
module Monkey
|
6
6
|
module TemplateRenderer
|
7
7
|
private
|
8
8
|
|
9
9
|
def render_template(view, template, layout_name, locals)
|
10
|
+
return super unless Proscenium.config.side_load
|
11
|
+
|
10
12
|
layout = find_layout(layout_name, locals.keys, [formats.first])
|
11
13
|
renderable = template.instance_variable_get(:@renderable)
|
12
14
|
|
@@ -14,20 +16,18 @@ class Proscenium::SideLoad
|
|
14
16
|
template.is_a?(ActionView::Template::Renderable) &&
|
15
17
|
renderable.class < ::ViewComponent::Base && renderable.class.format == :html
|
16
18
|
# Side load controller rendered ViewComponent
|
17
|
-
|
18
|
-
|
19
|
+
Importer.sideload "app/views/#{layout.virtual_path}" if layout
|
20
|
+
Importer.sideload "app/views/#{renderable.virtual_path}"
|
19
21
|
elsif template.respond_to?(:virtual_path) &&
|
20
22
|
template.respond_to?(:type) && template.type == :html
|
21
|
-
|
22
|
-
Proscenium::SideLoad.append "app/views/#{layout.virtual_path}" if layout
|
23
|
+
Importer.sideload "app/views/#{layout.virtual_path}" if layout
|
23
24
|
|
24
25
|
# Try side loading the variant template
|
25
26
|
if template.respond_to?(:variant) && template.variant
|
26
|
-
|
27
|
+
Importer.sideload "app/views/#{template.virtual_path}+#{template.variant}"
|
27
28
|
end
|
28
29
|
|
29
|
-
|
30
|
-
Proscenium::SideLoad.append "app/views/#{template.virtual_path}"
|
30
|
+
Importer.sideload "app/views/#{template.virtual_path}"
|
31
31
|
end
|
32
32
|
|
33
33
|
super
|
@@ -37,10 +37,14 @@ class Proscenium::SideLoad
|
|
37
37
|
module PartialRenderer
|
38
38
|
private
|
39
39
|
|
40
|
-
def
|
41
|
-
|
42
|
-
|
43
|
-
|
40
|
+
def render_partial_template(view, locals, template, layout, block)
|
41
|
+
if Proscenium.config.side_load && template.respond_to?(:virtual_path) &&
|
42
|
+
template.respond_to?(:type) && template.type == :html
|
43
|
+
Importer.sideload "app/views/#{layout.virtual_path}" if layout
|
44
|
+
Importer.sideload "app/views/#{template.virtual_path}"
|
45
|
+
end
|
46
|
+
|
47
|
+
super
|
44
48
|
end
|
45
49
|
end
|
46
50
|
end
|
@@ -1,16 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Proscenium
|
4
|
-
module Phlex::
|
5
|
-
|
4
|
+
module Phlex::CssModules
|
5
|
+
include Proscenium::CssModule
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
end
|
10
|
-
|
11
|
-
def before_template
|
12
|
-
self.class.side_load_cache ||= Set.new
|
13
|
-
super
|
7
|
+
def self.included(base)
|
8
|
+
base.extend CssModule::Path
|
14
9
|
end
|
15
10
|
|
16
11
|
# Resolve and side load any CSS modules in the "class" attributes, where a CSS module is a class
|
@@ -44,24 +39,15 @@ module Proscenium
|
|
44
39
|
# end
|
45
40
|
# end
|
46
41
|
#
|
47
|
-
# The given class name should be underscored, and the resulting CSS module name will be
|
48
|
-
# `camelCased` with a lower case first character.
|
49
|
-
#
|
50
42
|
# @raise [Proscenium::CssModule::Resolver::NotFound] If a CSS module file is not found for the
|
51
43
|
# Phlex class file path.
|
52
44
|
def process_attributes(**attributes)
|
53
45
|
if attributes.key?(:class) && (attributes[:class] = tokens(attributes[:class])).include?('@')
|
54
|
-
|
55
|
-
|
56
|
-
attributes[:class] = resolver.class_names
|
46
|
+
names = attributes[:class].is_a?(Array) ? attributes[:class] : attributes[:class].split
|
47
|
+
attributes[:class] = cssm.class_names(*names)
|
57
48
|
end
|
58
49
|
|
59
50
|
attributes
|
60
51
|
end
|
61
|
-
|
62
|
-
def after_template
|
63
|
-
super
|
64
|
-
self.class.side_load_cache&.each { |path| SideLoad.append! path, :css }
|
65
|
-
end
|
66
52
|
end
|
67
53
|
end
|
@@ -32,7 +32,7 @@ module Proscenium::Phlex::Page
|
|
32
32
|
|
33
33
|
def after_template
|
34
34
|
super
|
35
|
-
@_buffer.gsub!('<!-- [SIDE_LOAD_STYLESHEETS] -->', capture {
|
35
|
+
@_buffer.gsub!('<!-- [SIDE_LOAD_STYLESHEETS] -->', capture { include_stylesheets })
|
36
36
|
end
|
37
37
|
|
38
38
|
def page_title
|
@@ -56,7 +56,7 @@ module Proscenium::Phlex::Page
|
|
56
56
|
super do
|
57
57
|
yield if block_given?
|
58
58
|
|
59
|
-
|
59
|
+
include_javascripts type: :module, defer: true
|
60
60
|
end
|
61
61
|
end
|
62
62
|
end
|
@@ -1,69 +1,32 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
# Renders a div for use with
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
class
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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 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 template(**attributes, &block)
|
29
|
+
send root_tag, **{ data: data_attributes }.deep_merge(attributes), &block
|
22
30
|
end
|
23
31
|
end
|
24
|
-
|
25
|
-
self.abstract_class = true
|
26
|
-
|
27
|
-
include Proscenium::Phlex::ComponentConcerns::CssModules
|
28
|
-
|
29
|
-
attr_writer :props, :lazy
|
30
|
-
|
31
|
-
# @param props: [Hash]
|
32
|
-
# @param lazy: [Boolean] Lazy load the component using IntersectionObserver. Default: true.
|
33
|
-
def initialize(props: {}, lazy: true) # rubocop:disable Lint/MissingSuper
|
34
|
-
@props = props
|
35
|
-
@lazy = lazy
|
36
|
-
end
|
37
|
-
|
38
|
-
# @yield the given block to a `div` within the top level component div. If not given,
|
39
|
-
# `<div>loading...</div>` will be rendered. Use this to display a loading UI while the component
|
40
|
-
# is loading and rendered.
|
41
|
-
def template(**attributes, &block)
|
42
|
-
component_root(:div, **attributes, &block)
|
43
|
-
end
|
44
|
-
|
45
|
-
private
|
46
|
-
|
47
|
-
def component_root(element, **attributes, &block)
|
48
|
-
send element, data: { proscenium_component: component_data }, **attributes, &block
|
49
|
-
end
|
50
|
-
|
51
|
-
def props
|
52
|
-
@props ||= {}
|
53
|
-
end
|
54
|
-
|
55
|
-
def lazy
|
56
|
-
instance_variable_defined?(:@lazy) ? @lazy : (@lazy = false)
|
57
|
-
end
|
58
|
-
|
59
|
-
def component_data
|
60
|
-
{
|
61
|
-
path: virtual_path, lazy: lazy,
|
62
|
-
props: props.deep_transform_keys { |k| k.to_s.camelize :lower }
|
63
|
-
}.to_json
|
64
|
-
end
|
65
|
-
|
66
|
-
def virtual_path
|
67
|
-
path.to_s.delete_prefix(Rails.root.to_s)
|
68
|
-
end
|
69
32
|
end
|
data/lib/proscenium/phlex.rb
CHANGED
@@ -5,54 +5,35 @@ require 'phlex-rails'
|
|
5
5
|
module Proscenium
|
6
6
|
class Phlex < ::Phlex::HTML
|
7
7
|
extend ActiveSupport::Autoload
|
8
|
-
include Proscenium::CssModule
|
9
8
|
|
10
9
|
autoload :Page
|
10
|
+
autoload :CssModules
|
11
11
|
autoload :ReactComponent
|
12
|
-
autoload :ResolveCssModules
|
13
|
-
autoload :ComponentConcerns
|
14
12
|
|
15
13
|
extend ::Phlex::Rails::HelperMacros
|
16
14
|
include ::Phlex::Rails::Helpers::JavaScriptIncludeTag
|
17
15
|
include ::Phlex::Rails::Helpers::StyleSheetLinkTag
|
16
|
+
include Proscenium::SourcePath
|
17
|
+
include CssModules
|
18
18
|
|
19
|
-
define_output_helper :side_load_stylesheets
|
20
|
-
define_output_helper :
|
19
|
+
define_output_helper :side_load_stylesheets # deprecated
|
20
|
+
define_output_helper :include_stylesheets
|
21
|
+
define_output_helper :side_load_javascripts # deprecated
|
22
|
+
define_output_helper :include_javascripts
|
21
23
|
|
22
|
-
# Side loads the class, and its super classes that respond to `.path`. Assign the
|
23
|
-
# `abstract_class` class variable to any abstract class, and it will not be side loaded.
|
24
|
-
# Additionally, if the class responds to `side_load`, then that method is called.
|
25
24
|
module Sideload
|
26
25
|
def before_template
|
27
|
-
|
28
|
-
|
29
|
-
if !klass.abstract_class && respond_to?(:side_load, true)
|
30
|
-
side_load
|
31
|
-
klass = klass.superclass
|
32
|
-
end
|
33
|
-
|
34
|
-
while !klass.abstract_class && klass.respond_to?(:path) && klass.path
|
35
|
-
Proscenium::SideLoad.append klass.path
|
36
|
-
klass = klass.superclass
|
37
|
-
end
|
26
|
+
Proscenium::SideLoad.sideload_inheritance_chain self
|
38
27
|
|
39
28
|
super
|
40
29
|
end
|
41
30
|
end
|
42
31
|
|
43
32
|
class << self
|
44
|
-
attr_accessor :
|
33
|
+
attr_accessor :abstract_class
|
45
34
|
|
46
35
|
def inherited(child)
|
47
|
-
|
48
|
-
child.path = if caller_locations(1, 1).first.label == 'inherited'
|
49
|
-
Pathname.new caller_locations(2, 1).first.path
|
50
|
-
else
|
51
|
-
Pathname.new caller_locations(1, 1).first.path
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
child.prepend Sideload if Rails.application.config.proscenium.side_load
|
36
|
+
child.prepend Sideload
|
56
37
|
|
57
38
|
super
|
58
39
|
end
|
data/lib/proscenium/railtie.rb
CHANGED
@@ -6,11 +6,6 @@ require 'proscenium/log_subscriber'
|
|
6
6
|
ENV['RAILS_ENV'] = Rails.env
|
7
7
|
|
8
8
|
module Proscenium
|
9
|
-
FILE_EXTENSIONS = ['js', 'mjs', 'ts', 'jsx', 'tsx', 'css', 'js.map', 'mjs.map', 'jsx.map',
|
10
|
-
'ts.map', 'tsx.map', 'css.map'].freeze
|
11
|
-
|
12
|
-
APPLICATION_INCLUDE_PATHS = ['config', 'app/assets', 'app/views', 'lib', 'node_modules'].freeze
|
13
|
-
|
14
9
|
class << self
|
15
10
|
def config
|
16
11
|
@config ||= Railtie.config.proscenium
|
@@ -21,10 +16,19 @@ module Proscenium
|
|
21
16
|
isolate_namespace Proscenium
|
22
17
|
|
23
18
|
config.proscenium = ActiveSupport::OrderedOptions.new
|
19
|
+
config.proscenium.debug = false
|
24
20
|
config.proscenium.side_load = true
|
21
|
+
config.proscenium.code_splitting = true
|
22
|
+
config.proscenium.include_paths = Set.new(APPLICATION_INCLUDE_PATHS)
|
23
|
+
|
24
|
+
# TODO: implement!
|
25
25
|
config.proscenium.cache_query_string = Rails.env.production? && ENV.fetch('REVISION', nil)
|
26
26
|
config.proscenium.cache_max_age = 2_592_000 # 30 days
|
27
|
-
|
27
|
+
|
28
|
+
# List of environment variable names that should be passed to the builder, which will then be
|
29
|
+
# passed to esbuild's `Define` option. Being explicit about which environment variables are
|
30
|
+
# defined means a faster build, as esbuild will have less to do.
|
31
|
+
config.proscenium.env_vars = Set.new
|
28
32
|
|
29
33
|
# A hash of gems that can be side loaded. Assets from gems listed here can be side loaded.
|
30
34
|
#
|
@@ -50,31 +54,25 @@ module Proscenium
|
|
50
54
|
end
|
51
55
|
|
52
56
|
initializer 'proscenium.middleware' do |app|
|
53
|
-
app.middleware.insert_after ActionDispatch::Static,
|
57
|
+
app.middleware.insert_after ActionDispatch::Static, Middleware
|
54
58
|
app.middleware.insert_after ActionDispatch::Static, Rack::ETag, 'no-cache'
|
55
59
|
app.middleware.insert_after ActionDispatch::Static, Rack::ConditionalGet
|
56
60
|
end
|
57
61
|
|
58
|
-
initializer 'proscenium.
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
ActiveSupport.on_load(:action_view) do
|
63
|
-
ActionView::Base.include Proscenium::SideLoad::Helper
|
64
|
-
|
65
|
-
ActionView::TemplateRenderer.prepend SideLoad::Monkey::TemplateRenderer
|
66
|
-
ActionView::PartialRenderer.prepend SideLoad::Monkey::PartialRenderer
|
67
|
-
end
|
68
|
-
|
69
|
-
ActiveSupport.on_load(:action_controller) do
|
70
|
-
ActionController::Base.include Proscenium::SideLoad::EnsureLoaded
|
71
|
-
end
|
62
|
+
initializer 'proscenium.monkey_views' do
|
63
|
+
ActiveSupport.on_load(:action_view) do
|
64
|
+
ActionView::TemplateRenderer.prepend Monkey::TemplateRenderer
|
65
|
+
ActionView::PartialRenderer.prepend Monkey::PartialRenderer
|
72
66
|
end
|
73
67
|
end
|
74
68
|
|
75
69
|
initializer 'proscenium.helper' do
|
76
70
|
ActiveSupport.on_load(:action_view) do
|
77
|
-
ActionView::Base.include
|
71
|
+
ActionView::Base.include Helper
|
72
|
+
end
|
73
|
+
|
74
|
+
ActiveSupport.on_load(:action_controller) do
|
75
|
+
ActionController::Base.include EnsureLoaded
|
78
76
|
end
|
79
77
|
end
|
80
78
|
end
|
@@ -0,0 +1,94 @@
|
|
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
|
+
end
|
42
|
+
|
43
|
+
class_methods do
|
44
|
+
# Import only the component manager. The component itself is side loaded in the initializer,
|
45
|
+
# so that it can be lazy loaded based on the value of the `lazy` instance variable.
|
46
|
+
def sideload
|
47
|
+
Importer.import resolve: '@proscenium/react-manager/index.jsx'
|
48
|
+
Importer.sideload source_path, lazy: true
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# @param props: [Hash]
|
53
|
+
def initialize(lazy: self.class.lazy, loader: self.class.loader, props: {})
|
54
|
+
self.lazy = lazy
|
55
|
+
self.loader = loader
|
56
|
+
@props = props
|
57
|
+
end
|
58
|
+
|
59
|
+
# The absolute URL path to the javascript component.
|
60
|
+
def virtual_path
|
61
|
+
@virtual_path ||= Resolver.resolve self.class.source_path.sub_ext('.jsx').to_s
|
62
|
+
end
|
63
|
+
|
64
|
+
def props
|
65
|
+
@props ||= {}
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def data_attributes
|
71
|
+
{
|
72
|
+
proscenium_component_path: Pathname.new(virtual_path).to_s,
|
73
|
+
proscenium_component_props: prepared_props,
|
74
|
+
proscenium_component_lazy: lazy
|
75
|
+
}.tap do |x|
|
76
|
+
x[:proscenium_component_forward_children] = true if forward_children?
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def prepared_props
|
81
|
+
props.deep_transform_keys do |term|
|
82
|
+
# This ensures that the first letter after a slash is not capitalized.
|
83
|
+
string = term.to_s.split('/').map { |str| str.camelize :lower }.join('/')
|
84
|
+
|
85
|
+
# Reverses the effect of ActiveSupport::Inflector.camelize converting slashes into `::`.
|
86
|
+
string.gsub '::', '/'
|
87
|
+
end.to_json
|
88
|
+
end
|
89
|
+
|
90
|
+
def loader_component
|
91
|
+
render Loader::Component.new(loader, @html_class, data_attributes, tag: @html_tag)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,37 @@
|
|
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
|
+
def self.resolve(path) # rubocop:disable Metrics/AbcSize
|
15
|
+
self.resolved ||= {}
|
16
|
+
|
17
|
+
self.resolved[path] ||= begin
|
18
|
+
if path.start_with?('./', '../')
|
19
|
+
raise ArgumentError, 'path must be an absolute file system or URL path'
|
20
|
+
end
|
21
|
+
|
22
|
+
if (gem = Proscenium.config.side_load_gems.find { |_, x| path.start_with? "#{x[:root]}/" })
|
23
|
+
unless (package_name = gem[1][:package_name] || gem[0])
|
24
|
+
# TODO: manually resolve the path without esbuild
|
25
|
+
raise PathResolutionFailed, path
|
26
|
+
end
|
27
|
+
|
28
|
+
Builder.resolve "#{package_name}/#{path.delete_prefix("#{gem[1][:root]}/")}"
|
29
|
+
elsif path.start_with?("#{Rails.root}/")
|
30
|
+
path.delete_prefix Rails.root.to_s
|
31
|
+
else
|
32
|
+
Builder.resolve path
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|