proscenium 0.9.1-aarch64-linux → 0.11.0.pre.1-aarch64-linux
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,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ffi'
|
4
|
+
require 'oj'
|
5
|
+
|
6
|
+
module Proscenium
|
7
|
+
class Builder
|
8
|
+
class CompileError < StandardError; end
|
9
|
+
|
10
|
+
class Result < FFI::Struct
|
11
|
+
layout :success, :bool,
|
12
|
+
:response, :string
|
13
|
+
end
|
14
|
+
|
15
|
+
module Request
|
16
|
+
extend FFI::Library
|
17
|
+
ffi_lib Pathname.new(__dir__).join('ext/proscenium').to_s
|
18
|
+
|
19
|
+
enum :environment, [:development, 1, :test, :production]
|
20
|
+
|
21
|
+
attach_function :build, [
|
22
|
+
:string, # path or entry point. multiple can be given by separating with a semi-colon
|
23
|
+
:string, # base URL of the Rails app. eg. https://example.com
|
24
|
+
:string, # path to import map, relative to root
|
25
|
+
:string, # ENV variables as a JSON string
|
26
|
+
|
27
|
+
# Config
|
28
|
+
:string, # root
|
29
|
+
:environment, # Rails environment as a Symbol
|
30
|
+
:bool, # code splitting enabled?
|
31
|
+
:bool # debugging enabled?
|
32
|
+
], Result.by_value
|
33
|
+
|
34
|
+
attach_function :resolve, [
|
35
|
+
:string, # path or entry point
|
36
|
+
:string, # path to import map, relative to root
|
37
|
+
|
38
|
+
# Config
|
39
|
+
:string, # root
|
40
|
+
:environment # Rails environment as a Symbol
|
41
|
+
], Result.by_value
|
42
|
+
end
|
43
|
+
|
44
|
+
class BuildError < StandardError
|
45
|
+
attr_reader :error, :path
|
46
|
+
|
47
|
+
def initialize(path, error)
|
48
|
+
error = Oj.load(error, mode: :strict).deep_transform_keys(&:underscore)
|
49
|
+
|
50
|
+
super "Failed to build '#{path}' -- #{error['text']}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class ResolveError < StandardError
|
55
|
+
attr_reader :error_msg, :path
|
56
|
+
|
57
|
+
def initialize(path, error_msg)
|
58
|
+
super "Failed to resolve '#{path}' -- #{error_msg}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.build(path, root: nil, base_url: nil)
|
63
|
+
new(root: root, base_url: base_url).build(path)
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.resolve(path, root: nil)
|
67
|
+
new(root: root).resolve(path)
|
68
|
+
end
|
69
|
+
|
70
|
+
def initialize(root: nil, base_url: nil)
|
71
|
+
@root = root || Rails.root
|
72
|
+
@base_url = base_url
|
73
|
+
end
|
74
|
+
|
75
|
+
def build(path)
|
76
|
+
ActiveSupport::Notifications.instrument('build.proscenium', identifier: path) do
|
77
|
+
result = Request.build(path, @base_url, import_map, env_vars.to_json,
|
78
|
+
@root.to_s,
|
79
|
+
Rails.env.to_sym,
|
80
|
+
Proscenium.config.code_splitting,
|
81
|
+
Proscenium.config.debug)
|
82
|
+
|
83
|
+
raise BuildError.new(path, result[:response]) unless result[:success]
|
84
|
+
|
85
|
+
result[:response]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def resolve(path)
|
90
|
+
ActiveSupport::Notifications.instrument('resolve.proscenium', identifier: path) do
|
91
|
+
result = Request.resolve(path, import_map, @root.to_s, Rails.env.to_sym)
|
92
|
+
raise ResolveError.new(path, result[:response]) unless result[:success]
|
93
|
+
|
94
|
+
result[:response]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
# Build the ENV variables as determined by `Proscenium.config.env_vars` and
|
101
|
+
# `Proscenium::DEFAULT_ENV_VARS` to pass to esbuild.
|
102
|
+
def env_vars
|
103
|
+
ENV['NODE_ENV'] = ENV.fetch('RAILS_ENV', nil)
|
104
|
+
ENV.slice(*Proscenium.config.env_vars + Proscenium::DEFAULT_ENV_VARS)
|
105
|
+
end
|
106
|
+
|
107
|
+
def cache_query_string
|
108
|
+
q = Proscenium.config.cache_query_string
|
109
|
+
q ? "--cache-query-string #{q}" : nil
|
110
|
+
end
|
111
|
+
|
112
|
+
def import_map
|
113
|
+
return unless (path = Rails.root&.join('config'))
|
114
|
+
|
115
|
+
if (json = path.join('import_map.json')).exist?
|
116
|
+
return json.relative_path_from(@root).to_s
|
117
|
+
end
|
118
|
+
|
119
|
+
if (js = path.join('import_map.js')).exist?
|
120
|
+
return js.relative_path_from(@root).to_s
|
121
|
+
end
|
122
|
+
|
123
|
+
nil
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proscenium
|
4
|
+
module CssModule::Path
|
5
|
+
# Returns the path to the CSS module file for this class, where the file is located alongside
|
6
|
+
# the class file, and has the same name as the class file, but with a `.module.css` extension.
|
7
|
+
#
|
8
|
+
# If the CSS module file does not exist, it's ancestry is checked, returning the first that
|
9
|
+
# exists. Then finally `nil` is returned if never found.
|
10
|
+
#
|
11
|
+
# @return [Pathname]
|
12
|
+
def css_module_path
|
13
|
+
return @css_module_path if instance_variable_defined?(:@css_module_path)
|
14
|
+
|
15
|
+
path = source_path.sub_ext('.module.css')
|
16
|
+
@css_module_path = path.exist? ? path : nil
|
17
|
+
|
18
|
+
unless @css_module_path
|
19
|
+
klass = superclass
|
20
|
+
|
21
|
+
while klass.respond_to?(:css_module_path) && !klass.abstract_class
|
22
|
+
break if (@css_module_path = klass.css_module_path)
|
23
|
+
|
24
|
+
klass = klass.superclass
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
@css_module_path
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proscenium
|
4
|
+
class CssModule::Transformer
|
5
|
+
FILE_EXT = '.module.css'
|
6
|
+
|
7
|
+
def self.class_names(path, *names)
|
8
|
+
new(path).class_names(*names)
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(source_path)
|
12
|
+
@source_path = source_path
|
13
|
+
@source_path = Pathname.new(@source_path) unless @source_path.is_a?(Pathname)
|
14
|
+
@source_path = @source_path.sub_ext(FILE_EXT) unless @source_path.to_s.end_with?(FILE_EXT)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Transform each of the given class `names` to their respective CSS module name, which consist
|
18
|
+
# of the name, and suffixed with the digest of the resolved source path.
|
19
|
+
#
|
20
|
+
# Any name beginning with '@' will be transformed to a CSS module name. If `require_prefix` is
|
21
|
+
# false, then all names will be transformed to a CSS module name regardless of whether or not
|
22
|
+
# they begin with '@'.
|
23
|
+
#
|
24
|
+
# class_names :@my_module_name, :my_class_name
|
25
|
+
#
|
26
|
+
# Note that the generated digest is based on the resolved (URL) path, not the original path.
|
27
|
+
#
|
28
|
+
# You can also provide a path specifier and class name. The path will be the URL path to a
|
29
|
+
# stylesheet. The class name will be the name of the class to transform.
|
30
|
+
#
|
31
|
+
# class_names "/lib/button@default"
|
32
|
+
# class_names "mypackage/button@large"
|
33
|
+
# class_names "@scoped/package/button@small"
|
34
|
+
#
|
35
|
+
# @param names [String,Symbol,Array<String,Symbol>]
|
36
|
+
# @param require_prefix: [Boolean] whether or not to require the `@` prefix.
|
37
|
+
# @return [Array<String>] the transformed CSS module names.
|
38
|
+
def class_names(*names, require_prefix: true)
|
39
|
+
names.map do |name|
|
40
|
+
name = name.to_s if name.is_a?(Symbol)
|
41
|
+
|
42
|
+
if name.include?('/')
|
43
|
+
if name.start_with?('@')
|
44
|
+
# Scoped bare specifier (eg. "@scoped/package/lib/button@default").
|
45
|
+
_, path, name = name.split('@')
|
46
|
+
path = "@#{path}"
|
47
|
+
elsif name.start_with?('/')
|
48
|
+
# Local path with leading slash.
|
49
|
+
path, name = name[1..].split('@')
|
50
|
+
else
|
51
|
+
# Bare specifier (eg. "mypackage/lib/button@default").
|
52
|
+
path, name = name.split('@')
|
53
|
+
end
|
54
|
+
|
55
|
+
class_name! name, path: "#{path}#{FILE_EXT}"
|
56
|
+
elsif name.start_with?('@')
|
57
|
+
class_name! name[1..]
|
58
|
+
else
|
59
|
+
require_prefix ? name : class_name!(name)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def class_name!(name, path: @source_path)
|
65
|
+
resolved_path = Resolver.resolve(path.to_s)
|
66
|
+
digest = Importer.import(resolved_path)
|
67
|
+
|
68
|
+
sname = name.to_s
|
69
|
+
if sname.start_with?('_')
|
70
|
+
"_#{sname[1..]}-#{digest}"
|
71
|
+
else
|
72
|
+
"#{sname}-#{digest}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -3,41 +3,19 @@
|
|
3
3
|
module Proscenium::CssModule
|
4
4
|
extend ActiveSupport::Autoload
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
@pathname = pathname
|
9
|
-
super
|
10
|
-
end
|
11
|
-
|
12
|
-
def message
|
13
|
-
"Stylesheet is required, but does not exist: #{@pathname}"
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
autoload :ClassNamesResolver
|
18
|
-
autoload :Resolver # deprecated
|
19
|
-
|
20
|
-
# Like `css_modules`, but will raise if the stylesheet cannot be found.
|
21
|
-
#
|
22
|
-
# @param name [Array, String]
|
23
|
-
def css_module!(names)
|
24
|
-
cssm.class_names!(names).join ' '
|
25
|
-
end
|
6
|
+
autoload :Path
|
7
|
+
autoload :Transformer
|
26
8
|
|
27
9
|
# Accepts one or more CSS class names, and transforms them into CSS module names.
|
28
10
|
#
|
29
|
-
# @param name [Array,
|
30
|
-
def css_module(names)
|
31
|
-
cssm.class_names(names).join ' '
|
11
|
+
# @param name [String,Symbol,Array<String,Symbol>]
|
12
|
+
def css_module(*names)
|
13
|
+
cssm.class_names(*names, require_prefix: false).join ' '
|
32
14
|
end
|
33
15
|
|
34
16
|
private
|
35
17
|
|
36
|
-
def path
|
37
|
-
self.class.path
|
38
|
-
end
|
39
|
-
|
40
18
|
def cssm
|
41
|
-
@cssm ||=
|
19
|
+
@cssm ||= Transformer.new(self.class.css_module_path)
|
42
20
|
end
|
43
21
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proscenium
|
4
|
+
NotIncludedError = Class.new(StandardError)
|
5
|
+
|
6
|
+
module EnsureLoaded
|
7
|
+
def self.included(child)
|
8
|
+
child.class_eval do
|
9
|
+
append_after_action do
|
10
|
+
if request.format.html? && Importer.imported?
|
11
|
+
if Importer.js_imported?
|
12
|
+
raise NotIncludedError, 'There are javascripts to be included, but they have ' \
|
13
|
+
'not been included in the page. Did you forget to add the ' \
|
14
|
+
'`#include_javascripts` helper in your views?'
|
15
|
+
end
|
16
|
+
|
17
|
+
if Importer.css_imported?
|
18
|
+
raise NotIncludedError, 'There are stylesheets to be included, but they have ' \
|
19
|
+
'not been included in the page. Did you forget to add the ' \
|
20
|
+
'`#include_stylesheets` helper in your views?'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
Binary file
|
@@ -85,23 +85,30 @@ extern "C" {
|
|
85
85
|
|
86
86
|
// Build the given `path` in the `root`.
|
87
87
|
//
|
88
|
-
//
|
89
|
-
//
|
90
|
-
//
|
91
|
-
//
|
92
|
-
//
|
93
|
-
//
|
88
|
+
// BuildOptions
|
89
|
+
// - path - The path to build relative to `root`. Multiple paths can be given by separating them
|
90
|
+
// with a semi-colon.
|
91
|
+
// - baseUrl - base URL of the Rails app. eg. https://example.com
|
92
|
+
// - importMap - Path to the import map relative to `root`.
|
93
|
+
// - envVars - JSON string of environment variables.
|
94
|
+
// Config:
|
95
|
+
// - root - The working directory.
|
96
|
+
// - env - The environment (1 = development, 2 = test, 3 = production)
|
97
|
+
// - codeSpitting?
|
98
|
+
// - debug?
|
94
99
|
//
|
95
|
-
extern struct Result build(char* filepath, char*
|
100
|
+
extern struct Result build(char* filepath, char* baseUrl, char* importMap, char* envVars, char* root, unsigned int env, GoUint8 codeSplitting, GoUint8 debug);
|
96
101
|
|
97
102
|
// Resolve the given `path` relative to the `root`.
|
98
103
|
//
|
99
|
-
//
|
100
|
-
//
|
101
|
-
//
|
102
|
-
//
|
104
|
+
// ResolveOptions
|
105
|
+
// - path - The path to build relative to `root`.
|
106
|
+
// - importMap - Path to the import map relative to `root`.
|
107
|
+
// Config
|
108
|
+
// - root - The working directory.
|
109
|
+
// - env - The environment (1 = development, 2 = test, 3 = production)
|
103
110
|
//
|
104
|
-
extern struct Result resolve(char* path, char* root, unsigned int env
|
111
|
+
extern struct Result resolve(char* path, char* importMap, char* root, unsigned int env);
|
105
112
|
|
106
113
|
#ifdef __cplusplus
|
107
114
|
}
|
data/lib/proscenium/helper.rb
CHANGED
@@ -15,5 +15,67 @@ module Proscenium
|
|
15
15
|
|
16
16
|
super
|
17
17
|
end
|
18
|
+
|
19
|
+
# Accepts one or more CSS class names, and transforms them into CSS module names.
|
20
|
+
#
|
21
|
+
# @see CssModule::Transformer#class_names
|
22
|
+
# @param name [String,Symbol,Array<String,Symbol>]
|
23
|
+
def css_module(*names)
|
24
|
+
path = Pathname.new(@lookup_context.find(@virtual_path).identifier).sub_ext('')
|
25
|
+
CssModule::Transformer.new(path).class_names(*names, require_prefix: false).join ' '
|
26
|
+
end
|
27
|
+
|
28
|
+
def include_stylesheets(**options)
|
29
|
+
out = []
|
30
|
+
Importer.each_stylesheet(delete: true) do |path, _path_options|
|
31
|
+
out << stylesheet_link_tag(path, extname: false, **options)
|
32
|
+
end
|
33
|
+
out.join("\n").html_safe
|
34
|
+
end
|
35
|
+
alias side_load_stylesheets include_stylesheets
|
36
|
+
deprecate side_load_stylesheets: 'Use `include_stylesheets` instead', deprecator: Deprecator.new
|
37
|
+
|
38
|
+
def include_javascripts(**options) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
39
|
+
out = []
|
40
|
+
|
41
|
+
if Rails.application.config.proscenium.code_splitting && Importer.multiple_js_imported?
|
42
|
+
imports = Importer.imported.dup
|
43
|
+
|
44
|
+
paths_to_build = []
|
45
|
+
Importer.each_javascript(delete: true) do |x, _|
|
46
|
+
paths_to_build << x.delete_prefix('/')
|
47
|
+
end
|
48
|
+
|
49
|
+
result = Builder.build(paths_to_build.join(';'), base_url: request.base_url)
|
50
|
+
|
51
|
+
# Remove the react components from the results, so they are not side loaded. Instead they
|
52
|
+
# are lazy loaded by the component manager.
|
53
|
+
|
54
|
+
scripts = {}
|
55
|
+
result.split(';').each do |x|
|
56
|
+
inpath, outpath = x.split('::')
|
57
|
+
inpath.prepend '/'
|
58
|
+
outpath.delete_prefix! 'public'
|
59
|
+
|
60
|
+
next unless imports.key?(inpath)
|
61
|
+
|
62
|
+
if (import = imports[inpath]).delete(:lazy)
|
63
|
+
scripts[inpath] = import.merge(outpath: outpath)
|
64
|
+
else
|
65
|
+
out << javascript_include_tag(outpath, extname: false, **options)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
out << javascript_tag("window.prosceniumLazyScripts = #{scripts.to_json}")
|
70
|
+
else
|
71
|
+
Importer.each_javascript(delete: true) do |path, _path_options|
|
72
|
+
out << javascript_include_tag(path, extname: false, **options)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
out.join("\n").html_safe
|
77
|
+
end
|
78
|
+
alias side_load_javascripts include_javascripts
|
79
|
+
deprecate side_load_javascripts: 'Use `include_javascripts` instead', deprecator: Deprecator.new
|
18
80
|
end
|
19
81
|
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/current_attributes'
|
4
|
+
|
5
|
+
module Proscenium
|
6
|
+
class Importer < ActiveSupport::CurrentAttributes
|
7
|
+
JS_EXTENSIONS = %w[.tsx .ts .jsx .js].freeze
|
8
|
+
CSS_EXTENSIONS = %w[.module.css .css].freeze
|
9
|
+
|
10
|
+
# Holds the JS and CSS files to include in the current request.
|
11
|
+
#
|
12
|
+
# Example:
|
13
|
+
# {
|
14
|
+
# '/path/to/input/file.js': {
|
15
|
+
# output: '/path/to/compiled/file.js',
|
16
|
+
# **options
|
17
|
+
# }
|
18
|
+
# }
|
19
|
+
attribute :imported
|
20
|
+
|
21
|
+
class << self
|
22
|
+
# Import the given `filepath`. This is idempotent - it will never include duplicates.
|
23
|
+
#
|
24
|
+
# @param filepath [String] Absolute path (relative to Rails root) of the file to import.
|
25
|
+
# Should be the actual asset file, eg. app.css, some/component.js.
|
26
|
+
# @param resolve [String] description of the file to resolve and import.
|
27
|
+
# @return [String] the digest of the imported file path if a css module (*.module.css).
|
28
|
+
def import(filepath = nil, resolve: nil, **options)
|
29
|
+
self.imported ||= {}
|
30
|
+
|
31
|
+
filepath = Resolver.resolve(resolve) if !filepath && resolve
|
32
|
+
css_module = filepath.end_with?('.module.css')
|
33
|
+
|
34
|
+
unless self.imported.key?(filepath)
|
35
|
+
# ActiveSupport::Notifications.instrument('sideload.proscenium', identifier: value)
|
36
|
+
|
37
|
+
self.imported[filepath] = { **options }
|
38
|
+
self.imported[filepath][:digest] = Utils.digest(filepath) if css_module
|
39
|
+
end
|
40
|
+
|
41
|
+
css_module ? self.imported[filepath][:digest] : nil
|
42
|
+
end
|
43
|
+
|
44
|
+
# Sideloads JS and CSS assets for the given Ruby filepath.
|
45
|
+
#
|
46
|
+
# Any files with the same base name and matching a supported extension will be sideloaded.
|
47
|
+
# Only one JS and one CSS file will be sideloaded, with the first match used in the following
|
48
|
+
# order:
|
49
|
+
# - JS extensions: .tsx, .ts, .jsx, and .js.
|
50
|
+
# - CSS extensions: .css.module, and .css.
|
51
|
+
#
|
52
|
+
# Example:
|
53
|
+
# - `app/views/layouts/application.rb`
|
54
|
+
# - `app/views/layouts/application.css`
|
55
|
+
# - `app/views/layouts/application.js`
|
56
|
+
# - `app/views/layouts/application.tsx`
|
57
|
+
#
|
58
|
+
# A request to sideload `app/views/layouts/application.rb` will result in `application.css`
|
59
|
+
# and `application.tsx` being sideloaded. `application.js` will not be sideloaded because the
|
60
|
+
# `.tsx` extension is matched first.
|
61
|
+
#
|
62
|
+
# @param filepath [Pathname] Absolute file system path of the Ruby file to sideload.
|
63
|
+
def sideload(filepath, **options)
|
64
|
+
return unless Proscenium.config.side_load
|
65
|
+
|
66
|
+
filepath = Rails.root.join(filepath) unless filepath.is_a?(Pathname)
|
67
|
+
filepath = filepath.sub_ext('')
|
68
|
+
|
69
|
+
import_if_exists = lambda do |x|
|
70
|
+
if (fp = filepath.sub_ext(x)).exist?
|
71
|
+
import(Resolver.resolve(fp.to_s), sideloaded: true, **options)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
JS_EXTENSIONS.find(&import_if_exists)
|
76
|
+
CSS_EXTENSIONS.find(&import_if_exists)
|
77
|
+
end
|
78
|
+
|
79
|
+
def each_stylesheet(delete: false)
|
80
|
+
return if imported.blank?
|
81
|
+
|
82
|
+
blk = proc { |key, options| key.end_with?(*CSS_EXTENSIONS) && yield(key, options) }
|
83
|
+
delete ? imported.delete_if(&blk) : imported.each(&blk)
|
84
|
+
end
|
85
|
+
|
86
|
+
def each_javascript(delete: false)
|
87
|
+
return if imported.blank?
|
88
|
+
|
89
|
+
blk = proc { |key, options| key.end_with?(*JS_EXTENSIONS) && yield(key, options) }
|
90
|
+
delete ? imported.delete_if(&blk) : imported.each(&blk)
|
91
|
+
end
|
92
|
+
|
93
|
+
def css_imported?
|
94
|
+
imported&.keys&.any? { |x| x.end_with?(*CSS_EXTENSIONS) }
|
95
|
+
end
|
96
|
+
|
97
|
+
def js_imported?
|
98
|
+
imported&.keys&.any? { |x| x.end_with?(*JS_EXTENSIONS) }
|
99
|
+
end
|
100
|
+
|
101
|
+
def multiple_js_imported?
|
102
|
+
imported&.keys&.many? { |x| x.end_with?(*JS_EXTENSIONS) }
|
103
|
+
end
|
104
|
+
|
105
|
+
def imported?(filepath = nil)
|
106
|
+
filepath ? imported&.key?(filepath) : !imported.blank?
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
const elements = document.querySelectorAll("[data-proscenium-component-path]");
|
2
|
+
|
3
|
+
// Initialize only if there are components.
|
4
|
+
elements.length > 0 && init();
|
5
|
+
|
6
|
+
function init() {
|
7
|
+
/**
|
8
|
+
* Mounts component located at `path`, into the DOM `element`.
|
9
|
+
*
|
10
|
+
* The element at which the component is mounted must have the following data attributes:
|
11
|
+
*
|
12
|
+
* - `data-proscenium-component-path`: The URL path to the component's source file.
|
13
|
+
* - `data-proscenium-component-props`: JSON object of props to pass to the component.
|
14
|
+
* - `data-proscenium-component-lazy`: If present, will lazily load the component when in view
|
15
|
+
* using IntersectionObserver.
|
16
|
+
* - `data-proscenium-component-forward-children`: If the element should forward its `innerHTML`
|
17
|
+
* as the component's children prop.
|
18
|
+
*/
|
19
|
+
function mount(element, path, { children, ...props }) {
|
20
|
+
// For testing and simulation of slow connections.
|
21
|
+
// const sim = new Promise((resolve) => setTimeout(resolve, 5000));
|
22
|
+
|
23
|
+
if (!(path in window.prosceniumLazyScripts)) {
|
24
|
+
throw `[proscenium/react/manager] Cannot load component ${path} (not found in prosceniumLazyScripts)`;
|
25
|
+
}
|
26
|
+
|
27
|
+
const react = import("@proscenium/react-manager/react");
|
28
|
+
const Component = import(window.prosceniumLazyScripts[path].outpath);
|
29
|
+
|
30
|
+
const forwardChildren =
|
31
|
+
"prosceniumComponentForwardChildren" in element.dataset &&
|
32
|
+
element.innerHTML !== "";
|
33
|
+
|
34
|
+
Promise.all([react, Component]).then(([r, c]) => {
|
35
|
+
if (proscenium.env.RAILS_ENV === "development") {
|
36
|
+
console.groupCollapsed(
|
37
|
+
`[proscenium/react/manager] 🔥 %o mounted!`,
|
38
|
+
path
|
39
|
+
);
|
40
|
+
console.log("props: %o", props);
|
41
|
+
console.groupEnd();
|
42
|
+
}
|
43
|
+
|
44
|
+
let component;
|
45
|
+
if (forwardChildren) {
|
46
|
+
component = r.createElement(c.default, props, element.innerHTML);
|
47
|
+
} else if (children) {
|
48
|
+
component = r.createElement(c.default, props, children);
|
49
|
+
} else {
|
50
|
+
component = r.createElement(c.default, props);
|
51
|
+
}
|
52
|
+
|
53
|
+
r.createRoot(element).render(component);
|
54
|
+
});
|
55
|
+
}
|
56
|
+
|
57
|
+
Array.from(elements, (element) => {
|
58
|
+
const path = element.dataset.prosceniumComponentPath;
|
59
|
+
const isLazy = "prosceniumComponentLazy" in element.dataset;
|
60
|
+
const props = JSON.parse(element.dataset.prosceniumComponentProps);
|
61
|
+
|
62
|
+
if (proscenium.env.RAILS_ENV === "development") {
|
63
|
+
console.groupCollapsed(
|
64
|
+
`[proscenium/react/manager] ${isLazy ? "💤" : "⚡️"} %o`,
|
65
|
+
path
|
66
|
+
);
|
67
|
+
console.log("element: %o", element);
|
68
|
+
console.log("props: %o", props);
|
69
|
+
console.groupEnd();
|
70
|
+
}
|
71
|
+
|
72
|
+
if (isLazy) {
|
73
|
+
const observer = new IntersectionObserver((entries) => {
|
74
|
+
entries.forEach((entry) => {
|
75
|
+
if (entry.isIntersecting) {
|
76
|
+
observer.unobserve(element);
|
77
|
+
|
78
|
+
mount(element, path, props);
|
79
|
+
}
|
80
|
+
});
|
81
|
+
});
|
82
|
+
|
83
|
+
observer.observe(element);
|
84
|
+
} else {
|
85
|
+
mount(element, path, props);
|
86
|
+
}
|
87
|
+
});
|
88
|
+
}
|