proscenium 0.9.1-arm64-darwin → 0.11.0-arm64-darwin

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +451 -65
  3. data/lib/proscenium/builder.rb +144 -0
  4. data/lib/proscenium/css_module/path.rb +31 -0
  5. data/lib/proscenium/css_module/transformer.rb +82 -0
  6. data/lib/proscenium/css_module.rb +12 -25
  7. data/lib/proscenium/ensure_loaded.rb +27 -0
  8. data/lib/proscenium/ext/proscenium +0 -0
  9. data/lib/proscenium/ext/proscenium.h +20 -12
  10. data/lib/proscenium/helper.rb +85 -0
  11. data/lib/proscenium/importer.rb +110 -0
  12. data/lib/proscenium/libs/react-manager/index.jsx +101 -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/libs/test.js +1 -0
  16. data/lib/proscenium/log_subscriber.rb +1 -2
  17. data/lib/proscenium/middleware/base.rb +8 -8
  18. data/lib/proscenium/middleware/engines.rb +37 -0
  19. data/lib/proscenium/middleware/esbuild.rb +3 -5
  20. data/lib/proscenium/middleware/runtime.rb +18 -0
  21. data/lib/proscenium/middleware.rb +19 -4
  22. data/lib/proscenium/{side_load/monkey.rb → monkey.rb} +24 -12
  23. data/lib/proscenium/phlex/{resolve_css_modules.rb → css_modules.rb} +28 -16
  24. data/lib/proscenium/phlex/react_component.rb +27 -64
  25. data/lib/proscenium/phlex.rb +11 -30
  26. data/lib/proscenium/railtie.rb +49 -41
  27. data/lib/proscenium/react_componentable.rb +95 -0
  28. data/lib/proscenium/resolver.rb +37 -0
  29. data/lib/proscenium/side_load.rb +13 -72
  30. data/lib/proscenium/source_path.rb +15 -0
  31. data/lib/proscenium/templates/rescues/build_error.html.erb +30 -0
  32. data/lib/proscenium/utils.rb +13 -0
  33. data/lib/proscenium/version.rb +1 -1
  34. data/lib/proscenium/view_component/css_modules.rb +11 -0
  35. data/lib/proscenium/view_component/react_component.rb +15 -28
  36. data/lib/proscenium/view_component/sideload.rb +4 -0
  37. data/lib/proscenium/view_component.rb +8 -31
  38. data/lib/proscenium.rb +23 -68
  39. metadata +25 -59
  40. data/lib/proscenium/css_module/class_names_resolver.rb +0 -66
  41. data/lib/proscenium/css_module/resolver.rb +0 -76
  42. data/lib/proscenium/current.rb +0 -9
  43. data/lib/proscenium/esbuild/golib.rb +0 -97
  44. data/lib/proscenium/esbuild.rb +0 -32
  45. data/lib/proscenium/phlex/component_concerns.rb +0 -27
  46. data/lib/proscenium/phlex/page.rb +0 -62
  47. data/lib/proscenium/side_load/ensure_loaded.rb +0 -25
  48. data/lib/proscenium/side_load/helper.rb +0 -25
  49. data/lib/proscenium/view_component/tag_builder.rb +0 -23
