proscenium 0.6.0-x86_64-linux → 0.8.2-x86_64-linux

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +126 -109
  3. data/lib/proscenium/css_module/class_names_resolver.rb +66 -0
  4. data/lib/proscenium/css_module/resolver.rb +76 -0
  5. data/lib/proscenium/css_module.rb +18 -39
  6. data/lib/proscenium/esbuild/golib.rb +97 -0
  7. data/lib/proscenium/esbuild.rb +32 -0
  8. data/lib/proscenium/ext/proscenium +0 -0
  9. data/lib/proscenium/ext/proscenium.h +108 -0
  10. data/lib/proscenium/helper.rb +0 -23
  11. data/lib/proscenium/log_subscriber.rb +26 -0
  12. data/lib/proscenium/middleware/base.rb +28 -36
  13. data/lib/proscenium/middleware/esbuild.rb +18 -44
  14. data/lib/proscenium/middleware/url.rb +1 -6
  15. data/lib/proscenium/middleware.rb +12 -16
  16. data/lib/proscenium/phlex/component_concerns.rb +27 -0
  17. data/lib/proscenium/phlex/page.rb +62 -0
  18. data/lib/proscenium/phlex/react_component.rb +52 -8
  19. data/lib/proscenium/phlex/resolve_css_modules.rb +67 -0
  20. data/lib/proscenium/phlex.rb +34 -33
  21. data/lib/proscenium/railtie.rb +41 -67
  22. data/lib/proscenium/side_load/ensure_loaded.rb +25 -0
  23. data/lib/proscenium/side_load/helper.rb +25 -0
  24. data/lib/proscenium/side_load/monkey.rb +48 -0
  25. data/lib/proscenium/side_load.rb +58 -52
  26. data/lib/proscenium/version.rb +1 -1
  27. data/lib/proscenium/view_component/react_component.rb +14 -0
  28. data/lib/proscenium/view_component.rb +28 -18
  29. data/lib/proscenium.rb +79 -2
  30. metadata +35 -77
  31. data/app/channels/proscenium/connection.rb +0 -13
  32. data/app/channels/proscenium/reload_channel.rb +0 -9
  33. data/bin/esbuild +0 -0
  34. data/bin/lightningcss +0 -0
  35. data/config/routes.rb +0 -7
  36. data/lib/proscenium/compiler.js +0 -84
  37. data/lib/proscenium/compilers/esbuild/argument_error.js +0 -24
  38. data/lib/proscenium/compilers/esbuild/compile_error.js +0 -148
  39. data/lib/proscenium/compilers/esbuild/css/postcss.js +0 -67
  40. data/lib/proscenium/compilers/esbuild/css_plugin.js +0 -172
  41. data/lib/proscenium/compilers/esbuild/env_plugin.js +0 -46
  42. data/lib/proscenium/compilers/esbuild/http_bundle_plugin.js +0 -53
  43. data/lib/proscenium/compilers/esbuild/import_map/parser.js +0 -178
  44. data/lib/proscenium/compilers/esbuild/import_map/read.js +0 -64
  45. data/lib/proscenium/compilers/esbuild/import_map/resolver.js +0 -95
  46. data/lib/proscenium/compilers/esbuild/import_map/utils.js +0 -25
  47. data/lib/proscenium/compilers/esbuild/resolve_plugin.js +0 -207
  48. data/lib/proscenium/compilers/esbuild/setup_plugin.js +0 -45
  49. data/lib/proscenium/compilers/esbuild/solidjs_plugin.js +0 -24
  50. data/lib/proscenium/compilers/esbuild.bench.js +0 -14
  51. data/lib/proscenium/compilers/esbuild.js +0 -179
  52. data/lib/proscenium/link_to_helper.rb +0 -40
  53. data/lib/proscenium/middleware/lightningcss.rb +0 -64
  54. data/lib/proscenium/middleware/outside_root.rb +0 -26
  55. data/lib/proscenium/middleware/runtime.rb +0 -22
  56. data/lib/proscenium/middleware/static.rb +0 -14
  57. data/lib/proscenium/phlex/component.rb +0 -9
  58. data/lib/proscenium/precompile.rb +0 -31
  59. data/lib/proscenium/runtime/auto_reload.js +0 -40
  60. data/lib/proscenium/runtime/react_shim/index.js +0 -1
  61. data/lib/proscenium/runtime/react_shim/package.json +0 -5
  62. data/lib/proscenium/utils.js +0 -12
  63. data/lib/tasks/assets.rake +0 -19
