react_on_rails 11.0.5 → 13.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (199) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +338 -0
  3. data/.eslintignore +2 -1
  4. data/.eslintrc +32 -3
  5. data/.github/FUNDING.yml +1 -0
  6. data/.github/ISSUE_TEMPLATE/bug_report.md +23 -0
  7. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  8. data/.github/PULL_REQUEST_TEMPLATE.md +19 -0
  9. data/.github/workflows/lint-js-and-ruby.yml +54 -0
  10. data/.github/workflows/main.yml +183 -0
  11. data/.github/workflows/package-js-tests.yml +35 -0
  12. data/.github/workflows/rspec-package-specs.yml +46 -0
  13. data/.gitignore +3 -4
  14. data/.prettierignore +14 -0
  15. data/.prettierrc +20 -0
  16. data/.rubocop.yml +76 -34
  17. data/.travis.yml +15 -22
  18. data/CHANGELOG.md +443 -55
  19. data/CONTRIBUTING.md +62 -80
  20. data/Gemfile +1 -35
  21. data/Gemfile.development_dependencies +50 -0
  22. data/KUDOS.md +4 -1
  23. data/{docs/LICENSE.md → LICENSE.md} +1 -1
  24. data/NEWS.md +14 -4
  25. data/REACT-ON-RAILS-PRO-LICENSE +95 -0
  26. data/README.md +107 -802
  27. data/Rakefile +1 -8
  28. data/SUMMARY.md +51 -29
  29. data/book.json +5 -5
  30. data/docs/{basics/generator.md → additional-details/generator-details.md} +5 -13
  31. data/docs/{basics/installation-overview.md → additional-details/manual-installation-overview.md} +9 -14
  32. data/docs/{basics → additional-details}/migrating-from-react-rails.md +1 -1
  33. data/docs/additional-details/recommended-project-structure.md +69 -0
  34. data/docs/additional-details/tips-for-usage-with-sp6.md +15 -0
  35. data/docs/additional-details/upgrade-webpacker-v3-to-v4.md +10 -0
  36. data/docs/api/javascript-api.md +35 -6
  37. data/docs/api/redux-store-api.md +102 -0
  38. data/docs/api/view-helpers-api.md +133 -0
  39. data/docs/contributor-info/errors-with-hooks.md +45 -0
  40. data/docs/contributor-info/linters.md +5 -6
  41. data/docs/contributor-info/pull-requests.md +42 -0
  42. data/docs/contributor-info/releasing.md +1 -1
  43. data/docs/deployment/heroku-deployment.md +39 -0
  44. data/docs/getting-started.md +196 -0
  45. data/docs/guides/client-vs-server-rendering.md +27 -0
  46. data/docs/guides/configuration.md +289 -0
  47. data/docs/guides/deployment.md +5 -0
  48. data/docs/guides/file-system-based-automated-bundle-generation.md +197 -0
  49. data/docs/guides/hmr-and-hot-reloading-with-the-webpack-dev-server.md +104 -0
  50. data/docs/guides/how-react-on-rails-works.md +44 -0
  51. data/docs/guides/how-to-conditionally-server-render-based-on-device-type.md +40 -0
  52. data/docs/guides/how-to-use-different-files-for-client-and-server-rendering.md +98 -0
  53. data/docs/guides/i18n.md +87 -0
  54. data/docs/guides/installation-into-an-existing-rails-app.md +66 -0
  55. data/docs/guides/minitest-configuration.md +31 -0
  56. data/docs/guides/rails-webpacker-react-integration-options.md +213 -0
  57. data/docs/guides/react-on-rails-overview.md +29 -0
  58. data/docs/guides/react-server-rendering.md +32 -0
  59. data/docs/guides/render-functions-and-railscontext.md +205 -0
  60. data/docs/guides/rspec-configuration.md +73 -0
  61. data/docs/guides/tutorial.md +371 -0
  62. data/docs/{basics → guides}/upgrading-react-on-rails.md +126 -3
  63. data/docs/guides/webpack-configuration.md +42 -0
  64. data/docs/home.md +23 -0
  65. data/docs/javascript/asset-pipeline.md +12 -0
  66. data/docs/{additional-reading → javascript}/code-splitting.md +21 -11
  67. data/docs/javascript/converting-from-custom-webpack-config-to-rails-webpacker-config.md +10 -0
  68. data/docs/javascript/credits.md +10 -0
  69. data/docs/{additional-reading → javascript}/images.md +5 -6
  70. data/docs/javascript/react-helmet.md +100 -0
  71. data/docs/javascript/react-router.md +90 -0
  72. data/docs/{additional-reading → javascript}/server-rendering-tips.md +15 -12
  73. data/docs/javascript/troubleshooting-when-using-shakapacker.md +77 -0
  74. data/docs/{additional-reading → javascript}/webpack.md +2 -2
  75. data/docs/misc/articles.md +20 -0
  76. data/docs/misc/doctrine.md +5 -6
  77. data/docs/outdated/deferred-rendering.md +39 -0
  78. data/docs/{additional-reading → outdated}/rails-assets-relative-paths.md +4 -4
  79. data/docs/{additional-reading → outdated}/rails-assets.md +12 -20
  80. data/docs/{misc → outdated}/rails3.md +2 -2
  81. data/docs/rails/convert-rails-5-api-only-app.md +19 -0
  82. data/docs/rails/rails-engine-integration.md +32 -0
  83. data/docs/{additional-reading → rails}/rails_view_rendering_from_inline_javascript.md +2 -1
  84. data/docs/{additional-reading → rails}/turbolinks.md +13 -1
  85. data/docs/react-on-rails-pro/react-on-rails-pro.md +43 -0
  86. data/docs/testimonials/hvmn.md +25 -0
  87. data/docs/testimonials/resortpass.md +13 -0
  88. data/docs/testimonials/testimonials.md +28 -0
  89. data/jest.config.js +4 -0
  90. data/lib/generators/USAGE +1 -1
  91. data/lib/generators/react_on_rails/adapt_for_older_shakapacker_generator.rb +41 -0
  92. data/lib/generators/react_on_rails/base_generator.rb +55 -43
  93. data/lib/generators/react_on_rails/bin/dev +30 -0
  94. data/lib/generators/react_on_rails/bin/dev-static +30 -0
  95. data/lib/generators/react_on_rails/dev_tests_generator.rb +4 -3
  96. data/lib/generators/react_on_rails/generator_helper.rb +8 -6
  97. data/lib/generators/react_on_rails/generator_messages.rb +40 -0
  98. data/lib/generators/react_on_rails/install_generator.rb +37 -0
  99. data/lib/generators/react_on_rails/templates/.eslintrc +3 -1
  100. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev +4 -6
  101. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev-static +9 -0
  102. data/lib/generators/react_on_rails/templates/base/base/app/javascript/bundles/HelloWorld/components/HelloWorld.jsx +21 -40
  103. data/lib/generators/react_on_rails/templates/base/base/app/javascript/bundles/HelloWorld/components/HelloWorld.module.css +4 -0
  104. data/lib/generators/react_on_rails/templates/base/base/app/javascript/bundles/HelloWorld/components/HelloWorldServer.js +5 -0
  105. data/lib/generators/react_on_rails/templates/base/base/app/javascript/packs/server-bundle.js +8 -0
  106. data/lib/generators/react_on_rails/templates/base/base/app/views/layouts/hello_world.html.erb +2 -1
  107. data/lib/generators/react_on_rails/templates/base/base/babel.config.js.tt +32 -0
  108. data/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb +20 -4
  109. data/lib/generators/react_on_rails/templates/base/base/config/shakapacker.yml +62 -0
  110. data/lib/generators/react_on_rails/templates/base/base/config/webpack/clientWebpackConfig.js.tt +17 -0
  111. data/lib/generators/react_on_rails/templates/base/base/config/webpack/commonWebpackConfig.js.tt +17 -0
  112. data/lib/generators/react_on_rails/templates/base/base/config/webpack/development.js.tt +25 -0
  113. data/lib/generators/react_on_rails/templates/base/base/config/webpack/production.js.tt +9 -0
  114. data/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt +117 -0
  115. data/lib/generators/react_on_rails/templates/base/base/config/webpack/test.js.tt +9 -0
  116. data/lib/generators/react_on_rails/templates/base/base/config/webpack/webpack.config.js.tt +15 -0
  117. data/lib/generators/react_on_rails/templates/base/base/config/webpack/webpackConfig.js.tt +36 -0
  118. data/lib/generators/react_on_rails/templates/dev_tests/spec/rails_helper.rb +8 -2
  119. data/lib/generators/react_on_rails/templates/dev_tests/spec/simplecov_helper.rb +1 -1
  120. data/lib/generators/react_on_rails/templates/dev_tests/spec/{features → system}/hello_world_spec.rb +2 -2
  121. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/components/HelloWorld.jsx +6 -9
  122. data/lib/generators/react_on_rails/templates/redux/base/app/javascript/bundles/HelloWorld/store/helloWorldStore.js +1 -3
  123. data/lib/react_on_rails/configuration.rb +198 -145
  124. data/lib/react_on_rails/error.rb +2 -0
  125. data/lib/react_on_rails/git_utils.rb +5 -3
  126. data/lib/react_on_rails/{react_on_rails_helper.rb → helper.rb} +201 -190
  127. data/lib/react_on_rails/json_output.rb +1 -1
  128. data/lib/react_on_rails/json_parse_error.rb +28 -0
  129. data/lib/react_on_rails/locales/base.rb +169 -0
  130. data/lib/react_on_rails/locales/to_js.rb +33 -0
  131. data/lib/react_on_rails/locales/to_json.rb +23 -0
  132. data/lib/react_on_rails/packs_generator.rb +234 -0
  133. data/lib/react_on_rails/prerender_error.rb +35 -27
  134. data/lib/react_on_rails/react_component/render_options.rb +64 -9
  135. data/lib/react_on_rails/server_rendering_js_code.rb +55 -0
  136. data/lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb +126 -76
  137. data/lib/react_on_rails/server_rendering_pool.rb +0 -1
  138. data/lib/react_on_rails/test_helper/ensure_assets_compiled.rb +9 -8
  139. data/lib/react_on_rails/test_helper/webpack_assets_compiler.rb +17 -0
  140. data/lib/react_on_rails/test_helper/webpack_assets_status_checker.rb +13 -12
  141. data/lib/react_on_rails/test_helper.rb +24 -3
  142. data/lib/react_on_rails/utils.rb +94 -25
  143. data/lib/react_on_rails/version.rb +1 -1
  144. data/lib/react_on_rails/version_checker.rb +5 -1
  145. data/lib/react_on_rails/version_syntax_converter.rb +14 -12
  146. data/lib/react_on_rails/webpacker_utils.rb +105 -5
  147. data/lib/react_on_rails.rb +8 -2
  148. data/lib/tasks/assets.rake +28 -60
  149. data/lib/tasks/generate_packs.rake +11 -0
  150. data/lib/tasks/locale.rake +5 -4
  151. data/package-scripts.yml +49 -0
  152. data/package.json +52 -47
  153. data/rakelib/docker.rake +0 -5
  154. data/rakelib/dummy_apps.rake +5 -8
  155. data/rakelib/example_type.rb +12 -3
  156. data/rakelib/examples.rake +5 -4
  157. data/rakelib/lint.rake +5 -16
  158. data/rakelib/node_package.rake +2 -2
  159. data/rakelib/release.rake +37 -23
  160. data/rakelib/run_rspec.rake +16 -44
  161. data/rakelib/task_helpers.rb +16 -4
  162. data/react_on_rails.gemspec +6 -22
  163. data/tsconfig.json +14 -0
  164. data/webpackConfigLoader.js +5 -4
  165. data/yarn.lock +5935 -3106
  166. metadata +122 -272
  167. data/Gemfile.rails32 +0 -74
  168. data/docs/additional-reading/asset-pipeline.md +0 -20
  169. data/docs/additional-reading/babel.md +0 -5
  170. data/docs/additional-reading/caching-and-performance.md +0 -4
  171. data/docs/additional-reading/heroku-deployment.md +0 -92
  172. data/docs/additional-reading/hot-reloading-rails-development.md +0 -57
  173. data/docs/additional-reading/node-server-rendering.md +0 -5
  174. data/docs/additional-reading/rails-engine-integration.md +0 -34
  175. data/docs/additional-reading/react-helmet.md +0 -80
  176. data/docs/additional-reading/react-router.md +0 -113
  177. data/docs/additional-reading/recommended-project-structure.md +0 -49
  178. data/docs/additional-reading/rspec-configuration.md +0 -56
  179. data/docs/additional-reading/webpack-dev-server.md +0 -15
  180. data/docs/api/ruby-api-hot-reload-view-helpers.md +0 -44
  181. data/docs/api/ruby-api.md +0 -8
  182. data/docs/basics/configuration.md +0 -163
  183. data/docs/basics/i18n.md +0 -77
  184. data/docs/tutorial.md +0 -220
  185. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev-server +0 -12
  186. data/lib/react_on_rails/assets_precompile.rb +0 -150
  187. data/lib/react_on_rails/locales_to_js.rb +0 -134
  188. data/ruby-lint.yml +0 -25
  189. /data/docs/{additional-reading → additional-details}/updating-dependencies.md +0 -0
  190. /data/docs/{additional-reading → deployment}/elastic-beanstalk.md +0 -0
  191. /data/docs/{additional-reading → javascript}/angular-js-integration-migration.md +0 -0
  192. /data/docs/{additional-reading → javascript}/capistrano-deployment.md +0 -0
  193. /data/docs/{additional-reading → javascript}/foreman-issues.md +0 -0
  194. /data/docs/{additional-reading → javascript}/node-dependencies-and-npm.md +0 -0
  195. /data/docs/{additional-reading → javascript}/react-and-redux.md +0 -0
  196. /data/docs/{additional-reading → javascript}/troubleshooting-when-using-webpacker.md +0 -0
  197. /data/docs/{additional-reading → javascript}/webpack-v1-notes.md +0 -0
  198. /data/docs/{coding-style → misc}/style.md +0 -0
  199. /data/docs/{additional-reading → misc}/tips.md +0 -0
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # rubocop:disable Metrics/ModuleLength
4
+ # rubocop:disable Metrics/MethodLength
4
5
  # NOTE:
