react_on_rails 15.0.0.rc.2 → 16.0.1.rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +103 -34
- data/CLAUDE.md +102 -0
- data/CODING_AGENTS.md +312 -0
- data/CONTRIBUTING.md +378 -3
- data/Gemfile.lock +2 -1
- data/LICENSE.md +30 -4
- data/LICENSES/README.md +14 -0
- data/REACT-ON-RAILS-PRO-LICENSE.md +129 -0
- data/README.md +70 -20
- data/TODO.md +135 -0
- data/eslint.config.ts +5 -0
- data/knip.ts +20 -9
- data/lib/generators/USAGE +4 -5
- data/lib/generators/react_on_rails/USAGE +65 -0
- data/lib/generators/react_on_rails/base_generator.rb +263 -57
- data/lib/generators/react_on_rails/dev_tests_generator.rb +1 -0
- data/lib/generators/react_on_rails/generator_helper.rb +35 -1
- data/lib/generators/react_on_rails/generator_messages.rb +138 -17
- data/lib/generators/react_on_rails/install_generator.rb +336 -26
- data/lib/generators/react_on_rails/react_no_redux_generator.rb +19 -6
- data/lib/generators/react_on_rails/react_with_redux_generator.rb +111 -18
- data/lib/generators/react_on_rails/templates/base/base/Procfile.dev +5 -0
- data/lib/generators/react_on_rails/templates/base/base/Procfile.dev-prod-assets +8 -0
- data/lib/generators/react_on_rails/templates/base/base/Procfile.dev-static-assets +2 -0
- data/lib/generators/react_on_rails/templates/base/base/app/javascript/bundles/HelloWorld/components/HelloWorld.jsx +0 -5
- data/lib/generators/react_on_rails/templates/base/base/app/javascript/packs/server-bundle.js +1 -8
- data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.client.jsx +21 -0
- data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.client.tsx +25 -0
- data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.module.css +4 -0
- data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.server.jsx +5 -0
- data/lib/generators/react_on_rails/templates/base/base/app/javascript/src/HelloWorld/ror_components/HelloWorld.server.tsx +5 -0
- data/lib/generators/react_on_rails/templates/base/base/app/views/hello_world/index.html.erb.tt +1 -1
- data/lib/generators/react_on_rails/templates/base/base/app/views/layouts/hello_world.html.erb +4 -2
- data/lib/generators/react_on_rails/templates/base/base/babel.config.js.tt +5 -2
- data/lib/generators/react_on_rails/templates/base/base/bin/dev +34 -0
- data/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt +3 -3
- data/lib/generators/react_on_rails/templates/base/base/config/shakapacker.yml +76 -7
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/commonWebpackConfig.js.tt +1 -1
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/development.js.tt +8 -8
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/production.js.tt +2 -2
- data/lib/generators/react_on_rails/templates/base/base/config/webpack/test.js.tt +2 -2
- data/lib/generators/react_on_rails/templates/dev_tests/spec/system/hello_world_spec.rb +0 -2
- data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/actions/helloWorldActionCreators.ts +18 -0
- data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/components/HelloWorld.jsx +0 -6
- data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/components/HelloWorld.module.css +4 -0
- data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/components/HelloWorld.tsx +24 -0
- data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/constants/helloWorldConstants.ts +6 -0
- data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/containers/HelloWorldContainer.ts +20 -0
- data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/reducers/helloWorldReducer.ts +22 -0
- data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.client.tsx +23 -0
- data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.server.jsx +5 -0
- data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.server.tsx +5 -0
- data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/store/helloWorldStore.ts +18 -0
- data/lib/react_on_rails/configuration.rb +15 -11
- data/lib/react_on_rails/controller.rb +5 -3
- data/lib/react_on_rails/dev/file_manager.rb +78 -0
- data/lib/react_on_rails/dev/pack_generator.rb +27 -0
- data/lib/react_on_rails/dev/process_manager.rb +61 -0
- data/lib/react_on_rails/dev/server_manager.rb +487 -0
- data/lib/react_on_rails/dev.rb +20 -0
- data/lib/react_on_rails/doctor.rb +1149 -0
- data/lib/react_on_rails/engine.rb +6 -0
- data/lib/react_on_rails/git_utils.rb +12 -2
- data/lib/react_on_rails/helper.rb +19 -44
- data/lib/react_on_rails/packer_utils.rb +4 -18
- data/lib/react_on_rails/packs_generator.rb +134 -8
- data/lib/react_on_rails/pro/NOTICE +21 -0
- data/lib/react_on_rails/pro/helper.rb +122 -0
- data/lib/react_on_rails/pro/utils.rb +53 -0
- data/lib/react_on_rails/react_component/render_options.rb +8 -4
- data/lib/react_on_rails/server_rendering_js_code.rb +0 -1
- data/lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb +1 -0
- data/lib/react_on_rails/system_checker.rb +659 -0
- data/lib/react_on_rails/test_helper/webpack_assets_status_checker.rb +1 -0
- data/lib/react_on_rails/utils.rb +16 -1
- data/lib/react_on_rails/version.rb +1 -1
- data/lib/react_on_rails/version_syntax_converter.rb +1 -1
- data/lib/react_on_rails.rb +1 -0
- data/lib/tasks/doctor.rake +51 -0
- data/lib/tasks/generate_packs.rake +144 -1
- data/package-lock.json +11984 -0
- data/react_on_rails.gemspec +1 -0
- metadata +55 -11
- data/REACT-ON-RAILS-PRO-LICENSE +0 -95
- data/lib/generators/react_on_rails/adapt_for_older_shakapacker_generator.rb +0 -41
- data/lib/generators/react_on_rails/bin/dev +0 -30
- data/lib/generators/react_on_rails/bin/dev-static +0 -30
- data/lib/generators/react_on_rails/templates/base/base/Procfile.dev-static.tt +0 -9
- data/lib/generators/react_on_rails/templates/base/base/Procfile.dev.tt +0 -5
- data/lib/generators/react_on_rails/templates/base/base/app/javascript/packs/registration.js.tt +0 -8
- /data/lib/generators/react_on_rails/templates/base/base/config/webpack/{webpackConfig.js.tt → generateWebpackConfigs.js.tt} +0 -0
- /data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/startup/{HelloWorldApp.jsx → HelloWorldApp.client.jsx} +0 -0
@@ -8,5 +8,11 @@ module ReactOnRails
|
|
8
8
|
VersionChecker.build.log_if_gem_and_node_package_versions_differ
|
9
9
|
ReactOnRails::ServerRenderingPool.reset_pool
|
10
10
|
end
|
11
|
+
|
12
|
+
rake_tasks do
|
13
|
+
load File.expand_path("../tasks/generate_packs.rake", __dir__)
|
14
|
+
load File.expand_path("../tasks/assets.rake", __dir__)
|
15
|
+
load File.expand_path("../tasks/locale.rake", __dir__)
|
16
|
+
end
|
11
17
|
end
|
12
18
|
end
|
@@ -11,9 +11,19 @@ module ReactOnRails
|
|
11
11
|
return false if git_installed && status&.empty?
|
12
12
|
|
13
13
|
error = if git_installed
|
14
|
-
|
14
|
+
<<~MSG.strip
|
15
|
+
You have uncommitted changes. Please commit or stash them before continuing.
|
16
|
+
|
17
|
+
The React on Rails generator creates many new files and it's important to keep
|
18
|
+
your existing changes separate from the generated code for easier review.
|
19
|
+
MSG
|
15
20
|
else
|
16
|
-
|
21
|
+
<<~MSG.strip
|
22
|
+
Git is not installed. Please install Git and commit your changes before continuing.
|
23
|
+
|
24
|
+
The React on Rails generator creates many new files and version control helps
|
25
|
+
track what was generated versus your existing code.
|
26
|
+
MSG
|
17
27
|
end
|
18
28
|
message_handler.add_error(error)
|
19
29
|
true
|
@@ -11,10 +11,12 @@ require "addressable/uri"
|
|
11
11
|
require "react_on_rails/utils"
|
12
12
|
require "react_on_rails/json_output"
|
13
13
|
require "active_support/concern"
|
14
|
+
require "react_on_rails/pro/helper"
|
14
15
|
|
15
16
|
module ReactOnRails
|
16
17
|
module Helper
|
17
18
|
include ReactOnRails::Utils::Required
|
19
|
+
include ReactOnRails::Pro::Helper
|
18
20
|
|
19
21
|
COMPONENT_HTML_KEY = "componentHtml"
|
20
22
|
|
@@ -61,12 +63,13 @@ module ReactOnRails
|
|
61
63
|
|
62
64
|
case server_rendered_html
|
63
65
|
when String
|
64
|
-
build_react_component_result_for_server_rendered_string(
|
66
|
+
html = build_react_component_result_for_server_rendered_string(
|
65
67
|
server_rendered_html: server_rendered_html,
|
66
68
|
component_specification_tag: internal_result[:tag],
|
67
69
|
console_script: console_script,
|
68
70
|
render_options: render_options
|
69
71
|
)
|
72
|
+
html.html_safe
|
70
73
|
when Hash
|
71
74
|
msg = <<~MSG
|
72
75
|
Use react_component_hash (not react_component) to return a Hash to your ruby view code. See
|
@@ -126,7 +129,7 @@ module ReactOnRails
|
|
126
129
|
# stream_react_component doesn't have the prerender option
|
127
130
|
# Because setting prerender to false is equivalent to calling react_component with prerender: false
|
128
131
|
options[:prerender] = true
|
129
|
-
options = options.merge(
|
132
|
+
options = options.merge(immediate_hydration: true) unless options.key?(:immediate_hydration)
|
130
133
|
run_stream_inside_fiber do
|
131
134
|
internal_stream_react_component(component_name, options)
|
132
135
|
end
|
@@ -208,6 +211,7 @@ module ReactOnRails
|
|
208
211
|
#
|
209
212
|
def react_component_hash(component_name, options = {})
|
210
213
|
options[:prerender] = true
|
214
|
+
|
211
215
|
internal_result = internal_react_component(component_name, options)
|
212
216
|
server_rendered_html = internal_result[:result]["html"]
|
213
217
|
console_script = internal_result[:result]["consoleReplayScript"]
|
@@ -224,6 +228,7 @@ module ReactOnRails
|
|
224
228
|
console_script: console_script,
|
225
229
|
render_options: render_options
|
226
230
|
)
|
231
|
+
|
227
232
|
else
|
228
233
|
msg = <<~MSG
|
229
234
|
Render-Function used by react_component_hash for #{component_name} is expected to return
|
@@ -247,13 +252,14 @@ module ReactOnRails
|
|
247
252
|
# props: Ruby Hash or JSON string which contains the properties to pass to the redux store.
|
248
253
|
# Options
|
249
254
|
# defer: false -- pass as true if you wish to render this below your component.
|
250
|
-
#
|
251
|
-
# waiting for the page to load.
|
252
|
-
def redux_store(store_name, props: {}, defer: false,
|
253
|
-
|
255
|
+
# immediate_hydration: false -- React on Rails Pro (licensed) feature. Pass as true if you wish to
|
256
|
+
# hydrate this store immediately instead of waiting for the page to load.
|
257
|
+
def redux_store(store_name, props: {}, defer: false, immediate_hydration: nil)
|
258
|
+
immediate_hydration = ReactOnRails.configuration.immediate_hydration if immediate_hydration.nil?
|
259
|
+
|
254
260
|
redux_store_data = { store_name: store_name,
|
255
261
|
props: props,
|
256
|
-
|
262
|
+
immediate_hydration: immediate_hydration }
|
257
263
|
if defer
|
258
264
|
registered_stores_defer_render << redux_store_data
|
259
265
|
"YOU SHOULD NOT SEE THIS ON YOUR VIEW -- Uses as a code block, like <% redux_store %> " \
|
@@ -261,7 +267,7 @@ module ReactOnRails
|
|
261
267
|
else
|
262
268
|
registered_stores << redux_store_data
|
263
269
|
result = render_redux_store_data(redux_store_data)
|
264
|
-
prepend_render_rails_context(result)
|
270
|
+
prepend_render_rails_context(result).html_safe
|
265
271
|
end
|
266
272
|
end
|
267
273
|
|
@@ -334,7 +340,7 @@ module ReactOnRails
|
|
334
340
|
|
335
341
|
html = result["html"]
|
336
342
|
console_log_script = result["consoleLogScript"]
|
337
|
-
raw("#{html}#{render_options.replay_console
|
343
|
+
raw("#{html}#{console_log_script if render_options.replay_console}")
|
338
344
|
rescue ExecJS::ProgramError => err
|
339
345
|
raise ReactOnRails::PrerenderError.new(component_name: "N/A (server_render_js called)",
|
340
346
|
err: err,
|
@@ -401,7 +407,7 @@ module ReactOnRails
|
|
401
407
|
result.merge!(
|
402
408
|
# URL settings
|
403
409
|
href: uri.to_s,
|
404
|
-
location: "#{uri.path}#{
|
410
|
+
location: "#{uri.path}#{"?#{uri.query}" if uri.query.present?}",
|
405
411
|
scheme: uri.scheme, # http
|
406
412
|
host: uri.host, # foo.com
|
407
413
|
port: uri.port,
|
@@ -440,8 +446,6 @@ module ReactOnRails
|
|
440
446
|
|
441
447
|
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
442
448
|
|
443
|
-
private
|
444
|
-
|
445
449
|
def run_stream_inside_fiber
|
446
450
|
unless ReactOnRails::Utils.react_on_rails_pro?
|
447
451
|
raise ReactOnRails::Error,
|
@@ -641,24 +645,7 @@ module ReactOnRails
|
|
641
645
|
|
642
646
|
# Setup the page_loaded_js, which is the same regardless of prerendering or not!
|
643
647
|
# The reason is that React is smart about not doing extra work if the server rendering did its job.
|
644
|
-
component_specification_tag =
|
645
|
-
json_safe_and_pretty(render_options.client_props).html_safe,
|
646
|
-
type: "application/json",
|
647
|
-
class: "js-react-on-rails-component",
|
648
|
-
id: "js-react-on-rails-component-#{render_options.dom_id}",
|
649
|
-
"data-component-name" => render_options.react_component_name,
|
650
|
-
"data-trace" => (render_options.trace ? true : nil),
|
651
|
-
"data-dom-id" => render_options.dom_id,
|
652
|
-
"data-store-dependencies" => render_options.store_dependencies&.to_json,
|
653
|
-
"data-force-load" => (render_options.force_load ? true : nil))
|
654
|
-
|
655
|
-
if render_options.force_load
|
656
|
-
component_specification_tag.concat(
|
657
|
-
content_tag(:script, %(
|
658
|
-
typeof ReactOnRails === 'object' && ReactOnRails.reactOnRailsComponentLoaded('#{render_options.dom_id}');
|
659
|
-
).html_safe)
|
660
|
-
)
|
661
|
-
end
|
648
|
+
component_specification_tag = generate_component_script(render_options)
|
662
649
|
|
663
650
|
load_pack_for_generated_component(react_component_name, render_options)
|
664
651
|
# Create the HTML rendering part
|
@@ -672,20 +659,7 @@ typeof ReactOnRails === 'object' && ReactOnRails.reactOnRailsComponentLoaded('#{
|
|
672
659
|
end
|
673
660
|
|
674
661
|
def render_redux_store_data(redux_store_data)
|
675
|
-
store_hydration_data =
|
676
|
-
json_safe_and_pretty(redux_store_data[:props]).html_safe,
|
677
|
-
type: "application/json",
|
678
|
-
"data-js-react-on-rails-store" => redux_store_data[:store_name].html_safe,
|
679
|
-
"data-force-load" => (redux_store_data[:force_load] ? true : nil))
|
680
|
-
|
681
|
-
if redux_store_data[:force_load]
|
682
|
-
store_hydration_data.concat(
|
683
|
-
content_tag(:script, <<~JS.strip_heredoc.html_safe
|
684
|
-
typeof ReactOnRails === 'object' && ReactOnRails.reactOnRailsStoreLoaded('#{redux_store_data[:store_name]}');
|
685
|
-
JS
|
686
|
-
)
|
687
|
-
)
|
688
|
-
end
|
662
|
+
store_hydration_data = generate_store_script(redux_store_data)
|
689
663
|
|
690
664
|
prepend_render_rails_context(store_hydration_data)
|
691
665
|
end
|
@@ -814,6 +788,7 @@ typeof ReactOnRails === 'object' && ReactOnRails.reactOnRailsComponentLoaded('#{
|
|
814
788
|
|
815
789
|
if defined?(ScoutApm)
|
816
790
|
include ScoutApm::Tracer
|
791
|
+
|
817
792
|
instrument_method :react_component, type: "ReactOnRails", name: "react_component"
|
818
793
|
instrument_method :react_component_hash, type: "ReactOnRails", name: "react_component_hash"
|
819
794
|
end
|
@@ -3,27 +3,18 @@
|
|
3
3
|
module ReactOnRails
|
4
4
|
module PackerUtils
|
5
5
|
def self.using_packer?
|
6
|
-
using_shakapacker_const?
|
6
|
+
using_shakapacker_const?
|
7
7
|
end
|
8
8
|
|
9
9
|
def self.using_shakapacker_const?
|
10
10
|
return @using_shakapacker_const if defined?(@using_shakapacker_const)
|
11
11
|
|
12
12
|
@using_shakapacker_const = ReactOnRails::Utils.gem_available?("shakapacker") &&
|
13
|
-
shakapacker_version_requirement_met?("
|
14
|
-
end
|
15
|
-
|
16
|
-
def self.using_webpacker_const?
|
17
|
-
return @using_webpacker_const if defined?(@using_webpacker_const)
|
18
|
-
|
19
|
-
@using_webpacker_const = (ReactOnRails::Utils.gem_available?("shakapacker") &&
|
20
|
-
shakapacker_version_as_array[0] <= 6) ||
|
21
|
-
ReactOnRails::Utils.gem_available?("webpacker")
|
13
|
+
shakapacker_version_requirement_met?("8.2.0")
|
22
14
|
end
|
23
15
|
|
24
16
|
def self.packer_type
|
25
17
|
return "shakapacker" if using_shakapacker_const?
|
26
|
-
return "webpacker" if using_webpacker_const?
|
27
18
|
|
28
19
|
nil
|
29
20
|
end
|
@@ -31,12 +22,8 @@ module ReactOnRails
|
|
31
22
|
def self.packer
|
32
23
|
return nil unless using_packer?
|
33
24
|
|
34
|
-
|
35
|
-
|
36
|
-
return ::Shakapacker
|
37
|
-
end
|
38
|
-
require "webpacker"
|
39
|
-
::Webpacker
|
25
|
+
require "shakapacker"
|
26
|
+
::Shakapacker
|
40
27
|
end
|
41
28
|
|
42
29
|
def self.dev_server_running?
|
@@ -106,7 +93,6 @@ module ReactOnRails
|
|
106
93
|
end
|
107
94
|
|
108
95
|
def self.precompile?
|
109
|
-
return ::Webpacker.config.webpacker_precompile? if using_webpacker_const?
|
110
96
|
return ::Shakapacker.config.shakapacker_precompile? if using_shakapacker_const?
|
111
97
|
|
112
98
|
false
|
@@ -1,11 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "fileutils"
|
4
|
+
require "set"
|
4
5
|
|
5
6
|
module ReactOnRails
|
6
7
|
# rubocop:disable Metrics/ClassLength
|
7
8
|
class PacksGenerator
|
8
9
|
CONTAINS_CLIENT_OR_SERVER_REGEX = /\.(server|client)($|\.)/
|
10
|
+
COMPONENT_EXTENSIONS = /\.(jsx?|tsx?)$/
|
9
11
|
MINIMUM_SHAKAPACKER_VERSION = "6.5.1"
|
10
12
|
|
11
13
|
def self.instance
|
@@ -16,13 +18,20 @@ module ReactOnRails
|
|
16
18
|
return unless ReactOnRails.configuration.auto_load_bundle
|
17
19
|
|
18
20
|
add_generated_pack_to_server_bundle
|
21
|
+
|
22
|
+
# Clean any non-generated files from directories
|
23
|
+
clean_non_generated_files_with_feedback
|
24
|
+
|
19
25
|
are_generated_files_present_and_up_to_date = Dir.exist?(generated_packs_directory_path) &&
|
20
26
|
File.exist?(generated_server_bundle_file_path) &&
|
21
27
|
!stale_or_missing_packs?
|
22
28
|
|
23
|
-
|
29
|
+
if are_generated_files_present_and_up_to_date
|
30
|
+
puts Rainbow("✅ Generated packs are up to date, no regeneration needed").green
|
31
|
+
return
|
32
|
+
end
|
24
33
|
|
25
|
-
|
34
|
+
clean_generated_directories_with_feedback
|
26
35
|
generate_packs
|
27
36
|
end
|
28
37
|
|
@@ -182,9 +191,112 @@ module ReactOnRails
|
|
182
191
|
"#{generated_nonentrypoints_path}/#{generated_server_bundle_file_name}.js"
|
183
192
|
end
|
184
193
|
|
185
|
-
def
|
186
|
-
|
187
|
-
|
194
|
+
def clean_non_generated_files_with_feedback
|
195
|
+
directories_to_clean = [generated_packs_directory_path, generated_server_bundle_directory_path].compact.uniq
|
196
|
+
expected_files = build_expected_files_set
|
197
|
+
|
198
|
+
puts Rainbow("🧹 Cleaning non-generated files...").yellow
|
199
|
+
|
200
|
+
total_deleted = directories_to_clean.sum do |dir_path|
|
201
|
+
clean_unexpected_files_from_directory(dir_path, expected_files)
|
202
|
+
end
|
203
|
+
|
204
|
+
display_cleanup_summary(total_deleted)
|
205
|
+
end
|
206
|
+
|
207
|
+
def build_expected_files_set
|
208
|
+
expected_pack_files = Set.new
|
209
|
+
common_component_to_path.each_value { |path| expected_pack_files << generated_pack_path(path) }
|
210
|
+
client_component_to_path.each_value { |path| expected_pack_files << generated_pack_path(path) }
|
211
|
+
|
212
|
+
if ReactOnRails.configuration.server_bundle_js_file.present?
|
213
|
+
expected_server_bundle = generated_server_bundle_file_path
|
214
|
+
end
|
215
|
+
|
216
|
+
{ pack_files: expected_pack_files, server_bundle: expected_server_bundle }
|
217
|
+
end
|
218
|
+
|
219
|
+
def clean_unexpected_files_from_directory(dir_path, expected_files)
|
220
|
+
return 0 unless Dir.exist?(dir_path)
|
221
|
+
|
222
|
+
existing_files = Dir.glob("#{dir_path}/**/*").select { |f| File.file?(f) }
|
223
|
+
unexpected_files = find_unexpected_files(existing_files, dir_path, expected_files)
|
224
|
+
|
225
|
+
if unexpected_files.any?
|
226
|
+
delete_unexpected_files(unexpected_files, dir_path)
|
227
|
+
unexpected_files.length
|
228
|
+
else
|
229
|
+
puts Rainbow(" No unexpected files found in #{dir_path}").cyan
|
230
|
+
0
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def find_unexpected_files(existing_files, dir_path, expected_files)
|
235
|
+
existing_files.reject do |file|
|
236
|
+
if dir_path == generated_server_bundle_directory_path
|
237
|
+
file == expected_files[:server_bundle]
|
238
|
+
else
|
239
|
+
expected_files[:pack_files].include?(file)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
def delete_unexpected_files(unexpected_files, dir_path)
|
245
|
+
puts Rainbow(" Deleting #{unexpected_files.length} unexpected files from #{dir_path}:").cyan
|
246
|
+
unexpected_files.each do |file|
|
247
|
+
puts Rainbow(" - #{File.basename(file)}").blue
|
248
|
+
File.delete(file)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
def display_cleanup_summary(total_deleted)
|
253
|
+
if total_deleted.positive?
|
254
|
+
puts Rainbow("🗑️ Deleted #{total_deleted} unexpected files total").red
|
255
|
+
else
|
256
|
+
puts Rainbow("✨ No unexpected files to delete").green
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
def clean_generated_directories_with_feedback
|
261
|
+
directories_to_clean = [
|
262
|
+
generated_packs_directory_path,
|
263
|
+
generated_server_bundle_directory_path
|
264
|
+
].compact.uniq
|
265
|
+
|
266
|
+
puts Rainbow("🧹 Cleaning generated directories...").yellow
|
267
|
+
|
268
|
+
total_deleted = directories_to_clean.sum { |dir_path| clean_directory_with_feedback(dir_path) }
|
269
|
+
|
270
|
+
if total_deleted.positive?
|
271
|
+
puts Rainbow("🗑️ Deleted #{total_deleted} generated files total").red
|
272
|
+
else
|
273
|
+
puts Rainbow("✨ No files to delete, directories are clean").green
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def clean_directory_with_feedback(dir_path)
|
278
|
+
return create_directory_with_feedback(dir_path) unless Dir.exist?(dir_path)
|
279
|
+
|
280
|
+
files = Dir.glob("#{dir_path}/**/*").select { |f| File.file?(f) }
|
281
|
+
|
282
|
+
if files.any?
|
283
|
+
puts Rainbow(" Deleting #{files.length} files from #{dir_path}:").cyan
|
284
|
+
files.each { |file| puts Rainbow(" - #{File.basename(file)}").blue }
|
285
|
+
FileUtils.rm_rf(dir_path)
|
286
|
+
FileUtils.mkdir_p(dir_path)
|
287
|
+
files.length
|
288
|
+
else
|
289
|
+
puts Rainbow(" Directory #{dir_path} is already empty").cyan
|
290
|
+
FileUtils.rm_rf(dir_path)
|
291
|
+
FileUtils.mkdir_p(dir_path)
|
292
|
+
0
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
def create_directory_with_feedback(dir_path)
|
297
|
+
puts Rainbow(" Directory #{dir_path} does not exist, creating...").cyan
|
298
|
+
FileUtils.mkdir_p(dir_path)
|
299
|
+
0
|
188
300
|
end
|
189
301
|
|
190
302
|
def server_bundle_entrypoint
|
@@ -198,6 +310,13 @@ module ReactOnRails
|
|
198
310
|
"#{source_entry_path}/generated"
|
199
311
|
end
|
200
312
|
|
313
|
+
def generated_server_bundle_directory_path
|
314
|
+
return nil if ReactOnRails.configuration.make_generated_server_bundle_the_entrypoint
|
315
|
+
|
316
|
+
source_entrypoint_parent = Pathname(ReactOnRails::PackerUtils.packer_source_entry_path).parent
|
317
|
+
"#{source_entrypoint_parent}/generated"
|
318
|
+
end
|
319
|
+
|
201
320
|
def relative_component_path_from_generated_pack(ror_component_path)
|
202
321
|
component_file_pathname = Pathname.new(ror_component_path)
|
203
322
|
component_generated_pack_path = generated_pack_path(ror_component_path)
|
@@ -228,14 +347,20 @@ module ReactOnRails
|
|
228
347
|
paths.to_h { |path| [component_name(path), path] }
|
229
348
|
end
|
230
349
|
|
350
|
+
def filter_component_files(paths)
|
351
|
+
paths.grep(COMPONENT_EXTENSIONS)
|
352
|
+
end
|
353
|
+
|
231
354
|
def common_component_to_path
|
232
355
|
common_components_paths = Dir.glob("#{components_search_path}/*").grep_v(CONTAINS_CLIENT_OR_SERVER_REGEX)
|
233
|
-
|
356
|
+
filtered_paths = filter_component_files(common_components_paths)
|
357
|
+
component_name_to_path(filtered_paths)
|
234
358
|
end
|
235
359
|
|
236
360
|
def client_component_to_path
|
237
361
|
client_render_components_paths = Dir.glob("#{components_search_path}/*.client.*")
|
238
|
-
|
362
|
+
filtered_client_paths = filter_component_files(client_render_components_paths)
|
363
|
+
client_specific_components = component_name_to_path(filtered_client_paths)
|
239
364
|
|
240
365
|
duplicate_components = common_component_to_path.slice(*client_specific_components.keys)
|
241
366
|
duplicate_components.each_key { |component| raise_client_component_overrides_common(component) }
|
@@ -245,7 +370,8 @@ module ReactOnRails
|
|
245
370
|
|
246
371
|
def server_component_to_path
|
247
372
|
server_render_components_paths = Dir.glob("#{components_search_path}/*.server.*")
|
248
|
-
|
373
|
+
filtered_server_paths = filter_component_files(server_render_components_paths)
|
374
|
+
server_specific_components = component_name_to_path(filtered_server_paths)
|
249
375
|
|
250
376
|
duplicate_components = common_component_to_path.slice(*server_specific_components.keys)
|
251
377
|
duplicate_components.each_key { |component| raise_server_component_overrides_common(component) }
|
@@ -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,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# /*
|
4
|
+
# * Copyright (c) 2025 Shakacode LLC
|
5
|
+
# *
|
6
|
+
# * This file is NOT licensed under the MIT (open source) license.
|
7
|
+
# * It is part of the React on Rails Pro offering and is licensed separately.
|
8
|
+
# *
|
9
|
+
# * Unauthorized copying, modification, distribution, or use of this file,
|
10
|
+
# * via any medium, is strictly prohibited without a valid license agreement
|
11
|
+
# * from Shakacode LLC.
|
12
|
+
# *
|
13
|
+
# * For licensing terms, please see:
|
14
|
+
# * https://github.com/shakacode/react_on_rails/blob/master/REACT-ON-RAILS-PRO-LICENSE.md
|
15
|
+
# */
|
16
|
+
|
17
|
+
module ReactOnRails
|
18
|
+
module Pro
|
19
|
+
module Helper
|
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."
|
23
|
+
|
24
|
+
# Generates the complete component specification script tag.
|
25
|
+
# Handles both immediate hydration (Pro feature) and standard cases.
|
26
|
+
def generate_component_script(render_options)
|
27
|
+
# Setup the page_loaded_js, which is the same regardless of prerendering or not!
|
28
|
+
# The reason is that React is smart about not doing extra work if the server rendering did its job.
|
29
|
+
component_specification_tag = content_tag(:script,
|
30
|
+
json_safe_and_pretty(render_options.client_props).html_safe,
|
31
|
+
type: "application/json",
|
32
|
+
class: "js-react-on-rails-component",
|
33
|
+
id: "js-react-on-rails-component-#{render_options.dom_id}",
|
34
|
+
"data-component-name" => render_options.react_component_name,
|
35
|
+
"data-trace" => (render_options.trace ? true : nil),
|
36
|
+
"data-dom-id" => render_options.dom_id,
|
37
|
+
"data-store-dependencies" =>
|
38
|
+
render_options.store_dependencies&.to_json,
|
39
|
+
"data-immediate-hydration" =>
|
40
|
+
(render_options.immediate_hydration ? true : nil))
|
41
|
+
|
42
|
+
# Add immediate invocation script if immediate hydration is enabled
|
43
|
+
spec_tag = if render_options.immediate_hydration
|
44
|
+
# Escape dom_id for JavaScript context
|
45
|
+
escaped_dom_id = escape_javascript(render_options.dom_id)
|
46
|
+
immediate_script = content_tag(:script, %(
|
47
|
+
typeof ReactOnRails === 'object' && ReactOnRails.reactOnRailsComponentLoaded('#{escaped_dom_id}');
|
48
|
+
).html_safe)
|
49
|
+
"#{component_specification_tag}\n#{immediate_script}"
|
50
|
+
else
|
51
|
+
component_specification_tag
|
52
|
+
end
|
53
|
+
|
54
|
+
pro_warning_badge = pro_warning_badge_if_needed(render_options.explicitly_disabled_pro_options)
|
55
|
+
"#{pro_warning_badge}\n#{spec_tag}".html_safe
|
56
|
+
end
|
57
|
+
|
58
|
+
# Generates the complete store hydration script tag.
|
59
|
+
# Handles both immediate hydration (Pro feature) and standard cases.
|
60
|
+
def generate_store_script(redux_store_data)
|
61
|
+
pro_options_check_result = ReactOnRails::Pro::Utils.disable_pro_render_options_if_not_licensed(redux_store_data)
|
62
|
+
redux_store_data = pro_options_check_result[:raw_options]
|
63
|
+
explicitly_disabled_pro_options = pro_options_check_result[:explicitly_disabled_pro_options]
|
64
|
+
|
65
|
+
store_hydration_data = content_tag(:script,
|
66
|
+
json_safe_and_pretty(redux_store_data[:props]).html_safe,
|
67
|
+
type: "application/json",
|
68
|
+
"data-js-react-on-rails-store" => redux_store_data[:store_name].html_safe,
|
69
|
+
"data-immediate-hydration" =>
|
70
|
+
(redux_store_data[:immediate_hydration] ? true : nil))
|
71
|
+
|
72
|
+
# Add immediate invocation script if immediate hydration is enabled and Pro license is valid
|
73
|
+
store_hydration_scripts = if redux_store_data[:immediate_hydration]
|
74
|
+
# Escape store_name for JavaScript context
|
75
|
+
escaped_store_name = escape_javascript(redux_store_data[:store_name])
|
76
|
+
immediate_script = content_tag(:script, <<~JS.strip_heredoc.html_safe
|
77
|
+
typeof ReactOnRails === 'object' && ReactOnRails.reactOnRailsStoreLoaded('#{escaped_store_name}');
|
78
|
+
JS
|
79
|
+
)
|
80
|
+
"#{store_hydration_data}\n#{immediate_script}"
|
81
|
+
else
|
82
|
+
store_hydration_data
|
83
|
+
end
|
84
|
+
|
85
|
+
pro_warning_badge = pro_warning_badge_if_needed(explicitly_disabled_pro_options)
|
86
|
+
"#{pro_warning_badge}\n#{store_hydration_scripts}".html_safe
|
87
|
+
end
|
88
|
+
|
89
|
+
def pro_warning_badge_if_needed(explicitly_disabled_pro_options)
|
90
|
+
return "" unless explicitly_disabled_pro_options.any?
|
91
|
+
|
92
|
+
disabled_features_message = disabled_pro_features_message(explicitly_disabled_pro_options)
|
93
|
+
warning_message = "[REACT ON RAILS] #{disabled_features_message}\n" \
|
94
|
+
"Please visit https://shakacode.com/react-on-rails-pro to learn more."
|
95
|
+
puts warning_message
|
96
|
+
Rails.logger.warn warning_message
|
97
|
+
|
98
|
+
tooltip_text = "#{disabled_features_message} Click to learn more."
|
99
|
+
|
100
|
+
<<~HTML.strip
|
101
|
+
<a href="https://shakacode.com/react-on-rails-pro" target="_blank" rel="noopener noreferrer" title="#{tooltip_text}">
|
102
|
+
<div style="position: fixed; top: 0; right: 0; width: 180px; height: 180px; overflow: hidden; z-index: 9999; pointer-events: none;">
|
103
|
+
<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;">
|
104
|
+
React On Rails Pro Required
|
105
|
+
</div>
|
106
|
+
</div>
|
107
|
+
</a>
|
108
|
+
HTML
|
109
|
+
end
|
110
|
+
|
111
|
+
def disabled_pro_features_message(explicitly_disabled_pro_options)
|
112
|
+
return "".html_safe unless explicitly_disabled_pro_options.any?
|
113
|
+
|
114
|
+
feature_list = explicitly_disabled_pro_options.join(", ")
|
115
|
+
feature_word = explicitly_disabled_pro_options.size == 1 ? "feature" : "features"
|
116
|
+
"The '#{feature_list}' #{feature_word} " \
|
117
|
+
"#{explicitly_disabled_pro_options.size == 1 ? 'requires' : 'require'} a " \
|
118
|
+
"React on Rails Pro license. "
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# /*
|
4
|
+
# * Copyright (c) 2025 Shakacode LLC
|
5
|
+
# *
|
6
|
+
# * This file is NOT licensed under the MIT (open source) license.
|
7
|
+
# * It is part of the React on Rails Pro offering and is licensed separately.
|
8
|
+
# *
|
9
|
+
# * Unauthorized copying, modification, distribution, or use of this file,
|
10
|
+
# * via any medium, is strictly prohibited without a valid license agreement
|
11
|
+
# * from Shakacode LLC.
|
12
|
+
# *
|
13
|
+
# * For licensing terms, please see:
|
14
|
+
# * https://github.com/shakacode/react_on_rails/blob/master/REACT-ON-RAILS-PRO-LICENSE.md
|
15
|
+
# */
|
16
|
+
|
17
|
+
module ReactOnRails
|
18
|
+
module Pro
|
19
|
+
module Utils
|
20
|
+
PRO_ONLY_OPTIONS = %i[immediate_hydration].freeze
|
21
|
+
|
22
|
+
# Checks if React on Rails Pro features are available
|
23
|
+
# @return [Boolean] true if Pro license is valid, false otherwise
|
24
|
+
def self.support_pro_features?
|
25
|
+
ReactOnRails::Utils.react_on_rails_pro_licence_valid?
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.disable_pro_render_options_if_not_licensed(raw_options)
|
29
|
+
if support_pro_features?
|
30
|
+
return {
|
31
|
+
raw_options: raw_options,
|
32
|
+
explicitly_disabled_pro_options: []
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
raw_options_after_disable = raw_options.dup
|
37
|
+
|
38
|
+
explicitly_disabled_pro_options = PRO_ONLY_OPTIONS.select do |option|
|
39
|
+
# Use global configuration if it's not overridden in the options
|
40
|
+
next ReactOnRails.configuration.send(option) if raw_options[option].nil?
|
41
|
+
|
42
|
+
raw_options[option]
|
43
|
+
end
|
44
|
+
explicitly_disabled_pro_options.each { |option| raw_options_after_disable[option] = false }
|
45
|
+
|
46
|
+
{
|
47
|
+
raw_options: raw_options_after_disable,
|
48
|
+
explicitly_disabled_pro_options: explicitly_disabled_pro_options
|
49
|
+
}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|