Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for multiple server file bundles #343

Closed
wants to merge 8 commits into from
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ Place your JavaScript code inside of the provided `client/app` folder. Use modul
```
+ *Server-Side Rendering:*

If you are server rendering, `serverRegistration.jsx` will have this. Note, you might be initializing HelloWorld with version specialized for server rendering.
If you are server rendering, `server.jsx` will have this. Note, you might be initializing HelloWorld with version specialized for server rendering.

```javascript
import HelloWorld from '../components/HelloWorld';
Expand All @@ -237,7 +237,7 @@ See below section on how to setup redux stores that allow multiple components to
## ReactOnRails View Helpers API
Once the bundled files have been generated in your `app/assets/webpack` folder and you have exposed your components globally, you will want to run your code in your Rails views using the included helper method.

This is how you actually render the React components you exposed to `window` inside of `clientRegistration` (and `global` inside of `serverRegistration` if you are server rendering).
This is how you actually render the React components you exposed to `window` inside of `clientRegistration` (and `global` inside of `server` if you are server rendering).

### react_component
`react_component(component_name, options = {})`
Expand Down Expand Up @@ -404,7 +404,7 @@ The generated client code follows our organization scheme. Each unique set of fu

Inside of the generated "HelloWorld" domain you will find the following folders:

+ `startup`: two types of files, one that return a container component and implement any code that differs between client and server code (if using server-rendering), and a `clientRegistration` file that exposes the aforementioned files (as well as a `serverRegistration` file if using server rendering). These registration files are what webpack is using as an entry point.
+ `startup`: two types of files, one that return a container component and implement any code that differs between client and server code (if using server-rendering), and a `clientRegistration` file that exposes the aforementioned files (as well as a `server` file if using server rendering). These registration files are what webpack is using as an entry point.
+ `containers`: "smart components" (components that have functionality and logic that is passed to child "dumb components").
+ `components`: includes "dumb components", or components that simply render their properties and call functions given to them as properties by a parent component. Ultimately, at least one of these dumb components will have a parent container component.

Expand Down
16 changes: 12 additions & 4 deletions app/helpers/react_on_rails_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def env_stylesheet_link_tag(args = {})
# Exposing the react_component_name is necessary to both a plain ReactComponent as well as
# a generator:
# See README.md for how to "register" your react components.
# See spec/dummy/client/app/startup/serverRegistration.jsx and
# See spec/dummy/client/app/startup/server.jsx and
# spec/dummy/client/app/startup/ClientRegistration.jsx for examples of this
#
# options:
Expand All @@ -84,6 +84,8 @@ def env_stylesheet_link_tag(args = {})
# true, you'll still see the errors on the server.
# raise_on_prerender_error: <true/false> Default to false. True will raise exception on server
# if the JS code throws
# server_bundle_js_file: the name of the server bundle js file you want to use for rendering the component.
# If this is not specified, the default file specified in your config/initializers/react_on_rails.rb is used.
# Any other options are passed to the content tag, including the id.
def react_component(component_name, options = {}, other_options = nil)
# Create the JavaScript and HTML to allow either client or server rendering of the
Expand Down Expand Up @@ -217,7 +219,8 @@ def server_render_js(js_expression, options = {})
})()
JS

result = ReactOnRails::ServerRenderingPool.server_render_js_with_console_logging(wrapper_js)
server_js_file = server_bundle_js_file(options)
result = ReactOnRails::ServerRenderingPool.server_render_js_with_console_logging(server_js_file, wrapper_js)

# IMPORTANT: To ensure that Rails doesn't auto-escape HTML tags, use the 'raw' method.
html = result["html"]
Expand Down Expand Up @@ -258,7 +261,7 @@ def server_rendered_react_component_html(options, props, react_component_name, d
# On server `location` option is added (`location = request.fullpath`)
# React Router needs this to match the current route

# Make sure that we use up-to-date server-bundle
# Make sure that we use up-to-date server bundles js files
ReactOnRails::ServerRenderingPool.reset_pool_if_server_bundle_was_modified

# Since this code is not inserted on a web page, we don't need to escape props
Expand All @@ -277,7 +280,8 @@ def server_rendered_react_component_html(options, props, react_component_name, d
})()
JS