5
6
  # For any heredoc JS:
6
7
  # 1. The white spacing in this file matters!
@@ -15,73 +16,25 @@ module ReactOnRails
15
16
  module Helper
16
17
  include ReactOnRails::Utils::Required
17
18
 
18
- COMPONENT_HTML_KEY = "componentHtml".freeze
19
+ COMPONENT_HTML_KEY = "componentHtml"
19
20
 
20
- # The env_javascript_include_tag and env_stylesheet_link_tag support the usage of a webpack
21
- # dev server for providing the JS and CSS assets during development mode. See
22
- # https://github.com/shakacode/react-webpack-rails-tutorial/ for a working example.
21
+ # react_component_name: can be a React function or class component or a "Render-Function".
22
+ # "Render-Functions" differ from a React function in that they take two parameters, the
23
+ # props and the railsContext, like this:
23
24
  #
24
- # The key options are `static` and `hot` which specify what you want for static vs. hot. Both of
25
- # these params are optional, and support either a single value, or an array.
25
+ # let MyReactComponentApp = (props, railsContext) => <MyReactComponent {...props}/>;
26
26
  #
27
- # static vs. hot is picked based on whether
28
- # ENV["REACT_ON_RAILS_ENV"] == "HOT"
27
+ # Alternately, you can define the Render-Function with an additional property
28
+ # `.renderFunction = true`:
29
29
  #
