diff --git a/.gitignore b/.gitignore index f0178bd96a..aac5c06f93 100644 --- a/.gitignore +++ b/.gitignore @@ -61,5 +61,8 @@ yalc.lock # Generated by ROR FS-based Registry generated +# Server-side rendering generated bundles +ssr-generated + # Claude Code local settings .claude/settings.local.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 24c0790740..3ff1de26dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,45 @@ After a release, please make sure to run `bundle exec rake update_changelog`. Th Changes since the last non-beta release. +#### Breaking Changes + +- **Removed `generated_assets_dirs` configuration**: The legacy `config.generated_assets_dirs` option is no longer supported and will raise an error if used. Since Shakapacker is now required, asset paths are automatically determined from `shakapacker.yml` configuration. Remove any `config.generated_assets_dirs` from your `config/initializers/react_on_rails.rb` file. Use `public_output_path` in `config/shakapacker.yml` to customize asset output location instead. [PR 1798](https://github.com/shakacode/react_on_rails/pull/1798) + +#### New Features + +- **Server Bundle Security**: Added new configuration options for enhanced server bundle security and organization: + + - `server_bundle_output_path`: Configurable directory (relative to the Rails root) for server bundle output (default: "ssr-generated"). If set to `nil`, the server bundle will be loaded from the same public directory as client bundles. + - `enforce_private_server_bundles`: When enabled, ensures server bundles are only loaded from private directories outside the public folder (default: false for backward compatibility) + +- **Improved Bundle Path Resolution**: Bundle path resolution for server bundles now works as follows: + - If `server_bundle_output_path` is set, the server bundle is loaded from that directory. + - If `server_bundle_output_path` is not set, the server bundle falls back to the client bundle directory (typically the public output path). + - If `enforce_private_server_bundles` is enabled: + - The server bundle will only be loaded from the private directory specified by `server_bundle_output_path`. + - If the bundle is not found there, it will _not_ fall back to the public directory. + - If `enforce_private_server_bundles` is not enabled and the bundle is not found in the private directory, it will fall back to the public directory. + - This logic ensures that, when strict enforcement is enabled, server bundles are never loaded from public directories, improving security and clarity of bundle resolution. + +#### API Improvements + +- **Method Naming Clarification**: Added `public_bundles_full_path` method to clarify bundle path handling: + - `public_bundles_full_path`: New method specifically for webpack bundles in public directories + - `generated_assets_full_path`: Now deprecated (backwards-compatible alias) + - This eliminates confusion between webpack bundles and general Rails public assets + +#### Security Enhancements + +- **Private Server Bundle Enforcement**: When `enforce_private_server_bundles` is enabled, server bundles bypass public directory fallbacks and are only loaded from designated private locations +- **Path Validation**: Added validation to ensure `server_bundle_output_path` points to private directories when enforcement is enabled + +#### Bug Fixes + +- **Non-Packer Environment Compatibility**: Fixed potential NoMethodError when using bundle path resolution in environments without Shakapacker +- **Shakapacker version requirements**: Fixed inconsistent version requirements between basic pack generation (6.5.1+) and advanced auto-bundling features (7.0.0+). Added backward compatibility for users on Shakapacker 6.5.1-6.9.x while providing clear upgrade guidance for advanced features. Added new constants `MINIMUM_SHAKAPACKER_VERSION_FOR_AUTO_BUNDLING` and improved version checking performance with caching. [PR 1798](https://github.com/shakacode/react_on_rails/pull/1798) + +### [16.0.1-rc.2] - 2025-09-20 + #### Bug Fixes - **Packs generator**: Fixed error when `server_bundle_js_file` configuration is empty (default). Added safety check to prevent attempting operations on invalid file paths when server-side rendering is not configured. [PR 1802](https://github.com/shakacode/react_on_rails/pull/1802) diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md index 82f6731351..1d7a7e4b91 100644 --- a/docs/guides/configuration.md +++ b/docs/guides/configuration.md @@ -129,6 +129,51 @@ ReactOnRails.configure do |config| # 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" + ################################################################################ + # SERVER BUNDLE SECURITY AND ORGANIZATION + ################################################################################ + + # This configures the directory (relative to the Rails root) where the server bundle will be output. + # By default, this is "ssr-generated". If set to nil, the server bundle will be loaded from the same + # public directory as client bundles. For enhanced security, use this option in conjunction with + # `enforce_private_server_bundles` to ensure server bundles are only loaded from private directories + # config.server_bundle_output_path = "ssr-generated" + + # When set to true, React on Rails will only load server bundles from private, explicitly configured directories (such as `ssr-generated`), and will raise an error if a server bundle is found in a public or untrusted location. This helps prevent accidental or malicious execution of untrusted JavaScript on the server, and is strongly recommended for production environments. And prevent leakage of server-side code to the client (Especially in the case of RSC). + # Default is false for backward compatibility, but enabling this option is a best practice for security. + config.enforce_private_server_bundles = false + + ################################################################################ + # BUNDLE ORGANIZATION EXAMPLES + ################################################################################ + # + # This configuration creates a clear separation between client and server assets: + # + # CLIENT BUNDLES (Public, Web-Accessible): + # Location: public/webpack/[environment]/ or public/packs/ (According to your shakapacker.yml configuration) + # Files: application.js, manifest.json, CSS files + # Served by: Web server directly + # Access: ReactOnRails::Utils.public_bundles_full_path + # + # SERVER BUNDLES (Private, Server-Only): + # Location: ssr-generated/ (when server_bundle_output_path configured) + # Files: server-bundle.js, rsc-bundle.js + # Served by: Never served to browsers + # Access: ReactOnRails::Utils.server_bundle_js_file_path + # + # Example directory structure with recommended configuration: + # app/ + # ├── ssr-generated/ # Private server bundles + # │ ├── server-bundle.js + # │ └── rsc-bundle.js + # └── public/ + # └── webpack/development/ # Public client bundles + # ├── application.js + # ├── manifest.json + # └── styles.css + # + ################################################################################ + # `prerender` means server-side rendering # default is false. This is an option for view helpers `render_component` and `render_component_hash`. # Set to true to change the default value to true. diff --git a/lib/generators/react_on_rails/base_generator.rb b/lib/generators/react_on_rails/base_generator.rb index 2b3a197c67..06f5c208fd 100644 --- a/lib/generators/react_on_rails/base_generator.rb +++ b/lib/generators/react_on_rails/base_generator.rb @@ -24,7 +24,7 @@ def add_hello_world_route end def create_react_directories - # Create auto-registration directory structure for non-Redux components only + # Create auto-bundling directory structure for non-Redux components only # Redux components handle their own directory structure return if options.redux? @@ -136,14 +136,17 @@ def update_gitignore_for_auto_registration return unless File.exist?(gitignore_path) gitignore_content = File.read(gitignore_path) - return if gitignore_content.include?("**/generated/**") - append_to_file ".gitignore" do - <<~GITIGNORE + additions = [] + additions << "**/generated/**" unless gitignore_content.include?("**/generated/**") + additions << "ssr-generated" unless gitignore_content.include?("ssr-generated") + + return if additions.empty? - # Generated React on Rails packs - **/generated/** - GITIGNORE + append_to_file ".gitignore" do + lines = ["\n# Generated React on Rails packs"] + lines.concat(additions) + "#{lines.join("\n")}\n" end end diff --git a/lib/generators/react_on_rails/react_with_redux_generator.rb b/lib/generators/react_on_rails/react_with_redux_generator.rb index 19076b4718..643b306cfa 100644 --- a/lib/generators/react_on_rails/react_with_redux_generator.rb +++ b/lib/generators/react_on_rails/react_with_redux_generator.rb @@ -19,7 +19,7 @@ class ReactWithReduxGenerator < Rails::Generators::Base aliases: "-T" def create_redux_directories - # Create auto-registration directory structure for Redux + # Create auto-bundling directory structure for Redux empty_directory("app/javascript/src/HelloWorldApp/ror_components") # Create Redux support directories within the component directory @@ -31,7 +31,7 @@ def copy_base_files base_js_path = "redux/base" ext = component_extension(options) - # Copy Redux-connected component to auto-registration structure + # Copy Redux-connected component to auto-bundling structure copy_file("#{base_js_path}/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.client.#{ext}", "app/javascript/src/HelloWorldApp/ror_components/HelloWorldApp.client.#{ext}") copy_file("#{base_js_path}/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.server.#{ext}", diff --git a/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt b/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt index 223169639a..4b2f750f39 100644 --- a/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt +++ b/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt @@ -43,6 +43,20 @@ ReactOnRails.configure do |config| # config.server_bundle_js_file = "server-bundle.js" + # The location of the server bundle file defaults to the location of your client bundle file. + # However, you can change this location in your config/shakapacker.yml file, using Shakapacker v8.5.0 or greater + # If you are not using Shakapacker v8.5.0 or greater, then you can set the value here. v17 will probably remove configuration + # of the server_bundle_output_path. + + # Configure where server bundles are output. Defaults to "ssr-generated". + # This should match your webpack configuration for server bundles. + config.server_bundle_output_path = "ssr-generated" + + # Enforce that server bundles are only loaded from private (non-public) directories. + # When true, server bundles will only be loaded from the configured server_bundle_output_path. + # This is recommended for production to prevent server-side code from being exposed. + config.enforce_private_server_bundles = true + ################################################################################ ################################################################################ # FILE SYSTEM BASED COMPONENT REGISTRY diff --git a/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt b/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt index a1f40f065d..deb724704d 100644 --- a/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt +++ b/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt @@ -44,13 +44,14 @@ const configureServer = () => { // Custom output for the server-bundle that matches the config in // config/initializers/react_on_rails.rb + // Server bundles are output to a private directory (not public) for security serverWebpackConfig.output = { filename: 'server-bundle.js', globalObject: 'this', // If using the React on Rails Pro node server renderer, uncomment the next line // libraryTarget: 'commonjs2', - path: config.outputPath, - publicPath: config.publicPath, + path: require('path').resolve(__dirname, '../../ssr-generated'), + // No publicPath needed since server bundles are not served via web // https://webpack.js.org/configuration/output/#outputglobalobject }; diff --git a/lib/react_on_rails/configuration.rb b/lib/react_on_rails/configuration.rb index d3047a59ed..5cece82754 100644 --- a/lib/react_on_rails/configuration.rb +++ b/lib/react_on_rails/configuration.rb @@ -52,7 +52,9 @@ def self.configuration # If exceeded, an error will be thrown for server-side rendered components not registered on the client. # Set to 0 to disable the timeout and wait indefinitely for component registration. component_registry_timeout: DEFAULT_COMPONENT_REGISTRY_TIMEOUT, - generated_component_packs_loading_strategy: nil + generated_component_packs_loading_strategy: nil, + server_bundle_output_path: "ssr-generated", + enforce_private_server_bundles: false ) end @@ -68,7 +70,8 @@ class Configuration :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 + :react_client_manifest_file, :react_server_client_manifest_file, :component_registry_timeout, + :server_bundle_output_path, :enforce_private_server_bundles # rubocop:disable Metrics/AbcSize def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender: nil, @@ -85,7 +88,7 @@ def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender 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) + 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 self.generated_assets_dir = generated_assets_dir @@ -130,6 +133,8 @@ def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender self.defer_generated_component_packs = defer_generated_component_packs self.immediate_hydration = immediate_hydration self.generated_component_packs_loading_strategy = generated_component_packs_loading_strategy + self.server_bundle_output_path = server_bundle_output_path + self.enforce_private_server_bundles = enforce_private_server_bundles end # rubocop:enable Metrics/AbcSize @@ -139,13 +144,13 @@ def setup_config_values ensure_webpack_generated_files_exists configure_generated_assets_dirs_deprecation configure_skip_display_none_deprecation - ensure_generated_assets_dir_present check_server_render_method_is_only_execjs error_if_using_packer_and_generated_assets_dir_not_match_public_output_path # check_deprecated_settings adjust_precompile_task check_component_registry_timeout validate_generated_component_packs_loading_strategy + validate_enforce_private_server_bundles end private @@ -194,23 +199,41 @@ def validate_generated_component_packs_loading_strategy raise ReactOnRails::Error, "generated_component_packs_loading_strategy must be either :async, :defer, or :sync" end - def check_autobundling_requirements - raise_missing_components_subdirectory if auto_load_bundle && !components_subdirectory.present? - return unless components_subdirectory.present? + def validate_enforce_private_server_bundles + return unless enforce_private_server_bundles + + # Check if server_bundle_output_path is nil + if server_bundle_output_path.nil? + raise ReactOnRails::Error, "enforce_private_server_bundles is set to true, but " \ + "server_bundle_output_path is nil. Please set server_bundle_output_path " \ + "to a directory outside of the public directory." + end + + # Check if server_bundle_output_path is inside public directory + # Skip validation if Rails.root is not available (e.g., in tests) + return unless defined?(Rails) && Rails.root + + public_path = Rails.root.join("public").to_s + server_output_path = File.expand_path(server_bundle_output_path, Rails.root.to_s) + + return unless server_output_path.start_with?(public_path) - # Check basic pack generation support for auto_load_bundle + raise ReactOnRails::Error, "enforce_private_server_bundles is set to true, but " \ + "server_bundle_output_path (#{server_bundle_output_path}) is inside " \ + "the public directory. Please set it to a directory outside of public." + end + + def check_minimum_shakapacker_version ReactOnRails::PackerUtils.raise_shakapacker_version_incompatible_for_basic_pack_generation unless ReactOnRails::PackerUtils.supports_basic_pack_generation? + end - # Additional checks for advanced features requiring nested entries - if ReactOnRails::PackerUtils.supports_autobundling? - ReactOnRails::PackerUtils.raise_nested_entries_disabled unless ReactOnRails::PackerUtils.nested_entries? - else - # Warn users about missing advanced features but don't block basic functionality - min_version = ReactOnRails::PacksGenerator::MINIMUM_SHAKAPACKER_VERSION_FOR_AUTO_REGISTRATION - Rails.logger.warn("React on Rails: Basic pack generation enabled. " \ - "Upgrade to Shakapacker #{min_version}+ for advanced auto-registration features.") - end + def check_autobundling_requirements + raise_missing_components_subdirectory unless components_subdirectory.present? + + ReactOnRails::PackerUtils.raise_shakapacker_version_incompatible_for_autobundling unless + ReactOnRails::PackerUtils.supports_autobundling? + ReactOnRails::PackerUtils.raise_nested_entries_disabled unless ReactOnRails::PackerUtils.nested_entries? end def adjust_precompile_task @@ -250,15 +273,15 @@ def error_if_using_packer_and_generated_assets_dir_not_match_public_output_path if File.expand_path(generated_assets_dir) == packer_public_output_path.to_s Rails.logger.warn("You specified generated_assets_dir in `config/initializers/react_on_rails.rb` " \ - "with shakapacker. " \ + "with Shakapacker. " \ "Remove this line from your configuration file.") else msg = <<~MSG - Configuration mismatch in config/initializers/react_on_rails.rb: - - Your generated_assets_dir setting (#{generated_assets_dir}) does not match the value for public_output_path (#{packer_public_output_path}). - - Remove the generated_assets_dir configuration and let Shakapacker manage the output path. + Error configuring /config/initializers/react_on_rails.rb: You are using Shakapacker + and your specified value for generated_assets_dir = #{generated_assets_dir} + that does not match the value for public_output_path specified in + shakapacker.yml = #{packer_public_output_path}. You should remove the configuration + value for "generated_assets_dir" from your config/initializers/react_on_rails.rb file. MSG raise ReactOnRails::Error, msg end @@ -276,23 +299,20 @@ def check_server_render_method_is_only_execjs raise ReactOnRails::Error, msg end - def ensure_generated_assets_dir_present - return if generated_assets_dir.present? - - # When using Shakapacker, don't set a default generated_assets_dir since - # Shakapacker manages its own public_output_path configuration - # This prevents configuration mismatches between ReactOnRails and Shakapacker - Rails.logger.warn "ReactOnRails: No generated_assets_dir specified, using Shakapacker public_output_path" - end - def configure_generated_assets_dirs_deprecation return if generated_assets_dirs.blank? packer_public_output_path = ReactOnRails::PackerUtils.packer_public_output_path - Rails.logger.warn "You specified generated_assets_dirs in `config/initializers/react_on_rails.rb` " \ - "with Shakapacker. Remove this configuration as the output path is automatically " \ - "determined by `public_output_path` in shakapacker.yml " \ - "(currently: #{packer_public_output_path})." + + msg = <<~MSG + ReactOnRails Configuration Error: The 'generated_assets_dirs' configuration option is no longer supported. + Since Shakapacker is now required, asset paths are automatically determined from your shakapacker.yml configuration. + Please remove 'config.generated_assets_dirs' from your config/initializers/react_on_rails.rb file. + Assets will be loaded from: #{packer_public_output_path} + If you need to customize the output path, configure it in config/shakapacker.yml under 'public_output_path'. + MSG + + raise ReactOnRails::Error, msg end def ensure_webpack_generated_files_exists diff --git a/lib/react_on_rails/packer_utils.rb b/lib/react_on_rails/packer_utils.rb index 9e78e272e0..d1349466f7 100644 --- a/lib/react_on_rails/packer_utils.rb +++ b/lib/react_on_rails/packer_utils.rb @@ -41,7 +41,7 @@ def self.supports_basic_pack_generation? end def self.supports_autobundling? - min_version = ReactOnRails::PacksGenerator::MINIMUM_SHAKAPACKER_VERSION_FOR_AUTO_REGISTRATION + min_version = ReactOnRails::PacksGenerator::MINIMUM_SHAKAPACKER_VERSION_FOR_AUTO_BUNDLING ::Shakapacker.config.respond_to?(:nested_entries?) && shakapacker_version_requirement_met?(min_version) end @@ -115,7 +115,7 @@ def self.check_manifest_not_cached msg = <<-MSG.strip_heredoc ERROR: you have enabled cache_manifest in the #{Rails.env} env when using the ReactOnRails::TestHelper.configure_rspec_to_compile_assets helper - To fix this: edit your config/shaka::Shakapacker.yml file and set cache_manifest to false for test. + To fix this: edit your config/shakapacker.yml file and set cache_manifest to false for test. MSG puts wrap_message(msg) exit! @@ -135,8 +135,8 @@ def self.webpack_assets_status_checker def self.raise_nested_entries_disabled msg = <<~MSG - **ERROR** ReactOnRails: `nested_entries` is configured to be disabled in shaka::Shakapacker. Please update \ - config/shaka::Shakapacker.yml to enable nested entries. for more information read + **ERROR** ReactOnRails: `nested_entries` is configured to be disabled in shakapacker. Please update \ + config/shakapacker.yml to enable nested entries. for more information read https://www.shakacode.com/react-on-rails/docs/guides/file-system-based-automated-bundle-generation.md#enable-nested_entries-for-shakapacker MSG @@ -145,7 +145,7 @@ def self.raise_nested_entries_disabled def self.raise_shakapacker_version_incompatible_for_autobundling msg = <<~MSG - **ERROR** ReactOnRails: Please upgrade ::Shakapacker to version #{ReactOnRails::PacksGenerator::MINIMUM_SHAKAPACKER_VERSION_FOR_AUTO_REGISTRATION} or \ + **ERROR** ReactOnRails: Please upgrade ::Shakapacker to version #{ReactOnRails::PacksGenerator::MINIMUM_SHAKAPACKER_VERSION_FOR_AUTO_BUNDLING} or \ above to use the automated bundle generation feature (which requires nested_entries support). \ The currently installed version is #{ReactOnRails::PackerUtils.shakapacker_version}. \ Basic pack generation requires ::Shakapacker #{ReactOnRails::PacksGenerator::MINIMUM_SHAKAPACKER_VERSION} or above. @@ -162,15 +162,5 @@ def self.raise_shakapacker_version_incompatible_for_basic_pack_generation raise ReactOnRails::Error, msg end - - def self.raise_shakapacker_not_installed - msg = <<~MSG - **ERROR** ReactOnRails: Missing ::Shakapacker gem. Please upgrade to use ::Shakapacker \ - #{ReactOnRails::PacksGenerator::MINIMUM_SHAKAPACKER_VERSION} or above to use the \ - automated bundle generation feature. - MSG - - raise ReactOnRails::Error, msg - end end end diff --git a/lib/react_on_rails/packs_generator.rb b/lib/react_on_rails/packs_generator.rb index 171fe0e2d0..09807a288c 100644 --- a/lib/react_on_rails/packs_generator.rb +++ b/lib/react_on_rails/packs_generator.rb @@ -10,7 +10,7 @@ class PacksGenerator COMPONENT_EXTENSIONS = /\.(jsx?|tsx?)$/ MINIMUM_SHAKAPACKER_VERSION = "6.5.1" # Auto-registration requires nested_entries support which was added in 7.0.0 - MINIMUM_SHAKAPACKER_VERSION_FOR_AUTO_REGISTRATION = "7.0.0" + MINIMUM_SHAKAPACKER_VERSION_FOR_AUTO_BUNDLING = "7.0.0" def self.instance @instance ||= PacksGenerator.new @@ -166,7 +166,7 @@ def generated_server_pack_file_content def add_generated_pack_to_server_bundle return if ReactOnRails.configuration.make_generated_server_bundle_the_entrypoint - return if ReactOnRails.configuration.server_bundle_js_file.empty? + return if ReactOnRails.configuration.server_bundle_js_file.blank? relative_path_to_generated_server_bundle = relative_path(server_bundle_entrypoint, generated_server_bundle_file_path) diff --git a/lib/react_on_rails/system_checker.rb b/lib/react_on_rails/system_checker.rb index 935a2b8d0c..cc7bdca932 100644 --- a/lib/react_on_rails/system_checker.rb +++ b/lib/react_on_rails/system_checker.rb @@ -622,10 +622,10 @@ def report_shakapacker_version_with_threshold Gem::Version.new(version) if ReactOnRails::PackerUtils.supports_autobundling? - add_success("✅ Shakapacker #{version} (supports React on Rails auto-registration)") + add_success("✅ Shakapacker #{version} (supports React on Rails auto-bundling)") else add_warning("⚠️ Shakapacker #{version} - Version 7.0+ with nested_entries support needed " \ - "for React on Rails auto-registration") + "for React on Rails auto-bundling") end rescue ArgumentError # Fallback for invalid version strings diff --git a/lib/react_on_rails/utils.rb b/lib/react_on_rails/utils.rb index 1f148188d8..60e15664f5 100644 --- a/lib/react_on_rails/utils.rb +++ b/lib/react_on_rails/utils.rb @@ -72,30 +72,70 @@ def self.server_bundle_path_is_http? end def self.bundle_js_file_path(bundle_name) - # Either: - # 1. Using same bundle for both server and client, so server bundle will be hashed in manifest - # 2. Using a different bundle (different Webpack config), so file is not hashed, and - # bundle_js_path will throw so the default path is used without a hash. - # 3. The third option of having the server bundle hashed and a different configuration than - # the client bundle is not supported for 2 reasons: - # a. The webpack manifest plugin would have a race condition where the same manifest.json - # is edited by both the webpack-dev-server - # b. There is no good reason to hash the server bundle name. + # Priority order depends on bundle type: + # SERVER BUNDLES (normal case): Try private non-public locations first, then manifest, then legacy + # CLIENT BUNDLES (normal case): Try manifest first, then fallback locations if bundle_name == "manifest.json" # Default to the non-hashed name in the specified output directory, which, for legacy # React on Rails, this is the output directory picked up by the asset pipeline. # For Shakapacker, this is the public output path defined in the (shaka/web)packer.yml file. - File.join(generated_assets_full_path, bundle_name) + File.join(public_bundles_full_path, bundle_name) else - begin - ReactOnRails::PackerUtils.bundle_js_uri_from_packer(bundle_name) - rescue Shakapacker::Manifest::MissingEntryError - File.expand_path( - File.join(ReactOnRails::PackerUtils.packer_public_output_path, - bundle_name) - ) + bundle_js_file_path_with_packer(bundle_name) + end + end + + private_class_method def self.bundle_js_file_path_with_packer(bundle_name) + is_server_bundle = server_bundle?(bundle_name) + config = ReactOnRails.configuration + root_path = Rails.root || "." + + # If server bundle and server_bundle_output_path is configured, return that path directly + if is_server_bundle && config.server_bundle_output_path.present? + private_server_bundle_path = File.expand_path(File.join(root_path, config.server_bundle_output_path, + bundle_name)) + + # Don't fall back to public directory if enforce_private_server_bundles is enabled + if config.enforce_private_server_bundles || File.exist?(private_server_bundle_path) + return private_server_bundle_path + end + end + + # Try manifest lookup for all bundles + begin + ReactOnRails::PackerUtils.bundle_js_uri_from_packer(bundle_name) + rescue Shakapacker::Manifest::MissingEntryError + handle_missing_manifest_entry(bundle_name, is_server_bundle) + end + end + + 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 + end + + private_class_method def self.handle_missing_manifest_entry(bundle_name, is_server_bundle) + config = ReactOnRails.configuration + root_path = Rails.root || "." + + # For server bundles with server_bundle_output_path configured, use that + if is_server_bundle && config.server_bundle_output_path.present? + candidate_paths = [File.expand_path(File.join(root_path, config.server_bundle_output_path, bundle_name))] + unless config.enforce_private_server_bundles + candidate_paths << File.expand_path(File.join(ReactOnRails::PackerUtils.packer_public_output_path, + bundle_name)) + end + + candidate_paths.each do |path| + return path if File.exist?(path) end + return candidate_paths.first end + + # For client bundles and server bundles without special config, use packer's public path + # This returns the environment-specific path configured in shakapacker.yml + File.expand_path(File.join(ReactOnRails::PackerUtils.packer_public_output_path, bundle_name)) end def self.server_bundle_js_file_path @@ -130,7 +170,7 @@ def self.react_server_client_manifest_file_path "react_server_client_manifest_file is nil, ensure it is set in your configuration" end - @react_server_manifest_path = File.join(generated_assets_full_path, asset_name) + @react_server_manifest_path = File.join(public_bundles_full_path, asset_name) end def self.running_on_windows? @@ -166,10 +206,15 @@ def self.using_packer_source_path_is_not_defined_and_custom_node_modules? ReactOnRails.configuration.node_modules_location.present? end - def self.generated_assets_full_path + def self.public_bundles_full_path ReactOnRails::PackerUtils.packer_public_output_path end + # DEPRECATED: Use public_bundles_full_path for clarity about public vs private bundle paths + def self.generated_assets_full_path + public_bundles_full_path + end + def self.gem_available?(name) Gem.loaded_specs[name].present? rescue Gem::LoadError diff --git a/spec/dummy/config/initializers/react_on_rails.rb b/spec/dummy/config/initializers/react_on_rails.rb index b1c70a9722..b1a46ea5db 100644 --- a/spec/dummy/config/initializers/react_on_rails.rb +++ b/spec/dummy/config/initializers/react_on_rails.rb @@ -27,6 +27,11 @@ def self.adjust_props_for_client_side_hydration(component_name, props) config.server_bundle_js_file = "server-bundle.js" config.random_dom_id = false # default is true + # Set server_bundle_output_path to nil so bundles are read from public path + # This ensures the dummy app uses the standard webpack output location + config.server_bundle_output_path = nil + config.enforce_private_server_bundles = false + # Uncomment to test these # config.build_test_command = "yarn run build:test" # config.build_production_command = "RAILS_ENV=production NODE_ENV=production bin/shakapacker" diff --git a/spec/react_on_rails/configuration_spec.rb b/spec/react_on_rails/configuration_spec.rb index c3037546cf..4d79e0e6d2 100644 --- a/spec/react_on_rails/configuration_spec.rb +++ b/spec/react_on_rails/configuration_spec.rb @@ -12,6 +12,10 @@ module ReactOnRails before do ReactOnRails.instance_variable_set(:@configuration, nil) + # Mock PackerUtils to avoid Shakapacker dependency in tests + allow(Rails).to receive(:root).and_return(Pathname.new("/fake/rails/root")) unless Rails.root + allow(ReactOnRails::PackerUtils).to receive(:packer_public_output_path) + .and_return(File.expand_path(File.join(Rails.root, "public/packs"))) end after do @@ -21,13 +25,15 @@ module ReactOnRails describe "generated_assets_dir" do let(:using_packer) { true } let(:packer_public_output_path) do - File.expand_path(File.join(Rails.root, "public/webpack/dev")) + File.expand_path(File.join(Rails.root, "public/packs")) end before do allow(Rails).to receive(:root).and_return(File.expand_path(".")) allow(::Shakapacker).to receive_message_chain("config.public_output_path") .and_return(Pathname.new(packer_public_output_path)) + allow(ReactOnRails::PackerUtils).to receive(:packer_public_output_path) + .and_return(packer_public_output_path) end it "does not throw if the generated assets dir is blank with shakapacker" do @@ -41,7 +47,7 @@ module ReactOnRails it "does not throw if the packer_public_output_path does match the generated assets dir" do expect do ReactOnRails.configure do |config| - config.generated_assets_dir = "public/webpack/dev" + config.generated_assets_dir = "public/packs" end end.not_to raise_error end @@ -292,6 +298,8 @@ module ReactOnRails test_path = File.expand_path("public/webpack/test") allow(::Shakapacker).to receive_message_chain("config.public_output_path") .and_return(Pathname.new(test_path)) + allow(ReactOnRails::PackerUtils).to receive(:packer_public_output_path) + .and_return(test_path) ReactOnRails.configure do |config| config.generated_assets_dir = test_path @@ -307,6 +315,8 @@ module ReactOnRails test_path = File.expand_path("public/webpack/test") allow(::Shakapacker).to receive_message_chain("config.public_output_path") .and_return(Pathname.new(test_path)) + allow(ReactOnRails::PackerUtils).to receive(:packer_public_output_path) + .and_return(test_path) ReactOnRails.configure do |config| config.generated_assets_dir = test_path @@ -459,6 +469,62 @@ module ReactOnRails end end end + + describe "enforce_private_server_bundles validation" do + context "when enforce_private_server_bundles is true" do + before do + # Mock Rails.root for tests that need path validation + allow(Rails).to receive(:root).and_return(Pathname.new("/test/app")) + end + + it "raises error when server_bundle_output_path is nil" do + expect do + ReactOnRails.configure do |config| + config.server_bundle_output_path = nil + config.enforce_private_server_bundles = true + end + end.to raise_error(ReactOnRails::Error, /server_bundle_output_path is nil/) + end + + it "raises error when server_bundle_output_path is inside public directory" do + expect do + ReactOnRails.configure do |config| + config.server_bundle_output_path = "public/server-bundles" + config.enforce_private_server_bundles = true + end + end.to raise_error(ReactOnRails::Error, /is inside the public directory/) + end + + it "allows server_bundle_output_path outside public directory" do + expect do + ReactOnRails.configure do |config| + config.server_bundle_output_path = "ssr-generated" + config.enforce_private_server_bundles = true + end + end.not_to raise_error + end + end + + context "when enforce_private_server_bundles is false" do + it "allows server_bundle_output_path to be nil" do + expect do + ReactOnRails.configure do |config| + config.server_bundle_output_path = nil + config.enforce_private_server_bundles = false + end + end.not_to raise_error + end + + it "allows server_bundle_output_path inside public directory" do + expect do + ReactOnRails.configure do |config| + config.server_bundle_output_path = "public/server-bundles" + config.enforce_private_server_bundles = false + end + end.not_to raise_error + end + end + end end end diff --git a/spec/react_on_rails/packer_utils_spec.rb b/spec/react_on_rails/packer_utils_spec.rb index 4f94a7384a..6636eaa65f 100644 --- a/spec/react_on_rails/packer_utils_spec.rb +++ b/spec/react_on_rails/packer_utils_spec.rb @@ -118,7 +118,7 @@ module ReactOnRails it "returns true when ::Shakapacker >= 7.0.0 with nested_entries support" do allow(mock_config).to receive(:respond_to?).with(:nested_entries?).and_return(true) allow(described_class).to receive(:shakapacker_version_requirement_met?) - .with(ReactOnRails::PacksGenerator::MINIMUM_SHAKAPACKER_VERSION_FOR_AUTO_REGISTRATION).and_return(true) + .with(ReactOnRails::PacksGenerator::MINIMUM_SHAKAPACKER_VERSION_FOR_AUTO_BUNDLING).and_return(true) expect(described_class.supports_autobundling?).to be(true) end @@ -126,7 +126,7 @@ module ReactOnRails it "returns false when ::Shakapacker < 7.0.0" do allow(mock_config).to receive(:respond_to?).with(:nested_entries?).and_return(true) allow(described_class).to receive(:shakapacker_version_requirement_met?) - .with(ReactOnRails::PacksGenerator::MINIMUM_SHAKAPACKER_VERSION_FOR_AUTO_REGISTRATION).and_return(false) + .with(ReactOnRails::PacksGenerator::MINIMUM_SHAKAPACKER_VERSION_FOR_AUTO_BUNDLING).and_return(false) expect(described_class.supports_autobundling?).to be(false) end @@ -134,7 +134,7 @@ module ReactOnRails it "returns false when nested_entries method is not available" do allow(mock_config).to receive(:respond_to?).with(:nested_entries?).and_return(false) allow(described_class).to receive(:shakapacker_version_requirement_met?) - .with(ReactOnRails::PacksGenerator::MINIMUM_SHAKAPACKER_VERSION_FOR_AUTO_REGISTRATION).and_return(true) + .with(ReactOnRails::PacksGenerator::MINIMUM_SHAKAPACKER_VERSION_FOR_AUTO_BUNDLING).and_return(true) expect(described_class.supports_autobundling?).to be(false) end @@ -144,13 +144,13 @@ module ReactOnRails describe "version constants validation" do it "ensures MINIMUM_SHAKAPACKER_VERSION constants are properly defined" do expect(ReactOnRails::PacksGenerator::MINIMUM_SHAKAPACKER_VERSION).to eq("6.5.1") - expect(ReactOnRails::PacksGenerator::MINIMUM_SHAKAPACKER_VERSION_FOR_AUTO_REGISTRATION).to eq("7.0.0") + expect(ReactOnRails::PacksGenerator::MINIMUM_SHAKAPACKER_VERSION_FOR_AUTO_BUNDLING).to eq("7.0.0") end it "ensures version requirements are logically consistent" do basic_version = Gem::Version.new(ReactOnRails::PacksGenerator::MINIMUM_SHAKAPACKER_VERSION) auto_reg_version = Gem::Version.new( - ReactOnRails::PacksGenerator::MINIMUM_SHAKAPACKER_VERSION_FOR_AUTO_REGISTRATION + ReactOnRails::PacksGenerator::MINIMUM_SHAKAPACKER_VERSION_FOR_AUTO_BUNDLING ) expect(auto_reg_version).to be >= basic_version, diff --git a/spec/react_on_rails/test_helper/webpack_assets_status_checker_spec.rb b/spec/react_on_rails/test_helper/webpack_assets_status_checker_spec.rb index 3e48778c9d..76b46e52f5 100644 --- a/spec/react_on_rails/test_helper/webpack_assets_status_checker_spec.rb +++ b/spec/react_on_rails/test_helper/webpack_assets_status_checker_spec.rb @@ -62,7 +62,7 @@ let(:fixture_dirname) { "assets_with_manifest_exist_server_bundle_separate" } before do - Packer = Shakapacker # rubocop:disable Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration + stub_const("Packer", Shakapacker) allow(ReactOnRails::PackerUtils).to receive_messages( manifest_exists?: true, packer_public_output_path: generated_assets_full_path @@ -73,7 +73,7 @@ .and_return(File.join(generated_assets_full_path, "manifest.json")) allow(ReactOnRails::PackerUtils).to receive(:bundle_js_uri_from_packer) .with("server-bundle.js") - .and_raise(Packer::Manifest::MissingEntryError) + .and_raise(Shakapacker::Manifest::MissingEntryError) touch_files_in_dir(generated_assets_full_path) end diff --git a/spec/react_on_rails/utils_spec.rb b/spec/react_on_rails/utils_spec.rb index dae9850b83..34e94a0ab3 100644 --- a/spec/react_on_rails/utils_spec.rb +++ b/spec/react_on_rails/utils_spec.rb @@ -61,6 +61,10 @@ def mock_bundle_configs(server_bundle_name: random_bundle_name, rsc_bundle_name: .and_return(server_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") + .and_return(false) end def mock_dev_server_running @@ -88,6 +92,14 @@ def mock_dev_server_running end describe ".bundle_js_file_path" do + before do + # Mock configuration calls to avoid server bundle detection for regular client bundles + allow(ReactOnRails).to receive_message_chain("configuration.server_bundle_js_file") + .and_return("server-bundle.js") + allow(ReactOnRails).to receive_message_chain("configuration.rsc_bundle_js_file") + .and_return("rsc-bundle.js") + end + subject do described_class.bundle_js_file_path("webpack-bundle.js") end @@ -117,6 +129,90 @@ def mock_dev_server_running it { is_expected.to eq("#{packer_public_output_path}/manifest.json") } end + + context "when file not in manifest" do + before do + mock_missing_manifest_entry("webpack-bundle.js") + end + + let(:env_specific_path) { File.join(packer_public_output_path, "webpack-bundle.js") } + + it "returns environment-specific path" do + result = described_class.bundle_js_file_path("webpack-bundle.js") + expect(result).to eq(File.expand_path(env_specific_path)) + end + end + + context "with server bundle (SSR/RSC) file not in manifest" do + let(:server_bundle_name) { "server-bundle.js" } + let(:ssr_generated_path) { File.expand_path(File.join("ssr-generated", server_bundle_name)) } + + context "with server_bundle_output_path configured" do + before do + mock_missing_manifest_entry(server_bundle_name) + allow(ReactOnRails).to receive_message_chain("configuration.server_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(false) + end + + it "returns configured path directly without checking existence" do + # When enforce_private_server_bundles is false, it will check File.exist? + # for both private and public paths, but should still return the configured path + public_path = File.expand_path(File.join(packer_public_output_path, server_bundle_name)) + allow(File).to receive(:exist?).with(ssr_generated_path).and_return(false) + allow(File).to receive(:exist?).with(public_path).and_return(false) + + result = described_class.bundle_js_file_path(server_bundle_name) + expect(result).to eq(ssr_generated_path) + end + end + + context "without server_bundle_output_path configured" do + before do + mock_missing_manifest_entry(server_bundle_name) + allow(ReactOnRails).to receive_message_chain("configuration.server_bundle_js_file") + .and_return(server_bundle_name) + allow(ReactOnRails).to receive_message_chain("configuration.server_bundle_output_path") + .and_return(nil) + allow(ReactOnRails).to receive_message_chain("configuration.enforce_private_server_bundles") + .and_return(false) + end + + it "uses packer public output path" do + result = described_class.bundle_js_file_path(server_bundle_name) + expect(result).to eq(File.expand_path(File.join(packer_public_output_path, server_bundle_name))) + end + end + end + + context "with RSC bundle file not in manifest" do + let(:rsc_bundle_name) { "rsc-bundle.js" } + let(:ssr_generated_path) { File.expand_path(File.join("ssr-generated", rsc_bundle_name)) } + + 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") + .and_return(false) + end + + it "treats RSC bundles as server bundles and returns configured path directly" do + # When enforce_private_server_bundles is false, it will check File.exist? + # for both private and public paths, but should still return the configured path + public_path = File.expand_path(File.join(packer_public_output_path, rsc_bundle_name)) + allow(File).to receive(:exist?).with(ssr_generated_path).and_return(false) + allow(File).to receive(:exist?).with(public_path).and_return(false) + + result = described_class.bundle_js_file_path(rsc_bundle_name) + expect(result).to eq(ssr_generated_path) + end + end end end end @@ -157,25 +253,49 @@ def mock_dev_server_running include_context "with #{packer_type} enabled" context "with server file not in manifest", packer_type.to_sym do - it "returns the unhashed server path" do + it "returns the private ssr-generated path for server bundles" do server_bundle_name = "server-bundle.js" mock_bundle_configs(server_bundle_name: server_bundle_name) mock_missing_manifest_entry(server_bundle_name) path = described_class.server_bundle_js_file_path - expect(path).to end_with("public/webpack/development/#{server_bundle_name}") + expect(path).to end_with("ssr-generated/#{server_bundle_name}") + end + + context "with server_bundle_output_path configured" do + it "returns the configured path directly without checking file existence" do + server_bundle_name = "server-bundle.js" + mock_bundle_configs(server_bundle_name: server_bundle_name) + mock_missing_manifest_entry(server_bundle_name) + # NOTE: mock_bundle_configs sets enforce_private_server_bundles to false + + # Since server_bundle_output_path is configured, it will try manifest lookup first, + # then fall back to candidate paths. With enforce_private_server_bundles=false, + # it will check File.exist? for both private and public paths + ssr_generated_path = File.expand_path(File.join("ssr-generated", server_bundle_name)) + public_path = File.expand_path(File.join(packer_public_output_path, server_bundle_name)) + allow(File).to receive(:exist?).with(ssr_generated_path).and_return(false) + allow(File).to receive(:exist?).with(public_path).and_return(false) + + path = described_class.server_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 - packer = ::Shakapacker + # Use Shakapacker directly instead of packer method mock_bundle_configs(server_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(packer).to receive_message_chain("dev_server.running?") + allow(Shakapacker).to receive_message_chain("dev_server.running?") .and_return(false) path = described_class.server_bundle_js_file_path @@ -186,6 +306,9 @@ def mock_dev_server_running 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(server_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 @@ -202,6 +325,9 @@ def mock_dev_server_running packer_type.to_sym do it "returns the correct path hashed server path" do mock_bundle_configs(server_bundle_name: "server-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("server-bundle.js", "webpack/development/server-bundle-123456.js") @@ -220,25 +346,28 @@ def mock_dev_server_running include_context "with #{packer_type} enabled" context "with server file not in manifest", packer_type.to_sym do - it "returns the unhashed server path" 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("public/webpack/development/#{server_bundle_name}") + expect(path).to end_with("ssr-generated/#{server_bundle_name}") 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 - packer = ::Shakapacker + # 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(packer).to receive_message_chain("dev_server.running?") + allow(Shakapacker).to receive_message_chain("dev_server.running?") .and_return(false) path = described_class.rsc_bundle_js_file_path @@ -249,6 +378,9 @@ def mock_dev_server_running 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 @@ -265,6 +397,9 @@ def mock_dev_server_running 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") @@ -534,7 +669,7 @@ def mock_dev_server_running context "when in development environment" do before do allow(Rails.env).to receive(:development?).and_return(true) - allow(described_class).to receive(:generated_assets_full_path) + allow(described_class).to receive(:public_bundles_full_path) .and_return("/path/to/generated/assets") end @@ -554,7 +689,7 @@ def mock_dev_server_running context "when not in development environment" do before do - allow(described_class).to receive(:generated_assets_full_path) + allow(described_class).to receive(:public_bundles_full_path) .and_return("/path/to/generated/assets") end @@ -574,7 +709,7 @@ def mock_dev_server_running context "with different manifest file names" do before do - allow(described_class).to receive(:generated_assets_full_path) + allow(described_class).to receive(:public_bundles_full_path) .and_return("/path/to/generated/assets") end @@ -599,7 +734,7 @@ def mock_dev_server_running before do allow(ReactOnRails.configuration).to receive(:react_server_client_manifest_file) .and_return(nil) - allow(described_class).to receive(:generated_assets_full_path) + allow(described_class).to receive(:public_bundles_full_path) .and_return("/path/to/generated/assets") end