proscenium 0.5.1-x86_64-darwin → 0.7.0-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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +128 -92
  3. data/bin/proscenium +0 -0
  4. data/bin/proscenium.h +109 -0
  5. data/config/routes.rb +0 -3
  6. data/lib/proscenium/css_module/class_names_resolver.rb +66 -0
  7. data/lib/proscenium/css_module/resolver.rb +76 -0
  8. data/lib/proscenium/css_module.rb +18 -39
  9. data/lib/proscenium/esbuild/golib.rb +97 -0
  10. data/lib/proscenium/esbuild.rb +32 -0
  11. data/lib/proscenium/helper.rb +0 -23
  12. data/lib/proscenium/log_subscriber.rb +26 -0
  13. data/lib/proscenium/middleware/base.rb +28 -36
  14. data/lib/proscenium/middleware/esbuild.rb +18 -44
  15. data/lib/proscenium/middleware/url.rb +1 -6
  16. data/lib/proscenium/middleware.rb +12 -16
  17. data/lib/proscenium/phlex/component_concerns.rb +27 -0
  18. data/lib/proscenium/phlex/page.rb +62 -0
  19. data/lib/proscenium/phlex/react_component.rb +52 -8
  20. data/lib/proscenium/phlex/resolve_css_modules.rb +67 -0
  21. data/lib/proscenium/phlex.rb +34 -33
  22. data/lib/proscenium/railtie.rb +41 -67
  23. data/lib/proscenium/side_load/ensure_loaded.rb +25 -0
  24. data/lib/proscenium/side_load/helper.rb +25 -0
  25. data/lib/proscenium/side_load/monkey.rb +48 -0
  26. data/lib/proscenium/side_load.rb +58 -52
  27. data/lib/proscenium/version.rb +1 -1
  28. data/lib/proscenium/view_component/react_component.rb +14 -0
  29. data/lib/proscenium/view_component.rb +28 -18
  30. data/lib/proscenium.rb +79 -2
  31. metadata +35 -72
  32. data/app/channels/proscenium/connection.rb +0 -13
  33. data/app/channels/proscenium/reload_channel.rb +0 -9
  34. data/bin/esbuild +0 -0
  35. data/bin/lightningcss +0 -0
  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.js +0 -59
  44. data/lib/proscenium/compilers/esbuild/resolve_plugin.js +0 -205
  45. data/lib/proscenium/compilers/esbuild/setup_plugin.js +0 -45
  46. data/lib/proscenium/compilers/esbuild/solidjs_plugin.js +0 -24
  47. data/lib/proscenium/compilers/esbuild.bench.js +0 -14
  48. data/lib/proscenium/compilers/esbuild.js +0 -179
  49. data/lib/proscenium/link_to_helper.rb +0 -40
  50. data/lib/proscenium/middleware/lightningcss.rb +0 -64
  51. data/lib/proscenium/middleware/outside_root.rb +0 -26
  52. data/lib/proscenium/middleware/runtime.rb +0 -22
  53. data/lib/proscenium/middleware/static.rb +0 -14
  54. data/lib/proscenium/phlex/component.rb +0 -9
  55. data/lib/proscenium/precompile.rb +0 -31
  56. data/lib/proscenium/runtime/auto_reload.js +0 -40
  57. data/lib/proscenium/runtime/react_shim/index.js +0 -1
  58. data/lib/proscenium/runtime/react_shim/package.json +0 -5
  59. data/lib/proscenium/utils.js +0 -8
  60. data/lib/tasks/assets.rake +0 -19
@@ -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.5.1'
4
+ VERSION = '0.7.0'
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
@@ -4,37 +4,47 @@ require 'view_component'
4
4
 
5
5
  class Proscenium::ViewComponent < ViewComponent::Base
6
6
  extend ActiveSupport::Autoload
7
+ include Proscenium::CssModule
7
8
 
8
9
  autoload :TagBuilder
9
10
  autoload :ReactComponent
10
11
 
11
- def render_in(...)
12
- cssm.compile_class_names(super(...))
12
+ # Side loads the class, and its super classes that respond to `.path`. Assign the `abstract_class`
13
+ # class variable to any abstract class, and it will not be side loaded.
14
+ module Sideload
15
+ def before_render
16
+ klass = self.class
17
+ while !klass.abstract_class && klass.respond_to?(:path) && klass.path
18
+ Proscenium::SideLoad.append klass.path
19
+ klass = klass.superclass
20
+ end
21
+
22
+ super
23
+ end
13
24
  end
14
25
 
15
- def before_render
16
- side_load_assets unless self.class < ReactComponent
17
- end
26
+ class << self
27
+ attr_accessor :path, :abstract_class
18
28
 
19
- def css_module(name)
20
- cssm.class_names!(name).join ' '
21
- end
29
+ def inherited(child)
30
+ child.path = if caller_locations(1, 1).first.label == 'inherited'
31
+ Pathname.new caller_locations(2, 1).first.path
32
+ else
33
+ Pathname.new caller_locations(1, 1).first.path
34
+ end
22
35
 
23
- private
36
+ child.prepend Sideload if Rails.application.config.proscenium.side_load
24
37
 
25
- # Side load any CSS/JS assets for the component. This will side load any `index.{css|js}` in
26
- # the component directory.
27
- def side_load_assets
28
- Proscenium::SideLoad.append asset_path if Rails.application.config.proscenium.side_load
38
+ super
39
+ end
29
40
  end
30
41
 
31
- def asset_path
32
- @asset_path ||= "app/components#{virtual_path}"
42
+ # @override Auto compilation of class names to css modules.
43
+ def render_in(...)
44
+ cssm.compile_class_names(super(...))
33
45
  end
34
46
 
35
- def cssm
36
- @cssm ||= Proscenium::CssModule.new(asset_path)
37
- end
47
+ private
38
48
 
39
49
  # Overrides ActionView::Helpers::TagHelper::TagBuilder, allowing us to intercept the
40
50
  # `css_module` option from the HTML options argument of the `tag` and `content_tag` helpers, and