@@ -0,0 +1,144 @@
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, # Rails application root
29
+ :string, # Proscenium gem root
30
+ :environment, # Rails environment as a Symbol
31
+ :bool, # code splitting enabled?
32
+ :string, # engine names and paths as a JSON string
33
+ :bool # debugging enabled?
34
+ ], Result.by_value
35
+
36
+ attach_function :resolve, [
37
+ :string, # path or entry point
38
+ :string, # path to import map, relative to root
39
+
40
+ # Config
41
+ :string, # Rails application root
42
+ :string, # Proscenium gem root
43
+ :environment, # Rails environment as a Symbol
44
+ :bool # debugging enabled?
45
+ ], Result.by_value
46
+ end
47
+
48
+ class BuildError < StandardError
49
+ attr_reader :error
50
+
51
+ def initialize(error)
52
+ @error = Oj.load(error, mode: :strict).deep_transform_keys(&:underscore)
53
+
54
+ msg = @error['text']
55
+ if (location = @error['location'])
56
+ msg << " at #{location['file']}:#{location['line']}:#{location['column']}"
57
+ end
58
+
59
+ super msg
60
+ end
61
+ end
62
+
63
+ class ResolveError < StandardError
64
+ attr_reader :error_msg, :path
65
+
66
+ def initialize(path, error_msg)
67
+ super "Failed to resolve '#{path}' -- #{error_msg}"
68
+ end
69
+ end
70
+
71
+ def self.build(path, root: nil, base_url: nil)
72
+ new(root: root, base_url: base_url).build(path)
73
+ end
74
+
75
+ def self.resolve(path, root: nil)
76
+ new(root: root).resolve(path)
77
+ end
78
+
79
+ def initialize(root: nil, base_url: nil)
80
+ @root = root || Rails.root
81
+ @base_url = base_url
82
+ end
83
+
84
+ def build(path) # rubocop:disable Metrics/AbcSize
85
+ ActiveSupport::Notifications.instrument('build.proscenium', identifier: path) do
86
+ result = Request.build(path, @base_url, import_map, env_vars.to_json,
87
+ @root.to_s,
88
+ Pathname.new(__dir__).join('..', '..').to_s,
89
+ Rails.env.to_sym,
90
+ Proscenium.config.code_splitting,
91
+ engines.to_json,
92
+ Proscenium.config.debug)
93
+
94
+ raise BuildError, result[:response] unless result[:success]
95
+
96
+ result[:response]
97
+ end
98
+ end
99
+
100
+ def resolve(path)
101
+ ActiveSupport::Notifications.instrument('resolve.proscenium', identifier: path) do
102
+ result = Request.resolve(path, import_map, @root.to_s,
103
+ Pathname.new(__dir__).join('..', '..').to_s,
104
+ Rails.env.to_sym,
105
+ Proscenium.config.debug)
106
+ raise ResolveError.new(path, result[:response]) unless result[:success]
107
+
108
+ result[:response]
109
+ end
110
+ end
111
+
112
+ private
113
+
114
+ # Build the ENV variables as determined by `Proscenium.config.env_vars` and
115
+ # `Proscenium::DEFAULT_ENV_VARS` to pass to esbuild.
116
+ def env_vars
117
+ ENV['NODE_ENV'] = ENV.fetch('RAILS_ENV', nil)
118
+ ENV.slice(*Proscenium.config.env_vars + Proscenium::DEFAULT_ENV_VARS)
119
+ end
120
+
121
+ def cache_query_string
122
+ q = Proscenium.config.cache_query_string
123
+ q ? "--cache-query-string #{q}" : nil
124
+ end
125
+
126
+ def engines
127
+ Proscenium.config.engines.to_h { |e| [e.engine_name, e.root.to_s] }
128
+ end
129
+
130
+ def import_map
131
+ return unless (path = Rails.root&.join('config'))
132
+
133
+ if (json = path.join('import_map.json')).exist?
134
+ return json.relative_path_from(@root).to_s
135
+ end
136
+
137
+ if (js = path.join('import_map.js')).exist?
138
+ return js.relative_path_from(@root).to_s
139
+ end
140
+
141
+ nil
142
+ end
143
+ end
144
+ 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,82 @@
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
+ return unless (@source_path = source_path)
13
+
14
+ @source_path = Pathname.new(@source_path) unless @source_path.is_a?(Pathname)
15
+ @source_path = @source_path.sub_ext(FILE_EXT) unless @source_path.to_s.end_with?(FILE_EXT)
16
+ end
17
+
18
+ # Transform each of the given class `names` to their respective CSS module name, which consist
19
+ # of the name, and suffixed with the digest of the resolved source path.
20
+ #
21
+ # Any name beginning with '@' will be transformed to a CSS module name. If `require_prefix` is
22
+ # false, then all names will be transformed to a CSS module name regardless of whether or not
23
+ # they begin with '@'.
24
+ #
25
+ # class_names :@my_module_name, :my_class_name
26
+ #
27
+ # Note that the generated digest is based on the resolved (URL) path, not the original path.
28
+ #
29
+ # You can also provide a path specifier and class name. The path will be the URL path to a
30
+ # stylesheet. The class name will be the name of the class to transform.
31
+ #
32
+ # class_names "/lib/button@default"
33
+ # class_names "mypackage/button@large"
34
+ # class_names "@scoped/package/button@small"
35
+ #
36
+ # @param names [String,Symbol,Array<String,Symbol>]
37
+ # @param require_prefix: [Boolean] whether or not to require the `@` prefix.
38
+ # @return [Array<String>] the transformed CSS module names.
39
+ def class_names(*names, require_prefix: true)
40
+ names.map do |name|
41
+ original_name = name.dup
42
+ name = name.to_s if name.is_a?(Symbol)
43
+
44
+ if name.include?('/')
45
+ if name.start_with?('@')
46
+ # Scoped bare specifier (eg. "@scoped/package/lib/button@default").
47
+ _, path, name = name.split('@')
48
+ path = "@#{path}"
49
+ else
50
+ # Local path (eg. /some/path/to/button@default") or bare specifier (eg.
51
+ # "mypackage/lib/button@default").
52
+ path, name = name.split('@')
53
+ end
54
+
55
+ class_name! name, original_name, path: "#{path}#{FILE_EXT}"
56
+ elsif name.start_with?('@')
57
+ class_name! name[1..], original_name
58
+ else
59
+ require_prefix ? name : class_name!(name, original_name)
60
+ end
61
+ end
62
+ end
63
+
64
+ def class_name!(name, original_name, path: @source_path)
65
+ unless path
66
+ raise Proscenium::CssModule::TransformError.new(original_name, 'CSS module path not given')
67
+ end
68
+
69
+ resolved_path = Resolver.resolve(path.to_s)
70
+ digest = Importer.import(resolved_path)
71
+
72
+ transformed_name = name.to_s
73
+ transformed_name = if transformed_name.start_with?('_')
74
+ "_#{transformed_name[1..]}-#{digest}"
75
+ else
76
+ "#{transformed_name}-#{digest}"
77
+ end
78
+
79
+ [transformed_name, resolved_path]
80
+ end
81
+ end
82
+ end
@@ -3,41 +3,28 @@
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
6
+ autoload :Path
7
+ autoload :Transformer
16
8
 
