proscenium 0.6.0-x86_64-darwin → 0.7.0-x86_64-darwin

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 +128 -107
  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 -75
  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/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,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Proscenium::CssModule
4
- class NotFound < StandardError
3
+ module Proscenium::CssModule
4
+ extend ActiveSupport::Autoload
5
+
6
+ class StylesheetNotFound < StandardError
5
7
  def initialize(pathname)
6
8
  @pathname = pathname
7
9
  super
@@ -12,53 +14,30 @@ class Proscenium::CssModule
12
14
  end
13
15
  end
14
16
 
15
- def initialize(path)
16
- @path = path
17
- @css_module_path = "#{path}.module.css"
18
- end
17
+ autoload :ClassNamesResolver
18
+ autoload :Resolver # deprecated
19
19
 
20
- # Parses the given `content` for CSS modules names ('class' attributes beginning with '@'), and
21
- # returns the content with said CSS Modules replaced with the compiled class names.
20
+ # Like `css_modules`, but will raise if the stylesheet cannot be found.
22
21
  #
23
- # Example:
24
- # <div class="@my_css_module_name"></div>
25
- def compile_class_names(content)
26
- doc = Nokogiri::HTML::DocumentFragment.parse(content)
27
-
28
- return content if (modules = doc.css('[class*="@"]')).empty?
29
-
30
- modules.each do |ele|
31
- classes = ele.classes.map { |cls| cls.starts_with?('@') ? class_names!(cls[1..]) : cls }
32
- ele['class'] = classes.join(' ')
33
- end
34
-
35
- doc.to_html.html_safe
36
- end
37
-
38
- # @returns [Array] of class names generated from the given CSS module `names`.
39
- def class_names(*names)
40
- side_load_css_module
41
- names.flatten.compact.map { |name| "#{name.to_s.camelize(:lower)}#{hash}" }
22
+ # @param name [Array, String]
23
+ def css_module!(names)
24
+ cssm.class_names!(names).join ' '
42
25
  end
43
26
 
44
- # Like #class_names, but requires that the stylesheet exists.
27
+ # Accepts one or more CSS class names, and transforms them into CSS module names.
45
28
  #
46
- # @raises Proscenium::CssModule::NotFound if stylesheet does not exists.
47
- def class_names!(...)
48
- raise NotFound, @css_module_path unless Rails.root.join(@css_module_path).exist?
49
-
50
- class_names(...)
29
+ # @param name [Array, String]
30
+ def css_module(names)
31
+ cssm.class_names(names).join ' '
51
32
  end
52
33
 
53
34
  private
54
35
 
55
- def hash
56
- @hash ||= Digest::SHA1.hexdigest("/#{@css_module_path}")[..7]
36
+ def path
37
+ self.class.path
57
38
  end
58
39
 
59
- def side_load_css_module
60
- return unless Rails.application.config.proscenium.side_load
61
-
62
- Proscenium::SideLoad.append "#{@path}.module", :css
40
+ def cssm
41
+ @cssm ||= Resolver.new(path)
63
42
  end
64
43
  end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ffi'
