proscenium 0.7.0-aarch64-linux

Sign up to get free protection for your applications and to get access to all the features.
@@ -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'