diff --git a/.gitignore b/.gitignore index 9aee9436a5..e20bdba3b9 100644 --- a/.gitignore +++ b/.gitignore @@ -75,3 +75,4 @@ ssr-generated # Claude Code local settings .claude/settings.local.json +.claude/.fuse_hidden* diff --git a/CHANGELOG.md b/CHANGELOG.md index ce3f7c193c..738f818d0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,39 @@ To migrate to React on Rails Pro: **Note:** If you're not using any of the Pro-only methods listed above, no changes are required. +- **Pro-Specific Configurations Moved to Pro Gem**: The following React Server Components (RSC) configurations have been moved from `ReactOnRails.configure` to `ReactOnRailsPro.configure`: + + - `rsc_bundle_js_file` - Path to the RSC bundle file + - `react_server_client_manifest_file` - Path to the React server client manifest + - `react_client_manifest_file` - Path to the React client manifest + + **Migration:** If you're using RSC features, move these configurations from your `ReactOnRails.configure` block to `ReactOnRailsPro.configure`: + + ```ruby + # Before + ReactOnRails.configure do |config| + config.rsc_bundle_js_file = "rsc-bundle.js" + config.react_server_client_manifest_file = "react-server-client-manifest.json" + config.react_client_manifest_file = "react-client-manifest.json" + end + + # After + ReactOnRailsPro.configure do |config| + config.rsc_bundle_js_file = "rsc-bundle.js" + config.react_server_client_manifest_file = "react-server-client-manifest.json" + config.react_client_manifest_file = "react-client-manifest.json" + end + ``` + + See the [React on Rails Pro Configuration docs](https://github.com/shakacode/react_on_rails/blob/master/react_on_rails_pro/docs/configuration.md) for more details. + +- **Streaming View Helpers Moved to Pro Gem**: The following view helpers have been removed from the open-source gem and are now only available in React on Rails Pro: + + - `stream_react_component` - Progressive SSR using React 18+ streaming + - `rsc_payload_react_component` - RSC payload rendering + + These helpers are now defined exclusively in the `react-on-rails-pro` gem. + ### [16.1.1] - 2025-09-24 #### Bug Fixes diff --git a/docs/api-reference/configuration.md b/docs/api-reference/configuration.md index 1d7a7e4b91..34ddbfffbe 100644 --- a/docs/api-reference/configuration.md +++ b/docs/api-reference/configuration.md @@ -104,30 +104,21 @@ ReactOnRails.configure do |config| # you should include a name that matches your bundle name in your Webpack config. config.server_bundle_js_file = "server-bundle.js" - # When using React on Rails Pro with RSC support enabled, these configuration options work together: - # - # 1. In RORP, set `config.enable_rsc_support = true` in your react_on_rails_pro.rb initializer - # - # 2. The `rsc_bundle_js_file` (typically "rsc-bundle.js") contains only server components and - # references to client components. It's generated using the RSC Webpack Loader which transforms - # client components into references. This bundle is specifically used for generating RSC payloads - # and is configured with the `react-server` condition. - config.rsc_bundle_js_file = "rsc-bundle.js" + ################################################################################ + # REACT SERVER COMPONENTS (RSC) AND STREAMING CONFIGURATION + ################################################################################ # - # 3. The `react_client_manifest_file` contains mappings for client components that need hydration. - # It's generated by the React Server Components Webpack plugin and is required for client-side - # hydration of components. - # This manifest file is automatically generated by the React Server Components Webpack plugin. Only set this if you've configured the plugin to use a different filename. - config.react_client_manifest_file = "react-client-manifest.json" + # React Server Components and Streaming SSR are React on Rails Pro features. + # For detailed configuration of RSC and streaming features, see: + # https://github.com/shakacode/react_on_rails/blob/master/react_on_rails_pro/docs/configuration.md # - # 4. The `react_server_client_manifest_file` is used during server-side rendering with RSC to - # properly resolve references between server and client components. + # Key Pro configurations (configured in ReactOnRailsPro.configure block): + # - rsc_bundle_js_file: Path to RSC bundle + # - react_client_manifest_file: Client component manifest for RSC + # - react_server_client_manifest_file: Server manifest for RSC + # - enable_rsc_support: Enable React Server Components # - # These files are crucial when implementing React Server Components with streaming, which offers - # benefits like reduced JavaScript bundle sizes, faster page loading, and selective hydration - # of client components. - # This manifest file is automatically generated by the React Server Components Webpack plugin. Only set this if you've configured the plugin to use a different filename. - config.react_server_client_manifest_file = "react-server-client-manifest.json" + # See Pro documentation for complete setup instructions. ################################################################################ # SERVER BUNDLE SECURITY AND ORGANIZATION diff --git a/docs/api-reference/view-helpers-api.md b/docs/api-reference/view-helpers-api.md index 5204dae36d..c3100fedf6 100644 --- a/docs/api-reference/view-helpers-api.md +++ b/docs/api-reference/view-helpers-api.md @@ -80,21 +80,6 @@ export default (props, _railsContext) => { --- -### cached_react_component and cached_react_component_hash - -Fragment caching is a [React on Rails Pro](https://github.com/shakacode/react_on_rails/wiki) feature. The API is the same as the above, but for 2 differences: - -1. The `cache_key` takes the same parameters as any Rails `cache` view helper. -1. The **props** are passed via a block so that evaluation of the props is not done unless the cache is broken. Suppose you put your props calculation into some method called `some_slow_method_that_returns_props`: - -```erb -<%= cached_react_component("App", cache_key: [@user, @post], prerender: true) do - some_slow_method_that_returns_props -end %> -``` - ---- - ### rails_context You can call `rails_context` or `rails_context(server_side: true|false)` from your controller or view to see what values are in the Rails Context. Pass true or false depending on whether you want to see the server-side or the client-side `rails_context`. Typically, for computing cache keys, you should leave `server_side` as the default true. When calling this from a controller method, use `helpers.rails_context`. @@ -132,6 +117,47 @@ This is a helper method that takes any JavaScript expression and returns the out --- +## Pro-Only View Helpers + +The following view helpers are available exclusively with [React on Rails Pro](https://www.shakacode.com/react-on-rails-pro). These require a valid React on Rails Pro license and will not be available if the Pro gem is not installed or properly licensed. + +### cached_react_component and cached_react_component_hash + +Fragment caching helpers that cache React component rendering to improve performance. The API is the same as `react_component` and `react_component_hash`, but with these differences: + +1. The `cache_key` takes the same parameters as any Rails `cache` view helper. +2. The **props** are passed via a block so that evaluation of the props is not done unless the cache is broken. + +Example usage: + +```erb +<%= cached_react_component("App", cache_key: [@user, @post], prerender: true) do + some_slow_method_that_returns_props +end %> +``` + +### stream_react_component + +Progressive server-side rendering using React 18+ streaming with `renderToPipeableStream`. This enables: + +- Faster Time to First Byte (TTFB) +- Progressive page loading with Suspense boundaries +- Better perceived performance + +See the [Streaming Server Rendering guide](../building-features/streaming-server-rendering.md) for usage details. + +### rsc_payload_react_component + +Renders React Server Component (RSC) payloads in NDJSON format for client-side consumption. Used in conjunction with RSC support to enable: + +- Reduced JavaScript bundle sizes +- Server-side data fetching +- Selective client-side hydration + +See the [React on Rails Pro Configuration](https://github.com/shakacode/react_on_rails/blob/master/react_on_rails_pro/docs/configuration.md) for RSC setup. + +--- + # More details See the [lib/react_on_rails/helper.rb](https://github.com/shakacode/react_on_rails/tree/master/lib/react_on_rails/helper.rb) source. diff --git a/lib/react_on_rails/configuration.rb b/lib/react_on_rails/configuration.rb index 3ef7332d2c..20be673d42 100644 --- a/lib/react_on_rails/configuration.rb +++ b/lib/react_on_rails/configuration.rb @@ -9,8 +9,6 @@ def self.configure end DEFAULT_GENERATED_ASSETS_DIR = File.join(%w[public webpack], Rails.env).freeze - DEFAULT_REACT_CLIENT_MANIFEST_FILE = "react-client-manifest.json" - DEFAULT_REACT_SERVER_CLIENT_MANIFEST_FILE = "react-server-client-manifest.json" DEFAULT_COMPONENT_REGISTRY_TIMEOUT = 5000 def self.configuration @@ -20,9 +18,6 @@ def self.configuration # generated_assets_dirs is deprecated generated_assets_dir: "", server_bundle_js_file: "", - rsc_bundle_js_file: "", - react_client_manifest_file: DEFAULT_REACT_CLIENT_MANIFEST_FILE, - react_server_client_manifest_file: DEFAULT_REACT_SERVER_CLIENT_MANIFEST_FILE, prerender: false, auto_load_bundle: false, replay_console: true, @@ -72,8 +67,8 @@ class Configuration :server_render_method, :random_dom_id, :auto_load_bundle, :same_bundle_for_client_and_server, :rendering_props_extension, :make_generated_server_bundle_the_entrypoint, - :generated_component_packs_loading_strategy, :immediate_hydration, :rsc_bundle_js_file, - :react_client_manifest_file, :react_server_client_manifest_file, :component_registry_timeout, + :generated_component_packs_loading_strategy, :immediate_hydration, + :component_registry_timeout, :server_bundle_output_path, :enforce_private_server_bundles # rubocop:disable Metrics/AbcSize @@ -90,7 +85,6 @@ def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender i18n_dir: nil, i18n_yml_dir: nil, i18n_output_format: nil, i18n_yml_safe_load_options: nil, random_dom_id: nil, server_render_method: nil, rendering_props_extension: nil, components_subdirectory: nil, auto_load_bundle: nil, immediate_hydration: nil, - rsc_bundle_js_file: nil, react_client_manifest_file: nil, react_server_client_manifest_file: nil, component_registry_timeout: nil, server_bundle_output_path: nil, enforce_private_server_bundles: nil) self.node_modules_location = node_modules_location.present? ? node_modules_location : Rails.root self.generated_assets_dirs = generated_assets_dirs @@ -119,9 +113,6 @@ def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender # Server rendering: self.server_bundle_js_file = server_bundle_js_file - self.rsc_bundle_js_file = rsc_bundle_js_file - self.react_client_manifest_file = react_client_manifest_file - self.react_server_client_manifest_file = react_server_client_manifest_file self.same_bundle_for_client_and_server = same_bundle_for_client_and_server self.server_renderer_pool_size = self.development_mode ? 1 : server_renderer_pool_size self.server_renderer_timeout = server_renderer_timeout # seconds diff --git a/lib/react_on_rails/helper.rb b/lib/react_on_rails/helper.rb index 60cdd21020..9b35da8161 100644 --- a/lib/react_on_rails/helper.rb +++ b/lib/react_on_rails/helper.rb @@ -94,104 +94,6 @@ def react_component(component_name, options = {}) end end - # Streams a server-side rendered React component using React's `renderToPipeableStream`. - # Supports React 18 features like Suspense, concurrent rendering, and selective hydration. - # Enables progressive rendering and improved performance for large components. - # - # Note: This function can only be used with React on Rails Pro. - # The view that uses this function must be rendered using the - # `stream_view_containing_react_components` method from the React on Rails Pro gem. - # - # Example of an async React component that can benefit from streaming: - # - # const AsyncComponent = async () => { - # const data = await fetchData(); - # return
{data}
; - # }; - # - # function App() { - # return ( - # Loading...}> - # - # - # ); - # } - # - # @param [String] component_name Name of your registered component - # @param [Hash] options Options for rendering - # @option options [Hash] :props Props to pass to the react component - # @option options [String] :dom_id DOM ID of the component container - # @option options [Hash] :html_options Options passed to content_tag - # @option options [Boolean] :trace Set to true to add extra debugging information to the HTML - # @option options [Boolean] :raise_on_prerender_error Set to true to raise exceptions during server-side rendering - # Any other options are passed to the content tag, including the id. - def stream_react_component(component_name, options = {}) - # stream_react_component doesn't have the prerender option - # Because setting prerender to false is equivalent to calling react_component with prerender: false - options[:prerender] = true - options = options.merge(immediate_hydration: true) unless options.key?(:immediate_hydration) - run_stream_inside_fiber do - internal_stream_react_component(component_name, options) - end - end - - # Renders the React Server Component (RSC) payload for a given component. This helper generates - # a special format designed by React for serializing server components and transmitting them - # to the client. - # - # @return [String] Returns a Newline Delimited JSON (NDJSON) stream where each line contains a JSON object with: - # - html: The RSC payload containing the rendered server components and client component references - # - consoleReplayScript: JavaScript to replay server-side console logs in the client - # - hasErrors: Boolean indicating if any errors occurred during rendering - # - isShellReady: Boolean indicating if the initial shell is ready for hydration - # - # Example NDJSON stream: - # {"html":"","consoleReplayScript":"","hasErrors":false,"isShellReady":true} - # {"html":"","consoleReplayScript":"console.log('Loading...')","hasErrors":false,"isShellReady":true} - # - # The RSC payload within the html field contains: - # - The component's rendered output from the server - # - References to client components that need hydration - # - Data props passed to client components - # - # @param component_name [String] The name of the React component to render. This component should - # be a server component or a mixed component tree containing both server and client components. - # - # @param options [Hash] Options for rendering the component - # @option options [Hash] :props Props to pass to the component (default: {}) - # @option options [Boolean] :trace Enable tracing for debugging (default: false) - # @option options [String] :id Custom DOM ID for the component container (optional) - # - # @example Basic usage with a server component - # <%= rsc_payload_react_component("ReactServerComponentPage") %> - # - # @example With props and tracing enabled - # <%= rsc_payload_react_component("RSCPostsPage", - # props: { artificialDelay: 1000 }, - # trace: true) %> - # - # @note This helper requires React Server Components support to be enabled in your configuration: - # ReactOnRailsPro.configure do |config| - # config.enable_rsc_support = true - # end - # - # @raise [ReactOnRailsPro::Error] if RSC support is not enabled in configuration - # - # @note You don't have to deal directly with this helper function - it's used internally by the - # `rsc_payload_route` helper function. The returned data from this function is used internally by - # components registered using the `registerServerComponent` function. Don't use it unless you need - # more control over the RSC payload generation. To know more about RSC payload, see the following link: - # @see https://www.shakacode.com/react-on-rails-pro/docs/how-react-server-components-works.md - # for technical details about the RSC payload format - def rsc_payload_react_component(component_name, options = {}) - # rsc_payload_react_component doesn't have the prerender option - # Because setting prerender to false will not do anything - options[:prerender] = true - run_stream_inside_fiber do - internal_rsc_payload_react_component(component_name, options) - end - end - # react_component_hash is used to return multiple HTML strings for server rendering, such as for # adding meta-tags to a page. # It is exactly like react_component except for the following: @@ -446,32 +348,6 @@ def load_pack_for_generated_component(react_component_name, render_options) # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity - def run_stream_inside_fiber - unless ReactOnRails::Utils.react_on_rails_pro? - raise ReactOnRails::Error, - "You must use React on Rails Pro to use the stream_react_component method." - end - - if @rorp_rendering_fibers.nil? - raise ReactOnRails::Error, - "You must call stream_view_containing_react_components to render the view containing the react component" - end - - rendering_fiber = Fiber.new do - stream = yield - stream.each_chunk do |chunk| - Fiber.yield chunk - end - end - - @rorp_rendering_fibers << rendering_fiber - - # return the first chunk of the fiber - # It contains the initial html of the component - # all updates will be appended to the stream sent to browser - rendering_fiber.resume - end - def registered_stores @registered_stores ||= [] end @@ -494,25 +370,6 @@ def create_render_options(react_component_name, options) options: options) end - def internal_stream_react_component(component_name, options = {}) - options = options.merge(render_mode: :html_streaming) - result = internal_react_component(component_name, options) - build_react_component_result_for_server_streamed_content( - rendered_html_stream: result[:result], - component_specification_tag: result[:tag], - render_options: result[:render_options] - ) - end - - def internal_rsc_payload_react_component(react_component_name, options = {}) - options = options.merge(render_mode: :rsc_payload_streaming) - render_options = create_render_options(react_component_name, options) - json_stream = server_rendered_react_component(render_options) - json_stream.transform do |chunk| - "#{chunk.to_json}\n".html_safe - end - end - def generated_components_pack_path(component_name) "#{ReactOnRails::PackerUtils.packer_source_entry_path}/generated/#{component_name}.js" end @@ -544,32 +401,6 @@ def build_react_component_result_for_server_rendered_string( prepend_render_rails_context(result) end - def build_react_component_result_for_server_streamed_content( - rendered_html_stream:, - component_specification_tag:, - render_options: - ) - is_first_chunk = true - rendered_html_stream.transform do |chunk_json_result| - if is_first_chunk - is_first_chunk = false - build_react_component_result_for_server_rendered_string( - server_rendered_html: chunk_json_result["html"], - component_specification_tag: component_specification_tag, - console_script: chunk_json_result["consoleReplayScript"], - render_options: render_options - ) - else - result_console_script = render_options.replay_console ? chunk_json_result["consoleReplayScript"] : "" - # No need to prepend component_specification_tag or add rails context again - # as they're already included in the first chunk - compose_react_component_html_with_spec_and_console( - "", chunk_json_result["html"], result_console_script - ) - end - end - end - def build_react_component_result_for_server_rendered_hash( server_rendered_html: required("server_rendered_html"), component_specification_tag: required("component_specification_tag"), diff --git a/lib/react_on_rails/packer_utils.rb b/lib/react_on_rails/packer_utils.rb index d1349466f7..d6e6511000 100644 --- a/lib/react_on_rails/packer_utils.rb +++ b/lib/react_on_rails/packer_utils.rb @@ -55,8 +55,12 @@ def self.bundle_js_uri_from_packer(bundle_name) # the webpack-dev-server is provided by the config value # "same_bundle_for_client_and_server" where a value of true # would mean that the bundle is created by the webpack-dev-server - is_bundle_running_on_server = (bundle_name == ReactOnRails.configuration.server_bundle_js_file) || - (bundle_name == ReactOnRails.configuration.rsc_bundle_js_file) + is_bundle_running_on_server = bundle_name == ReactOnRails.configuration.server_bundle_js_file + + # Check Pro RSC bundle if Pro is available + if ReactOnRails::Utils.react_on_rails_pro? + is_bundle_running_on_server ||= (bundle_name == ReactOnRailsPro.configuration.rsc_bundle_js_file) + end if ::Shakapacker.dev_server.running? && (!is_bundle_running_on_server || ReactOnRails.configuration.same_bundle_for_client_and_server) diff --git a/lib/react_on_rails/test_helper/webpack_assets_status_checker.rb b/lib/react_on_rails/test_helper/webpack_assets_status_checker.rb index 35d200efb5..b3e23dac0c 100644 --- a/lib/react_on_rails/test_helper/webpack_assets_status_checker.rb +++ b/lib/react_on_rails/test_helper/webpack_assets_status_checker.rb @@ -47,16 +47,25 @@ def stale_generated_files(files) private + def resolve_asset_path(bundle_name) + # Check if this is a Pro RSC manifest file + if ReactOnRails::Utils.react_on_rails_pro? + pro_config = ReactOnRailsPro.configuration + if bundle_name == pro_config.react_client_manifest_file + return ReactOnRailsPro::Utils.react_client_manifest_file_path + end + if bundle_name == pro_config.react_server_client_manifest_file + return ReactOnRailsPro::Utils.react_server_client_manifest_file_path + end + end + + ReactOnRails::Utils.bundle_js_file_path(bundle_name) + end + def all_compiled_assets @all_compiled_assets ||= begin webpack_generated_files = @webpack_generated_files.map do |bundle_name| - if bundle_name == ReactOnRails.configuration.react_client_manifest_file - ReactOnRails::Utils.react_client_manifest_file_path - elsif bundle_name == ReactOnRails.configuration.react_server_client_manifest_file - ReactOnRails::Utils.react_server_client_manifest_file_path - else - ReactOnRails::Utils.bundle_js_file_path(bundle_name) - end + resolve_asset_path(bundle_name) end if webpack_generated_files.present? diff --git a/lib/react_on_rails/utils.rb b/lib/react_on_rails/utils.rb index 624040a7ed..b95c5dbea8 100644 --- a/lib/react_on_rails/utils.rb +++ b/lib/react_on_rails/utils.rb @@ -111,9 +111,16 @@ def self.bundle_js_file_path(bundle_name) private_class_method def self.server_bundle?(bundle_name) config = ReactOnRails.configuration - bundle_name == config.server_bundle_js_file || - bundle_name == config.rsc_bundle_js_file || - bundle_name == config.react_server_client_manifest_file + return true if bundle_name == config.server_bundle_js_file + + # Check Pro configurations if Pro is available + if react_on_rails_pro? + pro_config = ReactOnRailsPro.configuration + return true if bundle_name == pro_config.rsc_bundle_js_file || + bundle_name == pro_config.react_server_client_manifest_file + end + + false end private_class_method def self.handle_missing_manifest_entry(bundle_name, is_server_bundle) @@ -146,34 +153,6 @@ def self.server_bundle_js_file_path @server_bundle_path = bundle_js_file_path(bundle_name) end - def self.rsc_bundle_js_file_path - return @rsc_bundle_path if @rsc_bundle_path && !Rails.env.development? - - bundle_name = ReactOnRails.configuration.rsc_bundle_js_file - @rsc_bundle_path = bundle_js_file_path(bundle_name) - end - - def self.react_client_manifest_file_path - return @react_client_manifest_path if @react_client_manifest_path && !Rails.env.development? - - file_name = ReactOnRails.configuration.react_client_manifest_file - @react_client_manifest_path = ReactOnRails::PackerUtils.asset_uri_from_packer(file_name) - end - - # React Server Manifest is generated by the server bundle. - # So, it will never be served from the dev server. - def self.react_server_client_manifest_file_path - return @react_server_manifest_path if @react_server_manifest_path && !Rails.env.development? - - asset_name = ReactOnRails.configuration.react_server_client_manifest_file - if asset_name.nil? - raise ReactOnRails::Error, - "react_server_client_manifest_file is nil, ensure it is set in your configuration" - end - - @react_server_manifest_path = bundle_js_file_path(asset_name) - end - def self.running_on_windows? (/cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM) != nil end @@ -254,13 +233,12 @@ def self.react_on_rails_pro_version end end + # RSC support detection has been moved to React on Rails Pro + # See react_on_rails_pro/lib/react_on_rails_pro/utils.rb def self.rsc_support_enabled? return false unless react_on_rails_pro? - return @rsc_support_enabled if defined?(@rsc_support_enabled) - - rorp_config = ReactOnRailsPro.configuration - @rsc_support_enabled = rorp_config.respond_to?(:enable_rsc_support) && rorp_config.enable_rsc_support + ReactOnRailsPro::Utils.rsc_support_enabled? end def self.full_text_errors_enabled? diff --git a/react_on_rails_pro/CHANGELOG.md b/react_on_rails_pro/CHANGELOG.md index 73268d64ab..36dbc16183 100644 --- a/react_on_rails_pro/CHANGELOG.md +++ b/react_on_rails_pro/CHANGELOG.md @@ -19,9 +19,21 @@ You can find the **package** version numbers from this repo's tags and below in ### Added - Added `cached_stream_react_component` helper method, similar to `cached_react_component` but for streamed components. - **License Validation System**: Implemented comprehensive JWT-based license validation with offline verification using RSA-256 signatures. License validation occurs at startup in both Ruby and Node.js environments. Supports required fields (`sub`, `iat`, `exp`) and optional fields (`plan`, `organization`, `iss`). FREE evaluation licenses are available for 3 months at [shakacode.com/react-on-rails-pro](https://shakacode.com/react-on-rails-pro). [PR #1857](https://github.com/shakacode/react_on_rails/pull/1857) by [AbanoubGhadban](https://github.com/AbanoubGhadban). +- **Pro-Specific Configurations Moved from Open-Source**: The following React Server Components (RSC) configurations are now exclusively in the Pro gem and should be configured in `ReactOnRailsPro.configure`: + - `rsc_bundle_js_file` - Path to the RSC bundle file + - `react_server_client_manifest_file` - Path to the React server client manifest + - `react_client_manifest_file` - Path to the React client manifest + + These configurations were previously available in the open-source `ReactOnRails.configure` block but have been moved to Pro where they belong since RSC is a Pro-only feature. +- **Streaming View Helpers Now Pro-Exclusive**: The following view helpers are now defined exclusively in the Pro gem: + - `stream_react_component` - Progressive SSR using React 18+ streaming + - `rsc_payload_react_component` - RSC payload rendering + + These helpers were previously in the open-source gem but have been moved to Pro as they are Pro-only features. ### Changed (Breaking) - `config.prerender_caching`, which controls caching for non-streaming components, now also controls caching for streamed components. To disable caching for an individual render, pass `internal_option(:skip_prerender_cache)`. +- **Configuration Migration Required**: If you are using RSC features, you must move the RSC-related configurations from `ReactOnRails.configure` to `ReactOnRailsPro.configure` in your initializers. See the migration example in the [React on Rails CHANGELOG](https://github.com/shakacode/react_on_rails/blob/master/CHANGELOG.md#unreleased). ## [4.0.0-rc.15] - 2025-08-11 diff --git a/react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb b/react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb index 9c6ae05b14..b3bcad2a9b 100644 --- a/react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb +++ b/react_on_rails_pro/app/helpers/react_on_rails_pro_helper.rb @@ -6,6 +6,7 @@ require "react_on_rails/helper" +# rubocop:disable Metrics/ModuleLength module ReactOnRailsProHelper def fetch_react_component(component_name, options) if ReactOnRailsPro::Cache.use_cache?(options) @@ -91,6 +92,104 @@ def cached_react_component_hash(component_name, raw_options = {}, &block) end end + # Streams a server-side rendered React component using React's `renderToPipeableStream`. + # Supports React 18 features like Suspense, concurrent rendering, and selective hydration. + # Enables progressive rendering and improved performance for large components. + # + # Note: This function can only be used with React on Rails Pro. + # The view that uses this function must be rendered using the + # `stream_view_containing_react_components` method from the React on Rails Pro gem. + # + # Example of an async React component that can benefit from streaming: + # + # const AsyncComponent = async () => { + # const data = await fetchData(); + # return
{data}
; + # }; + # + # function App() { + # return ( + # Loading...}> + # + # + # ); + # } + # + # @param [String] component_name Name of your registered component + # @param [Hash] options Options for rendering + # @option options [Hash] :props Props to pass to the react component + # @option options [String] :dom_id DOM ID of the component container + # @option options [Hash] :html_options Options passed to content_tag + # @option options [Boolean] :trace Set to true to add extra debugging information to the HTML + # @option options [Boolean] :raise_on_prerender_error Set to true to raise exceptions during server-side rendering + # Any other options are passed to the content tag, including the id. + def stream_react_component(component_name, options = {}) + # stream_react_component doesn't have the prerender option + # Because setting prerender to false is equivalent to calling react_component with prerender: false + options[:prerender] = true + options = options.merge(immediate_hydration: true) unless options.key?(:immediate_hydration) + run_stream_inside_fiber do + internal_stream_react_component(component_name, options) + end + end + + # Renders the React Server Component (RSC) payload for a given component. This helper generates + # a special format designed by React for serializing server components and transmitting them + # to the client. + # + # @return [String] Returns a Newline Delimited JSON (NDJSON) stream where each line contains a JSON object with: + # - html: The RSC payload containing the rendered server components and client component references + # - consoleReplayScript: JavaScript to replay server-side console logs in the client + # - hasErrors: Boolean indicating if any errors occurred during rendering + # - isShellReady: Boolean indicating if the initial shell is ready for hydration + # + # Example NDJSON stream: + # {"html":"","consoleReplayScript":"","hasErrors":false,"isShellReady":true} + # {"html":"","consoleReplayScript":"console.log('Loading...')","hasErrors":false,"isShellReady":true} + # + # The RSC payload within the html field contains: + # - The component's rendered output from the server + # - References to client components that need hydration + # - Data props passed to client components + # + # @param component_name [String] The name of the React component to render. This component should + # be a server component or a mixed component tree containing both server and client components. + # + # @param options [Hash] Options for rendering the component + # @option options [Hash] :props Props to pass to the component (default: {}) + # @option options [Boolean] :trace Enable tracing for debugging (default: false) + # @option options [String] :id Custom DOM ID for the component container (optional) + # + # @example Basic usage with a server component + # <%= rsc_payload_react_component("ReactServerComponentPage") %> + # + # @example With props and tracing enabled + # <%= rsc_payload_react_component("RSCPostsPage", + # props: { artificialDelay: 1000 }, + # trace: true) %> + # + # @note This helper requires React Server Components support to be enabled in your configuration: + # ReactOnRailsPro.configure do |config| + # config.enable_rsc_support = true + # end + # + # @raise [ReactOnRailsPro::Error] if RSC support is not enabled in configuration + # + # @note You don't have to deal directly with this helper function - it's used internally by the + # `rsc_payload_route` helper function. The returned data from this function is used internally by + # components registered using the `registerServerComponent` function. Don't use it unless you need + # more control over the RSC payload generation. To know more about RSC payload, see the following link: + # @see https://www.shakacode.com/react-on-rails-pro/docs/how-react-server-components-works.md + # for technical details about the RSC payload format + def rsc_payload_react_component(component_name, options = {}) + # rsc_payload_react_component doesn't have the prerender option + # Because setting prerender to false will not do anything + options[:prerender] = true + run_stream_inside_fiber do + internal_rsc_payload_react_component(component_name, options) + end + end + # Provide caching support for stream_react_component in a manner akin to Rails fragment caching. # All the same options as stream_react_component apply with the following differences: # @@ -191,4 +290,71 @@ def check_caching_options!(raw_options, block) raise ReactOnRailsPro::Error, "Option 'cache_key' is required for React on Rails caching" end + + def run_stream_inside_fiber + if @rorp_rendering_fibers.nil? + raise ReactOnRails::Error, + "You must call stream_view_containing_react_components to render the view containing the react component" + end + + rendering_fiber = Fiber.new do + stream = yield + stream.each_chunk do |chunk| + Fiber.yield chunk + end + end + + @rorp_rendering_fibers << rendering_fiber + + # return the first chunk of the fiber + # It contains the initial html of the component + # all updates will be appended to the stream sent to browser + rendering_fiber.resume + end + + def internal_stream_react_component(component_name, options = {}) + options = options.merge(render_mode: :html_streaming) + result = internal_react_component(component_name, options) + build_react_component_result_for_server_streamed_content( + rendered_html_stream: result[:result], + component_specification_tag: result[:tag], + render_options: result[:render_options] + ) + end + + def internal_rsc_payload_react_component(react_component_name, options = {}) + options = options.merge(render_mode: :rsc_payload_streaming) + render_options = create_render_options(react_component_name, options) + json_stream = server_rendered_react_component(render_options) + json_stream.transform do |chunk| + "#{chunk.to_json}\n".html_safe + end + end + + def build_react_component_result_for_server_streamed_content( + rendered_html_stream:, + component_specification_tag:, + render_options: + ) + is_first_chunk = true + rendered_html_stream.transform do |chunk_json_result| + if is_first_chunk + is_first_chunk = false + build_react_component_result_for_server_rendered_string( + server_rendered_html: chunk_json_result["html"], + component_specification_tag: component_specification_tag, + console_script: chunk_json_result["consoleReplayScript"], + render_options: render_options + ) + else + result_console_script = render_options.replay_console ? chunk_json_result["consoleReplayScript"] : "" + # No need to prepend component_specification_tag or add rails context again + # as they're already included in the first chunk + compose_react_component_html_with_spec_and_console( + "", chunk_json_result["html"], result_console_script + ) + end + end + end end +# rubocop:enable Metrics/ModuleLength diff --git a/react_on_rails_pro/docs/configuration.md b/react_on_rails_pro/docs/configuration.md index 8661fa5129..1d36e29e49 100644 --- a/react_on_rails_pro/docs/configuration.md +++ b/react_on_rails_pro/docs/configuration.md @@ -121,5 +121,45 @@ ReactOnRailsPro.configure do |config| Rails.root.join("public", "webpack", Rails.env, "loadable-stats.json"), Rails.root.join("public", "webpack", Rails.env, "manifest.json") ] + + ################################################################################ + # REACT SERVER COMPONENTS (RSC) CONFIGURATION + ################################################################################ + + # Enable React Server Components support + # When enabled, React on Rails Pro will support RSC rendering and streaming + # Default is false + config.enable_rsc_support = true + + # Path to the RSC bundle file (relative to webpack output directory or absolute path) + # The RSC bundle contains only server components and references to client components. + # It's generated using the RSC Webpack Loader which transforms client components into + # references. This bundle is specifically used for generating RSC payloads and is + # configured with the 'react-server' condition. + # Default is "rsc-bundle.js" + config.rsc_bundle_js_file = "rsc-bundle.js" + + # Path to the React client manifest file (typically in your webpack output directory) + # This manifest contains mappings for client components that need hydration. + # It's automatically generated by the React Server Components Webpack plugin and is + # required for client-side hydration of components. + # Only set this if you've configured the plugin to use a different filename. + # Default is "react-client-manifest.json" + config.react_client_manifest_file = "react-client-manifest.json" + + # Path to the React server-client manifest file (typically in your webpack output directory) + # This manifest is used during server-side rendering with RSC to properly resolve + # references between server and client components. + # It's automatically generated by the React Server Components Webpack plugin. + # Only set this if you've configured the plugin to use a different filename. + # Default is "react-server-client-manifest.json" + config.react_server_client_manifest_file = "react-server-client-manifest.json" + + # These RSC configuration files are crucial when implementing React Server Components + # with streaming, which offers benefits like: + # - Reduced JavaScript bundle sizes + # - Faster page loading + # - Selective hydration of client components + # - Progressive rendering with Suspense boundaries end ``` diff --git a/react_on_rails_pro/lib/react_on_rails_pro/configuration.rb b/react_on_rails_pro/lib/react_on_rails_pro/configuration.rb index 3f4b541fe3..12d913a457 100644 --- a/react_on_rails_pro/lib/react_on_rails_pro/configuration.rb +++ b/react_on_rails_pro/lib/react_on_rails_pro/configuration.rb @@ -29,7 +29,10 @@ def self.configuration profile_server_rendering_js_code: Configuration::DEFAULT_PROFILE_SERVER_RENDERING_JS_CODE, raise_non_shell_server_rendering_errors: Configuration::DEFAULT_RAISE_NON_SHELL_SERVER_RENDERING_ERRORS, enable_rsc_support: Configuration::DEFAULT_ENABLE_RSC_SUPPORT, - rsc_payload_generation_url_path: Configuration::DEFAULT_RSC_PAYLOAD_GENERATION_URL_PATH + rsc_payload_generation_url_path: Configuration::DEFAULT_RSC_PAYLOAD_GENERATION_URL_PATH, + rsc_bundle_js_file: Configuration::DEFAULT_RSC_BUNDLE_JS_FILE, + react_client_manifest_file: Configuration::DEFAULT_REACT_CLIENT_MANIFEST_FILE, + react_server_client_manifest_file: Configuration::DEFAULT_REACT_SERVER_CLIENT_MANIFEST_FILE ) end @@ -53,6 +56,9 @@ class Configuration # rubocop:disable Metrics/ClassLength DEFAULT_RAISE_NON_SHELL_SERVER_RENDERING_ERRORS = false DEFAULT_ENABLE_RSC_SUPPORT = false DEFAULT_RSC_PAYLOAD_GENERATION_URL_PATH = "rsc_payload/" + DEFAULT_RSC_BUNDLE_JS_FILE = "rsc-bundle.js" + DEFAULT_REACT_CLIENT_MANIFEST_FILE = "react-client-manifest.json" + DEFAULT_REACT_SERVER_CLIENT_MANIFEST_FILE = "react-server-client-manifest.json" attr_accessor :renderer_url, :renderer_password, :tracing, :server_renderer, :renderer_use_fallback_exec_js, :prerender_caching, @@ -61,7 +67,8 @@ class Configuration # rubocop:disable Metrics/ClassLength :remote_bundle_cache_adapter, :ssr_pre_hook_js, :assets_to_copy, :renderer_request_retry_limit, :throw_js_errors, :ssr_timeout, :profile_server_rendering_js_code, :raise_non_shell_server_rendering_errors, :enable_rsc_support, - :rsc_payload_generation_url_path + :rsc_payload_generation_url_path, :rsc_bundle_js_file, :react_client_manifest_file, + :react_server_client_manifest_file def initialize(renderer_url: nil, renderer_password: nil, server_renderer: nil, # rubocop:disable Metrics/AbcSize renderer_use_fallback_exec_js: nil, prerender_caching: nil, @@ -71,7 +78,8 @@ def initialize(renderer_url: nil, renderer_password: nil, server_renderer: nil, remote_bundle_cache_adapter: nil, ssr_pre_hook_js: nil, assets_to_copy: nil, renderer_request_retry_limit: nil, throw_js_errors: nil, ssr_timeout: nil, profile_server_rendering_js_code: nil, raise_non_shell_server_rendering_errors: nil, - enable_rsc_support: nil, rsc_payload_generation_url_path: nil) + enable_rsc_support: nil, rsc_payload_generation_url_path: nil, + rsc_bundle_js_file: nil, react_client_manifest_file: nil, react_server_client_manifest_file: nil) self.renderer_url = renderer_url self.renderer_password = renderer_password self.server_renderer = server_renderer @@ -94,6 +102,9 @@ def initialize(renderer_url: nil, renderer_password: nil, server_renderer: nil, self.raise_non_shell_server_rendering_errors = raise_non_shell_server_rendering_errors self.enable_rsc_support = enable_rsc_support self.rsc_payload_generation_url_path = rsc_payload_generation_url_path + self.rsc_bundle_js_file = rsc_bundle_js_file + self.react_client_manifest_file = react_client_manifest_file + self.react_server_client_manifest_file = react_server_client_manifest_file end def setup_config_values diff --git a/react_on_rails_pro/lib/react_on_rails_pro/request.rb b/react_on_rails_pro/lib/react_on_rails_pro/request.rb index de5ab15658..c08259e36c 100644 --- a/react_on_rails_pro/lib/react_on_rails_pro/request.rb +++ b/react_on_rails_pro/lib/react_on_rails_pro/request.rb @@ -49,7 +49,7 @@ def upload_assets # Add RSC bundle if enabled if ReactOnRailsPro.configuration.enable_rsc_support - rsc_bundle_path = ReactOnRails::Utils.rsc_bundle_js_file_path + rsc_bundle_path = ReactOnRailsPro::Utils.rsc_bundle_js_file_path unless File.exist?(rsc_bundle_path) raise ReactOnRailsPro::Error, "RSC bundle not found at #{rsc_bundle_path}. " \ "Please build your bundles before uploading assets." @@ -154,7 +154,7 @@ def populate_form_with_bundle_and_assets(form, check_bundle:) if ReactOnRailsPro.configuration.enable_rsc_support add_bundle_to_form( form, - bundle_path: ReactOnRails::Utils.rsc_bundle_js_file_path, + bundle_path: ReactOnRailsPro::Utils.rsc_bundle_js_file_path, bundle_file_name: pool.rsc_renderer_bundle_file_name, bundle_hash: pool.rsc_bundle_hash, check_bundle: check_bundle @@ -178,8 +178,8 @@ def add_assets_to_form(form) assets_to_copy = (ReactOnRailsPro.configuration.assets_to_copy || []).dup # react_client_manifest and react_server_manifest files are needed to generate react server components payload if ReactOnRailsPro.configuration.enable_rsc_support - assets_to_copy << ReactOnRails::Utils.react_client_manifest_file_path - assets_to_copy << ReactOnRails::Utils.react_server_client_manifest_file_path + assets_to_copy << ReactOnRailsPro::Utils.react_client_manifest_file_path + assets_to_copy << ReactOnRailsPro::Utils.react_server_client_manifest_file_path end return form unless assets_to_copy.present? diff --git a/react_on_rails_pro/lib/react_on_rails_pro/server_rendering_js_code.rb b/react_on_rails_pro/lib/react_on_rails_pro/server_rendering_js_code.rb index 806bbcfd1a..89ef9f136c 100644 --- a/react_on_rails_pro/lib/react_on_rails_pro/server_rendering_js_code.rb +++ b/react_on_rails_pro/lib/react_on_rails_pro/server_rendering_js_code.rb @@ -64,8 +64,9 @@ def render(props_string, rails_context, redux_stores, react_component_name, rend "'serverRenderReactComponent'" end rsc_params = if ReactOnRailsPro.configuration.enable_rsc_support && render_options.streaming? - react_client_manifest_file = ReactOnRails.configuration.react_client_manifest_file - react_server_client_manifest_file = ReactOnRails.configuration.react_server_client_manifest_file + config = ReactOnRailsPro.configuration + react_client_manifest_file = config.react_client_manifest_file + react_server_client_manifest_file = config.react_server_client_manifest_file <<-JS railsContext.reactClientManifestFileName = '#{react_client_manifest_file}'; railsContext.reactServerClientManifestFileName = '#{react_server_client_manifest_file}'; diff --git a/react_on_rails_pro/lib/react_on_rails_pro/utils.rb b/react_on_rails_pro/lib/react_on_rails_pro/utils.rb index f67f613cc3..d5f7f5a84f 100644 --- a/react_on_rails_pro/lib/react_on_rails_pro/utils.rb +++ b/react_on_rails_pro/lib/react_on_rails_pro/utils.rb @@ -16,6 +16,41 @@ def self.rorp_puts(message) puts "[ReactOnRailsPro] #{message}" end + # RSC Configuration Utility Methods + # These methods were moved from ReactOnRails::Utils as they are Pro-only features + + def self.rsc_bundle_js_file_path + return @rsc_bundle_path if @rsc_bundle_path && !Rails.env.development? + + bundle_name = ReactOnRailsPro.configuration.rsc_bundle_js_file + @rsc_bundle_path = ReactOnRails::Utils.bundle_js_file_path(bundle_name) + end + + def self.react_client_manifest_file_path + return @react_client_manifest_path if @react_client_manifest_path && !Rails.env.development? + + file_name = ReactOnRailsPro.configuration.react_client_manifest_file + @react_client_manifest_path = ReactOnRails::PackerUtils.asset_uri_from_packer(file_name) + end + + # React Server Manifest is generated by the server bundle. + # So, it will never be served from the dev server. + def self.react_server_client_manifest_file_path + return @react_server_manifest_path if @react_server_manifest_path && !Rails.env.development? + + asset_name = ReactOnRailsPro.configuration.react_server_client_manifest_file + if asset_name.nil? + raise ReactOnRailsPro::Error, + "react_server_client_manifest_file is nil, ensure it is set in your configuration" + end + + @react_server_manifest_path = ReactOnRails::Utils.bundle_js_file_path(asset_name) + end + + def self.rsc_support_enabled? + ReactOnRailsPro.configuration.enable_rsc_support + end + # Validates the license and raises an exception if invalid. # # @return [Boolean] true if license is valid @@ -66,7 +101,7 @@ def self.bundle_hash def self.rsc_bundle_hash return @rsc_bundle_hash if @rsc_bundle_hash && !(Rails.env.development? || Rails.env.test?) - server_rsc_bundle_js_file_path = ReactOnRails::Utils.rsc_bundle_js_file_path + server_rsc_bundle_js_file_path = rsc_bundle_js_file_path return @rsc_bundle_hash if @rsc_bundle_hash && bundle_mtime_same?(server_rsc_bundle_js_file_path) @@ -114,7 +149,7 @@ def self.bundle_mtime_same?(server_bundle_js_file_path) def self.contains_hash?(server_bundle_basename) # TODO: Need to consider if the configuration value has the ".js" on the end. ReactOnRails.configuration.server_bundle_js_file != server_bundle_basename && - ReactOnRails.configuration.rsc_bundle_js_file != server_bundle_basename + ReactOnRailsPro.configuration.rsc_bundle_js_file != server_bundle_basename end def self.with_trace(message = nil) diff --git a/react_on_rails_pro/spec/dummy/config/initializers/react_on_rails.rb b/react_on_rails_pro/spec/dummy/config/initializers/react_on_rails.rb index f265623882..fb674fecd6 100644 --- a/react_on_rails_pro/spec/dummy/config/initializers/react_on_rails.rb +++ b/react_on_rails_pro/spec/dummy/config/initializers/react_on_rails.rb @@ -27,7 +27,6 @@ def self.adjust_props_for_client_side_hydration(_component_name, props) ReactOnRails.configure do |config| config.server_bundle_js_file = "server-bundle.js" - config.rsc_bundle_js_file = "rsc-bundle.js" config.random_dom_id = false # default is true # Next 2 lines are commented out because we've set test.compile to true diff --git a/react_on_rails_pro/spec/dummy/config/initializers/react_on_rails_pro.rb b/react_on_rails_pro/spec/dummy/config/initializers/react_on_rails_pro.rb index 1bd2a24727..276b83461f 100644 --- a/react_on_rails_pro/spec/dummy/config/initializers/react_on_rails_pro.rb +++ b/react_on_rails_pro/spec/dummy/config/initializers/react_on_rails_pro.rb @@ -7,6 +7,9 @@ config.rendering_returns_promises = true + # RSC bundle configuration (moved from ReactOnRails.configure) + config.rsc_bundle_js_file = "rsc-bundle.js" + config.server_renderer = "NodeRenderer" # If you want Honeybadger or Sentry on the Node renderer side to report rendering errors config.throw_js_errors = false diff --git a/react_on_rails_pro/spec/react_on_rails_pro/configuration_spec.rb b/react_on_rails_pro/spec/react_on_rails_pro/configuration_spec.rb index af004c6d1f..152fe27b4e 100644 --- a/react_on_rails_pro/spec/react_on_rails_pro/configuration_spec.rb +++ b/react_on_rails_pro/spec/react_on_rails_pro/configuration_spec.rb @@ -198,5 +198,65 @@ def self.fetch(*) /ExecJS profiler only supports Node.js \(V8\) or V8 runtimes./) end end + + describe "RSC configuration options" do + it "has default values for RSC bundle and manifest files" do + ReactOnRailsPro.configure {} # rubocop:disable Lint/EmptyBlock + + expect(ReactOnRailsPro.configuration.rsc_bundle_js_file).to eq("rsc-bundle.js") + expect(ReactOnRailsPro.configuration.react_client_manifest_file).to eq("react-client-manifest.json") + expect(ReactOnRailsPro.configuration.react_server_client_manifest_file).to eq("react-server-client-manifest.json") + end + + it "allows setting rsc_bundle_js_file" do + ReactOnRailsPro.configure do |config| + config.rsc_bundle_js_file = "custom-rsc-bundle.js" + end + + expect(ReactOnRailsPro.configuration.rsc_bundle_js_file).to eq("custom-rsc-bundle.js") + end + + it "allows setting react_client_manifest_file" do + ReactOnRailsPro.configure do |config| + config.react_client_manifest_file = "custom-client-manifest.json" + end + + expect(ReactOnRailsPro.configuration.react_client_manifest_file).to eq("custom-client-manifest.json") + end + + it "allows setting react_server_client_manifest_file" do + ReactOnRailsPro.configure do |config| + config.react_server_client_manifest_file = "custom-server-client-manifest.json" + end + + expect(ReactOnRailsPro.configuration.react_server_client_manifest_file).to eq("custom-server-client-manifest.json") + end + + it "allows nil values for RSC configuration options" do + ReactOnRailsPro.configure do |config| + config.rsc_bundle_js_file = nil + config.react_client_manifest_file = nil + config.react_server_client_manifest_file = nil + end + + expect(ReactOnRailsPro.configuration.rsc_bundle_js_file).to be_nil + expect(ReactOnRailsPro.configuration.react_client_manifest_file).to be_nil + expect(ReactOnRailsPro.configuration.react_server_client_manifest_file).to be_nil + end + + it "configures all RSC options together for a typical RSC setup" do + ReactOnRailsPro.configure do |config| + config.enable_rsc_support = true + config.rsc_bundle_js_file = "rsc-bundle.js" + config.react_client_manifest_file = "client-manifest.json" + config.react_server_client_manifest_file = "server-client-manifest.json" + end + + expect(ReactOnRailsPro.configuration.enable_rsc_support).to be(true) + expect(ReactOnRailsPro.configuration.rsc_bundle_js_file).to eq("rsc-bundle.js") + expect(ReactOnRailsPro.configuration.react_client_manifest_file).to eq("client-manifest.json") + expect(ReactOnRailsPro.configuration.react_server_client_manifest_file).to eq("server-client-manifest.json") + end + end end end diff --git a/react_on_rails_pro/spec/react_on_rails_pro/request_spec.rb b/react_on_rails_pro/spec/react_on_rails_pro/request_spec.rb index 7151b9476f..6c775ae799 100644 --- a/react_on_rails_pro/spec/react_on_rails_pro/request_spec.rb +++ b/react_on_rails_pro/spec/react_on_rails_pro/request_spec.rb @@ -21,8 +21,7 @@ File.write(rsc_server_bundle_path, 'console.log("mock RSC bundle");') clear_stream_mocks - allow(ReactOnRailsPro.configuration).to receive(:renderer_url).and_return(renderer_url) - allow(ReactOnRailsPro.configuration).to receive(:renderer_http_pool_size).and_return(20) + allow(ReactOnRailsPro.configuration).to receive_messages(renderer_url: renderer_url, renderer_http_pool_size: 20) original_httpx_plugin = HTTPX.method(:plugin) allow(HTTPX).to receive(:plugin) do |*args| @@ -30,13 +29,12 @@ end allow(Rails).to receive(:logger).and_return(logger_mock) - allow(ReactOnRailsPro::ServerRenderingPool::NodeRenderingPool).to receive(:renderer_bundle_file_name) - .and_return(renderer_bundle_file_name) - allow(ReactOnRailsPro::ServerRenderingPool::NodeRenderingPool).to receive(:rsc_renderer_bundle_file_name) - .and_return(rsc_renderer_bundle_file_name) + allow(ReactOnRailsPro::ServerRenderingPool::NodeRenderingPool).to receive_messages( + renderer_bundle_file_name: renderer_bundle_file_name, rsc_renderer_bundle_file_name: rsc_renderer_bundle_file_name + ) allow(ReactOnRails::Utils).to receive(:server_bundle_js_file_path).and_return(server_bundle_path) - allow(ReactOnRails::Utils).to receive(:rsc_bundle_js_file_path).and_return(rsc_server_bundle_path) + allow(ReactOnRailsPro::Utils).to receive(:rsc_bundle_js_file_path).and_return(rsc_server_bundle_path) end after do diff --git a/react_on_rails_pro/spec/react_on_rails_pro/spec_helper.rb b/react_on_rails_pro/spec/react_on_rails_pro/spec_helper.rb index b5757e748a..05de00d9c5 100644 --- a/react_on_rails_pro/spec/react_on_rails_pro/spec_helper.rb +++ b/react_on_rails_pro/spec/react_on_rails_pro/spec_helper.rb @@ -43,6 +43,7 @@ Dir[File.join(__dir__, "support", "**", "*.rb")].each { |f| require f } RSpec.configure do |config| + Rails.logger = Logger.new($stdout) config.example_status_persistence_file_path = "spec/examples.txt" config.run_all_when_everything_filtered = true diff --git a/react_on_rails_pro/spec/react_on_rails_pro/utils_spec.rb b/react_on_rails_pro/spec/react_on_rails_pro/utils_spec.rb index 9f1a985cf4..a31ceebf0f 100644 --- a/react_on_rails_pro/spec/react_on_rails_pro/utils_spec.rb +++ b/react_on_rails_pro/spec/react_on_rails_pro/utils_spec.rb @@ -5,6 +5,10 @@ # rubocop:disable Metrics/ModuleLength module ReactOnRailsPro RSpec.describe Utils do + before do + allow(LicenseValidator).to receive(:validated_license_data!).and_return({}) + end + describe "cache helpers .bundle_hash and .bundle_file_name" do context "with file in manifest", :webpacker do before do @@ -78,13 +82,16 @@ module ReactOnRailsPro rsc_bundle_js_file_path = File.expand_path("./public/#{rsc_bundle_js_file}") allow(Shakapacker).to receive_message_chain("manifest.lookup!") .and_return(rsc_bundle_js_file) - allow(ReactOnRails::Utils).to receive_messages( - server_bundle_js_file_path: rsc_bundle_js_file_path.gsub("rsc-", ""), - rsc_bundle_js_file_path: rsc_bundle_js_file_path - ) + allow(ReactOnRails::Utils).to receive(:server_bundle_js_file_path) + .and_return(rsc_bundle_js_file_path.gsub("rsc-", "")) + allow(described_class).to receive(:rsc_bundle_js_file_path) + .and_return(rsc_bundle_js_file_path) allow(ReactOnRails.configuration) - .to receive_messages(server_bundle_js_file: "webpack-bundle.js", - rsc_bundle_js_file: "rsc-webpack-bundle.js") + .to receive(:server_bundle_js_file) + .and_return("webpack-bundle.js") + allow(ReactOnRailsPro.configuration) + .to receive(:rsc_bundle_js_file) + .and_return("rsc-webpack-bundle.js") allow(Digest::MD5).to receive(:file) .with(rsc_bundle_js_file_path) .and_return("barfoobarfoo") @@ -219,6 +226,216 @@ module ReactOnRailsPro it { expect(printable_cache_key).to eq("1_2_3_4_5") } end + describe ".rsc_support_enabled?" do + context "when RSC support is enabled" do + before do + allow(ReactOnRailsPro.configuration).to receive(:enable_rsc_support).and_return(true) + end + + it "returns true" do + expect(described_class.rsc_support_enabled?).to be(true) + end + end + + context "when RSC support is disabled" do + before do + allow(ReactOnRailsPro.configuration).to receive(:enable_rsc_support).and_return(false) + end + + it "returns false" do + expect(described_class.rsc_support_enabled?).to be(false) + end + end + end + + describe ".rsc_bundle_js_file_path" do + before do + described_class.instance_variable_set(:@rsc_bundle_path, nil) + allow(ReactOnRailsPro.configuration).to receive(:rsc_bundle_js_file).and_return("rsc-bundle.js") + end + + after do + described_class.instance_variable_set(:@rsc_bundle_path, nil) + end + + it "calls bundle_js_file_path with the rsc_bundle_js_file name" do + allow(ReactOnRails::Utils).to receive(:bundle_js_file_path).with("rsc-bundle.js") + .and_return("/some/path/rsc-bundle.js") + + result = described_class.rsc_bundle_js_file_path + + expect(ReactOnRails::Utils).to have_received(:bundle_js_file_path).with("rsc-bundle.js") + expect(result).to eq("/some/path/rsc-bundle.js") + end + + it "caches the path when not in development" do + allow(Rails.env).to receive(:development?).and_return(false) + allow(ReactOnRails::Utils).to receive(:bundle_js_file_path).with("rsc-bundle.js") + .and_return("/some/path/rsc-bundle.js") + + result1 = described_class.rsc_bundle_js_file_path + result2 = described_class.rsc_bundle_js_file_path + + expect(ReactOnRails::Utils).to have_received(:bundle_js_file_path).once.with("rsc-bundle.js") + expect(result1).to eq("/some/path/rsc-bundle.js") + expect(result2).to eq("/some/path/rsc-bundle.js") + end + + it "does not cache the path in development" do + allow(Rails.env).to receive(:development?).and_return(true) + allow(ReactOnRails::Utils).to receive(:bundle_js_file_path).with("rsc-bundle.js") + .and_return("/some/path/rsc-bundle.js") + + result1 = described_class.rsc_bundle_js_file_path + result2 = described_class.rsc_bundle_js_file_path + + expect(ReactOnRails::Utils).to have_received(:bundle_js_file_path).twice.with("rsc-bundle.js") + expect(result1).to eq("/some/path/rsc-bundle.js") + expect(result2).to eq("/some/path/rsc-bundle.js") + end + end + + describe ".react_client_manifest_file_path" do + before do + described_class.instance_variable_set(:@react_client_manifest_path, nil) + allow(ReactOnRailsPro.configuration).to receive(:react_client_manifest_file) + .and_return("react-client-manifest.json") + end + + after do + described_class.instance_variable_set(:@react_client_manifest_path, nil) + end + + context "when using packer" do + let(:public_output_path) { "/path/to/public/webpack/dev" } + + before do + allow(::Shakapacker).to receive_message_chain("config.public_output_path") + .and_return(Pathname.new(public_output_path)) + allow(::Shakapacker).to receive_message_chain("config.public_path") + .and_return(Pathname.new("/path/to/public")) + end + + context "when dev server is running" do + before do + allow(::Shakapacker).to receive(:dev_server).and_return( + instance_double( + ::Shakapacker::DevServer, + running?: true, + protocol: "http", + host_with_port: "localhost:3035" + ) + ) + end + + it "returns manifest URL with dev server path" do + expected_url = "http://localhost:3035/webpack/dev/react-client-manifest.json" + expect(described_class.react_client_manifest_file_path).to eq(expected_url) + end + end + + context "when dev server is not running" do + before do + allow(::Shakapacker).to receive_message_chain("dev_server.running?") + .and_return(false) + end + + it "returns file path to the manifest" do + expected_path = File.join(public_output_path, "react-client-manifest.json") + expect(described_class.react_client_manifest_file_path).to eq(expected_path) + end + end + end + + it "caches the path when not in development" do + allow(Rails.env).to receive(:development?).and_return(false) + allow(ReactOnRails::PackerUtils).to receive(:asset_uri_from_packer) + .with("react-client-manifest.json") + .and_return("/some/path/react-client-manifest.json") + + result1 = described_class.react_client_manifest_file_path + result2 = described_class.react_client_manifest_file_path + + expect(ReactOnRails::PackerUtils).to have_received(:asset_uri_from_packer).once + expect(result1).to eq("/some/path/react-client-manifest.json") + expect(result2).to eq("/some/path/react-client-manifest.json") + end + + it "does not cache the path in development" do + allow(Rails.env).to receive(:development?).and_return(true) + allow(ReactOnRails::PackerUtils).to receive(:asset_uri_from_packer) + .with("react-client-manifest.json") + .and_return("/some/path/react-client-manifest.json") + + result1 = described_class.react_client_manifest_file_path + result2 = described_class.react_client_manifest_file_path + + expect(ReactOnRails::PackerUtils).to have_received(:asset_uri_from_packer).twice + expect(result1).to eq("/some/path/react-client-manifest.json") + expect(result2).to eq("/some/path/react-client-manifest.json") + end + end + + describe ".react_server_client_manifest_file_path" do + let(:asset_name) { "react-server-client-manifest.json" } + + before do + described_class.instance_variable_set(:@react_server_manifest_path, nil) + allow(ReactOnRailsPro.configuration).to receive(:react_server_client_manifest_file).and_return(asset_name) + allow(Rails.env).to receive(:development?).and_return(false) + end + + after do + described_class.instance_variable_set(:@react_server_manifest_path, nil) + end + + it "calls bundle_js_file_path with the correct asset name and returns its value" do + allow(ReactOnRails::Utils).to receive(:bundle_js_file_path).with(asset_name) + .and_return("/some/path/#{asset_name}") + + result = described_class.react_server_client_manifest_file_path + + expect(ReactOnRails::Utils).to have_received(:bundle_js_file_path).with(asset_name) + expect(result).to eq("/some/path/#{asset_name}") + end + + it "caches the path when not in development" do + allow(ReactOnRails::Utils).to receive(:bundle_js_file_path).with(asset_name) + .and_return("/some/path/#{asset_name}") + + result1 = described_class.react_server_client_manifest_file_path + result2 = described_class.react_server_client_manifest_file_path + + expect(ReactOnRails::Utils).to have_received(:bundle_js_file_path).once.with(asset_name) + expect(result1).to eq("/some/path/#{asset_name}") + expect(result2).to eq("/some/path/#{asset_name}") + end + + it "does not cache the path in development" do + allow(Rails.env).to receive(:development?).and_return(true) + allow(ReactOnRails::Utils).to receive(:bundle_js_file_path).with(asset_name) + .and_return("/some/path/#{asset_name}") + + result1 = described_class.react_server_client_manifest_file_path + result2 = described_class.react_server_client_manifest_file_path + + expect(ReactOnRails::Utils).to have_received(:bundle_js_file_path).twice.with(asset_name) + expect(result1).to eq("/some/path/#{asset_name}") + expect(result2).to eq("/some/path/#{asset_name}") + end + + context "when manifest file name is nil" do + before do + allow(ReactOnRailsPro.configuration).to receive(:react_server_client_manifest_file).and_return(nil) + end + + it "raises an error" do + expect { described_class.react_server_client_manifest_file_path } + .to raise_error(ReactOnRailsPro::Error, /react_server_client_manifest_file is nil/) + end + end + end + describe ".pro_attribution_comment" do context "when license is valid and not in grace period" do before do diff --git a/spec/dummy/spec/packs_generator_spec.rb b/spec/dummy/spec/packs_generator_spec.rb index 105756dd17..89ccc7c3a7 100644 --- a/spec/dummy/spec/packs_generator_spec.rb +++ b/spec/dummy/spec/packs_generator_spec.rb @@ -234,8 +234,13 @@ def self.configuration stub_packer_source_path(component_name: components_directory, packer_source_path: packer_source_path) allow(ReactOnRails::Utils).to receive(:react_on_rails_pro?).and_return(true) - allow(ReactOnRailsPro.configuration).to receive_messages( - enable_rsc_support: true + stub_const("ReactOnRailsPro::Utils", Class.new do + def self.rsc_support_enabled? + true + end + end) + allow(ReactOnRailsPro::Utils).to receive_messages( + rsc_support_enabled?: true ) end @@ -309,7 +314,7 @@ def self.configuration context "when RSC support is disabled" do before do - allow(ReactOnRailsPro.configuration).to receive(:enable_rsc_support).and_return(false) + allow(ReactOnRailsPro::Utils).to receive(:rsc_support_enabled?).and_return(false) described_class.instance.generate_packs_if_stale end diff --git a/spec/react_on_rails/configuration_spec.rb b/spec/react_on_rails/configuration_spec.rb index 4d79e0e6d2..e3123fcdb0 100644 --- a/spec/react_on_rails/configuration_spec.rb +++ b/spec/react_on_rails/configuration_spec.rb @@ -190,110 +190,6 @@ module ReactOnRails end end - describe "RSC configuration options" do - before do - allow(ReactOnRails::PackerUtils).to receive_messages( - supports_autobundling?: true, - nested_entries?: true - ) - end - - it "has default values for RSC-related configuration options" do - ReactOnRails.configure {} # rubocop:disable Lint/EmptyBlock - - expect(ReactOnRails.configuration.rsc_bundle_js_file).to eq("") - expect(ReactOnRails.configuration.react_client_manifest_file).to eq("react-client-manifest.json") - expect(ReactOnRails.configuration.react_server_client_manifest_file).to eq("react-server-client-manifest.json") - end - - it "allows setting rsc_bundle_js_file" do - ReactOnRails.configure do |config| - config.rsc_bundle_js_file = "custom-rsc-bundle.js" - end - - expect(ReactOnRails.configuration.rsc_bundle_js_file).to eq("custom-rsc-bundle.js") - end - - it "allows setting react_client_manifest_file" do - ReactOnRails.configure do |config| - config.react_client_manifest_file = "custom-client-manifest.json" - end - - expect(ReactOnRails.configuration.react_client_manifest_file).to eq("custom-client-manifest.json") - end - - it "allows setting react_server_client_manifest_file" do - ReactOnRails.configure do |config| - config.react_server_client_manifest_file = "custom-server-client-manifest.json" - end - - expect(ReactOnRails.configuration.react_server_client_manifest_file).to eq("custom-server-client-manifest.json") - end - - it "includes rsc files in webpack_generated_files when not blank" do - ReactOnRails.configure do |config| - config.rsc_bundle_js_file = "rsc-bundle.js" - config.webpack_generated_files = [] - end - - expect(ReactOnRails.configuration.webpack_generated_files).to include("rsc-bundle.js") - end - - it "includes client manifest in webpack_generated_files" do - ReactOnRails.configure do |config| - config.react_client_manifest_file = "custom-client-manifest.json" - config.webpack_generated_files = [] - end - - expect(ReactOnRails.configuration.webpack_generated_files).to include("custom-client-manifest.json") - end - - it "includes server-client manifest in webpack_generated_files" do - ReactOnRails.configure do |config| - config.react_server_client_manifest_file = "custom-server-client-manifest.json" - config.webpack_generated_files = [] - end - - expect(ReactOnRails.configuration.webpack_generated_files).to include("custom-server-client-manifest.json") - end - - it "configures all RSC options together for a typical RSC setup" do - ReactOnRails.configure do |config| - config.rsc_bundle_js_file = "rsc-bundle.js" - config.react_client_manifest_file = "client-manifest.json" - config.react_server_client_manifest_file = "server-client-manifest.json" - config.webpack_generated_files = [] - end - - expect(ReactOnRails.configuration.rsc_bundle_js_file).to eq("rsc-bundle.js") - expect(ReactOnRails.configuration.react_client_manifest_file).to eq("client-manifest.json") - expect(ReactOnRails.configuration.react_server_client_manifest_file).to eq("server-client-manifest.json") - - # All RSC files should be included in webpack_generated_files - expect(ReactOnRails.configuration.webpack_generated_files).to include("rsc-bundle.js") - expect(ReactOnRails.configuration.webpack_generated_files).to include("client-manifest.json") - expect(ReactOnRails.configuration.webpack_generated_files).to include("server-client-manifest.json") - end - - it "allows nil values for RSC configuration options" do - ReactOnRails.configure do |config| - config.rsc_bundle_js_file = nil - config.react_client_manifest_file = nil - config.react_server_client_manifest_file = nil - config.webpack_generated_files = [] - end - - expect(ReactOnRails.configuration.rsc_bundle_js_file).to be_nil - expect(ReactOnRails.configuration.react_client_manifest_file).to be_nil - expect(ReactOnRails.configuration.react_server_client_manifest_file).to be_nil - - # Nil values should not be included in webpack_generated_files - expect(ReactOnRails.configuration.webpack_generated_files).not_to include(nil) - # Only manifest.json should be in the list by default - expect(ReactOnRails.configuration.webpack_generated_files).to eq(["manifest.json"]) - end - end - it "changes the configuration of the gem, such as setting the prerender option to false" do test_path = File.expand_path("public/webpack/test") allow(::Shakapacker).to receive_message_chain("config.public_output_path") diff --git a/spec/react_on_rails/utils_spec.rb b/spec/react_on_rails/utils_spec.rb index d701c49f55..ca2629704d 100644 --- a/spec/react_on_rails/utils_spec.rb +++ b/spec/react_on_rails/utils_spec.rb @@ -244,11 +244,31 @@ def mock_dev_server_running let(:public_path) { File.expand_path(File.join(packer_public_output_path, rsc_bundle_name)) } let(:ssr_generated_path) { File.expand_path(File.join("ssr-generated", rsc_bundle_name)) } + before do + # Mock Pro gem being available + allow(described_class).to receive(:react_on_rails_pro?).and_return(true) + + # Create a mock Pro module with configuration method + pro_module = Module.new do + def self.configuration + @configuration + end + + def self.configuration=(config) + @configuration = config + end + end + stub_const("ReactOnRailsPro", pro_module) + + pro_config = double("ProConfiguration") # rubocop:disable RSpec/VerifiedDoubles + allow(pro_config).to receive_messages(rsc_bundle_js_file: rsc_bundle_name, + react_server_client_manifest_file: nil) + ReactOnRailsPro.configuration = pro_config + end + context "with enforce_private_server_bundles=false" do before do mock_missing_manifest_entry(rsc_bundle_name) - allow(ReactOnRails).to receive_message_chain("configuration.rsc_bundle_js_file") - .and_return(rsc_bundle_name) allow(ReactOnRails).to receive_message_chain("configuration.server_bundle_output_path") .and_return("ssr-generated") allow(ReactOnRails).to receive_message_chain("configuration.enforce_private_server_bundles") @@ -283,8 +303,6 @@ def mock_dev_server_running context "with enforce_private_server_bundles=true" do before do mock_missing_manifest_entry(rsc_bundle_name) - allow(ReactOnRails).to receive_message_chain("configuration.rsc_bundle_js_file") - .and_return(rsc_bundle_name) allow(ReactOnRails).to receive_message_chain("configuration.server_bundle_output_path") .and_return("ssr-generated") allow(ReactOnRails).to receive_message_chain("configuration.enforce_private_server_bundles") @@ -448,102 +466,6 @@ def mock_dev_server_running end end end - - describe ".rsc_bundle_js_file_path with #{packer_type} enabled" do - let(:packer_public_output_path) { Pathname.new("public/webpack/development") } - - include_context "with #{packer_type} enabled" - - context "with server file not in manifest", packer_type.to_sym do - context "with enforce_private_server_bundles=false" do - it "returns the private ssr-generated path for RSC bundles" do - server_bundle_name = "rsc-bundle.js" - mock_bundle_configs(rsc_bundle_name: server_bundle_name) - mock_missing_manifest_entry(server_bundle_name) - - path = described_class.rsc_bundle_js_file_path - - expect(path).to end_with("ssr-generated/#{server_bundle_name}") - end - end - - context "with enforce_private_server_bundles=true" do - it "returns the private ssr-generated path for RSC bundles without checking public paths" do - server_bundle_name = "rsc-bundle.js" - mock_missing_manifest_entry(server_bundle_name) - allow(ReactOnRails).to receive_message_chain("configuration.server_bundle_js_file") - .and_return("server-bundle.js") # Different from RSC bundle name - allow(ReactOnRails).to receive_message_chain("configuration.rsc_bundle_js_file") - .and_return(server_bundle_name) - allow(ReactOnRails).to receive_message_chain("configuration.server_bundle_output_path") - .and_return("ssr-generated") - allow(ReactOnRails).to receive_message_chain("configuration.enforce_private_server_bundles") - .and_return(true) - - # Should not check public paths when enforcement is enabled - public_path = File.expand_path(File.join(packer_public_output_path, server_bundle_name)) - expect(File).not_to receive(:exist?).with(public_path) - - path = described_class.rsc_bundle_js_file_path - expect(path).to end_with("ssr-generated/#{server_bundle_name}") - end - end - end - - context "with server file in the manifest, used for client", packer_type.to_sym do - it "returns the correct path hashed server path" do - # Use Shakapacker directly instead of packer method - mock_bundle_configs(rsc_bundle_name: "webpack-bundle.js") - # Clear server_bundle_output_path to test manifest behavior - allow(ReactOnRails).to receive_message_chain("configuration.server_bundle_output_path") - .and_return(nil) - allow(ReactOnRails).to receive_message_chain("configuration.same_bundle_for_client_and_server") - .and_return(true) - mock_bundle_in_manifest("webpack-bundle.js", "webpack/development/webpack-bundle-123456.js") - allow(Shakapacker).to receive_message_chain("dev_server.running?") - .and_return(false) - - path = described_class.rsc_bundle_js_file_path - expect(path).to end_with("public/webpack/development/webpack-bundle-123456.js") - expect(path).to start_with("/") - end - - context "with webpack-dev-server running, and same file used for server and client" do - it "returns the correct path hashed server path" do - mock_bundle_configs(rsc_bundle_name: "webpack-bundle.js") - # Clear server_bundle_output_path to test manifest behavior - allow(ReactOnRails).to receive_message_chain("configuration.server_bundle_output_path") - .and_return(nil) - allow(ReactOnRails).to receive_message_chain("configuration.same_bundle_for_client_and_server") - .and_return(true) - mock_dev_server_running - mock_bundle_in_manifest("webpack-bundle.js", "/webpack/development/webpack-bundle-123456.js") - - path = described_class.rsc_bundle_js_file_path - - expect(path).to eq("http://localhost:3035/webpack/development/webpack-bundle-123456.js") - end - end - end - - context "with dev-server running, and server file in the manifest, and separate client/server files", - packer_type.to_sym do - it "returns the correct path hashed server path" do - mock_bundle_configs(rsc_bundle_name: "rsc-bundle.js") - # Clear server_bundle_output_path to test manifest behavior - allow(ReactOnRails).to receive_message_chain("configuration.server_bundle_output_path") - .and_return(nil) - allow(ReactOnRails).to receive_message_chain("configuration.same_bundle_for_client_and_server") - .and_return(false) - mock_bundle_in_manifest("rsc-bundle.js", "webpack/development/server-bundle-123456.js") - mock_dev_server_running - - path = described_class.rsc_bundle_js_file_path - - expect(path).to end_with("/public/webpack/development/server-bundle-123456.js") - end - end - end end describe ".wrap_message" do @@ -734,109 +656,7 @@ def mock_dev_server_running end end - describe ".react_client_manifest_file_path" do - before do - described_class.instance_variable_set(:@react_client_manifest_path, nil) - allow(ReactOnRails.configuration).to receive(:react_client_manifest_file) - .and_return("react-client-manifest.json") - end - - after do - described_class.instance_variable_set(:@react_client_manifest_path, nil) - end - - context "when using packer" do - let(:public_output_path) { "/path/to/public/webpack/dev" } - - before do - allow(::Shakapacker).to receive_message_chain("config.public_output_path") - .and_return(Pathname.new(public_output_path)) - allow(::Shakapacker).to receive_message_chain("config.public_path") - .and_return(Pathname.new("/path/to/public")) - end - - context "when dev server is running" do - before do - allow(::Shakapacker).to receive(:dev_server).and_return( - instance_double( - ::Shakapacker::DevServer, - running?: true, - protocol: "http", - host_with_port: "localhost:3035" - ) - ) - end - - it "returns manifest URL with dev server path" do - expected_url = "http://localhost:3035/webpack/dev/react-client-manifest.json" - expect(described_class.react_client_manifest_file_path).to eq(expected_url) - end - end - - context "when dev server is not running" do - before do - allow(::Shakapacker).to receive_message_chain("dev_server.running?") - .and_return(false) - end - - it "returns file path to the manifest" do - expected_path = File.join(public_output_path, "react-client-manifest.json") - expect(described_class.react_client_manifest_file_path).to eq(expected_path) - end - end - end - end - - describe ".react_server_client_manifest_file_path" do - let(:asset_name) { "react-server-client-manifest.json" } - - before do - described_class.instance_variable_set(:@react_server_manifest_path, nil) - allow(ReactOnRails.configuration).to receive(:react_server_client_manifest_file).and_return(asset_name) - allow(Rails.env).to receive(:development?).and_return(false) - end - - after do - described_class.instance_variable_set(:@react_server_manifest_path, nil) - end - - it "calls bundle_js_file_path with the correct asset name and returns its value" do - allow(described_class).to receive(:bundle_js_file_path).with(asset_name).and_return("/some/path/#{asset_name}") - result = described_class.react_server_client_manifest_file_path - expect(described_class).to have_received(:bundle_js_file_path).with(asset_name) - expect(result).to eq("/some/path/#{asset_name}") - end - - it "caches the path when not in development" do - allow(described_class).to receive(:bundle_js_file_path).with(asset_name).and_return("/some/path/#{asset_name}") - result1 = described_class.react_server_client_manifest_file_path - result2 = described_class.react_server_client_manifest_file_path - expect(described_class).to have_received(:bundle_js_file_path).once.with(asset_name) - expect(result1).to eq("/some/path/#{asset_name}") - expect(result2).to eq("/some/path/#{asset_name}") - end - - it "does not cache the path in development" do - allow(Rails.env).to receive(:development?).and_return(true) - allow(described_class).to receive(:bundle_js_file_path).with(asset_name).and_return("/some/path/#{asset_name}") - result1 = described_class.react_server_client_manifest_file_path - result2 = described_class.react_server_client_manifest_file_path - expect(described_class).to have_received(:bundle_js_file_path).twice.with(asset_name) - expect(result1).to eq("/some/path/#{asset_name}") - expect(result2).to eq("/some/path/#{asset_name}") - end - - context "when manifest file name is nil" do - before do - allow(ReactOnRails.configuration).to receive(:react_server_client_manifest_file).and_return(nil) - end - - it "raises an error" do - expect { described_class.react_server_client_manifest_file_path } - .to raise_error(ReactOnRails::Error, /react_server_client_manifest_file is nil/) - end - end - end + # RSC utility method tests moved to react_on_rails_pro/spec/react_on_rails_pro/utils_spec.rb end end # rubocop:enable Metrics/ModuleLength, Metrics/BlockLength