4
+ require 'oj'
5
+
6
+ module Proscenium
7
+ class Esbuild::Golib
8
+ class Result < FFI::Struct
9
+ layout :success, :bool,
10
+ :response, :string
11
+ end
12
+
13
+ module Request
14
+ extend FFI::Library
15
+ ffi_lib Pathname.new(__dir__).join('../../../bin/proscenium').to_s
16
+
17
+ enum :environment, [:development, 1, :test, :production]
18
+
19
+ attach_function :build, [
20
+ :string, # path or entry point
21
+ :string, # root
22
+ :string, # base URL of the Rails app. eg. https://example.com
23
+ :environment, # Rails environment as a Symbol
24
+ :string, # path to import map, relative to root
25
+ :bool # debugging enabled?
26
+ ], Result.by_value
27
+
28
+ attach_function :resolve, [
29
+ :string, # path or entry point
30
+ :string, # root
31
+ :environment, # Rails environment as a Symbol
32
+ :string # path to import map, relative to root
33
+ ], Result.by_value
34
+ end
35
+
36
+ class BuildError < StandardError
37
+ attr_reader :error, :path
38
+
39
+ def initialize(path, error)
40
+ error = Oj.load(error, mode: :strict).deep_transform_keys(&:underscore)
41
+
42
+ super "Failed to build '#{path}' -- #{error['text']}"
43
+ end
44
+ end
45
+
46
+ class ResolveError < StandardError
47
+ attr_reader :error_msg, :path
48
+
49
+ def initialize(path, error_msg)
50
+ super "Failed to resolve '#{path}' -- #{error_msg}"
51
+ end
52
+ end
53
+
54
+ def initialize(root: nil, base_url: nil)
55
+ @root = root || Rails.root
56
+ @base_url = base_url
57
+ end
58
+
59
+ def self.resolve(path)
60
+ new.resolve(path)
61
+ end
62
+
63
+ def self.build(path)
64
+ new.build(path)
65
+ end
66
+
67
+ def build(path)
68
+ result = Request.build(path, @root.to_s, @base_url, Rails.env.to_sym, import_map,
69
+ Rails.env.development?)
70
+ raise BuildError.new(path, result[:response]) unless result[:success]
71
+
72
+ result[:response]
73
+ end
74
+
75
+ def resolve(path)
76
+ result = Request.resolve(path, @root.to_s, Rails.env.to_sym, import_map)
77
+ raise ResolveError.new(path, result[:response]) unless result[:success]
78
+
79
+ result[:response]
80
+ end
81
+
82
+ private
83
+
84
+ def import_map
85
+ return unless (path = Rails.root&.join('config'))
86
+
87
+ if (json = path.join('import_map.json')).exist?
88
+ return json.relative_path_from(@root).to_s
89
+ end
90
+ if (js = path.join('import_map.js')).exist?
91
+ return js.relative_path_from(@root).to_s
92
+ end
93
+
94
+ nil
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proscenium
4
+ class Esbuild
5
+ class CompileError < StandardError; end
6
+
7
+ extend ActiveSupport::Autoload
8
+
9
+ autoload :Golib
10
+
11
+ def self.build(...)
12
+ new(...).build
13
+ end
14
+
15
+ def initialize(path, root:, base_url:)
16
+ @path = path
17
+ @root = root
18
+ @base_url = base_url
19
+ end
20
+
21
+ def build
22
+ Proscenium::Esbuild::Golib.new(root: @root, base_url: @base_url).build(@path)
23
+ end
24
+
25
+ private
26
+
27
+ def cache_query_string
28
+ q = Proscenium.config.cache_query_string
29
+ q ? "--cache-query-string #{q}" : nil
30
+ end
31
+ end
32
+ end
@@ -15,28 +15,5 @@ module Proscenium
15
15
 
16
16
  super
17
17
  end
