lib/roda/plugins/precompile_templates.rb



# frozen-string-literal: true

#
class Roda
  module RodaPlugins
    # The precompile_templates plugin adds support for precompiling template code.
    # This can result in a large memory savings for applications that have large
    # templates or a large number of small templates if the application uses a
    # forking webserver.  By default, template compilation is lazy, so all the
    # child processes in a forking webserver will have their own copy of the
    # compiled template.  By using the precompile_templates plugin, you can
    # precompile the templates in the parent process before forking, and then
    # all of the child processes can use the same precompiled templates, which
    # saves memory.
    #
    # Another advantage of the precompile_templates plugin is that after
    # template precompilation, access to the template file in the file system is
    # no longer needed, so this can be used with security features that do not
    # allow access to the template files at runtime.
    #
    # After loading the plugin, you should call precompile_views with an array
    # of views to compile, using the same argument you are passing to view or
    # render:
    #
    #   plugin :precompile_templates
    #   precompile_views %w'view1 view2'
    #   
    # If the view requires local variables, you should call precompile_views with a second
    # argument for the local variables:
    #
    #   plugin :precompile_templates
    #   precompile_views :view3, [:local_var1, :local_var2]
    #
    # After all templates are precompiled, you can optionally use freeze_template_caches!,
    # which will freeze the template caches so that any template compilation at runtime
    # will result in an error.  This also speeds up template cache access, since the
    # template caches no longer need a mutex.
    #
    #   freeze_template_caches!
    #
    # Note that you should use Tilt 2.0.1+ if you are using this plugin, so
    # that locals are handled in the same order.
    module PrecompileTemplates
      # Load the render plugin as precompile_templates depends on it.
      def self.load_dependencies(app, opts=OPTS)
        app.plugin :render
      end

      module ClassMethods
        # Freeze the template caches.  Should be called after precompiling all templates during
        # application startup, if you don't want to allow templates to be cached at runtime.
        # In addition to ensuring that no templates are compiled at runtime, this also speeds
        # up rendering by freezing the template caches, so that a mutex is not needed to access
        # them.
        def freeze_template_caches!
          _freeze_layout_method

          opts[:render] = render_opts.merge(
            :cache=>render_opts[:cache].freeze,
            :template_method_cache=>render_opts[:template_method_cache].freeze,
          ).freeze
          self::RodaCompiledTemplates.freeze

          nil
        end

        # Precompile the templates using the given options.  Note that this doesn't
        # handle optimized template methods supported in newer versions of Roda, but
        # there are still cases where makes sense to use it.
        #
        # You can call +precompile_templates+ with the pattern of templates you would
        # like to precompile:
        #
        #   precompile_templates "views/**/*.erb"
        #
        # That will precompile all erb template files in the views directory or
        # any subdirectory.
        #
        # If the templates use local variables, you need to specify which local
        # variables to precompile, which should be an array of symbols:
        #
        #   precompile_templates 'views/users/_*.erb', locals: [:user]
        #
        # You can specify other render options when calling +precompile_templates+,
        # including +:cache_key+, +:template_class+, and +:template_opts+.  If you
        # are passing any of those options to render/view for the template, you
        # should pass the same options when precompiling the template.
        #
        # To compile inline templates, just pass a single hash containing an :inline
        # to +precompile_templates+:
        #
        #   precompile_templates inline: some_template_string
        def precompile_templates(pattern, opts=OPTS)
          if pattern.is_a?(Hash)
            opts = pattern.merge(opts)
          end

          if locals = opts[:locals]
            locals.sort!
          else
            locals = EMPTY_ARRAY
          end

          compile_opts = if pattern.is_a?(Hash)
            [opts]
          else
            Dir[pattern].map{|file| opts.merge(:path=>File.expand_path(file, nil))}
          end

          instance = allocate
          compile_opts.each do |compile_opt|
            template = instance.send(:retrieve_template, compile_opt)
            begin
              Render.tilt_template_compiled_method(template, locals, self)
            rescue NotImplementedError
              # When freezing template caches, you may want to precompile a template for a
              # template type that doesn't support template precompilation, just to populate
              # the cache.  Tilt rescues NotImplementedError in this case, which we can ignore.
              nil
            end
          end

          nil
        end

        # Precompile the given views with the given locals, handling optimized template methods.
        def precompile_views(views, locals=EMPTY_ARRAY)
          instance = allocate
          views = Array(views)

          if locals.empty?
            opts = OPTS
          else
            locals_hash = {}
            locals.each{|k| locals_hash[k] = nil}
            opts = {:locals=>locals_hash}
          end

          views.each do |view|
            instance.send(:retrieve_template, instance.send(:render_template_opts, view, opts))
          end

          if locals_hash
            views.each do |view|
              instance.send(:_optimized_render_method_for_locals, view, locals_hash)
            end
          end

          nil
        end
      end
    end

    register_plugin(:precompile_templates, PrecompileTemplates)
  end
end