# frozen_string_literal: truerequire"concurrent-ruby"moduleViewComponentclassCompiler# Compiler development mode. Can be either:# * true (a blocking mode which ensures thread safety when redefining the `call` method for components,# default in Rails development and test mode)# * false(a non-blocking mode, default in Rails production mode)class_attribute:development_mode,default: falsedefinitialize(component)@component=component@lock=Mutex.newenddefcompiled?CompileCache.compiled?(@component)enddefcompile(raise_errors: false,force: false)returnifcompiled?&&!forcereturnif@component==ViewComponent::Base@lock.synchronizedo# this check is duplicated so that concurrent compile calls can still# early exitreturnifcompiled?&&!forcegather_templatesifself.class.development_mode&&@templates.any?(&:requires_compiled_superclass?)@component.superclass.compile(raise_errors: raise_errors)endiftemplate_errors.present?raiseTemplateError.new(template_errors)ifraise_errors# this return is load bearing, and prevents the component from being considered "compiled?"returnfalseendifraise_errors@component.validate_initialization_parameters!@component.validate_collection_parameter!enddefine_render_template_for@component.register_default_slots@component.build_i18n_backendCompileCache.register(@component)endendprivateattr_reader:templatesdefdefine_render_template_for@templates.eachdo|template|template.compile_to_componentendmethod_body=if@templates.one?@templates.first.safe_method_name_callelsif(template=@templates.find(&:inline?))template.safe_method_name_callelsebranches=[]@templates.eachdo|template|conditional=iftemplate.inline_call?"variant&.to_sym == #{template.variant.inspect}"else[template.default_format??"(format == #{ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT.inspect} || format.nil?)":"format == #{template.format.inspect}",template.variant.nil??"variant.nil?":"variant&.to_sym == #{template.variant.inspect}"].join(" && ")endbranches<<[conditional,template.safe_method_name_call]endout=branches.each_with_object(+"")do|(conditional,branch_body),memo|memo<<"#{(!memo.present?)?"if":"elsif"}#{conditional}\n#{branch_body}\n"endout<<"else\n#{templates.find{_1.variant.nil?&&_1.default_format?}.safe_method_name_call}\nend"end@component.silence_redefinition_of_method(:render_template_for)@component.class_eval<<-RUBY,__FILE__,__LINE__+1
def render_template_for(variant = nil, format = nil)
#{method_body}
end
RUBYenddeftemplate_errors@_template_errors||=beginerrors=[]errors<<"Couldn't find a template file or inline render method for #{@component}."if@templates.empty?# We currently allow components to have both an inline call method and a template for a variant, with the# inline call method overriding the template. We should aim to change this in v4 to instead# raise an error.@templates.reject(&:inline_call?).map{|template|[template.variant,template.format]}.tally.select{|_,count|count>1}.eachdo|tally|variant,this_format=tally.firstvariant_string=" for variant `#{variant}`"ifvariant.present?errors<<"More than one #{this_format.upcase} template found#{variant_string} for #{@component}. "enddefault_template_types=@templates.each_with_object(Set.new)do|template,memo|nextiftemplate.variantmemo<<:template_fileif!template.inline_call?memo<<:inline_renderiftemplate.inline_call?&&template.defined_on_self?memoendifdefault_template_types.length>1errors<<"Template file and inline render method found for #{@component}. "\"There can only be a template file or inline render method per component."end# If a template has inline calls, they can conflict with template files the component may use# to render. This attempts to catch and raise that issue before run time. For example,# `def render_mobile` would conflict with a sidecar template of `component.html+mobile.erb`duplicate_template_file_and_inline_call_variants=@templates.reject(&:inline_call?).map(&:variant)&@templates.select{_1.inline_call?&&_1.defined_on_self?}.map(&:variant)unlessduplicate_template_file_and_inline_call_variants.empty?count=duplicate_template_file_and_inline_call_variants.counterrors<<"Template #{"file".pluralize(count)} and inline render #{"method".pluralize(count)} "\"found for #{"variant".pluralize(count)} "\"#{duplicate_template_file_and_inline_call_variants.map{|v|"'#{v}'"}.to_sentence} "\"in #{@component}. There can only be a template file or inline render method per variant."end@templates.select(&:variant).each_with_object(Hash.new{|h,k|h[k]=Set.new})do|template,memo|memo[template.normalized_variant_name]<<template.variantmemoend.eachdo|_,variant_names|nextunlessvariant_names.length>1errors<<"Colliding templates #{variant_names.sort.map{|v|"'#{v}'"}.to_sentence} found in #{@component}."enderrorsendenddefgather_templates@templates||=begintemplates=@component.sidecar_files(ActionView::Template.template_handler_extensions).mapdo|path|# Extract format and variant from template filenamethis_format,variant=File.basename(path)# "variants_component.html+mini.watch.erb".split(".")[1..-2]# ["html+mini", "watch"].join(".")# "html+mini.watch".split("+")# ["html", "mini.watch"].map(&:to_sym)# [:html, :"mini.watch"]out=Template.new(component: @component,type: :file,path: path,lineno: 0,extension: path.split(".").last,this_format: this_format.to_s.split(".").last&.to_sym,# strip locale from this_format, see #2113variant: variant)outendcomponent_instance_methods_on_self=@component.instance_methods(false)(@component.ancestors.take_while{|ancestor|ancestor!=ViewComponent::Base}-@component.included_modules).flat_map{|ancestor|ancestor.instance_methods(false).grep(/^call(_|$)/)}.uniq.eachdo|method_name|templates<<Template.new(component: @component,type: :inline_call,this_format: ViewComponent::Base::VC_INTERNAL_DEFAULT_FORMAT,variant: method_name.to_s.include?("call_")?method_name.to_s.sub("call_","").to_sym:nil,method_name: method_name,defined_on_self: component_instance_methods_on_self.include?(method_name))endif@component.inline_template.present?templates<<Template.new(component: @component,type: :inline,path: @component.inline_template.path,lineno: @component.inline_template.lineno,source: @component.inline_template.source.dup,extension: @component.inline_template.language)endtemplatesendendendend