30
- # <%= env_stylesheet_link_tag(static: 'application_static',
31
- # hot: 'application_non_webpack',
32
- # media: 'all',
33
- # 'data-turbolinks-track' => "reload") %>
30
+ # let MyReactComponentApp = (props) => <MyReactComponent {...props}/>;
31
+ # MyReactComponent.renderFunction = true;
34
32
  #
35
- # <!-- These do not use turbolinks, so no data-turbolinks-track -->
36
- # <!-- This is to load the hot assets. -->
37
- # <%= env_javascript_include_tag(hot: ['http://localhost:3500/vendor-bundle.js',
38
- # 'http://localhost:3500/app-bundle.js']) %>
39
- #
40
- # <!-- These do use turbolinks -->
41
- # <%= env_javascript_include_tag(static: 'application_static',
42
- # hot: 'application_non_webpack',
43
- # 'data-turbolinks-track' => "reload") %>
44
- #
45
- # NOTE: for Turbolinks 2.x, use 'data-turbolinks-track' => true
46
- # See application.html.erb for usage example
47
- # https://github.com/shakacode/react-webpack-rails-tutorial/blob/master/app%2Fviews%2Flayouts%2Fapplication.html.erb
48
- def env_javascript_include_tag(args = {})
49
- send_tag_method(:javascript_include_tag, args)
50
- end
51
-
52
- # Helper to set CSS assets depending on if we want static or "hot", which means from the
53
- # Webpack dev server.
54
- #
55
- # In this example, application_non_webpack is simply a CSS asset pipeline file which includes
56
- # styles not placed in the webpack build.
57
- #
58
- # We don't need styles from the webpack build, as those will come via the JavaScript include
59
- # tags.
60
- #
61
- # The key options are `static` and `hot` which specify what you want for static vs. hot. Both of
62
- # these params are optional, and support either a single value, or an array.
63
- #
64
- # <%= env_stylesheet_link_tag(static: 'application_static',
65
- # hot: 'application_non_webpack',
66
- # media: 'all',
67
- # 'data-turbolinks-track' => true) %>
68
- #
69
- def env_stylesheet_link_tag(args = {})
70
- send_tag_method(:stylesheet_link_tag, args)
71
- end
72
-
73
- # react_component_name: can be a React component, created using a ES6 class, or
74
- # React.createClass, or a
75
- # `generator function` that returns a React component
76
- # using ES6
77
- # let MyReactComponentApp = (props, railsContext) => <MyReactComponent {...props}/>;
78
- # or using ES5
79
- # var MyReactComponentApp = function(props, railsContext) { return <YourReactComponent {...props}/>; }
80
33
  # Exposing the react_component_name is necessary to both a plain ReactComponent as well as