@@ -1,10 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #
4
- # Renders a div for use with component-manager.
4
+ # Renders a div for use with @proscenium/component-manager.
5
5
  #
6
- class Proscenium::Phlex::ReactComponent < Proscenium::Phlex::Component
7
- attr_accessor :props, :lazy
6
+ # You can pass props to the component in the `:props` keyword argument.
7
+ #
8
+ # By default, the component is lazy loaded when intersecting using IntersectionObserver. Pass in
9
+ # :lazy as false to disable this and render the component immediately.
10
+ #
11
+ # React components are not side loaded at all.
12
+ #
13
+ class Proscenium::Phlex::ReactComponent < Phlex::HTML
14
+ class << self
15
+ attr_accessor :path, :abstract_class
16
+
17
+ def inherited(child)
18
+ position = caller_locations(1, 1).first.label == 'inherited' ? 2 : 1
19
+ child.path = Pathname.new caller_locations(position, 1).first.path.sub(/\.rb$/, '')
20
+
21
+ super
22
+ end
23
+ end
24
+
25
+ self.abstract_class = true
26
+
27
+ include Proscenium::Phlex::ComponentConcerns::CssModules
28
+
29
+ attr_writer :props, :lazy
8
30
 
9
31
  # @param props: [Hash]
10
32
  # @param lazy: [Boolean] Lazy load the component using IntersectionObserver. Default: true.
@@ -16,10 +38,32 @@ class Proscenium::Phlex::ReactComponent < Proscenium::Phlex::Component
16
38
  # @yield the given block to a `div` within the top level component div. If not given,
17
39
  # `<div>loading...</div>` will be rendered. Use this to display a loading UI while the component
18
40
  # is loading and rendered.
19
- def template(&block)
20
- div class: ['componentManagedByProscenium', '@component'],
21
- data: { component: { path: virtual_path, props: props, lazy: lazy }.to_json } do
22
- block ? div(&block) : div { 'loading...' }
23
- end
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)
24
68
  end
25
69
  end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proscenium
4
+ module Phlex::ResolveCssModules
5
+ extend ActiveSupport::Concern
6
+
7
+ class_methods do
8
+ attr_accessor :side_load_cache
9
+ end
10
+
11
+ def before_template
12
+ self.class.side_load_cache ||= Set.new
13
+ super
14
+ end
15
+
16
+ # Resolve and side load any CSS modules in the "class" attributes, where a CSS module is a class
17
+ # name beginning with a `@`. The class name is resolved to a CSS module name based on the file
18
+ # system path of the Phlex class, and any CSS file is side loaded.
19
+ #
20
+ # For example, the following will side load the CSS module file at
21
+ # app/components/user/component.module.css, and add the CSS Module name `user_name` to the
22
+ # <div>.
23
+ #
24
+ # # app/components/user/component.rb
25
+ # class User::Component < Proscenium::Phlex
26
+ # def template
27
+ # div class: :@user_name do
28
+ # 'Joel Moss'
29
+ # end
30
+ # end
31
+ # end
32
+ #
33
+ # Additionally, any class name containing a `/` is resolved as a CSS module path. Allowing you
34
+ # to use the same syntax as a CSS module, but without the need to manually import the CSS file.
35
+ #
36
+ # For example, the following will side load the CSS module file at /lib/users.module.css, and
37
+ # add the CSS Module name `name` to the <div>.
38
+ #
39
+ # class User::Component < Proscenium::Phlex
40
+ # def template
41
+ # div class: '/lib/users@name' do
42
+ # 'Joel Moss'
43
+ # end
44
+ # end
45
+ # end
46
+ #
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
+ # @raise [Proscenium::CssModule::Resolver::NotFound] If a CSS module file is not found for the
51
+ # Phlex class file path.
52
+ def process_attributes(**attributes)
53
+ if attributes.key?(:class) && (attributes[:class] = tokens(attributes[:class])).include?('@')
54
+ resolver = CssModule::ClassNamesResolver.new(attributes[:class], path)
55
+ self.class.side_load_cache.merge resolver.stylesheets
56
+ attributes[:class] = resolver.class_names
57
+ end
58
+
59
+ attributes
60
+ end
61
+
62
+ def after_template
63
+ super
64
+ self.class.side_load_cache&.each { |path| SideLoad.append! path, :css }
65
+ end
66
+ end
67
+ end
@@ -1,60 +1,61 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'phlex'
3
+ require 'phlex-rails'
4
4
 