result = ReactOnRails::ServerRenderingPool.server_render_js_with_console_logging(wrapper_js)
server_js_file = server_bundle_js_file(options)
result = ReactOnRails::ServerRenderingPool.server_render_js_with_console_logging(server_js_file, wrapper_js)

if result["hasErrors"] && raise_on_prerender_error(options)
# We caught this exception on our backtrace handler
Expand Down Expand Up @@ -337,6 +341,10 @@ def replay_console(options)
options.fetch(:replay_console) { ReactOnRails.configuration.replay_console }
end

def server_bundle_js_file(options = {})
options[:server_bundle_js_file] || ReactOnRails::Utils.default_server_bundle_js_file
end

# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/PerceivedComplexity
def parse_options_props(component_name, options, other_options)
Expand Down
2 changes: 1 addition & 1 deletion lib/generators/react_on_rails/base_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ def install_server_rendering_files_if_enabled
return unless options.server_rendering?
base_path = "base/server_rendering/"
%w(client/webpack.server.rails.config.js
client/app/bundles/HelloWorld/startup/serverRegistration.jsx).each do |file|
client/app/bundles/HelloWorld/startup/server.jsx).each do |file|
copy_file(base_path + file, file)
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ ReactOnRails.configure do |config|
# Server bundle is a single file for all server rendering of components.
# Set the server_bundle_js_file to "" if you know that you will not be server rendering.
<%- if options.server_rendering? %>
config.server_bundle_js_file = "server-bundle.js"
config.server_bundle_js_files = ["server-bundle.js"]
<% else %>
config.server_bundle_js_file = ""
config.server_bundle_js_files = []
<%- end %>
# increase if you're on JRuby
config.server_renderer_pool_size = 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ module.exports = {
context: __dirname,
entry: [
'babel-polyfill',
'./app/bundles/HelloWorld/startup/serverRegistration',
'./app/bundles/HelloWorld/startup/server',
],
output: {
filename: 'server-bundle.js',
Expand Down
30 changes: 19 additions & 11 deletions lib/react_on_rails/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ def self.configure
def self.setup_config_values
if @configuration.webpack_generated_files.empty?
files = ["client-bundle.js"]
if @configuration.server_bundle_js_file.present?
files << @configuration.server_bundle_js_file
if @configuration.server_bundle_js_files.present?
files += @configuration.server_bundle_js_files
end
@configuration.webpack_generated_files = files
end
Expand All @@ -31,11 +31,7 @@ def self.setup_config_values
puts "ReactOnRails: Set generated_assets_dir to default: #{DEFAULT_GENERATED_ASSETS_DIR}"
end

if @configuration.server_bundle_js_file.include?(File::SEPARATOR)
puts "[DEPRECATION] ReactOnRails: remove path from server_bundle_js_file in configuration. "\
"All generated files must go in #{@configuration.generated_assets_dir}"
@configuration.server_bundle_js_file = File.basename(@configuration.server_bundle_js_file)
end
@configuration.normalize_server_bundle_js_files!
end

def self.configuration
Expand All @@ -45,7 +41,7 @@ def self.configuration
# generated_assets_dirs is deprecated
generated_assets_dir: "",

server_bundle_js_file: "",
server_bundle_js_files: [],
prerender: false,
replay_console: true,
logging_on_server: true,
Expand All @@ -60,20 +56,20 @@ def self.configuration
end

class Configuration
attr_accessor :server_bundle_js_file, :prerender, :replay_console,
attr_accessor :server_bundle_js_files, :prerender, :replay_console,
:trace, :development_mode,
:logging_on_server, :server_renderer_pool_size,
:server_renderer_timeout, :raise_on_prerender_error,
:skip_display_none, :generated_assets_dirs, :generated_assets_dir,
:webpack_generated_files

def initialize(server_bundle_js_file: nil, prerender: nil, replay_console: nil,
def initialize(server_bundle_js_files: [], prerender: nil, replay_console: nil,
trace: nil, development_mode: nil,
logging_on_server: nil, server_renderer_pool_size: nil,
server_renderer_timeout: nil, raise_on_prerender_error: nil,
skip_display_none: nil, generated_assets_dirs: nil,
generated_assets_dir: nil, webpack_generated_files: nil)
self.server_bundle_js_file = server_bundle_js_file
self.server_bundle_js_files = server_bundle_js_files
self.generated_assets_dirs = generated_assets_dirs
self.generated_assets_dir = generated_assets_dir

Expand All @@ -95,5 +91,17 @@ def initialize(server_bundle_js_file: nil, prerender: nil, replay_console: nil,

self.webpack_generated_files = webpack_generated_files
end

def normalize_server_bundle_js_files!
@server_bundle_js_files.map! do |server_bundle_js_file|
if server_bundle_js_file.include?(File::SEPARATOR)
puts "[DEPRECATION] ReactOnRails: remove path from server_bundle_js_files in configuration. "\
"All generated files must go in #{@generated_assets_dir}"
File.basename(server_bundle_js_file)
else
server_bundle_js_file
end
end
end
end
end
62 changes: 37 additions & 25 deletions lib/react_on_rails/server_rendering_pool.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,23 @@ class ServerRenderingPool
def self.reset_pool
options = { size: ReactOnRails.configuration.server_renderer_pool_size,
timeout: ReactOnRails.configuration.server_renderer_pool_size }
@js_context_pool = ConnectionPool.new(options) { create_js_context }
@js_context_pools =
ReactOnRails.configuration.server_bundle_js_files.each_with_object({}) do |server_bundle_js_file, hash|
hash[server_bundle_js_file] = ConnectionPool.new(options) { create_js_context(server_bundle_js_file) }
end
end

def self.reset_pool_if_server_bundle_was_modified
return unless ReactOnRails.configuration.development_mode
file_mtime = File.mtime(ReactOnRails::Utils.default_server_bundle_js_file_path)
@server_bundle_timestamp ||= file_mtime
return if @server_bundle_timestamp == file_mtime
ReactOnRails::ServerRenderingPool.reset_pool
@server_bundle_timestamp = file_mtime
@server_bundle_timestamps ||= {}
do_reset_pool = false
ReactOnRails.configuration.server_bundle_js_files.each do |server_bundle_js_file|
file_mtime = File.mtime(ReactOnRails::Utils.server_bundle_js_file_path(server_bundle_js_file))
next if @server_bundle_timestamps[server_bundle_js_file] == file_mtime
@server_bundle_timestamps[server_bundle_js_file] = file_mtime
do_reset_pool = true
end
ReactOnRails::ServerRenderingPool.reset_pool if do_reset_pool
end

# js_code: JavaScript expression that returns a string.
Expand All @@ -28,11 +35,10 @@ def self.reset_pool_if_server_bundle_was_modified
# Note, js_code does not have to be based on React.
# js_code MUST RETURN json stringify Object
# Calling code will probably call 'html_safe' on return value before rendering to the view.
def self.server_render_js_with_console_logging(js_code)
trace_messsage(js_code)
json_string = eval_js(js_code)
def self.server_render_js_with_console_logging(server_bundle_js_file, js_code)
trace_message(js_code)
json_string = eval_js(server_bundle_js_file, js_code)
result = JSON.parse(json_string)

if ReactOnRails.configuration.logging_on_server
console_script = result["consoleReplayScript"]
console_script_lines = console_script.split("\n")
Expand All @@ -51,7 +57,7 @@ def self.server_render_js_with_console_logging(js_code)
class << self
private

def trace_messsage(js_code, file_name = "tmp/server-generated.js")
def trace_message(js_code, file_name = "tmp/server-generated.js")
return unless ENV["TRACE_REACT_ON_RAILS"].present?
# Set to anything to print generated code.
puts "Z" * 80
Expand All @@ -61,18 +67,26 @@ def trace_messsage(js_code, file_name = "tmp/server-generated.js")
puts "Z" * 80
end

def eval_js(js_code)
@js_context_pool.with do |js_context|
def eval_js(server_js_file, js_code)
server_js_file_context_pool = js_context_pool_for_file(server_js_file)
raise "Bundle [#{server_js_file}] not set in js context pools" if server_js_file_context_pool.nil?
server_js_file_context_pool.with do |js_context|
result = js_context.eval(js_code)
js_context.eval("console.history = []")
result
end
end

def create_js_context
server_js_file = ReactOnRails::Utils.default_server_bundle_js_file_path
if server_js_file.present? && File.exist?(server_js_file)
bundle_js_code = File.read(server_js_file)
def js_context_pool_for_file(server_js_file)
@js_context_pools[server_js_file]
end

def create_js_context(server_js_file)
return unless server_js_file.present?

server_js_file_path = ReactOnRails::Utils.server_bundle_js_file_path(server_js_file)
if File.exist?(server_js_file_path)
bundle_js_code = File.read(server_js_file_path)
base_js_code = <<-JS
#{console_polyfill}
#{execjs_timer_polyfills}
Expand All @@ -88,17 +102,15 @@ def create_js_context
"\n\n#{e.backtrace.join("\n")}"
puts msg
Rails.logger.error(msg)
trace_messsage(base_js_code, file_name)
trace_message(base_js_code, file_name)
raise e
end
else
if server_js_file.present?
msg = "You specified server rendering JS file: #{server_js_file}, but it cannot be "\
"read. You may set the server_bundle_js_file in your configuration to be \"\" to "\
"avoid this warning"
Rails.logger.warn msg
puts msg
end
msg = "You specified server rendering JS file: #{server_js_file}, but it cannot be "\
"read. You may set the server_bundle_js_files in your configuration to be \"[]\" to "\
"avoid this warning"
Rails.logger.warn msg
puts msg
ExecJS.compile("")
end
end
Expand Down
13 changes: 11 additions & 2 deletions lib/react_on_rails/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,25 @@ def self.object_to_boolean(value)
end

def self.server_rendering_is_enabled?
ReactOnRails.configuration.server_bundle_js_file.present?
ReactOnRails.configuration.server_bundle_js_files.present?
end

def self.last_process_completed_successfully?
$CHILD_STATUS.exitstatus == 0
end

def self.server_bundle_js_file_path(server_bundle_js_file)
File.join(ReactOnRails.configuration.generated_assets_dir,
server_bundle_js_file)
end

def self.default_server_bundle_js_file
ReactOnRails.configuration.server_bundle_js_files.first
end

def self.default_server_bundle_js_file_path
File.join(ReactOnRails.configuration.generated_assets_dir,
ReactOnRails.configuration.server_bundle_js_file)
default_server_bundle_js_file)
end
end
end
2 changes: 2 additions & 0 deletions spec/dummy/app/controllers/pages_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,7 @@ def data
name: "Mrs. Client Side Hello Again"
}
}

@custom_server_bundle_js_file = "alternative-server-bundle.js"
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
</li>
<li>
Expose the HelloWorld Component on the server side:
spec/dummy/client/app/startup/serverRegistration.jsx
spec/dummy/client/app/startup/server.jsx
<br/>
<pre>
import HelloWorld from '../components/HelloWorld';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<%= render "header" %>

<%= react_component("HelloWorld",
@app_props_server_render.to_json,
prerender: true,
trace: true,
id: "my-hello-world-id",
class: "my-hello-world-class",
data: { x: 1, y: 2 },
server_bundle_js_file: @custom_server_bundle_js_file
) %>
<hr/>

<h1>React Rails Server Rendering with options</h1>
<p>
This example demonstrates passing extra options to the example
<%= link_to "Hello World Component Server Rendered", server_side_hello_world_path %>
The differences include:
</p>
<ul>
<li>
Sending the props as already converted from JSON to a string.
</li>
<li>
Passing extra params that get passed to the tag shown in the HTML, including the option to set
the id of the component.
</li>
</ul>
<pre>
<%%= react_component("HelloWorld",
@app_props_server_render.to_json,
prerender: true,
trace: true,
id: "my-hello-world-id",
class: "my-hello-world-class",
data: { x: 1, y: 2} ) %>
</pre>
2 changes: 1 addition & 1 deletion spec/dummy/app/views/pages/server_side_redux_app.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
</li>
<li>
Expose the Redux Component on the server side:
spec/dummy/client/app/startup/serverRegistration.jsx
spec/dummy/client/app/startup/server.jsx
<br/>
<pre>
import ReduxApp from './ServerReduxApp';
Expand Down
Loading