81
34
  # a generator:
82
35
  # See README.md for how to "register" your react components.
83
- # See spec/dummy/client/app/startup/serverRegistration.jsx and
84
- # spec/dummy/client/app/startup/ClientRegistration.jsx for examples of this
36
+ # See spec/dummy/client/app/packs/server-bundle.js and
37
+ # spec/dummy/client/app/packs/client-bundle.js for examples of this.
85
38
  #
86
39
  # options:
87
40
  # props: Ruby Hash or JSON string which contains the properties to pass to the react object. Do
@@ -98,33 +51,41 @@ module ReactOnRails
98
51
  # raise_on_prerender_error: <true/false> Default to false. True will raise exception on server
99
52
  # if the JS code throws
100
53
  # Any other options are passed to the content tag, including the id.
54
+ # random_dom_id can be set to override the default from the config/initializers. That's only
55
+ # used if you have multiple instance of the same component on the Rails view.
101
56
  def react_component(component_name, options = {})
102
57
  internal_result = internal_react_component(component_name, options)
103
58
  server_rendered_html = internal_result[:result]["html"]
104
59
  console_script = internal_result[:result]["consoleReplayScript"]
60
+ render_options = internal_result[:render_options]
105
61
 
106
- if server_rendered_html.is_a?(String)
62
+ case server_rendered_html
63
+ when String
107
64
  build_react_component_result_for_server_rendered_string(
108
65
  server_rendered_html: server_rendered_html,
109
66
  component_specification_tag: internal_result[:tag],
110
67
  console_script: console_script,
111
- render_options: internal_result[:render_options]
68
+ render_options: render_options
112
69
  )
113
- elsif server_rendered_html.is_a?(Hash)
114
- msg = <<-MSG.strip_heredoc
115
- Use react_component_hash (not react_component) to return a Hash to your ruby view code. See
116
- https://github.com/shakacode/react_on_rails/blob/master/spec/dummy/client/app/startup/ReactHelmetServerApp.jsx
117
- for an example of the necessary javascript configuration."
70
+ when Hash
71
+ msg = <<~MSG
72
+ Use react_component_hash (not react_component) to return a Hash to your ruby view code. See
73
+ https://github.com/shakacode/react_on_rails/blob/master/spec/dummy/client/app/startup/ReactHelmetServerApp.jsx
74
+ for an example of the necessary javascript configuration.
118
75
  MSG
119
76
  raise ReactOnRails::Error, msg
120
-
121
77
  else
122
- msg = <<-MSG.strip_heredoc
123
- ReactOnRails: server_rendered_html is expected to be a String for #{component_name}. If you're
124
- trying to use a generator function to return a Hash to your ruby view code, then use
125
- react_component_hash instead of react_component and see
126
- https://github.com/shakacode/react_on_rails/blob/master/spec/dummy/client/app/startup/ReactHelmetServerApp.jsx
127
- for an example of the JavaScript code."
78
+ class_name = server_rendered_html.class.name
79
+ msg = <<~MSG
80
+ ReactOnRails: server_rendered_html is expected to be a String or Hash for #{component_name}.
81
+ Type is #{class_name}
82
+ Value:
83
+ #{server_rendered_html}
84
+
85
+ If you're trying to use a Render-Function to return a Hash to your ruby view code, then use
86
+ react_component_hash instead of react_component and see
87
+ https://github.com/shakacode/react_on_rails/blob/master/spec/dummy/client/app/startup/ReactHelmetServerApp.jsx
88
+ for an example of the JavaScript code.
128
89
  MSG
129
90
  raise ReactOnRails::Error, msg
130
91
  end
@@ -135,7 +96,7 @@ module ReactOnRails
135
96
  # It is exactly like react_component except for the following:
136
97
  # 1. prerender: true is automatically added, as this method doesn't make sense for client only
137
98
  # rendering.