5
5
  module Proscenium
6
- class Phlex < ::Phlex::View
6
+ class Phlex < ::Phlex::HTML
7
7
  extend ActiveSupport::Autoload
8
+ include Proscenium::CssModule
8
9
 
9
- autoload :Component
10
+ autoload :Page
10
11
  autoload :ReactComponent
12
+ autoload :ResolveCssModules
13
+ autoload :ComponentConcerns
11
14
 
12
- module Helpers
13
- def side_load_javascripts(...)
14
- return unless (output = @_view_context.side_load_javascripts(...))
15
+ extend ::Phlex::Rails::HelperMacros
16
+ include ::Phlex::Rails::Helpers::JavaScriptIncludeTag
17
+ include ::Phlex::Rails::Helpers::StyleSheetLinkTag
15
18
 
16
- @_target << output
17
- end
19
+ define_output_helper :side_load_stylesheets
20
+ define_output_helper :side_load_javascripts
21
+
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
+ module Sideload
26
+ def before_template
27
+ klass = self.class
18
28
 
19
- %i[side_load_stylesheets proscenium_dev].each do |name|
20
- define_method name do
21
- if (output = @_view_context.send(name))
22
- @_target << output
23
- end
29
+ if !klass.abstract_class && respond_to?(:side_load, true)
30
+ side_load
31
+ klass = klass.superclass
24
32
  end
25
- end
26
- end
27
33
 
28
- module Sideload
29
- def template(...)
30
- Proscenium::SideLoad.append self.class.path if Rails.application.config.proscenium.side_load
34
+ while !klass.abstract_class && klass.respond_to?(:path) && klass.path
35
+ Proscenium::SideLoad.append klass.path
36
+ klass = klass.superclass
37
+ end
31
38
 
32
39
  super
33
40
  end
34
41
  end
35
42
 
36
43
  class << self
37
- attr_accessor :path
44
+ attr_accessor :path, :abstract_class
38
45
 
39
46
  def inherited(child)
40
- path = caller_locations(1, 1)[0].path
41
- child.path = path.delete_prefix(::Rails.root.to_s).delete_suffix('.rb')[1..]
47
+ unless child.path
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
42
54
 
43
- child.prepend Sideload
44
- child.include Helpers
55
+ child.prepend Sideload if Rails.application.config.proscenium.side_load
45
56
 
46
57
  super
47
58
  end
48
59
  end
49
-
50
- def css_module(name)
51
- cssm.class_names!(name).join ' '
52
- end
53
-
54
- private
55
-
56
- def cssm
57
- @cssm ||= Proscenium::CssModule.new(self.class.path)
58
- end
59
60
  end
60
61
  end
@@ -1,23 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rails'
4
- require 'action_cable/engine'
5
- require 'listen'
4
+ require 'proscenium/log_subscriber'
6
5
 
7
6
  ENV['RAILS_ENV'] = Rails.env
8
7
 
9
8
  module Proscenium