17
- autoload :ClassNamesResolver
18
- autoload :Resolver # deprecated
9
+ class TransformError < StandardError
10
+ def initialize(name, additional_msg = nil)
11
+ msg = "Failed to transform CSS module `#{name}`"
12
+ msg << ' - ' << additional_msg if additional_msg
19
13
 
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 ' '
14
+ super msg
15
+ end
25
16
  end
26
17
 
27
18
  # Accepts one or more CSS class names, and transforms them into CSS module names.
28
19
  #
29
- # @param name [Array, String]
30
- def css_module(names)
31
- cssm.class_names(names).join ' '
20
+ # @param name [String,Symbol,Array<String,Symbol>]
21
+ def css_module(*names)
22
+ cssm.class_names(*names, require_prefix: false).map { |name, _| name }.join(' ')
32
23
  end
33
24
 
34
25
  private
35
26
 
36
- def path
37
- self.class.path
38
- end
39
-
40
27
  def cssm
41
- @cssm ||= Resolver.new(path)
28
+ @cssm ||= Transformer.new(self.class.css_module_path)
42
29
  end
43
30
  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,31 @@ 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* appRoot, char* gemPath, unsigned int env, GoUint8 codeSplitting, char* engines, 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)
110
+ // - debug?
103
111
  //
104
- extern struct Result resolve(char* path, char* root, unsigned int env, char* importMap);
112
+ extern struct Result resolve(char* path, char* importMap, char* appRoot, char* gemPath, unsigned int env, GoUint8 debug);
105
113
 
106
114
  #ifdef __cplusplus
107
115
  }
@@ -15,5 +15,90 @@ 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).map do |name, _|
26
+ name
27
+ end.join(' ')
28
+ end
29
+
30
+ def include_stylesheets(**options)
31
+ out = []
32
+ Importer.each_stylesheet(delete: true) do |path, _path_options|
33
+ out << stylesheet_link_tag(path, extname: false, **options)
34
+ end
35
+ out.join("\n").html_safe
36
+ end
37
+ alias side_load_stylesheets include_stylesheets
38
+ deprecate side_load_stylesheets: 'Use `include_stylesheets` instead', deprecator: Deprecator.new
39
+
40
+ # Includes all javascripts that have been imported and side loaded.
41
+ #
42
+ # @param extract_lazy_scripts [Boolean] if true, any lazy scripts will be extracted using
43
+ # `content_for` to `:proscenium_lazy_scripts` for later use. Be sure to include this in your
44
+ # page with the `declare_lazy_scripts` helper, or simply
45
+ # `content_for :proscenium_lazy_scripts`.
46
+ # @return [String] the HTML tags for the javascripts.
47
+ def include_javascripts(extract_lazy_scripts: false, **options) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
48
+ out = []
49
+
50
+ if Rails.application.config.proscenium.code_splitting && Importer.multiple_js_imported?
51
+ imports = Importer.imported.dup
52
+
53
+ paths_to_build = []
54
+ Importer.each_javascript(delete: true) do |x, _|
55
+ paths_to_build << x.delete_prefix('/')
56
+ end
57
+
58
+ result = Builder.build(paths_to_build.join(';'), base_url: request.base_url)
59
+
60
+ # Remove the react components from the results, so they are not side loaded. Instead they
61
+ # are lazy loaded by the component manager.
62
+
63
+ scripts = {}
64
+ result.split(';').each do |x|
65
+ inpath, outpath = x.split('::')
66
+ inpath.prepend '/'
67
+ outpath.delete_prefix! 'public'
68
+
69
+ next unless imports.key?(inpath)
70
+
71
+ if (import = imports[inpath]).delete(:lazy)
72
+ scripts[inpath] = import.merge(outpath: outpath)
73
+ else
74
+ out << javascript_include_tag(outpath, extname: false, **options)
75
+ end
76
+ end
77
+
78
+ if extract_lazy_scripts
79
+ content_for :proscenium_lazy_scripts do
80
+ tag.script type: 'application/json', id: 'prosceniumLazyScripts' do
81
+ raw scripts.to_json
82
+ end
83
+ end
84
+ else
85
+ out << tag.script(type: 'application/json', id: 'prosceniumLazyScripts') do
86
+ raw scripts.to_json
87
+ end
88
+ end
89
+ else
90
+ Importer.each_javascript(delete: true) do |path, _|
91
+ out << javascript_include_tag(path, extname: false, **options)
92
+ end
93
+ end
94
+
95
+ out.join("\n").html_safe
96
+ end
97
+ alias side_load_javascripts include_javascripts
98
+ deprecate side_load_javascripts: 'Use `include_javascripts` instead', deprecator: Deprecator.new
99
+
100
+ def declare_lazy_scripts
101
+ content_for :proscenium_lazy_scripts
102
+ end
18
103
  end