138
- # 2. Your JavaScript for server rendering must return an Object for the key server_rendered_html.
99
+ # 2. Your JavaScript Render-Function for server rendering must return an Object rather than a React component.
139
100
  # 3. Your view code must expect an object and not a string.
140
101
  #
141
102
  # Here is an example of the view code:
@@ -152,6 +113,7 @@ module ReactOnRails
152
113
  internal_result = internal_react_component(component_name, options)
153
114
  server_rendered_html = internal_result[:result]["html"]
154
115
  console_script = internal_result[:result]["consoleReplayScript"]
116
+ render_options = internal_result[:render_options]
155
117
 
156
118
  if server_rendered_html.is_a?(String) && internal_result[:result]["hasErrors"]
157
119
  server_rendered_html = { COMPONENT_HTML_KEY => internal_result[:result]["html"] }
@@ -162,13 +124,15 @@ module ReactOnRails
162
124
  server_rendered_html: server_rendered_html,
163
125
  component_specification_tag: internal_result[:tag],
164
126
  console_script: console_script,
165
- render_options: internal_result[:render_options]
127
+ render_options: render_options
166
128
  )
167
129
  else
168
- msg = <<-MSG.strip_heredoc
169
- Generator function used by react_component_hash for #{component_name} is expected to return
130
+ msg = <<~MSG
131
+ Render-Function used by react_component_hash for #{component_name} is expected to return
170
132
  an Object. See https://github.com/shakacode/react_on_rails/blob/master/spec/dummy/client/app/startup/ReactHelmetServerApp.jsx
171
- for an example of the JavaScript code."
133
+ for an example of the JavaScript code.
134
+ Note, your Render-Function must either take 2 params or have the property
135
+ `.renderFunction = true` added to it to distinguish it from a React Function Component.
172
136
  MSG
173
137
  raise ReactOnRails::Error, msg
174
138
  end
@@ -177,6 +141,9 @@ module ReactOnRails
177
141
  # Separate initialization of store from react_component allows multiple react_component calls to
178
142
  # use the same Redux store.
179
143
  #
144
+ # NOTE: This technique not recommended as it prevents dynamic code splitting for performance.
145
+ # Instead, you should use the standard react_component view helper.
146
+ #
180
147
  # store_name: name of the store, corresponding to your call to ReactOnRails.registerStores in your
181
148
  # JavaScript code.
182
149
  # props: Ruby Hash or JSON string which contains the properties to pass to the redux store.
@@ -189,7 +156,7 @@ module ReactOnRails
189
156
  @registered_stores_defer_render ||= []
190
157
  @registered_stores_defer_render << redux_store_data
191
158
  "YOU SHOULD NOT SEE THIS ON YOUR VIEW -- Uses as a code block, like <% redux_store %> "\
192
- "and not <%= redux store %>"
159
+ "and not <%= redux store %>"
193
160
  else
194
161
  @registered_stores ||= []
195
162
  @registered_stores << redux_store_data
@@ -205,7 +172,8 @@ module ReactOnRails
205
172
  # that contains a data props.
206
173
  def redux_store_hydration_data
207
174
  return if @registered_stores_defer_render.blank?
208
- @registered_stores_defer_render.reduce("".dup) do |accum, redux_store_data|
175
+
176
+ @registered_stores_defer_render.reduce(+"") do |accum, redux_store_data|
209
177
  accum << render_redux_store_data(redux_store_data)
210
178
  end.html_safe
211
179
  end
@@ -217,7 +185,7 @@ module ReactOnRails
217
185
  # Helper method to take javascript expression and returns the output from evaluating it.
218
186
  # If you have more than one line that needs to be executed, wrap it in an IIFE.
219
187
  # JS exceptions are caught and console messages are handled properly.
220
- # Options include:{ prerender:, trace:, raise_on_prerender_error: }
188
+ # Options include:{ prerender:, trace:, raise_on_prerender_error:, throw_js_errors: }
221
189
  def server_render_js(js_expression, options = {})
222
190
  render_options = ReactOnRails::ReactComponent::RenderOptions
223
191
  .new(react_component_name: "generic-js", options: options)
@@ -227,6 +195,8 @@ module ReactOnRails
227
195
  var htmlResult = '';
228
196
  var consoleReplayScript = '';
229
197
  var hasErrors = false;
198
+ var renderingError = null;
199
+ var renderingErrorObject = {};
230
200
 
231
201
  try {
232
202
  htmlResult =
@@ -234,9 +204,17 @@ module ReactOnRails
234
204
  return #{js_expression};
235
205
  })();
236
206
  } catch(e) {
207
+ renderingError = e;
208
+ if (#{render_options.throw_js_errors}) {
209
+ throw e;
210
+ }
237
211
  htmlResult = ReactOnRails.handleError({e: e, name: null,
238
212
  jsCode: '#{escape_javascript(js_expression)}', serverSide: true});
239
213
  hasErrors = true;
214
+ renderingErrorObject = {
215
+ message: renderingError.message,
216
+ stack: renderingError.stack,
217
+ }
240
218
  }
241
219
 
242
220
  consoleReplayScript = ReactOnRails.buildConsoleReplay();
@@ -244,7 +222,8 @@ module ReactOnRails
244
222
  return JSON.stringify({
245
223
  html: htmlResult,
246
224
  consoleReplayScript: consoleReplayScript,
247
- hasErrors: hasErrors
225
+ hasErrors: hasErrors,
226
+ renderingError: renderingErrorObject
248
227
  });
249
228
 
250
229
  })()
@@ -257,16 +236,17 @@ module ReactOnRails
257
236
  console_log_script = result["consoleLogScript"]
258
237
  raw("#{html}#{render_options.replay_console ? console_log_script : ''}")
