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