18
-
19
- def side_load_stylesheets
20
- return unless Proscenium::Current.loaded
21
-
22
- Proscenium::Current.loaded[:css].map do |sheet|
23
- stylesheet_link_tag(sheet, id: "_#{Digest::SHA1.hexdigest("/#{sheet}")[..7]}")
24
- end.join("\n").html_safe
25
- end
26
-
27
- def side_load_javascripts(**options)
28
- return unless Proscenium::Current.loaded
29
-
30
- javascript_include_tag(*Proscenium::Current.loaded[:js], options)
31
- end
32
-
33
- def proscenium_dev
34
- return unless Proscenium.config.auto_reload
35
-
36
- javascript_tag %(
37
- import autoReload from '/proscenium-runtime/auto_reload.js';
38
- autoReload('#{Proscenium::Railtie.websocket_mount_path}');
39
- ), type: 'module', defer: true
40
- end
41
18
  end
42
19
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/log_subscriber'
4
+
5
+ module Proscenium
6
+ class LogSubscriber < ActiveSupport::LogSubscriber
7
+ def sideload(event)
8
+ info do
9
+ " [Proscenium] Side loaded #{event.payload[:identifier]}"
10
+ end
11
+ end
12
+
13
+ def build(event)
14
+ path = event.payload[:identifier]
15
+ path = path.start_with?(/https?%3A%2F%2F/) ? CGI.unescape(path) : path
16
+
17
+ info do
18
+ message = +"[Proscenium] Building #{path}"
19
+ message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
20
+ message << "\n" if defined?(Rails.env) && Rails.env.development?
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ Proscenium::LogSubscriber.attach_to :proscenium
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'open3'
4
+ require 'oj'
4
5
 
5
6
  module Proscenium
6
7
  class Middleware
@@ -9,7 +10,15 @@ module Proscenium
9
10
 
10
11
  # Error when the result of the build returns an error. For example, when esbuild returns
11
12
  # errors.
12
- class CompileError < StandardError; end
13
+ class CompileError < StandardError
14
+ attr_reader :detail, :file
15
+
16
+ def initialize(args)
17
+ @detail = args[:detail]
18
+ @file = args[:file]
19
+ super "Failed to build '#{args[:file]}' -- #{detail}"
20
+ end
21
+ end
13
22
 
14
23
  def self.attempt(request)
15
24
  new(request).renderable!&.attempt
@@ -25,12 +34,25 @@ module Proscenium
25
34
 
26
35
  private
27
36
 
37
+ def real_path
38
+ @request.path
39
+ end
40
+
41
+ # @return [String] the path to the file without the leading slash which will be built.
42
+ def path_to_build
43
+ @request.path[1..]
44
+ end
45
+
46
+ def sourcemap?
47
+ @request.path.ends_with?('.map')
48
+ end
49
+
28
50
  def renderable?
29
51
  file_readable?
30
52
  end
31
53
 
32
- def file_readable?(file = @request.path_info)
33
- return unless (path = clean_path(file))
54
+ def file_readable?
55
+ return unless (path = clean_path(sourcemap? ? real_path[0...-4] : real_path))
34
56
 
35
57
  file_stat = File.stat(Pathname(root).join(path.delete_prefix('/').b).to_s)
36
58
  rescue SystemCallError
@@ -50,7 +72,7 @@ module Proscenium
50
72
 
51
73
  def content_type
52
74
  @content_type ||
53
- ::Rack::Mime.mime_type(::File.extname(@request.path_info), nil) ||
75
+ ::Rack::Mime.mime_type(::File.extname(path_to_build), nil) ||
54
76
  'application/javascript'
55
77
  end
56
78
 
@@ -59,9 +81,10 @@ module Proscenium
59
81
  response.write content
60
82
  response.content_type = content_type
61
83
  response['X-Proscenium-Middleware'] = name
84
+ response.set_header 'SourceMap', "#{@request.path_info}.map"
62
85
 
63
86
  if Proscenium.config.cache_query_string && Proscenium.config.cache_max_age
64
- response['Cache-Control'] = "public, max-age=#{Proscenium.config.cache_max_age}"
87
+ response.cache! Proscenium.config.cache_max_age
65
88
  end
66
89
 
67
90
  yield response if block_given?
@@ -69,37 +92,6 @@ module Proscenium
69
92
  response.finish
70
93
  end
71
94
 
72
- def build(cmd)
73
- stdout, stderr, status = Open3.capture3(cmd)
74
-
75
- unless status.success?
76
- raise self.class::CompileError, stderr if status.exitstatus == 2
77
-
78
- raise BuildError, stderr
79
- end
80
-
81
- unless stderr.empty?
82
- raise BuildError, "Proscenium build of #{name}:'#{@request.fullpath}' failed -- #{stderr}"
83
- end
84
-
85
- stdout
86
- end
87
-
88
- def benchmark(type)
89
- super logging_message(type)
90
- end
91
-
92
- # rubocop:disable Style/FormatStringToken
93
- def logging_message(type)
94
- format '[Proscenium] Request (%s) %s for %s at %s',
95
- type, @request.fullpath, @request.ip, Time.now.to_default_s
96
- end
97
- # rubocop:enable Style/FormatStringToken
98
-
99
- def logger
100
- Rails.logger
101
- end
102
-
103
95
  def name
104
96
  @name ||= self.class.name.split('::').last.downcase
105
97
  end
@@ -3,55 +3,29 @@
3
3
  module Proscenium
4
4
  class Middleware
5
5
  class Esbuild < Base
6
- class CompileError < StandardError
7
- attr_reader :detail
8
-
9
- def initialize(detail)
10
- @detail = ActiveSupport::HashWithIndifferentAccess.new(Oj.load(detail, mode: :strict))
11
-
12
- super "#{@detail[:text]} in #{@detail[:location][:file]}:#{@detail[:location][:line]}"
6
+ class CompileError < Base::CompileError
7
+ def initialize(args)
8
+ detail = args[:detail]
9
+ detail = ActiveSupport::HashWithIndifferentAccess.new(Oj.load(detail, mode: :strict))
10
+
11
+ args[:detail] = if detail[:location]
12
+ "#{detail[:text]} in #{detail[:location][:file]}:" +
13
+ detail[:location][:line].to_s
14
+ else
15
+ detail[:text]
16
+ end
17
+
18
+ super args
13
19
  end
14
20
  end
15
21
 
16
22
  def attempt
17
- benchmark :esbuild do
18
- render_response build([
19
- "#{cli} --root #{root}",
20
- cache_query_string,
21
- "--lightningcss-bin #{lightningcss_cli} #{path}"
22
- ].compact.join(' '))
23
- end
24
- rescue CompileError => e
25
- render_response "export default #{e.detail.to_json}" do |response|
26
- response['X-Proscenium-Middleware'] = 'Esbuild::CompileError'
27
- end
28
- end
29
-
30
- private
31
-
32
- def path
33
- @request.path[1..]
34
- end
35
-
36
- def cli
37
- if ENV['PROSCENIUM_TEST']
38
- 'deno run -q --import-map import_map.json -A lib/proscenium/compilers/esbuild.js'
39
- else
40
- Gem.bin_path 'proscenium', 'esbuild'
23
+ ActiveSupport::Notifications.instrument('build.proscenium', identifier: path_to_build) do
24
+ render_response Proscenium::Esbuild.build(path_to_build, root: root,
25
+ base_url: @request.base_url)
41
26
  end
42
- end
43
-
44
- def lightningcss_cli
45
- if ENV['PROSCENIUM_TEST']
46
- 'bin/lightningcss'
47
- else
48
- Gem.bin_path 'proscenium', 'lightningcss'
49
- end
50
- end
51
-
52
- def cache_query_string
53
- q = Proscenium.config.cache_query_string
54
- q ? "--cache-query-string #{q}" : nil
27
+ rescue Proscenium::Esbuild::CompileError => e
28
+ raise self.class::CompileError, { file: @request.fullpath, detail: e.message }, caller
55
29
  end
56
30
  end
57
31
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Proscenium
4
4
  class Middleware
5
- # Handles requests prefixed with "url:https://"; downloading, caching, and compiling them.
5
+ # Handles requests for URL encoded URL's.
6
6
  class Url < Esbuild
7
7
  private
8
8
 
@@ -11,11 +11,6 @@ module Proscenium
11
11
  def renderable?
12
12
  true
13
13
  end
14
-
15
- # @override [Esbuild]
16
- def path
17
- CGI.unescape(@request.path)[1..]
18
- end
19
14
  end
20
15
  end
21
16
  end
@@ -9,9 +9,7 @@ module Proscenium
9
9
 
10
10
  autoload :Base
11
11
  autoload :Esbuild
12
- autoload :Runtime
13
12
  autoload :Url
14
- autoload :OutsideRoot
15
13
 
16
14
  def initialize(app)
17
15
  @app = app
@@ -27,37 +25,35 @@ module Proscenium
27
25
 
28
26
  private
29
27
 
30
- # Look for the precompiled file in public/assets first, then fallback to the Proscenium
31
- # middleware that matches the type of file requested, ie: .js => esbuild.
32
- # See Rails.application.config.proscenium.glob_types.
33
28
  def attempt(request)
34
29
  return unless (type = find_type(request))
35
30
 
36
- file_handler.attempt(request.env) || type.attempt(request)
31
+ # file_handler.attempt(request.env) || type.attempt(request)
32
+
33
+ type.attempt(request)
37
34
  end
38
35
 
39
- # Returns the type of file being requested using Rails.application.config.proscenium.glob_types.
36
+ # Returns the type of file being requested using Proscenium::MIDDLEWARE_GLOB_TYPES.
40
37
  def find_type(request)
41
38
  path = Pathname.new(request.path)
42
39
 
43
- # Non-production only!
44
- if request.query_string == 'outsideRoot'
45
- return if Rails.env.production?
46
- return OutsideRoot if path.fnmatch?(glob_types[:outsideRoot], File::FNM_EXTGLOB)
47
- end
48
-
49
40
  return Url if request.path.match?(glob_types[:url])
50
- return Runtime if path.fnmatch?(glob_types[:runtime], File::FNM_EXTGLOB)
51
- return Esbuild if path.fnmatch?(glob_types[:esbuild], File::FNM_EXTGLOB)
41
+ return Esbuild if path.fnmatch?(application_glob_type, File::FNM_EXTGLOB)
52
42
  end
53
43
 
44
+ # TODO: handle precompiled assets
54
45
  def file_handler
55
46
  ::ActionDispatch::FileHandler.new Rails.public_path.join('assets').to_s,
56
47
  headers: { 'X-Proscenium-Middleware' => 'precompiled' }
57
48
  end
58
49
 
59
50
  def glob_types
60
- Rails.application.config.proscenium.glob_types
51
+ @glob_types ||= Proscenium::MIDDLEWARE_GLOB_TYPES
52
+ end
53
+
54
+ def application_glob_type
55
+ paths = Rails.application.config.proscenium.include_paths.join(',')
56
+ "/{#{paths}}#{glob_types[:application]}"
61
57
  end
62
58
  end
63
59
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proscenium::Phlex::ComponentConcerns
4
+ module CssModules
5
+ extend ActiveSupport::Concern
6
+ include Proscenium::CssModule
7
+ include Proscenium::Phlex::ResolveCssModules
8
+
9
+ # class_methods do
10
+ # # FIXME: Still needed?
11
+ # def path
12
+ # pp name, super
13
+ # pp Module.const_source_location(name).first
14
+
15
+ # name && Pathname.new(Module.const_source_location(name).first)
16
+ # rescue NameError
17
+ # nil
18
+ # end
19
+ # end
20
+
21
+ private
22
+
23
+ def path
24
+ self.class.path
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'phlex/rails'
4
+
5
+ # Include this in your view for additional logic for rendering a full HTML page, usually from a
6
+ # controller.
7
+ module Proscenium::Phlex::Page
8
+ include Phlex::Rails::Helpers::CSPMetaTag
9
+ include Phlex::Rails::Helpers::CSRFMetaTags
10
+ include Phlex::Rails::Helpers::FaviconLinkTag
11
+ include Phlex::Rails::Helpers::PreloadLinkTag
12
+ include Phlex::Rails::Helpers::StyleSheetLinkTag
13
+ include Phlex::Rails::Helpers::ActionCableMetaTag
14
+ include Phlex::Rails::Helpers::AutoDiscoveryLinkTag
15
+ include Phlex::Rails::Helpers::JavaScriptIncludeTag
16
+ include Phlex::Rails::Helpers::JavaScriptImportMapTags
17
+ include Phlex::Rails::Helpers::JavaScriptImportModuleTag
18
+
19
+ def self.included(klass)
20
+ klass.extend(Phlex::Rails::Layout::Interface)
21
+ end
22
+
23
+ def template(&block)
24
+ doctype
25
+ html do
26
+ head
27
+ body(&block)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def after_template
34
+ super
35
+ @_buffer.gsub!('<!-- [SIDE_LOAD_STYLESHEETS] -->', capture { side_load_stylesheets })
36
+ end
37
+
38
+ def page_title
39
+ Rails.application.class.name.deconstantize
40
+ end
41
+
42
+ def head
43
+ super do
44
+ title { page_title }
45
+
46
+ yield if block_given?
47
+
48
+ csp_meta_tag
49
+ csrf_meta_tags
50
+
51
+ comment { '[SIDE_LOAD_STYLESHEETS]' }
52
+ end
53
+ end
54
+
55
+ def body
56
+ super do
57
+ yield if block_given?
58
+
59
+ side_load_javascripts defer: true, type: :module
60
+ end
61
+ end
62
+ end
@@ -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