10
- # These globs should actually be Deno supported globs, and not ruby globs. This is because when
11
- # precompiling, the glob paths are passed as is to the compiler run by Deno.
12
- #
13
- # See https://doc.deno.land/https://deno.land/std@0.145.0/path/mod.ts/~/globToRegExp
14
- DEFAULT_GLOB_TYPES = {
15
- esbuild: '/{config,app,lib,node_modules}/**.{js,mjs,jsx,css}',
16
- runtime: '/proscenium-runtime/**.{js,jsx}',
17
- url: %r{^/url:https?%3A%2F%2F},
18
- outsideRoot: '/**/*.{js,jsx,mjs,css}'
9
+ FILE_EXTENSIONS = ['js', 'mjs', 'jsx', 'css', 'js.map', 'mjs.map', 'jsx.map', 'css.map'].freeze
10
+
11
+ MIDDLEWARE_GLOB_TYPES = {
12
+ application: "/**.{#{FILE_EXTENSIONS.join(',')}}",
13
+ url: %r{^/https?%3A%2F%2F}
19
14
  }.freeze
20
15
 
16
+ APPLICATION_INCLUDE_PATHS = ['config', 'app/views', 'lib', 'node_modules'].freeze
17
+
21
18
  class << self
22
19
  def config
23
20
  @config ||= Railtie.config.proscenium
@@ -31,21 +28,29 @@ module Proscenium
31
28
  config.proscenium.side_load = true
32
29
  config.proscenium.cache_query_string = Rails.env.production? && ENV.fetch('REVISION', nil)
33
30
  config.proscenium.cache_max_age = 2_592_000 # 30 days
34
- config.proscenium.auto_reload = Rails.env.development?
35
- config.proscenium.auto_reload_paths ||= %w[lib app config]
36
- config.proscenium.auto_reload_extensions ||= /\.(css|jsx?)$/
31
+ config.proscenium.include_paths = Set.new(APPLICATION_INCLUDE_PATHS)
32
+
33
+ # A hash of gems that can be side loaded. Assets from gems listed here can be side loaded.
34
+ #
35
+ # Because side loading uses URL paths, any gem dependencies that side load assets will fail,
36
+ # because the URL path will be relative to the application's root, and not the gem's root. By
37
+ # specifying a list of gems that can be side loaded, Proscenium will be able to resolve the URL
38
+ # path to the gem's root, and side load the asset.
39
+ #
40
+ # Side loading gems rely on NPM and a package.json file in the gem root. This ensures that any
41
+ # dependencies are resolved correctly. This is required even if your gem has no package
42
+ # dependencies.
43
+ #
44
+ # Example:
45
+ # config.proscenium.side_load_gems['mygem'] = {
46
+ # root: gem_root,
47
+ # package_name: 'mygem'
48
+ # }
49
+ config.proscenium.side_load_gems = {}
37
50
 
38
51
  initializer 'proscenium.configuration' do |app|
39
52
  options = app.config.proscenium
40
-
41
- options.glob_types = DEFAULT_GLOB_TYPES if options.glob_types.blank?
42
- options.auto_reload_paths.filter! { |path| Dir.exist? path }
43
- options.cable_mount_path ||= '/proscenium-cable'
44
- options.cable_logger ||= Rails.logger
45
- end
46
-
47
- initializer 'proscenium.side_load' do |_app|
48
- Proscenium::Current.loaded ||= SideLoad::EXTENSIONS.to_h { |e| [e, Set[]] }
53
+ options.include_paths = Set.new(APPLICATION_INCLUDE_PATHS) if options.include_paths.blank?
49
54
  end
50
55
 
51
56
  initializer 'proscenium.middleware' do |app|
@@ -54,57 +59,26 @@ module Proscenium
54
59
  app.middleware.insert_after ActionDispatch::Static, Rack::ConditionalGet
55
60
  end
56
61
 
57
- initializer 'proscenium.helpers' do |_app|
58
- ActiveSupport.on_load(:action_view) do
59
- ActionView::Base.include Proscenium::Helper
62
+ initializer 'proscenium.side_loading' do |app|
63
+ if app.config.proscenium.side_load
64
+ Proscenium::Current.loaded ||= SideLoad::EXTENSIONS.to_h { |e| [e, Set.new] }
65
+
66
+ ActiveSupport.on_load(:action_view) do
67
+ ActionView::Base.include Proscenium::SideLoad::Helper
60
68
 
61
- if Rails.application.config.proscenium.side_load
62
69
  ActionView::TemplateRenderer.prepend SideLoad::Monkey::TemplateRenderer
70
+ ActionView::PartialRenderer.prepend SideLoad::Monkey::PartialRenderer
63
71
  end
64
72
 
65
- ActionView::Helpers::UrlHelper.prepend Proscenium::LinkToHelper
66
- end
67
- end
68
-
69
- config.after_initialize do
70
- next unless config.proscenium.auto_reload
71
-
72
- @listener = Listen.to(*config.proscenium.auto_reload_paths,
73
- only: config.proscenium.auto_reload_extensions) do |mod, add, rem|
74
- Proscenium::Railtie.websocket&.broadcast('reload', {
75
- modified: mod,
76
- removed: rem,
77
- added: add
78
- })
73
+ ActiveSupport.on_load(:action_controller) do
74
+ ActionController::Base.include Proscenium::SideLoad::EnsureLoaded
75
+ end
79
76
  end
80
-
81
- @listener.start
82
- end
83
-
84
- at_exit do
85
- @listener&.stop
86
77
  end
87
78
 
88
- class << self
89
- def websocket
90
- return @websocket unless @websocket.nil?
91
- return unless config.proscenium.auto_reload
92
-
93
- cable = ActionCable::Server::Configuration.new
94
- cable.cable = { adapter: 'async' }.with_indifferent_access
95
- cable.mount_path = config.proscenium.cable_mount_path
96
- cable.connection_class = -> { Proscenium::Connection }
97
- cable.logger = config.proscenium.cable_logger
98
-
99
- @websocket ||= ActionCable::Server::Base.new(config: cable)
100
- end
101
-
102
- def websocket_mount_path
103
- "#{mounted_path}#{config.proscenium.cable_mount_path}" if websocket
104
- end
105
-
106
- def mounted_path
107
- Proscenium::Railtie.routes.find_script_name({})
79
+ initializer 'proscenium.helper' do
80
+ ActiveSupport.on_load(:action_view) do
81
+ ActionView::Base.include Proscenium::Helper
108
82
  end
109
83
  end
110
84
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Proscenium::SideLoad
4
+ module EnsureLoaded
5
+ def self.included(child)
6
+ child.class_eval do
7
+ append_after_action do
8
+ if Proscenium::Current.loaded
9
+ if Proscenium::Current.loaded[:js].present?
10
+ raise NotIncludedError, 'There are javascripts to be side loaded, but they have not ' \
11
+ 'been included. Did you forget to add the ' \
12
+ '`#side_load_javascripts` helper in your views?'
13
+ end
14
+
15
+ if Proscenium::Current.loaded[:css].present?
16
+ raise NotIncludedError, 'There are stylesheets to be side loaded, but they have not ' \
17
+ 'been included. Did you forget to add the ' \
18
+ '`#side_load_stylesheets` helper in your views?'
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proscenium
4
+ module SideLoad::Helper
5
+ def side_load_stylesheets
6
+ return unless Proscenium::Current.loaded
7
+
8
+ out = []
9
+ Proscenium::Current.loaded[:css].delete_if do |path|
10
+ out << stylesheet_link_tag(path)
11
+ end
12
+ out.join("\n").html_safe
13
+ end
14
+
15
+ def side_load_javascripts(**options)
16
+ return unless Proscenium::Current.loaded
17
+
18
+ out = []
19
+ Proscenium::Current.loaded[:js].delete_if do |path|
20
+ out << javascript_include_tag(path, options)
21
+ end
22
+ out.join("\n").html_safe
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Proscenium::SideLoad
4
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
5
+ module Monkey
6
+ module TemplateRenderer
7
+ private
8
+
9
+ def render_template(view, template, layout_name, locals)
10
+ layout = find_layout(layout_name, locals.keys, [formats.first])
11
+ renderable = template.instance_variable_get(:@renderable)
12
+
13
+ if Object.const_defined?(:ViewComponent) &&
14
+ template.is_a?(ActionView::Template::Renderable) &&
15
+ renderable.class < ::ViewComponent::Base && renderable.class.format == :html
16
+ # Side load controller rendered ViewComponent
17
+ Proscenium::SideLoad.append "app/views/#{layout.virtual_path}" if layout
18
+ Proscenium::SideLoad.append "app/views/#{renderable.virtual_path}"
19
+ elsif template.respond_to?(:virtual_path) &&
20
+ template.respond_to?(:type) && template.type == :html
21
+ # Side load regular view template.
22
+ Proscenium::SideLoad.append "app/views/#{layout.virtual_path}" if layout
23
+
24
+ # Try side loading the variant template
25
+ if template.respond_to?(:variant) && template.variant
26
+ Proscenium::SideLoad.append "app/views/#{template.virtual_path}+#{template.variant}"
27
+ end
28
+
29
+ # The variant template may not exist (above), so we try the regular non-variant path.
30
+ Proscenium::SideLoad.append "app/views/#{template.virtual_path}"
31
+ end
32
+
33
+ super
34
+ end
35
+ end
36
+
37
+ module PartialRenderer
38
+ private
39
+
40
+ def build_rendered_template(content, template)
41
+ path = Rails.root.join('app', 'views', template.virtual_path)
42
+ cssm = Proscenium::CssModule::Resolver.new(path)
43
+ super cssm.compile_class_names(content), template
44
+ end
45
+ end
46
+ end
47
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
48
+ end
@@ -1,72 +1,78 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
4
3
  module Proscenium
5
- module SideLoad
6
- DEFAULT_EXTENSIONS = %i[js css].freeze
7
- EXTENSIONS = %i[js css].freeze
4
+ class SideLoad
5
+ extend ActiveSupport::Autoload
8
6
 
9
- module_function
7
+ NotIncludedError = Class.new(StandardError)
10
8
 
11
- # Side load the given asset `path`, by appending it to `Proscenium::Current.loaded`, which is a
12
- # Set of 'js' and 'css' asset paths. This is safe to call multiple times, as it uses Set's.
13
- # Meaning that side loading will never include duplicates.
14
- def append(path, *extensions)
15
- Proscenium::Current.loaded ||= EXTENSIONS.to_h { |e| [e, Set[]] }
9
+ autoload :Monkey
10
+ autoload :Helper
11
+ autoload :EnsureLoaded
16
12
 
17
- unless (unknown_extensions = extensions.difference(EXTENSIONS)).empty?
18
- raise ArgumentError, "unsupported extension(s): #{unknown_extensions.join(',')}"
19
- end
13
+ EXTENSIONS = %i[js css].freeze
14
+ EXTENSION_MAP = { '.css' => :css, '.js' => :js }.freeze
20
15
 
21
- loaded_types = []
16
+ attr_reader :path
22
17
 
23
- (extensions.empty? ? DEFAULT_EXTENSIONS : extensions).each do |ext|
24
- path_with_ext = "#{path}.#{ext}"
25
- ext = ext.to_sym
18
+ class << self
19
+ # Side load the given asset `path`, by appending it to `Proscenium::Current.loaded`, which is a
20
+ # Set of 'js' and 'css' asset paths. This is idempotent, so side loading will never include
21
+ # duplicates.
22
+ #
23
+ # @return [Array] appended URL paths
24
+ def append(path, extension_map = EXTENSION_MAP)
25
+ new(path, extension_map).append
26
+ end
26
27
 
27
- next if Proscenium::Current.loaded[ext].include?(path_with_ext)
28
- next unless Rails.root.join(path_with_ext).exist?
28
+ # Side load the given `path` at `type`, without first resolving the path. This still respects
29
+ # idempotency of `Proscenium::Current.loaded`.
30
+ #
31
+ # @param path [String]
32
+ # @param type [Symbol] :js or :css
33
+ def append!(path, type)
34
+ return if Proscenium::Current.loaded[type].include?(path)
29
35
 
30
- Proscenium::Current.loaded[ext] << path_with_ext
31
- loaded_types << ext
36
+ Proscenium::Current.loaded[type] << log(path)
32
37
  end
33
38
 
34
- !loaded_types.empty? && Rails.logger.debug do
35
- "[Proscenium] Side loaded /#{path}.(#{loaded_types.join(',')})"
39
+ def log(value)
40
+ ActiveSupport::Notifications.instrument('sideload.proscenium', identifier: value)
41
+
42
+ value
36
43
  end
37
44
  end
38
45
 
39
- module Monkey
40
- module TemplateRenderer
41
- private
42
-
43
- def render_template(view, template, layout_name, locals)
44
- layout = find_layout(layout_name, locals.keys, [formats.first])
45
- renderable = template.instance_variable_get(:@renderable)
46
-
47
- if template.is_a?(ActionView::Template::Renderable) &&
48
- renderable.class < ::ViewComponent::Base && renderable.class.format == :html
49
- # Side load controller rendered ViewComponent
50
- Proscenium::SideLoad.append "app/views/#{layout.virtual_path}" if layout
51
- Proscenium::SideLoad.append "app/views/#{renderable.virtual_path}"
52
- elsif template.respond_to?(:virtual_path) &&
53
- template.respond_to?(:type) && template.type == :html
54
- # Side load regular view template.
55
- Proscenium::SideLoad.append "app/views/#{layout.virtual_path}" if layout
56
-
57
- # Try side loading the variant template
58
- if template.respond_to?(:variant) && template.variant
59
- Proscenium::SideLoad.append "app/views/#{template.virtual_path}+#{template.variant}"
60
- end
61
-
62
- # The variant template may not exist (above), so we try the regular non-variant path.
63
- Proscenium::SideLoad.append "app/views/#{template.virtual_path}"
64
- end
65
-
66
- super
46
+ # @param path [Pathname, String] The path of the file to be side loaded.
47
+ # @param extension_map [Hash] File extensions to side load.
48
+ def initialize(path, extension_map = EXTENSION_MAP)
49
+ @path = (path.is_a?(Pathname) ? path : Rails.root.join(path)).sub_ext('')
50
+ @extension_map = extension_map
51
+
52
+ Proscenium::Current.loaded ||= EXTENSIONS.index_with { |_e| Set.new }
53
+ end
54
+
55
+ def append
56
+ @extension_map.filter_map do |ext, type|
57
+ next unless (resolved_path = resolve_path(path.sub_ext(ext)))
58
+
59
+ # Make sure path is not already side loaded.
60
+ unless Proscenium::Current.loaded[type].include?(resolved_path)
61
+ Proscenium::Current.loaded[type] << log(resolved_path)
67
62
  end
63
+
64
+ resolved_path
68
65
  end
69
66
  end
67
+
68
+ private
69
+
70
+ def log(...)
71
+ self.class.log(...)
72
+ end
73
+
74
+ def resolve_path(path)
75
+ path.exist? ? Utils.resolve_path(path.to_s) : nil
76
+ end
70
77
  end
71
78
  end
72
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Proscenium
4
- VERSION = '0.6.0'
4
+ VERSION = '0.8.2'
5
5
  end
@@ -1,10 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ #
4
+ # Renders HTML markup suitable for use with @proscenium/component-manager.
5
+ #
6
+ # If a content block is given, that content will be rendered inside the component, allowing for a
7
+ # "loading" UI. If no block is given, then a loading text will be rendered.
8
+ #
9
+ # The parent div is not decorated with any attributes, apart from the selector class required by
10
+ # component-manager. But if your component has a side loaded CSS module stylesheet
11
+ # (component.module.css), with a `.component` class defined, then that class will be assigned to the
12
+ # parent div as a CSS module.
13
+ #
3
14
  class Proscenium::ViewComponent::ReactComponent < Proscenium::ViewComponent
15
+ self.abstract_class = true
16
+
4
17
  attr_accessor :props, :lazy
5
18
 
6
19
  # @param props: [Hash]
7
20
  # @param lazy: [Boolean] Lazy load the component using IntersectionObserver. Default: true.
21
+ # @param [Block]
8
22
  def initialize(props: {}, lazy: true)
9
23
  @props = props
10
24
  @lazy = lazy