259
238
  rescue ExecJS::ProgramError => err
260
- raise ReactOnRails::PrerenderError, component_name: "N/A (server_render_js called)",
261
- err: err,
262
- js_code: js_code
239
+ raise ReactOnRails::PrerenderError.new(component_name: "N/A (server_render_js called)",
240
+ err: err,
241
+ js_code: js_code)
263
242
  end
264
243
 
265
244
  def json_safe_and_pretty(hash_or_string)
266
245
  return "{}" if hash_or_string.nil?
267
- unless hash_or_string.class.in?([Hash, String])
246
+
247
+ unless hash_or_string.is_a?(String) || hash_or_string.is_a?(Hash)
268
248
  raise ReactOnRails::Error, "#{__method__} only accepts String or Hash as argument "\
269
- "(#{hash_or_string.class} given)."
249
+ "(#{hash_or_string.class} given)."
270
250
  end
271
251
 
272
252
  json_value = hash_or_string.is_a?(String) ? hash_or_string : hash_or_string.to_json
@@ -274,8 +254,86 @@ module ReactOnRails
274
254
  ReactOnRails::JsonOutput.escape(json_value)
275
255
  end
276
256
 
257
+ # This is the definitive list of the default values used for the rails_context, which is the
258
+ # second parameter passed to both component and store Render-Functions.
259
+ # This method can be called from views and from the controller, as `helpers.rails_context`
260
+ #
261
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
262
+ def rails_context(server_side: true)
263
+ # ALERT: Keep in sync with node_package/src/types/index.ts for the properties of RailsContext
264
+ @rails_context ||= begin
265
+ result = {
266
+ railsEnv: Rails.env,
267
+ inMailer: in_mailer?,
268
+ # Locale settings
269
+ i18nLocale: I18n.locale,
270
+ i18nDefaultLocale: I18n.default_locale,
271
+ rorVersion: ReactOnRails::VERSION,
272
+ # TODO: v13 just use the version if existing
273
+ rorPro: ReactOnRails::Utils.react_on_rails_pro?
274
+ }
275
+ if ReactOnRails::Utils.react_on_rails_pro?
276
+ result[:rorProVersion] = ReactOnRails::Utils.react_on_rails_pro_version
277
+ end
278
+
279
+ if defined?(request) && request.present?
280
+ # Check for encoding of the request's original_url and try to force-encoding the
281
+ # URLs as UTF-8. This situation can occur in browsers that do not encode the
282
+ # entire URL as UTF-8 already, mostly on the Windows platform (IE11 and lower).
283
+ original_url_normalized = request.original_url
284
+ if original_url_normalized.encoding == Encoding::BINARY
285
+ original_url_normalized = original_url_normalized.force_encoding(Encoding::ISO_8859_1)
286
+ .encode(Encoding::UTF_8)
287
+ end
288
+
289
+ # Using Addressable instead of standard URI to better deal with
290
+ # non-ASCII characters (see https://github.com/shakacode/react_on_rails/pull/405)
291
+ uri = Addressable::URI.parse(original_url_normalized)
292
+ # uri = Addressable::URI.parse("http://foo.com:3000/posts?id=30&limit=5#time=1305298413")
293
+
294
+ result.merge!(
295
+ # URL settings
296
+ href: uri.to_s,
297
+ location: "#{uri.path}#{uri.query.present? ? "?#{uri.query}" : ''}",
298
+ scheme: uri.scheme, # http
299
+ host: uri.host, # foo.com
300
+ port: uri.port,
301
+ pathname: uri.path, # /posts
302
+ search: uri.query, # id=30&limit=5
303
+ httpAcceptLanguage: request.env["HTTP_ACCEPT_LANGUAGE"]
304
+ )
305
+ end
306
+ if ReactOnRails.configuration.rendering_extension
307
+ custom_context = ReactOnRails.configuration.rendering_extension.custom_context(self)
308
+ result.merge!(custom_context) if custom_context
309
+ end
310
+ result
311
+ end
312
+
313
+ @rails_context.merge(serverSide: server_side)
314
+ end
315
+
316
+ def load_pack_for_generated_component(react_component_name, render_options)
317
+ return unless render_options.auto_load_bundle
318
+
319
+ ReactOnRails::WebpackerUtils.raise_nested_entries_disabled unless ReactOnRails::WebpackerUtils.nested_entries?
320
+ if Rails.env.development?
321
+ is_component_pack_present = File.exist?(generated_components_pack_path(react_component_name))
322
+ raise_missing_autoloaded_bundle(react_component_name) unless is_component_pack_present
323
+ end
324
+ append_javascript_pack_tag("generated/#{react_component_name}",
325
+ defer: ReactOnRails.configuration.defer_generated_component_packs)
326
+ append_stylesheet_pack_tag("generated/#{react_component_name}")
327
+ end
328
+
329
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
330
+
277
331
  private
278
332
 
333
+ def generated_components_pack_path(react_component_name)
334
+ "#{ReactOnRails::WebpackerUtils.webpacker_source_entry_path}/generated/#{react_component_name}.js"
335
+ end
336
+
279
337
  def build_react_component_result_for_server_rendered_string(
280
338
  server_rendered_html: required("server_rendered_html"),
281
339
  component_specification_tag: required("component_specification_tag"),
@@ -283,9 +341,15 @@ module ReactOnRails
283
341
  render_options: required("render_options")
284
342
  )
285
343
  content_tag_options = render_options.html_options
344
+ if content_tag_options.key?(:tag)
345
+ content_tag_options_html_tag = content_tag_options[:tag]
346
+ content_tag_options.delete(:tag)
347
+ else
348
+ content_tag_options_html_tag = "div"
349
+ end
286
350
  content_tag_options[:id] = render_options.dom_id
