Skip to content

Webpacker Server Rendering #683

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

Merged
merged 10 commits into from
Apr 7, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions lib/assets/javascripts/react_ujs.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,15 +125,16 @@ module.exports = function(ujs) {
// Assume className is simple and can be found at top-level (window).
// Fallback to eval to handle cases like 'My.React.ComponentName'.
// Also, try to gracefully import Babel 6 style default exports
var topLevel = typeof window === "undefined" ? this : window;

module.exports = function(className) {
var constructor;
var topLevel = typeof window === "undefined" ? this : window;
// Try to access the class globally first
constructor = topLevel[className];

// If that didn't work, try eval
if (!constructor) {
constructor = eval.call(topLevel, className);
constructor = eval(className);
}

// Lastly, if there is a default attribute try that
Expand Down Expand Up @@ -308,6 +309,12 @@ if (typeof window !== "undefined") {
detectEvents(ReactRailsUJS)
}

// It's a bit of a no-no to populate the global namespace,
// but we really need it!
// We need access to this object for server rendering, and
// we can't do a dynamic `require`, so we'll grab it from here:
this.ReactRailsUJS = ReactRailsUJS

module.exports = ReactRailsUJS


Expand Down
5 changes: 5 additions & 0 deletions lib/react/rails/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ class Railtie < ::Rails::Railtie
# Changing files with these extensions in these directories will cause the server renderer to reload:
config.react.server_renderer_directories = ["/app/assets/javascripts/"]
config.react.server_renderer_extensions = ["jsx"]
if defined?(Webpacker)
config.react.server_renderer_directories << "app/javascript"
config.react.server_renderer_extensions << "js"
end
# View helper implementation:
config.react.view_helper_implementation = nil # Defaults to ComponentMount

Expand All @@ -30,6 +34,7 @@ class Railtie < ::Rails::Railtie
memo[app_dir] = config.react.server_renderer_extensions
memo
end

app.reloaders << ActiveSupport::FileUpdateChecker.new([], reload_paths) do
React::ServerRendering.reset_pool
end
Expand Down
2 changes: 1 addition & 1 deletion lib/react/server_rendering/exec_js_renderer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def render_from_parts(before, main, after)

def main_render(component_name, props, prerender_options)
render_function = prerender_options.fetch(:render_function, "renderToString")
"ReactRailsUJS.serverRender('#{render_function}', #{component_name}, #{props})"
"this.ReactRailsUJS.serverRender('#{render_function}', '#{component_name}', #{props})"
end

def compose_js(before, main, after)
Expand Down
38 changes: 25 additions & 13 deletions lib/react/server_rendering/sprockets_renderer.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require "react/server_rendering/environment_container"
require "react/server_rendering/manifest_container"
require "react/server_rendering/webpacker_manifest_container"
require "react/server_rendering/yaml_manifest_container"

module React
Expand Down Expand Up @@ -57,19 +58,7 @@ class << self
#
# @return [#find_asset(logical_path)] An object that returns asset contents by logical path
def asset_container
@asset_container ||= if self.class.asset_container_class.present?
self.class.asset_container_class.new
elsif assets_precompiled? && ManifestContainer.compatible?
ManifestContainer.new
elsif assets_precompiled? && YamlManifestContainer.compatible?
YamlManifestContainer.new
else
EnvironmentContainer.new
end
end

def assets_precompiled?
!::Rails.application.config.assets.compile
@asset_container ||= asset_container_class.new
end

private
Expand Down Expand Up @@ -97,6 +86,29 @@ def render_function(opts)
def prepare_props(props)
props.is_a?(String) ? props : props.to_json
end

def assets_precompiled?
!::Rails.application.config.assets.compile
end

def asset_container_class
if self.class.asset_container_class.present?
self.class.asset_container_class
elsif WebpackerManifestContainer.compatible?
WebpackerManifestContainer
elsif assets_precompiled?
if ManifestContainer.compatible?
ManifestContainer
elsif YamlManifestContainer.compatible?
YamlManifestContainer
else
# Even though they are precompiled, we can't find them :S
EnvironmentContainer
end
else
EnvironmentContainer
end
end
end
end
end
27 changes: 27 additions & 0 deletions lib/react/server_rendering/webpacker_manifest_container.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
require "open-uri"

module React
module ServerRendering
# Get a compiled file from Webpacker
class WebpackerManifestContainer
def find_asset(logical_path)
asset_path = Webpacker::Manifest.lookup(logical_path) # raises if not found
if asset_path.start_with?("http")
# TODO: this includes webpack-dev-server code which causes ExecJS to 💥
dev_server_asset = open(asset_path).read
else
full_path = File.join(
# TODO: using `.parent` here won't work for nonstandard configurations
Webpacker::Configuration.output_path.parent,
asset_path
)
File.read(full_path)
end
end

def self.compatible?
!!defined?(Webpacker)
end
end
end
end
2 changes: 1 addition & 1 deletion lib/react/server_rendering/yaml_manifest_container.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module React
module ServerRendering
# Get asset content by reading the compiled file from disk using the generated maniftest.yml file
# Get asset content by reading the compiled file from disk using the generated manifest.yml file
#
# This is good for Rails production when assets are compiled to public/assets
# but sometimes, they're compiled to other directories (or other servers)
Expand Down
11 changes: 9 additions & 2 deletions react_ujs/dist/react_ujs.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,15 +125,16 @@ module.exports = function(ujs) {
// Assume className is simple and can be found at top-level (window).
// Fallback to eval to handle cases like 'My.React.ComponentName'.
// Also, try to gracefully import Babel 6 style default exports
var topLevel = typeof window === "undefined" ? this : window;

module.exports = function(className) {
var constructor;
var topLevel = typeof window === "undefined" ? this : window;
// Try to access the class globally first
constructor = topLevel[className];

// If that didn't work, try eval
if (!constructor) {
constructor = eval.call(topLevel, className);
constructor = eval(className);
}

// Lastly, if there is a default attribute try that
Expand Down Expand Up @@ -308,6 +309,12 @@ if (typeof window !== "undefined") {
detectEvents(ReactRailsUJS)
}

// It's a bit of a no-no to populate the global namespace,
// but we really need it!
// We need access to this object for server rendering, and
// we can't do a dynamic `require`, so we'll grab it from here:
this.ReactRailsUJS = ReactRailsUJS

module.exports = ReactRailsUJS


Expand Down
6 changes: 6 additions & 0 deletions react_ujs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,10 @@ if (typeof window !== "undefined") {
detectEvents(ReactRailsUJS)
}

// It's a bit of a no-no to populate the global namespace,
// but we really need it!
// We need access to this object for server rendering, and
// we can't do a dynamic `require`, so we'll grab it from here:
self.ReactRailsUJS = ReactRailsUJS

module.exports = ReactRailsUJS
5 changes: 3 additions & 2 deletions react_ujs/src/getConstructor/fromGlobal.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
// Assume className is simple and can be found at top-level (window).
// Fallback to eval to handle cases like 'My.React.ComponentName'.
// Also, try to gracefully import Babel 6 style default exports
var topLevel = typeof window === "undefined" ? this : window;

module.exports = function(className) {
var constructor;
var topLevel = typeof window === "undefined" ? this : window;
// Try to access the class globally first
constructor = topLevel[className];

// If that didn't work, try eval
if (!constructor) {
constructor = eval.call(topLevel, className);
constructor = eval(className);
}

// Lastly, if there is a default attribute try that
Expand Down
16 changes: 8 additions & 8 deletions react_ujs/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -993,8 +993,8 @@ [email protected]:
resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765"

nan@^2.3.0:
version "2.5.1"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.5.1.tgz#d5b01691253326a97a2bbee9e61c55d8d60351e2"
version "2.6.1"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.1.tgz#8c84f7b14c96b89f57fbc838012180ec8ca39a01"

node-libs-browser@^2.0.0:
version "2.0.0"
Expand Down Expand Up @@ -1252,8 +1252,8 @@ randombytes@^2.0.0, randombytes@^2.0.1:
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.3.tgz#674c99760901c3c4112771a31e521dc349cc09ec"

rc@^1.1.7:
version "1.2.0"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.0.tgz#c7de973b7b46297c041366b2fd3d2363b1697c66"
version "1.2.1"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95"
dependencies:
deep-extend "~0.4.0"
ini "~1.3.0"
Expand All @@ -1275,7 +1275,7 @@ read-pkg@^1.0.0:
normalize-package-data "^2.3.2"
path-type "^1.0.0"

"readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.1.0, readable-stream@^2.1.4:
"readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.1.4, readable-stream@^2.2.6:
version "2.2.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.6.tgz#8b43aed76e71483938d12a8d46c6cf1a00b1f816"
dependencies:
Expand Down Expand Up @@ -1447,12 +1447,12 @@ stream-browserify@^2.0.1:
readable-stream "^2.0.2"

stream-http@^2.3.1:
version "2.6.3"
resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.6.3.tgz#4c3ddbf9635968ea2cfd4e48d43de5def2625ac3"
version "2.7.0"
resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.7.0.tgz#cec1f4e3b494bc4a81b451808970f8b20b4ed5f6"
dependencies:
builtin-status-codes "^3.0.0"
inherits "^2.0.1"
readable-stream "^2.1.0"
readable-stream "^2.2.6"
to-arraybuffer "^1.0.0"
xtend "^4.0.0"

Expand Down
1 change: 1 addition & 0 deletions test/dummy/app/controllers/server_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
class ServerController < ApplicationController
def show
@component_name = params[:component_name] || "TodoList"
@todos = %w{todo1 todo2 todo3}
end

Expand Down
23 changes: 23 additions & 0 deletions test/dummy/app/javascript/components/GreetingMessage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
var React = require("react")

module.exports = React.createClass({
getInitialState: function() {
var initialGreeting = 'Hello';
if (typeof global !== "undefined" && global.ctx && global.ctx.greeting) {
initialGreeting = global.ctx.greeting
}

return {
greeting: initialGreeting
}
},
goodbye: function() {
this.setState({greeting: 'Goodbye'});
},
render: function() {
return React.DOM.div({},
React.DOM.div({}, this.state.greeting, ' ', this.props.name),
React.DOM.button({onClick: this.goodbye}, 'Goodbye')
);
}
});
6 changes: 6 additions & 0 deletions test/dummy/app/javascript/components/Todo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
var React = require("react")
module.exports = React.createClass({
render: function() {
return React.createElement("li", null, this.props.todo)
}
})
21 changes: 21 additions & 0 deletions test/dummy/app/javascript/components/TodoList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
var React = require("react")

module.exports = React.createClass({
getInitialState: function() {
return({mounted: "nope"});
},
componentWillMount: function() {
this.setState({mounted: 'yep'});
},
render: function() {
return (
<ul>
<li id='status'>{this.state.mounted}</li>
{this.props.todos.map(function(todo, i) {
return (<li key={i}>{todo}</li>)
})}
<li>From Webpacker</li>
</ul>
)
}
})
25 changes: 25 additions & 0 deletions test/dummy/app/javascript/components/TodoListWithConsoleLog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
var React = require("react")

module.exports = React.createClass({
getInitialState: function() {
console.log('got initial state');
return({mounted: "nope"});
},
componentWillMount: function() {
console.warn('mounted component');
this.setState({mounted: 'yep'});
},
render: function() {
var x = 'foo';
console.error('rendered!', x);
return (
<ul>
<li>Console Logged</li>
<li id='status'>{this.state.mounted}</li>
{this.props.todos.map(function(todo, i) {
return (<li key={i}>{todo}</li>)
})}
</ul>
)
}
})
10 changes: 10 additions & 0 deletions test/dummy/app/javascript/components/WithSetTimeout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
var React = require("react")
module.exports = React.createClass({
componentWillMount: function () {
setTimeout(function () {}, 1000)
clearTimeout(0)
},
render: function () {
return <span>I am rendered!</span>
}
})
5 changes: 5 additions & 0 deletions test/dummy/app/javascript/packs/server_rendering.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// By default, this pack is loaded for server-side rendering.
// It must expose react_ujs as `ReactRailsUJS` and prepare a require context.
var componentRequireContext = require.context("components", true)
var ReactRailsUJS = require("../../../../../react_ujs/index")
ReactRailsUJS.loadContext(componentRequireContext)
2 changes: 1 addition & 1 deletion test/dummy/app/views/server/show.html.erb
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<%= react_component "TodoList", {todos: @todos}, {prerender: true} %>
<%= react_component @component_name, {todos: @todos}, {prerender: true} %>
10 changes: 10 additions & 0 deletions test/helper_files/TodoListWithUpdates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
var React = require("react")
module.exports = React.createClass({
render: function() {
return (
<ul>
<li>Updated</li>
</ul>
)
}
})
1 change: 1 addition & 0 deletions test/react/rails/component_mount_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
when_sprockets_available do
class ComponentMountTest < ActionDispatch::IntegrationTest
setup do
WebpackerHelpers.compile_if_missing
@helper = React::Rails::ComponentMount.new
end

Expand Down
1 change: 1 addition & 0 deletions test/react/rails/controller_lifecycle_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def react_component(*args)

class ControllerLifecycleTest < ActionDispatch::IntegrationTest
def setup
WebpackerHelpers.compile_if_missing
@previous_helper_implementation = React::Rails::ViewHelper.helper_implementation_class
React::Rails::ViewHelper.helper_implementation_class = DummyHelperImplementation
end
Expand Down
Loading