proscenium 0.7.0-aarch64-linux

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.
@@ -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
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'phlex-rails'
4
+
5
+ module Proscenium
6
+ class Phlex < ::Phlex::HTML
7
+ extend ActiveSupport::Autoload
8
+ include Proscenium::CssModule
9
+
10
+ autoload :Page
11
+ autoload :ReactComponent
12
+ autoload :ResolveCssModules
13
+ autoload :ComponentConcerns
14
+
15
+ extend ::Phlex::Rails::HelperMacros
16
+ include ::Phlex::Rails::Helpers::JavaScriptIncludeTag
17
+ include ::Phlex::Rails::Helpers::StyleSheetLinkTag
18
+
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
28
+
29
+ if !klass.abstract_class && respond_to?(:side_load, true)
30
+ side_load
31
+ klass = klass.superclass
32
+ end
33
+
34
+ while !klass.abstract_class && klass.respond_to?(:path) && klass.path
35
+ Proscenium::SideLoad.append klass.path
36
+ klass = klass.superclass
37
+ end
38
+
39
+ super
40
+ end
41
+ end
42
+
43
+ class << self
44
+ attr_accessor :path, :abstract_class
45
+
46
+ def inherited(child)
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
54
+
55
+ child.prepend Sideload if Rails.application.config.proscenium.side_load
56
+
57
+ super
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails'
4
+ require 'proscenium/log_subscriber'
5
+
6
+ ENV['RAILS_ENV'] = Rails.env
7
+
8
+ module Proscenium
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}
14
+ }.freeze
15
+
16
+ APPLICATION_INCLUDE_PATHS = ['config', 'app/views', 'lib', 'node_modules'].freeze
17
+
18
+ class << self
19
+ def config
20
+ @config ||= Railtie.config.proscenium
21
+ end
22
+ end
23
+
24
+ class Railtie < ::Rails::Engine
25
+ isolate_namespace Proscenium
26
+
27
+ config.proscenium = ActiveSupport::OrderedOptions.new
28
+ config.proscenium.side_load = true
29
+ config.proscenium.cache_query_string = Rails.env.production? && ENV.fetch('REVISION', nil)
30
+ config.proscenium.cache_max_age = 2_592_000 # 30 days
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 = {}
50
+
51
+ initializer 'proscenium.configuration' do |app|
52
+ options = app.config.proscenium
53
+ options.include_paths = Set.new(APPLICATION_INCLUDE_PATHS) if options.include_paths.blank?
54
+ end
55
+
56
+ initializer 'proscenium.middleware' do |app|
57
+ app.middleware.insert_after ActionDispatch::Static, Proscenium::Middleware
58
+ app.middleware.insert_after ActionDispatch::Static, Rack::ETag, 'no-cache'
59
+ app.middleware.insert_after ActionDispatch::Static, Rack::ConditionalGet
60
+ end
61
+
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
68
+
69
+ ActionView::TemplateRenderer.prepend SideLoad::Monkey::TemplateRenderer
70
+ ActionView::PartialRenderer.prepend SideLoad::Monkey::PartialRenderer
71
+ end
72
+
73
+ ActiveSupport.on_load(:action_controller) do
74
+ ActionController::Base.include Proscenium::SideLoad::EnsureLoaded
75
+ end
76
+ end
77
+ end
78
+
79
+ initializer 'proscenium.helper' do
80
+ ActiveSupport.on_load(:action_view) do
81
+ ActionView::Base.include Proscenium::Helper
82
+ end
83
+ end
84
+ end
85
+ 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
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proscenium
4
+ class SideLoad
5
+ extend ActiveSupport::Autoload
6
+
7
+ NotIncludedError = Class.new(StandardError)
8
+
9
+ autoload :Monkey
10
+ autoload :Helper
11
+ autoload :EnsureLoaded
12
+
13
+ EXTENSIONS = %i[js css].freeze
14
+ EXTENSION_MAP = { '.css' => :css, '.js' => :js }.freeze
15
+
16
+ attr_reader :path
17
+
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
27
+
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)
35
+
36
+ Proscenium::Current.loaded[type] << log(path)
37
+ end
38
+
39
+ def log(value)
40
+ ActiveSupport::Notifications.instrument('sideload.proscenium', identifier: value)
41
+
42
+ value
43
+ end
44
+ end
45
+
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)
62
+ end
63
+
64
+ resolved_path
65
+ end
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
77
+ end
78
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proscenium
4
+ VERSION = '0.7.0'
5
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
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
+ #
14
+ class Proscenium::ViewComponent::ReactComponent < Proscenium::ViewComponent
15
+ self.abstract_class = true
16
+
17
+ attr_accessor :props, :lazy
18
+
19
+ # @param props: [Hash]
20
+ # @param lazy: [Boolean] Lazy load the component using IntersectionObserver. Default: true.
21
+ # @param [Block]
22
+ def initialize(props: {}, lazy: true)
23
+ @props = props
24
+ @lazy = lazy
25
+
26
+ super
27
+ end
28
+
29
+ def call
30
+ tag.div class: ['componentManagedByProscenium', css_module(:component)],
31
+ data: { component: { path: virtual_path, props: props, lazy: lazy } } do
32
+ tag.div content || 'loading...'
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Proscenium::ViewComponent::TagBuilder < ActionView::Helpers::TagHelper::TagBuilder
4
+ def tag_options(options, escape = true) # rubocop:disable Style/OptionalBooleanParameter
5
+ super(css_module_option(options), escape)
6
+ end
7
+
8
+ private
9
+
10
+ def css_module_option(options)
11
+ return options if options.blank?
12
+
13
+ unless (css_module = options.delete(:css_module) || options.delete('css_module'))
14
+ return options
15
+ end
16
+
17
+ css_module = @view_context.css_module(css_module)
18
+
19
+ options.tap do |x|
20
+ x[:class] = "#{css_module} #{options.delete(:class) || options.delete('class')}".strip
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'view_component'
4
+
5
+ class Proscenium::ViewComponent < ViewComponent::Base
6
+ extend ActiveSupport::Autoload
7
+ include Proscenium::CssModule
8
+
9
+ autoload :TagBuilder
10
+ autoload :ReactComponent
11
+
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
24
+ end
25
+
26
+ class << self
27
+ attr_accessor :path, :abstract_class
28
+
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
35
+
36
+ child.prepend Sideload if Rails.application.config.proscenium.side_load
37
+
38
+ super
39
+ end
40
+ end
41
+
42
+ # @override Auto compilation of class names to css modules.
43
+ def render_in(...)
44
+ cssm.compile_class_names(super(...))
45
+ end
46
+
47
+ private
48
+
49
+ # Overrides ActionView::Helpers::TagHelper::TagBuilder, allowing us to intercept the
50
+ # `css_module` option from the HTML options argument of the `tag` and `content_tag` helpers, and
51
+ # prepend it to the HTML `class` attribute.
52
+ def tag_builder
53
+ @tag_builder ||= Proscenium::ViewComponent::TagBuilder.new(self)
54
+ end
55
+ end
data/lib/proscenium.rb ADDED
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/dependencies/autoload'
4
+
5
+ module Proscenium
6
+ extend ActiveSupport::Autoload
7
+
8
+ autoload :Current
9
+ autoload :Middleware
10
+ autoload :SideLoad
11
+ autoload :CssModule
12
+ autoload :ViewComponent
13
+ autoload :Phlex
14
+ autoload :Helper
15
+ autoload :Esbuild
16
+
17
+ def self.reset_current_side_loaded
18
+ Current.reset
19
+ Current.loaded = SideLoad::EXTENSIONS.to_h { |e| [e, Set.new] }
20
+ end
21
+
22
+ class PathResolutionFailed < StandardError
23
+ def initialize(path)
24
+ @path = path
25
+ super
26
+ end
27
+
28
+ def message
29
+ "Path #{@path.inspect} cannot be resolved"
30
+ end
31
+ end
32
+
33
+ module Utils
34
+ module_function
35
+
36
+ # @param value [#to_s] The value to create the digest from. This will usually be a `Pathname`.
37
+ # @return [String] string digest of the given value.
38
+ def digest(value)
39
+ Digest::SHA1.hexdigest(value.to_s)[..7]
40
+ end
41
+
42
+ # Resolve the given `path` to a URL path.
43
+ #
44
+ # @param path [String] Can be URL path, file system path, or bare specifier (ie. NPM package).
45
+ # @return [String] URL path.
46
+ def resolve_path(path) # rubocop:disable Metrics/AbcSize
47
+ raise ArgumentError, 'path must be a string' unless path.is_a?(String)
48
+
49
+ if path.starts_with?('./', '../')
50
+ raise ArgumentError, 'path must be an absolute file system or URL path'
51
+ end
52
+
53
+ matched_gem = Proscenium.config.side_load_gems.find do |_, opts|
54
+ path.starts_with?("#{opts[:root]}/")
55
+ end
56
+
57
+ if matched_gem
58
+ sroot = "#{matched_gem[1][:root]}/"
59
+ relpath = path.delete_prefix(sroot)
60
+
61
+ if matched_gem[1][:package_name]
62
+ return Esbuild::Golib.resolve("#{matched_gem[1][:package_name]}/#{relpath}")
63
+ end
64
+
65
+ # TODO: manually resolve the path without esbuild
66
+ raise PathResolutionFailed, path
67
+ end
68
+
69
+ return path.delete_prefix(Rails.root.to_s) if path.starts_with?("#{Rails.root}/")
70
+
71
+ Esbuild::Golib.resolve(path)
72
+ end
73
+
74
+ # Resolves CSS class `names` to CSS module names. Each name will be converted to a CSS module
75
+ # name, consisting of the camelCased name (lower case first character), and suffixed with the
76
+ # given `digest`.
77
+ #
78
+ # @param names [String, Array]
79
+ # @param digest: [String]
80
+ # @returns [Array] of class names generated from the given CSS module `names` and `digest`.
81
+ def css_modularise_class_names(*names, digest: nil)
82
+ names.flatten.compact.map { |name| css_modularise_class_name name, digest: digest }
83
+ end
84
+
85
+ def css_modularise_class_name(name, digest: nil)
86
+ sname = name.to_s
87
+ if sname.starts_with?('_')
88
+ "_#{sname[1..].camelize(:lower)}#{digest}"
89
+ else
90
+ "#{sname.camelize(:lower)}#{digest}"
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ require 'proscenium/railtie'