proscenium 0.9.1-x86_64-darwin → 0.11.0.pre.1-x86_64-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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +423 -63
  3. data/lib/proscenium/builder.rb +126 -0
  4. data/lib/proscenium/css_module/path.rb +31 -0
  5. data/lib/proscenium/css_module/transformer.rb +76 -0
  6. data/lib/proscenium/css_module.rb +6 -28
  7. data/lib/proscenium/ensure_loaded.rb +27 -0
  8. data/lib/proscenium/ext/proscenium +0 -0
  9. data/lib/proscenium/ext/proscenium.h +19 -12
  10. data/lib/proscenium/helper.rb +62 -0
  11. data/lib/proscenium/importer.rb +110 -0
  12. data/lib/proscenium/libs/react-manager/index.jsx +88 -0
  13. data/lib/proscenium/libs/react-manager/react.js +2 -0
  14. data/lib/proscenium/libs/stimulus-loading.js +83 -0
  15. data/lib/proscenium/log_subscriber.rb +1 -2
  16. data/lib/proscenium/middleware/base.rb +1 -1
  17. data/lib/proscenium/middleware/esbuild.rb +3 -5
  18. data/lib/proscenium/middleware.rb +7 -1
  19. data/lib/proscenium/{side_load/monkey.rb → monkey.rb} +16 -12
  20. data/lib/proscenium/phlex/{resolve_css_modules.rb → css_modules.rb} +6 -20
  21. data/lib/proscenium/phlex/page.rb +2 -2
  22. data/lib/proscenium/phlex/react_component.rb +27 -64
  23. data/lib/proscenium/phlex.rb +10 -29
  24. data/lib/proscenium/railtie.rb +20 -22
  25. data/lib/proscenium/react_componentable.rb +94 -0
  26. data/lib/proscenium/resolver.rb +37 -0
  27. data/lib/proscenium/side_load.rb +13 -72
  28. data/lib/proscenium/source_path.rb +15 -0
  29. data/lib/proscenium/utils.rb +13 -0
  30. data/lib/proscenium/version.rb +1 -1
  31. data/lib/proscenium/view_component/css_modules.rb +11 -0
  32. data/lib/proscenium/view_component/react_component.rb +15 -28
  33. data/lib/proscenium/view_component/sideload.rb +4 -0
  34. data/lib/proscenium/view_component.rb +8 -31
  35. data/lib/proscenium.rb +24 -68
  36. metadata +21 -58
  37. data/lib/proscenium/css_module/class_names_resolver.rb +0 -66
  38. data/lib/proscenium/css_module/resolver.rb +0 -76
  39. data/lib/proscenium/current.rb +0 -9
  40. data/lib/proscenium/esbuild/golib.rb +0 -97
  41. data/lib/proscenium/esbuild.rb +0 -32
  42. data/lib/proscenium/phlex/component_concerns.rb +0 -27
  43. data/lib/proscenium/side_load/ensure_loaded.rb +0 -25
  44. data/lib/proscenium/side_load/helper.rb +0 -25
  45. 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
- class StylesheetNotFound < StandardError
7
- def initialize(pathname)
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, String]
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 ||= Resolver.new(path)
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
- // - path - The path to build relative to `root`.
89
- // - root - The working directory.
90
- // - baseUrl - base URL of the Rails app. eg. https://example.com
91
- // - env - The environment (1 = development, 2 = test, 3 = production)
92
- // - importMap - Path to the import map relative to `root`.
93
- // - debug
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* root, char* baseUrl, unsigned int env, char* importMap, GoUint8 debug);
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
- // - path - The path to build relative to `root`.
100
- // - root - The working directory.
101
- // - env - The environment (1 = development, 2 = test, 3 = production)
102
- // - importMap - Path to the import map relative to `root`.
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, char* importMap);
111
+ extern struct Result resolve(char* path, char* importMap, char* root, unsigned int env);
105
112
 
106
113
  #ifdef __cplusplus
107
114
  }
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ export { createElement } from "react";
2
+ export { createRoot } from "react-dom/client";