diff --git a/LICENSE.md b/LICENSE.md
index 1d78ca5849..a6ba12b5c4 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -11,7 +11,7 @@ This repository contains code under two different licenses:
The following directories and all their contents are licensed under the **MIT License** (see full text below):
-- `lib/react_on_rails/` (excluding `lib/react_on_rails/pro/`)
+- `lib/react_on_rails/` (entire directory)
- `packages/react-on-rails/` (entire package)
- All other directories in this repository not explicitly listed as Pro-licensed
@@ -19,7 +19,6 @@ The following directories and all their contents are licensed under the **MIT Li
The following directories and all their contents are licensed under the **React on Rails Pro License**:
-- `lib/react_on_rails/pro/`
- `packages/react-on-rails-pro/` (entire package)
- `react_on_rails_pro/` (entire directory)
diff --git a/docs/MONOREPO_MERGER_PLAN.md b/docs/MONOREPO_MERGER_PLAN.md
index ac4bf0054c..16979f45b4 100644
--- a/docs/MONOREPO_MERGER_PLAN.md
+++ b/docs/MONOREPO_MERGER_PLAN.md
@@ -404,7 +404,74 @@ After the initial merge, the following CI adjustments may be needed:
---
-#### PR #5: Add Pro Node Renderer Package
+#### PR #5: Move Pro Features from Core Gem to Pro Gem
+
+**Branch:** `move-pro-features-to-pro-gem`
+
+**Objectives:**
+
+- Move all Pro features from `lib/react_on_rails/pro/` to Pro gem
+- Delete `lib/react_on_rails/pro/` directory entirely
+- Ensure core gem is 100% MIT licensed with zero Pro code
+
+**Tasks:**
+
+- [x] Move `immediate_hydration` config from core gem to Pro gem (default: true in Pro)
+- [x] Refactor `RenderOptions` to remove Pro utilities
+- [x] Refactor helper methods (`generate_component_script`, `generate_store_script`) to use data enhancement pattern
+- [x] Create Pro helper module in Pro gem with enhancement methods
+- [x] Delete `lib/react_on_rails/pro/` directory entirely
+- [x] Update LICENSE.md to remove `lib/react_on_rails/pro/` reference
+- [x] Update tests in both gems
+
+**Implementation Details:**
+
+The `immediate_hydration` feature was the only Pro feature in the core gem. The refactoring uses a data enhancement pattern:
+
+1. Core gem collects script attributes/content as data structures (not HTML)
+2. If Pro gem loaded, it modifies the data (adds attributes, adds extra scripts)
+3. Core gem generates final HTML from the (possibly enhanced) data
+
+**Benefits:**
+
+- ✅ Clean separation: Core gem = 100% MIT, Pro gem = 100% Pro license
+- ✅ No HTML parsing needed
+- ✅ No Pro warning badge needed (can't enable Pro features without Pro gem)
+- ✅ Better architecture: Core gem doesn't know about Pro internals
+
+**License Compliance:**
+
+- [x] **CRITICAL: Update LICENSE.md:**
+
+ ```md
+ ## MIT License applies to:
+
+ - `lib/react_on_rails/` (entire directory)
+ - `packages/react-on-rails/` (entire package)
+
+ ## React on Rails Pro License applies to:
+
+ - `packages/react-on-rails-pro/` (entire package)
+ - `react_on_rails_pro/` (entire directory)
+ ```
+
+- [x] Verify no Pro code remains in core gem directories
+
+**Success Criteria:** ✅ All CI checks pass + `lib/react_on_rails/pro/` deleted + Core gem is 100% MIT licensed
+
+**Estimated Duration:** 2-3 days
+
+**Risk Level:** Medium (requires careful refactoring of helper methods)
+
+**Developer Notes:**
+
+- The core gem now calls `ReactOnRailsPro::Helper.enhance_component_script_data` and `ReactOnRailsPro::Helper.enhance_store_script_data` if Pro gem is loaded
+- The `immediate_hydration` method in `RenderOptions` now uses `retrieve_react_on_rails_pro_config_value_for(:immediate_hydration)`
+- Tests have been updated to mock Pro gem functionality
+
+---
+
+#### PR #6: Add Pro Node Renderer Package
**Branch:** `add-pro-node-renderer`
@@ -455,7 +522,7 @@ After the initial merge, the following CI adjustments may be needed:
### Phase 6: Final Monorepo Restructuring
-#### PR #6: Restructure Ruby Gems to Final Layout
+#### PR #7: Restructure Ruby Gems to Final Layout
**Branch:** `restructure-ruby-gems`
@@ -522,7 +589,7 @@ After the initial merge, the following CI adjustments may be needed:
### Phase 7: CI/CD & Tooling Unification
-#### PR #7: Unify CI/CD Configuration
+#### PR #8: Unify CI/CD Configuration
**Branch:** `unify-cicd`
@@ -588,7 +655,7 @@ After the initial merge, the following CI adjustments may be needed:
### Phase 8: Documentation & Polish
-#### PR #8: Update Documentation & Examples
+#### PR #9: Update Documentation & Examples
**Branch:** `update-docs-examples`
diff --git a/lib/react_on_rails/configuration.rb b/lib/react_on_rails/configuration.rb
index 3ef7332d2c..2338d763dc 100644
--- a/lib/react_on_rails/configuration.rb
+++ b/lib/react_on_rails/configuration.rb
@@ -46,8 +46,6 @@ def self.configuration
components_subdirectory: nil,
make_generated_server_bundle_the_entrypoint: false,
defer_generated_component_packs: false,
- # React on Rails Pro (licensed) feature - enables immediate hydration of React components
- immediate_hydration: false,
# Maximum time in milliseconds to wait for client-side component registration after page load.
# 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.
@@ -72,7 +70,7 @@ 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,
+ :generated_component_packs_loading_strategy, :rsc_bundle_js_file,
:react_client_manifest_file, :react_server_client_manifest_file, :component_registry_timeout,
:server_bundle_output_path, :enforce_private_server_bundles
@@ -89,7 +87,7 @@ def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender
same_bundle_for_client_and_server: nil,
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,
+ components_subdirectory: nil, auto_load_bundle: 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
@@ -134,7 +132,6 @@ def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender
self.auto_load_bundle = auto_load_bundle
self.make_generated_server_bundle_the_entrypoint = make_generated_server_bundle_the_entrypoint
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
diff --git a/lib/react_on_rails/controller.rb b/lib/react_on_rails/controller.rb
index ae254b1a5a..6fb82fb5ea 100644
--- a/lib/react_on_rails/controller.rb
+++ b/lib/react_on_rails/controller.rb
@@ -15,7 +15,10 @@ module Controller
# Be sure to include view helper `redux_store_hydration_data` at the end of your layout or view
# or else there will be no client side hydration of your stores.
def redux_store(store_name, props: {}, immediate_hydration: nil)
- immediate_hydration = ReactOnRails.configuration.immediate_hydration if immediate_hydration.nil?
+ if immediate_hydration.nil? && ReactOnRails::Utils.react_on_rails_pro?
+ immediate_hydration = ReactOnRailsPro.configuration.immediate_hydration
+ end
+ immediate_hydration = false if immediate_hydration.nil?
redux_store_data = { store_name: store_name,
props: props,
immediate_hydration: immediate_hydration }
diff --git a/lib/react_on_rails/helper.rb b/lib/react_on_rails/helper.rb
index da6dd8995b..b60a46d906 100644
--- a/lib/react_on_rails/helper.rb
+++ b/lib/react_on_rails/helper.rb
@@ -11,12 +11,10 @@
require "react_on_rails/utils"
require "react_on_rails/json_output"
require "active_support/concern"
-require "react_on_rails/pro/helper"
module ReactOnRails
module Helper
include ReactOnRails::Utils::Required
- include ReactOnRails::Pro::Helper
COMPONENT_HTML_KEY = "componentHtml"
@@ -255,7 +253,10 @@ def react_component_hash(component_name, options = {})
# immediate_hydration: false -- React on Rails Pro (licensed) feature. Pass as true if you wish to
# hydrate this store immediately instead of waiting for the page to load.
def redux_store(store_name, props: {}, defer: false, immediate_hydration: nil)
- immediate_hydration = ReactOnRails.configuration.immediate_hydration if immediate_hydration.nil?
+ if immediate_hydration.nil? && ReactOnRails::Utils.react_on_rails_pro?
+ immediate_hydration = ReactOnRailsPro.configuration.immediate_hydration
+ end
+ immediate_hydration = false if immediate_hydration.nil?
redux_store_data = { store_name: store_name,
props: props,
@@ -793,6 +794,68 @@ def in_mailer?
instrument_method :react_component_hash, type: "ReactOnRails", name: "react_component_hash"
end
+ # Generates the complete component specification script tag.
+ # Handles both immediate hydration (Pro feature) and standard cases.
+ def generate_component_script(render_options)
+ # Collect script data
+ script_attrs = {
+ type: "application/json",
+ class: "js-react-on-rails-component",
+ id: "js-react-on-rails-component-#{render_options.dom_id}",
+ "data-component-name" => render_options.react_component_name,
+ "data-trace" => (render_options.trace ? true : nil),
+ "data-dom-id" => render_options.dom_id,
+ "data-store-dependencies" => render_options.store_dependencies&.to_json
+ }
+
+ script_content = json_safe_and_pretty(render_options.client_props).html_safe
+ additional_scripts = []
+
+ # Let Pro gem enhance if available
+ if ReactOnRails::Utils.react_on_rails_pro?
+ result = ReactOnRailsPro::Helper.enhance_component_script_data(
+ script_attrs: script_attrs,
+ script_content: script_content,
+ render_options: render_options
+ )
+ script_attrs = result[:script_attrs]
+ script_content = result[:script_content]
+ additional_scripts = result[:additional_scripts]
+ end
+
+ # Generate final HTML
+ main_script = content_tag(:script, script_content, script_attrs)
+ ([main_script] + additional_scripts).join("\n").html_safe
+ end
+
+ # Generates the complete store hydration script tag.
+ # Handles both immediate hydration (Pro feature) and standard cases.
+ def generate_store_script(redux_store_data)
+ script_attrs = {
+ type: "application/json",
+ "data-js-react-on-rails-store" => redux_store_data[:store_name].html_safe
+ }
+
+ script_content = json_safe_and_pretty(redux_store_data[:props]).html_safe
+ additional_scripts = []
+
+ # Let Pro gem enhance if available
+ if ReactOnRails::Utils.react_on_rails_pro?
+ result = ReactOnRailsPro::Helper.enhance_store_script_data(
+ script_attrs: script_attrs,
+ script_content: script_content,
+ redux_store_data: redux_store_data
+ )
+ script_attrs = result[:script_attrs]
+ script_content = result[:script_content]
+ additional_scripts = result[:additional_scripts]
+ end
+
+ # Generate final HTML
+ main_script = content_tag(:script, script_content, script_attrs)
+ ([main_script] + additional_scripts).join("\n").html_safe
+ end
+
def raise_missing_autoloaded_bundle(react_component_name)
msg = <<~MSG
**ERROR** ReactOnRails: Component "#{react_component_name}" is configured as "auto_load_bundle: true"
diff --git a/lib/react_on_rails/pro/NOTICE b/lib/react_on_rails/pro/NOTICE
deleted file mode 100644
index 2c0b1bb4cd..0000000000
--- a/lib/react_on_rails/pro/NOTICE
+++ /dev/null
@@ -1,21 +0,0 @@
-# React on Rails Pro License
-
-The files in this directory and its subdirectories are licensed under the **React on Rails Pro** license, which is separate from the MIT license that covers the core React on Rails functionality.
-
-## License Terms
-
-These files are proprietary software and are **NOT** covered by the MIT license found in the root LICENSE.md file. Usage requires a valid React on Rails Pro license.
-
-## Distribution
-
-Files in this directory will be **omitted** from future distributions of the open source React on Rails Ruby gem. They are exclusively available to React on Rails Pro licensees.
-
-## License Reference
-
-For the complete React on Rails Pro license terms, see: `REACT-ON-RAILS-PRO-LICENSE.md` in the root directory of this repository.
-
-## More Information
-
-For React on Rails Pro licensing information and to obtain a license, please visit:
-- [React on Rails Pro](https://www.shakacode.com/react-on-rails-pro/)
-- Contact: [react_on_rails@shakacode.com](mailto:react_on_rails@shakacode.com)
\ No newline at end of file
diff --git a/lib/react_on_rails/pro/helper.rb b/lib/react_on_rails/pro/helper.rb
deleted file mode 100644
index 54db997f03..0000000000
--- a/lib/react_on_rails/pro/helper.rb
+++ /dev/null
@@ -1,122 +0,0 @@
-# frozen_string_literal: true
-
-# /*
-# * Copyright (c) 2025 Shakacode LLC
-# *
-# * This file is NOT licensed under the MIT (open source) license.
-# * It is part of the React on Rails Pro offering and is licensed separately.
-# *
-# * Unauthorized copying, modification, distribution, or use of this file,
-# * via any medium, is strictly prohibited without a valid license agreement
-# * from Shakacode LLC.
-# *
-# * For licensing terms, please see:
-# * https://github.com/shakacode/react_on_rails/blob/master/REACT-ON-RAILS-PRO-LICENSE.md
-# */
-
-module ReactOnRails
- module Pro
- module Helper
- IMMEDIATE_HYDRATION_PRO_WARNING = "[REACT ON RAILS] The 'immediate_hydration' feature requires a " \
- "React on Rails Pro license. " \
- "Please visit https://shakacode.com/react-on-rails-pro to learn more."
-
- # Generates the complete component specification script tag.
- # Handles both immediate hydration (Pro feature) and standard cases.
- def generate_component_script(render_options)
- # Setup the page_loaded_js, which is the same regardless of prerendering or not!
- # The reason is that React is smart about not doing extra work if the server rendering did its job.
- component_specification_tag = content_tag(:script,
- json_safe_and_pretty(render_options.client_props).html_safe,
- type: "application/json",
- class: "js-react-on-rails-component",
- id: "js-react-on-rails-component-#{render_options.dom_id}",
- "data-component-name" => render_options.react_component_name,
- "data-trace" => (render_options.trace ? true : nil),
- "data-dom-id" => render_options.dom_id,
- "data-store-dependencies" =>
- render_options.store_dependencies&.to_json,
- "data-immediate-hydration" =>
- (render_options.immediate_hydration ? true : nil))
-
- # Add immediate invocation script if immediate hydration is enabled
- spec_tag = if render_options.immediate_hydration
- # Escape dom_id for JavaScript context
- escaped_dom_id = escape_javascript(render_options.dom_id)
- immediate_script = content_tag(:script, %(
- typeof ReactOnRails === 'object' && ReactOnRails.reactOnRailsComponentLoaded('#{escaped_dom_id}');
- ).html_safe)
- "#{component_specification_tag}\n#{immediate_script}"
- else
- component_specification_tag
- end
-
- pro_warning_badge = pro_warning_badge_if_needed(render_options.explicitly_disabled_pro_options)
- "#{pro_warning_badge}\n#{spec_tag}".html_safe
- end
-
- # Generates the complete store hydration script tag.
- # Handles both immediate hydration (Pro feature) and standard cases.
- def generate_store_script(redux_store_data)
- pro_options_check_result = ReactOnRails::Pro::Utils.disable_pro_render_options_if_not_licensed(redux_store_data)
- redux_store_data = pro_options_check_result[:raw_options]
- explicitly_disabled_pro_options = pro_options_check_result[:explicitly_disabled_pro_options]
-
- store_hydration_data = content_tag(:script,
- json_safe_and_pretty(redux_store_data[:props]).html_safe,
- type: "application/json",
- "data-js-react-on-rails-store" => redux_store_data[:store_name].html_safe,
- "data-immediate-hydration" =>
- (redux_store_data[:immediate_hydration] ? true : nil))
-
- # Add immediate invocation script if immediate hydration is enabled and Pro license is valid
- store_hydration_scripts = if redux_store_data[:immediate_hydration]
- # Escape store_name for JavaScript context
- escaped_store_name = escape_javascript(redux_store_data[:store_name])
- immediate_script = content_tag(:script, <<~JS.strip_heredoc.html_safe
- typeof ReactOnRails === 'object' && ReactOnRails.reactOnRailsStoreLoaded('#{escaped_store_name}');
- JS
- )
- "#{store_hydration_data}\n#{immediate_script}"
- else
- store_hydration_data
- end
-
- pro_warning_badge = pro_warning_badge_if_needed(explicitly_disabled_pro_options)
- "#{pro_warning_badge}\n#{store_hydration_scripts}".html_safe
- end
-
- def pro_warning_badge_if_needed(explicitly_disabled_pro_options)
- return "" unless explicitly_disabled_pro_options.any?
-
- disabled_features_message = disabled_pro_features_message(explicitly_disabled_pro_options)
- warning_message = "[REACT ON RAILS] #{disabled_features_message}\n" \
- "Please visit https://shakacode.com/react-on-rails-pro to learn more."
- puts warning_message
- Rails.logger.warn warning_message
-
- tooltip_text = "#{disabled_features_message} Click to learn more."
-
- <<~HTML.strip
-
-