proscenium 0.9.1-arm64-darwin → 0.11.0.pre.1-arm64-darwin
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 +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
|