287
351
 
288
- rendered_output = content_tag(:div,
352
+ rendered_output = content_tag(content_tag_options_html_tag.to_sym,
289
353
  server_rendered_html.html_safe,
290
354
  content_tag_options)
291
355
 
@@ -333,13 +397,11 @@ module ReactOnRails
333
397
 
334
398
  def compose_react_component_html_with_spec_and_console(component_specification_tag, rendered_output, console_script)
335
399
  # IMPORTANT: Ensure that we mark string as html_safe to avoid escaping.
336
- # rubocop:disable Layout/IndentHeredoc
337
- <<-HTML.html_safe
338
- #{rendered_output}
339
- #{component_specification_tag}
340
- #{console_script}
400
+ <<~HTML.html_safe
401
+ #{rendered_output}
402
+ #{component_specification_tag}
403
+ #{console_script}
341
404
  HTML
342
- # rubocop:enable Layout/IndentHeredoc
343
405
  end
344
406
 
345
407
  # prepend the rails_context if not yet applied
@@ -372,7 +434,7 @@ module ReactOnRails
372
434
  # Setup the page_loaded_js, which is the same regardless of prerendering or not!
373
435
  # The reason is that React is smart about not doing extra work if the server rendering did its job.
374
436
  component_specification_tag = content_tag(:script,
375
- json_safe_and_pretty(render_options.props).html_safe,
437
+ json_safe_and_pretty(render_options.client_props).html_safe,
376
438
  type: "application/json",
377
439
  class: "js-react-on-rails-component",
378
440
  "data-component-name" => render_options.react_component_name,
@@ -382,6 +444,8 @@ module ReactOnRails
382
444
  # Create the HTML rendering part
383
445
  result = server_rendered_react_component(render_options)
384
446
 
447
+ load_pack_for_generated_component(react_component_name, render_options)
448
+
385
449
  {
386
450
  render_options: render_options,
387
451
  tag: component_specification_tag,
@@ -430,56 +494,48 @@ module ReactOnRails
430
494
  #
431
495
  # Read more here: http://timelessrepo.com/json-isnt-a-javascript-subset
432
496
 
433
- # rubocop:disable Layout/IndentHeredoc
434
- js_code = <<-JS
435
- (function() {
436
- var railsContext = #{rails_context(server_side: true).to_json};
437
- #{initialize_redux_stores}
438
- var props = #{props_string(props).gsub("\u2028", '\u2028').gsub("\u2029", '\u2029')};
439
- return ReactOnRails.serverRenderReactComponent({
440
- name: '#{react_component_name}',
441
- domNodeId: '#{render_options.dom_id}',
442
- props: props,
443
- trace: #{render_options.trace},
444
- railsContext: railsContext
445
- });
446
- })()
447
- JS
448
- # rubocop:enable Layout/IndentHeredoc
497
+ js_code = ReactOnRails::ServerRenderingJsCode.server_rendering_component_js_code(
498
+ props_string: props_string(props).gsub("\u2028", '\u2028').gsub("\u2029", '\u2029'),
499
+ rails_context: rails_context(server_side: true).to_json,
500
+ redux_stores: initialize_redux_stores,
501
+ react_component_name: react_component_name,
502
+ render_options: render_options
503
+ )
449
504
 
450
505
  begin
451
506
  result = ReactOnRails::ServerRenderingPool.server_render_js_with_console_logging(js_code, render_options)
452
507
  rescue StandardError => err
453
508
  # This error came from the renderer
454
- raise ReactOnRails::PrerenderError, component_name: react_component_name,
455
- # Sanitize as this might be browser logged
456
- props: sanitized_props_string(props),
457
- err: err,
458
- js_code: js_code
509
+ raise ReactOnRails::PrerenderError.new(component_name: react_component_name,
510
+ # Sanitize as this might be browser logged
511
+ props: sanitized_props_string(props),
512
+ err: err,
513
+ js_code: js_code)
459
514
  end
460
515
 
461
516
  if result["hasErrors"] && render_options.raise_on_prerender_error
462
517
  # We caught this exception on our backtrace handler
463
- raise ReactOnRails::PrerenderError, component_name: react_component_name,
464
- # Sanitize as this might be browser logged
465
- props: sanitized_props_string(props),
466
- err: nil,
467
- js_code: js_code,
468
- console_messages: result["consoleReplayScript"]
518
+ raise ReactOnRails::PrerenderError.new(component_name: react_component_name,
519
+ # Sanitize as this might be browser logged
520
+ props: sanitized_props_string(props),
521
+ err: nil,
522
+ js_code: js_code,
523
+ console_messages: result["consoleReplayScript"])
469
524
 
470
525
  end
471
526
  result
472
527
  end
473
528
 
474
529
  def initialize_redux_stores
475
- return "" unless @registered_stores.present? || @registered_stores_defer_render.present?
476
- declarations = "var reduxProps, store, storeGenerator;\n".dup
477
- all_stores = (@registered_stores || []) + (@registered_stores_defer_render || [])
478
-
479
- result = <<-JS.dup
530
+ result = +<<-JS
480
531
  ReactOnRails.clearHydratedStores();
481
532
  JS
482
533
 
534
+ return result unless @registered_stores.present? || @registered_stores_defer_render.present?
535
+
536
+ declarations = +"var reduxProps, store, storeGenerator;\n"
537
+ all_stores = (@registered_stores || []) + (@registered_stores_defer_render || [])
538
+
483
539
  result << all_stores.each_with_object(declarations) do |redux_store_data, memo|
484
540
  store_name = redux_store_data[:store_name]
485
541
  props = props_string(redux_store_data[:props])
@@ -493,79 +549,34 @@ module ReactOnRails
493
549
  result
494
550
  end
495
551
 
496
- # This is the definitive list of the default values used for the rails_context, which is the
497
- # second parameter passed to both component and store generator functions.
498
- # rubocop:disable Metrics/AbcSize
499
- def rails_context(server_side: required("server_side"))
500
- @rails_context ||= begin
501
- result = {
502
- railsEnv: Rails.env,
503
- inMailer: in_mailer?,
504
- # Locale settings
505
- i18nLocale: I18n.locale,
506
- i18nDefaultLocale: I18n.default_locale,
507
- rorVersion: ReactOnRails::VERSION,
508
- rorPro: ReactOnRails::Utils.react_on_rails_pro?
509
- }
510
- if defined?(request) && request.present?
511
- # Check for encoding of the request's original_url and try to force-encoding the
512
- # URLs as UTF-8. This situation can occur in browsers that do not encode the
513
- # entire URL as UTF-8 already, mostly on the Windows platform (IE11 and lower).
514
- original_url_normalized = request.original_url
515
- if original_url_normalized.encoding.to_s == "ASCII-8BIT"
516
- original_url_normalized = original_url_normalized.force_encoding("ISO-8859-1").encode("UTF-8")
517
- end
518
-
519
- # Using Addressable instead of standard URI to better deal with
520
- # non-ASCII characters (see https://github.com/shakacode/react_on_rails/pull/405)
521
- uri = Addressable::URI.parse(original_url_normalized)
522
- # uri = Addressable::URI.parse("http://foo.com:3000/posts?id=30&limit=5#time=1305298413")
523
-
524
- result.merge!(
525
- # URL settings
526
- href: uri.to_s,
527
- location: "#{uri.path}#{uri.query.present? ? "?#{uri.query}" : ''}",
528
- scheme: uri.scheme, # http
529
- host: uri.host, # foo.com
530
- port: uri.port,
531
- pathname: uri.path, # /posts
532
- search: uri.query, # id=30&limit=5
533
- httpAcceptLanguage: request.env["HTTP_ACCEPT_LANGUAGE"]
534
- )
535
- end
536
- if ReactOnRails.configuration.rendering_extension
537
- custom_context = ReactOnRails.configuration.rendering_extension.custom_context(self)
538
- result.merge!(custom_context) if custom_context
539
- end
540
- result
541
- end
542
-
543
- @rails_context.merge(serverSide: server_side)
544
- end
545
-
546
- # rubocop:enable Metrics/AbcSize
547
-
548
552
  def replay_console_option(val)
549
553
  val.nil? ? ReactOnRails.configuration.replay_console : val
550
554
  end
551
555
 
552
- def use_hot_reloading?
553
- ENV["REACT_ON_RAILS_ENV"] == "HOT"
554
- end
555
-
556
- def send_tag_method(tag_method_name, args)
557
- asset_type = use_hot_reloading? ? :hot : :static
558
- assets = Array(args[asset_type])
559
- options = args.delete_if { |key, _value| %i[hot static].include?(key) }
560
- send(tag_method_name, *assets, options) unless assets.empty?
561
- end
562
-
563
556
  def in_mailer?
564
557
  return false unless defined?(controller)
565
558
  return false unless defined?(ActionMailer::Base)
566
559
 
567
560
  controller.is_a?(ActionMailer::Base)
568
561
  end
562
+
563
+ if defined?(ScoutApm)
564
+ include ScoutApm::Tracer
565
+ instrument_method :react_component, type: "ReactOnRails", name: "react_component"
566
+ instrument_method :react_component_hash, type: "ReactOnRails", name: "react_component_hash"
567
+ end
568
+
569
+ def raise_missing_autoloaded_bundle(react_component_name)
570
+ msg = <<~MSG
571
+ **ERROR** ReactOnRails: Component "#{react_component_name}" is configured as "auto_load_bundle: true"
572
+ but the generated component entrypoint, which should have been at #{generated_components_pack_path(react_component_name)},
573
+ is missing. You might want to check that this component is in a directory named "#{ReactOnRails.configuration.components_subdirectory}"
574
+ & that "bundle exec rake react_on_rails:generate_packs" has been run.
575
+ MSG
576
+
577
+ raise ReactOnRails::Error, msg
578
+ end
569
579
  end
570
580
  end
571
581
  # rubocop:enable Metrics/ModuleLength
582
+ # rubocop:enable Metrics/MethodLength
@@ -11,7 +11,7 @@ module ReactOnRails
11
11
  "\u2028" => '\u2028',
12
12
  "\u2029" => '\u2029'
13
13
  }.freeze
14
- ESCAPE_REGEXP = /[\u2028\u2029&><]/u
14
+ ESCAPE_REGEXP = /[\u2028\u2029&><]/u.freeze
15
15
 
16
16
  def self.escape(json)
17
17
  return escape_without_erb_util(json) if Utils.rails_version_less_than_4_1_1
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReactOnRails
4
+ class JsonParseError < ::ReactOnRails::Error
5
+ attr_reader :json
6
+
7
+ def initialize(parse_error:, json:)
8
+ @json = json
9
+ @original_error = parse_error
10
+ super(parse_error.message)
11
+ end
12
+
13
+ def to_honeybadger_context
14
+ to_error_context
15
+ end
16
+
17
+ def raven_context
18
+ to_error_context
19
+ end
20
+
21
+ def to_error_context
22
+ {
23
+ original_error: @original_error,
24
+ json: @json
25
+ }
26
+ end
27
+ end
28
+ end