Skip to content

Commit 3e664d9

Browse files
Implement Pro license structure and feature gating (#1791)
* Separate core MIT and Pro-licensed functionality into distinct directories * Add runtime Pro license validation with graceful fallback to core features * Enhance security with CSS.escape() for DOM selectors and XSS protection * Centralize Pro feature utilities and clean architectural separation * Add comprehensive license documentation with NOTICE files in Pro directories This major refactoring establishes clear boundaries between open source and proprietary code while maintaining full backward compatibility. Pro features now require valid licensing and display warning badges when unavailable.
1 parent 2b14683 commit 3e664d9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+700
-187
lines changed

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,21 @@ After a release, please make sure to run `bundle exec rake update_changelog`. Th
2323

2424
Changes since the last non-beta release.
2525

26+
#### Pro License Structure Implementation
27+
28+
**🔐 License Architecture**
29+
30+
- **Core/Pro separation**: Moved Pro features into dedicated `lib/react_on_rails/pro/` and `node_package/src/pro/` directories with clear licensing boundaries [PR 1791](https://github.com/shakacode/react_on_rails/pull/1791) by [abanoubghadban](https://github.com/AbanoubGhadban)
31+
- **Runtime license validation**: Implemented Pro license gating with graceful fallback to core functionality when Pro license unavailable
32+
- **License documentation**: Added NOTICE files in Pro directories referencing canonical `REACT-ON-RAILS-PRO-LICENSE.md`
33+
- **Updated LICENSE.md**: Clearly distinguishes core MIT license from Pro-licensed directories
34+
35+
**⚡ Pro Feature Enhancements**
36+
37+
- **Immediate hydration**: Enhanced immediate hydration functionality with Pro license validation and warning badges
38+
- **Security improvements**: Hardened DOM selectors using `CSS.escape()` and proper JavaScript escaping for XSS protection
39+
- **Architecture refactoring**: Centralized Pro utilities and clean separation between core and Pro helper functionality
40+
2641
#### Enhanced TypeScript Generator Support
2742

2843
**🔧 Generator Improvements**

LICENSE.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55

66
---
77

8-
# MIT License
8+
## MIT License for Core React on Rails
9+
10+
This license applies to all files within this repository, with the exception of the code located in the following directories, which are licensed separately under the React on Rails Pro License:
11+
12+
- `lib/react_on_rails/pro/`
13+
- `node_package/src/pro/`
914

1015
Copyright (c) 2017, 2018 Justin Gordon and ShakaCode
1116
Copyright (c) 2015–2025 ShakaCode, LLC
@@ -31,3 +36,12 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
3136
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
3237
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
3338
SOFTWARE.
39+
40+
---
41+
42+
## React on Rails Pro License
43+
44+
The code in the directories listed above is part of the React on Rails Pro framework and is licensed under the React on Rails Pro License.
45+
46+
You can find the full text of the license agreement here:
47+
[REACT-ON-RAILS-PRO-LICENSE.md](./REACT-ON-RAILS-PRO-LICENSE.md)

knip.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,32 @@ const config: KnipConfig = {
66
'.': {
77
entry: [
88
'node_package/src/ReactOnRails.node.ts!',
9-
'node_package/src/ReactOnRailsRSC.ts!',
10-
'node_package/src/registerServerComponent/client.tsx!',
11-
'node_package/src/registerServerComponent/server.tsx!',
12-
'node_package/src/registerServerComponent/server.rsc.ts!',
13-
'node_package/src/wrapServerComponentRenderer/server.tsx!',
14-
'node_package/src/wrapServerComponentRenderer/server.rsc.tsx!',
15-
'node_package/src/RSCRoute.tsx!',
16-
'node_package/src/ServerComponentFetchError.ts!',
9+
'node_package/src/pro/ReactOnRailsRSC.ts!',
10+
'node_package/src/pro/registerServerComponent/client.tsx!',
11+
'node_package/src/pro/registerServerComponent/server.tsx!',
12+
'node_package/src/pro/registerServerComponent/server.rsc.ts!',
13+
'node_package/src/pro/wrapServerComponentRenderer/server.tsx!',
14+
'node_package/src/pro/wrapServerComponentRenderer/server.rsc.tsx!',
15+
'node_package/src/pro/RSCRoute.tsx!',
16+
'node_package/src/pro/ServerComponentFetchError.ts!',
17+
'node_package/src/pro/getReactServerComponent.server.ts!',
18+
'node_package/src/pro/transformRSCNodeStream.ts!',
19+
'node_package/src/loadJsonFile.ts!',
1720
'eslint.config.ts',
1821
],
1922
project: ['node_package/src/**/*.[jt]s{x,}!', 'node_package/tests/**/*.[jt]s{x,}'],
2023
babel: {
2124
config: ['node_package/babel.config.js'],
2225
},
23-
ignore: ['node_package/tests/emptyForTesting.js'],
26+
ignore: [
27+
'node_package/tests/emptyForTesting.js',
28+
// Pro features exported for external consumption
29+
'node_package/src/pro/streamServerRenderedReactComponent.ts:transformRenderStreamChunksToResultObject',
30+
'node_package/src/pro/streamServerRenderedReactComponent.ts:streamServerRenderedComponent',
31+
'node_package/src/pro/ServerComponentFetchError.ts:isServerComponentFetchError',
32+
'node_package/src/pro/RSCRoute.tsx:RSCRouteProps',
33+
'node_package/src/pro/streamServerRenderedReactComponent.ts:StreamingTrackers',
34+
],
2435
ignoreBinaries: [
2536
// Knip fails to detect it's declared in devDependencies
2637
'nps',

lib/react_on_rails/helper.rb

Lines changed: 9 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,14 @@
1111
require "react_on_rails/utils"
1212
require "react_on_rails/json_output"
1313
require "active_support/concern"
14+
require "react_on_rails/pro/helper"
1415

1516
module ReactOnRails
1617
module Helper
1718
include ReactOnRails::Utils::Required
19+
include ReactOnRails::Pro::Helper
1820

1921
COMPONENT_HTML_KEY = "componentHtml"
20-
IMMEDIATE_HYDRATION_PRO_WARNING = "[REACT ON RAILS] The 'immediate_hydration' feature requires a " \
21-
"React on Rails Pro license. " \
22-
"Please visit https://shakacode.com/react-on-rails-pro to learn more."
2322

2423
# react_component_name: can be a React function or class component or a "Render-Function".
2524
# "Render-Functions" differ from a React function in that they take two parameters, the
@@ -61,7 +60,6 @@ def react_component(component_name, options = {})
6160
server_rendered_html = internal_result[:result]["html"]
6261
console_script = internal_result[:result]["consoleReplayScript"]
6362
render_options = internal_result[:render_options]
64-
badge = pro_warning_badge_if_needed(internal_result[:immediate_hydration_requested])
6563

6664
case server_rendered_html
6765
when String
@@ -71,7 +69,7 @@ def react_component(component_name, options = {})
7169
console_script: console_script,
7270
render_options: render_options
7371
)
74-
(badge + html).html_safe
72+
html.html_safe
7573
when Hash
7674
msg = <<~MSG
7775
Use react_component_hash (not react_component) to return a Hash to your ruby view code. See
@@ -218,21 +216,19 @@ def react_component_hash(component_name, options = {})
218216
server_rendered_html = internal_result[:result]["html"]
219217
console_script = internal_result[:result]["consoleReplayScript"]
220218
render_options = internal_result[:render_options]
221-
badge = pro_warning_badge_if_needed(internal_result[:immediate_hydration_requested])
222219

223220
if server_rendered_html.is_a?(String) && internal_result[:result]["hasErrors"]
224221
server_rendered_html = { COMPONENT_HTML_KEY => internal_result[:result]["html"] }
225222
end
226223

227224
if server_rendered_html.is_a?(Hash)
228-
result = build_react_component_result_for_server_rendered_hash(
225+
build_react_component_result_for_server_rendered_hash(
229226
server_rendered_html: server_rendered_html,
230227
component_specification_tag: internal_result[:tag],
231228
console_script: console_script,
232229
render_options: render_options
233230
)
234-
result[COMPONENT_HTML_KEY] = badge + result[COMPONENT_HTML_KEY]
235-
result
231+
236232
else
237233
msg = <<~MSG
238234
Render-Function used by react_component_hash for #{component_name} is expected to return
@@ -260,8 +256,6 @@ def react_component_hash(component_name, options = {})
260256
# hydrate this store immediately instead of waiting for the page to load.
261257
def redux_store(store_name, props: {}, defer: false, immediate_hydration: nil)
262258
immediate_hydration = ReactOnRails.configuration.immediate_hydration if immediate_hydration.nil?
263-
badge = pro_warning_badge_if_needed(immediate_hydration)
264-
immediate_hydration = false unless support_pro_features?
265259

266260
redux_store_data = { store_name: store_name,
267261
props: props,
@@ -273,7 +267,7 @@ def redux_store(store_name, props: {}, defer: false, immediate_hydration: nil)
273267
else
274268
registered_stores << redux_store_data
275269
result = render_redux_store_data(redux_store_data)
276-
(badge + prepend_render_rails_context(result)).html_safe
270+
prepend_render_rails_context(result).html_safe
277271
end
278272
end
279273

@@ -452,33 +446,6 @@ def load_pack_for_generated_component(react_component_name, render_options)
452446

453447
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
454448

455-
# Checks if React on Rails Pro features are available
456-
# @return [Boolean] true if Pro license is valid, false otherwise
457-
def support_pro_features?
458-
ReactOnRails::Utils.react_on_rails_pro_licence_valid?
459-
end
460-
461-
def pro_warning_badge_if_needed(immediate_hydration)
462-
return "".html_safe unless immediate_hydration
463-
return "".html_safe if support_pro_features?
464-
465-
puts IMMEDIATE_HYDRATION_PRO_WARNING
466-
Rails.logger.warn IMMEDIATE_HYDRATION_PRO_WARNING
467-
468-
tooltip_text = "The 'immediate_hydration' feature requires a React on Rails Pro license. Click to learn more."
469-
470-
badge_html = <<~HTML
471-
<a href="https://shakacode.com/react-on-rails-pro" target="_blank" rel="noopener noreferrer" title="#{tooltip_text}">
472-
<div style="position: fixed; top: 0; right: 0; width: 180px; height: 180px; overflow: hidden; z-index: 9999; pointer-events: none;">
473-
<div style="position: absolute; top: 50px; right: -40px; transform: rotate(45deg); background-color: rgba(220, 53, 69, 0.85); color: white; padding: 7px 40px; text-align: center; font-weight: bold; font-family: sans-serif; font-size: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.3); pointer-events: auto;">
474-
React On Rails Pro Required
475-
</div>
476-
</div>
477-
</a>
478-
HTML
479-
badge_html.strip.html_safe
480-
end
481-
482449
def run_stream_inside_fiber
483450
unless ReactOnRails::Utils.react_on_rails_pro?
484451
raise ReactOnRails::Error,
@@ -675,31 +642,10 @@ def internal_react_component(react_component_name, options = {})
675642
# server has already rendered the HTML.
676643

677644
render_options = create_render_options(react_component_name, options)
678-
# Capture the originally requested value so we can show a badge while still disabling the feature.
679-
immediate_hydration_requested = render_options.immediate_hydration
680-
render_options.set_option(:immediate_hydration, false) unless support_pro_features?
681645

682646
# Setup the page_loaded_js, which is the same regardless of prerendering or not!
683647
# The reason is that React is smart about not doing extra work if the server rendering did its job.
684-
component_specification_tag = content_tag(:script,
685-
json_safe_and_pretty(render_options.client_props).html_safe,
686-
type: "application/json",
687-
class: "js-react-on-rails-component",
688-
id: "js-react-on-rails-component-#{render_options.dom_id}",
689-
"data-component-name" => render_options.react_component_name,
690-
"data-trace" => (render_options.trace ? true : nil),
691-
"data-dom-id" => render_options.dom_id,
692-
"data-store-dependencies" => render_options.store_dependencies&.to_json,
693-
"data-immediate-hydration" =>
694-
(render_options.immediate_hydration ? true : nil))
695-
696-
if render_options.immediate_hydration
697-
component_specification_tag.concat(
698-
content_tag(:script, %(
699-
typeof ReactOnRails === 'object' && ReactOnRails.reactOnRailsComponentLoaded('#{render_options.dom_id}');
700-
).html_safe)
701-
)
702-
end
648+
component_specification_tag = generate_component_script(render_options)
703649

704650
load_pack_for_generated_component(react_component_name, render_options)
705651
# Create the HTML rendering part
@@ -708,27 +654,12 @@ def internal_react_component(react_component_name, options = {})
708654
{
709655
render_options: render_options,
710656
tag: component_specification_tag,
711-
result: result,
712-
immediate_hydration_requested: immediate_hydration_requested
657+
result: result
713658
}
714659
end
715660

716661
def render_redux_store_data(redux_store_data)
717-
store_hydration_data = content_tag(:script,
718-
json_safe_and_pretty(redux_store_data[:props]).html_safe,
719-
type: "application/json",
720-
"data-js-react-on-rails-store" => redux_store_data[:store_name].html_safe,
721-
"data-immediate-hydration" =>
722-
(redux_store_data[:immediate_hydration] ? true : nil))
723-
724-
if redux_store_data[:immediate_hydration]
725-
store_hydration_data.concat(
726-
content_tag(:script, <<~JS.strip_heredoc.html_safe
727-
typeof ReactOnRails === 'object' && ReactOnRails.reactOnRailsStoreLoaded('#{redux_store_data[:store_name]}');
728-
JS
729-
)
730-
)
731-
end
662+
store_hydration_data = generate_store_script(redux_store_data)
732663

733664
prepend_render_rails_context(store_hydration_data)
734665
end

lib/react_on_rails/pro/NOTICE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# React on Rails Pro License
2+
3+
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.
4+
5+
## License Terms
6+
7+
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.
8+
9+
## Distribution
10+
11+
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.
12+
13+
## License Reference
14+
15+
For the complete React on Rails Pro license terms, see: `REACT-ON-RAILS-PRO-LICENSE.md` in the root directory of this repository.
16+
17+
## More Information
18+
19+
For React on Rails Pro licensing information and to obtain a license, please visit:
20+
- [React on Rails Pro](https://www.shakacode.com/react-on-rails-pro/)
21+
- Contact: [react_on_rails@shakacode.com](mailto:react_on_rails@shakacode.com)

0 commit comments

Comments
 (0)