reactjs/react-rails

View on GitHub
lib/react/server_rendering/bundle_renderer.rb

Summary

Maintainability
A
55 mins
Test Coverage
# frozen_string_literal: true

require "react/server_rendering/environment_container"
require "react/server_rendering/manifest_container"
require "react/server_rendering/yaml_manifest_container"
require "react/server_rendering/separate_server_bundle_container"

module React
  module ServerRendering
    # Extends ExecJSRenderer for the Rails environment
    # - fetches JS code from the Rails app (Shakapacker or Sprockets)
    # - stringifies props
    # - implements console replay
    class BundleRenderer < ExecJSRenderer
      # Reimplement console methods for replaying on the client
      CONSOLE_POLYFILL = File.read(File.join(File.dirname(__FILE__), "bundle_renderer/console_polyfill.js"))
      CONSOLE_REPLAY   = File.read(File.join(File.dirname(__FILE__), "bundle_renderer/console_replay.js"))
      CONSOLE_RESET    = File.read(File.join(File.dirname(__FILE__), "bundle_renderer/console_reset.js"))
      TIMEOUT_POLYFILL = File.read(File.join(File.dirname(__FILE__), "bundle_renderer/timeout_polyfill.js"))

      def initialize(options = {})
        @replay_console = options.fetch(:replay_console, true)
        filenames = options.fetch(:files, ["server_rendering.js"])
        js_code = CONSOLE_POLYFILL.dup
        js_code << TIMEOUT_POLYFILL.dup
        js_code << options.fetch(:code, "")

        filenames.each do |filename|
          js_code << asset_container.find_asset(filename)
        end

        super(options.merge(code: js_code))
      end

      # Prerender options are expected to be a Hash however might also be a symbol.
      # pass prerender: :static to use renderToStaticMarkup
      # pass prerender: true to enable default prerender
      # pass prerender: {} to proxy some custom options
      def render(component_name, props, prerender_options)
        t_options = prepare_options(prerender_options)
        t_props = prepare_props(props)
        super(component_name, t_props, t_options)
      end

      def before_render(_component_name, _props, _prerender_options)
        @replay_console ? CONSOLE_RESET : ""
      end

      def after_render(_component_name, _props, _prerender_options)
        @replay_console ? CONSOLE_REPLAY : ""
      end

      class << self
        attr_accessor :asset_container_class
      end

      # Get an object which exposes assets by their logical path.
      #
      # Out of the box, it supports a Sprockets::Environment (application.assets)
      # and a Sprockets::Manifest (application.assets_manifest), which covers the
      # default Rails setups.
      #
      # You can provide a custom asset container
      # with `React::ServerRendering::BundleRenderer.asset_container_class = MyAssetContainer`.
      #
      # @return [#find_asset(logical_path)] An object that returns asset contents by logical path
      def asset_container
        @asset_container ||= asset_container_class.new
      end

      private

      def prepare_options(options)
        r_func = render_function(options)
        opts = case options
               when Hash then options
               else
                 {}
               end
        # This seems redundant to pass
        opts.merge(render_function: r_func)
      end

      def render_function(opts)
        if opts == :static
          "renderToStaticMarkup"
        else
          "renderToString"
        end
      end

      def prepare_props(props)
        props.is_a?(String) ? props : props.to_json
      end

      def assets_precompiled?
        !::Rails.application.config.assets.compile
      end

      # Detect what kind of asset system is in use and choose that container.
      # Or, if the user has provided {.asset_container_class}, use that.
      # @return [Class] suitable for {#asset_container}
      def asset_container_class
        return self.class.asset_container_class if self.class.asset_container_class.present?
        return SeparateServerBundleContainer if SeparateServerBundleContainer.compatible?

        return EnvironmentContainer unless assets_precompiled?

        return ManifestContainer if ManifestContainer.compatible?
        return YamlManifestContainer if YamlManifestContainer.compatible?

        # Even though they are precompiled, we can't find them :S
        EnvironmentContainer
      end
    end
  end
end