proscenium 0.6.0-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 (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