19
104
  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 URL 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,101 @@
1
+ window.Proscenium = window.Proscenium || { lazyScripts: {} };
2
+
3
+ const element = document.querySelector("#prosceniumLazyScripts");
4
+ if (element) {
5
+ const scripts = JSON.parse(element.text);
6
+ window.Proscenium.lazyScripts = {
7
+ ...window.Proscenium.lazyScripts,
8
+ ...scripts,
9
+ };
10
+ }
11
+
12
+ const elements = document.querySelectorAll("[data-proscenium-component-path]");
13
+ elements.length > 0 && init(elements);
14
+
15
+ function init() {
16
+ /**
17
+ * Mounts component located at `path`, into the DOM `element`.
18
+ *
19
+ * The element at which the component is mounted must have the following data attributes:
20
+ *
21
+ * - `data-proscenium-component-path`: The URL path to the component's source file.
22
+ * - `data-proscenium-component-props`: JSON object of props to pass to the component.
23
+ * - `data-proscenium-component-lazy`: If present, will lazily load the component when in view
24
+ * using IntersectionObserver.
25
+ * - `data-proscenium-component-forward-children`: If the element should forward its `innerHTML`
26
+ * as the component's children prop.
27
+ */
28
+ function mount(element, path, { children, ...props }) {
29
+ // For testing and simulation of slow connections.
30
+ // const sim = new Promise((resolve) => setTimeout(resolve, 5000));
31
+
32
+ if (!window.Proscenium.lazyScripts[path]) {
33
+ throw `[proscenium/react/manager] Cannot load component ${path} (not found in Proscenium.lazyScripts)`;
34
+ }
35
+
36
+ const react = import("@proscenium/react-manager/react");
37
+ const Component = import(window.Proscenium.lazyScripts[path].outpath);
38
+
39
+ const forwardChildren =
40
+ "prosceniumComponentForwardChildren" in element.dataset &&
41
+ element.innerHTML !== "";
42
+
43
+ Promise.all([react, Component])
44
+ .then(([r, c]) => {
45
+ if (proscenium.env.RAILS_ENV === "development") {
46
+ console.groupCollapsed(
47
+ `[proscenium/react/manager] 🔥 %o mounted!`,
48
+ path
49
+ );
50
+ console.log("props: %o", props);
51
+ console.groupEnd();
52
+ }
53
+
54
+ let component;
55
+ if (forwardChildren) {
56
+ component = r.createElement(c.default, props, element.innerHTML);
57
+ } else if (children) {
58
+ component = r.createElement(c.default, props, children);
59
+ } else {
60
+ component = r.createElement(c.default, props);
61
+ }
62
+
63
+ r.createRoot(element).render(component);
64
+ })
65
+ .catch((error) => {
66
+ console.error("[proscenium/react/manager] %o - %o", path, error);
67
+ });
68
+ }
69
+
70
+ Array.from(elements, (element) => {
71
+ const path = element.dataset.prosceniumComponentPath;
72
+ const isLazy = "prosceniumComponentLazy" in element.dataset;
73
+ const props = JSON.parse(element.dataset.prosceniumComponentProps);
74
+
75
+ if (proscenium.env.RAILS_ENV === "development") {
76
+ console.groupCollapsed(
77
+ `[proscenium/react/manager] ${isLazy ? "💤" : "⚡️"} %o`,
78
+ path
79
+ );
80
+ console.log("element: %o", element);
81
+ console.log("props: %o", props);
82
+ console.groupEnd();
83
+ }
84
+
85
+ if (isLazy) {
86
+ const observer = new IntersectionObserver((entries) => {
87
+ entries.forEach((entry) => {
88
+ if (entry.isIntersecting) {
89
+ observer.unobserve(element);
90
+
91
+ mount(element, path, props);
92
+ }
93
+ });
94
+ });
95
+
96
+ observer.observe(element);
97
+ } else {
98
+ mount(element, path, props);
99
+ }
100
+ });
101
+ }