diff --git a/lib/assets/javascripts/react_ujs.js b/lib/assets/javascripts/react_ujs.js index 3480af23..ee472397 100644 --- a/lib/assets/javascripts/react_ujs.js +++ b/lib/assets/javascripts/react_ujs.js @@ -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 @@ -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 diff --git a/lib/react/rails/railtie.rb b/lib/react/rails/railtie.rb index ee3e5044..bd75eb1f 100644 --- a/lib/react/rails/railtie.rb +++ b/lib/react/rails/railtie.rb @@ -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 @@ -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 diff --git a/lib/react/server_rendering/exec_js_renderer.rb b/lib/react/server_rendering/exec_js_renderer.rb index 0afba4d3..bc8ef755 100644 --- a/lib/react/server_rendering/exec_js_renderer.rb +++ b/lib/react/server_rendering/exec_js_renderer.rb @@ -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) diff --git a/lib/react/server_rendering/sprockets_renderer.rb b/lib/react/server_rendering/sprockets_renderer.rb index ed43c20b..b6d69360 100644 --- a/lib/react/server_rendering/sprockets_renderer.rb +++ b/lib/react/server_rendering/sprockets_renderer.rb @@ -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 @@ -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 @@ -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 diff --git a/lib/react/server_rendering/webpacker_manifest_container.rb b/lib/react/server_rendering/webpacker_manifest_container.rb new file mode 100644 index 00000000..5cac670e --- /dev/null +++ b/lib/react/server_rendering/webpacker_manifest_container.rb @@ -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 diff --git a/lib/react/server_rendering/yaml_manifest_container.rb b/lib/react/server_rendering/yaml_manifest_container.rb index 8f5634cf..9d51cb79 100644 --- a/lib/react/server_rendering/yaml_manifest_container.rb +++ b/lib/react/server_rendering/yaml_manifest_container.rb @@ -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) diff --git a/react_ujs/dist/react_ujs.js b/react_ujs/dist/react_ujs.js index 3480af23..ee472397 100644 --- a/react_ujs/dist/react_ujs.js +++ b/react_ujs/dist/react_ujs.js @@ -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 @@ -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 diff --git a/react_ujs/index.js b/react_ujs/index.js index 5934ac0e..3cc3ead6 100644 --- a/react_ujs/index.js +++ b/react_ujs/index.js @@ -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 diff --git a/react_ujs/src/getConstructor/fromGlobal.js b/react_ujs/src/getConstructor/fromGlobal.js index 90ddb2cf..5c4aa3ff 100644 --- a/react_ujs/src/getConstructor/fromGlobal.js +++ b/react_ujs/src/getConstructor/fromGlobal.js @@ -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 diff --git a/react_ujs/yarn.lock b/react_ujs/yarn.lock index e3e3e5c4..52133cbf 100644 --- a/react_ujs/yarn.lock +++ b/react_ujs/yarn.lock @@ -993,8 +993,8 @@ ms@0.7.2: 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" @@ -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" @@ -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: @@ -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" diff --git a/test/dummy/app/controllers/server_controller.rb b/test/dummy/app/controllers/server_controller.rb index f72d1952..de3cb5af 100644 --- a/test/dummy/app/controllers/server_controller.rb +++ b/test/dummy/app/controllers/server_controller.rb @@ -1,5 +1,6 @@ class ServerController < ApplicationController def show + @component_name = params[:component_name] || "TodoList" @todos = %w{todo1 todo2 todo3} end diff --git a/test/dummy/app/javascript/components/GreetingMessage.js b/test/dummy/app/javascript/components/GreetingMessage.js new file mode 100644 index 00000000..387550df --- /dev/null +++ b/test/dummy/app/javascript/components/GreetingMessage.js @@ -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') + ); + } +}); diff --git a/test/dummy/app/javascript/components/Todo.js b/test/dummy/app/javascript/components/Todo.js new file mode 100644 index 00000000..5c16713d --- /dev/null +++ b/test/dummy/app/javascript/components/Todo.js @@ -0,0 +1,6 @@ +var React = require("react") +module.exports = React.createClass({ + render: function() { + return React.createElement("li", null, this.props.todo) + } +}) diff --git a/test/dummy/app/javascript/components/TodoList.js b/test/dummy/app/javascript/components/TodoList.js new file mode 100644 index 00000000..1a751260 --- /dev/null +++ b/test/dummy/app/javascript/components/TodoList.js @@ -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 ( + + ) + } +}) diff --git a/test/dummy/app/javascript/components/TodoListWithConsoleLog.js b/test/dummy/app/javascript/components/TodoListWithConsoleLog.js new file mode 100644 index 00000000..1d426869 --- /dev/null +++ b/test/dummy/app/javascript/components/TodoListWithConsoleLog.js @@ -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 ( + + ) + } +}) diff --git a/test/dummy/app/javascript/components/WithSetTimeout.js b/test/dummy/app/javascript/components/WithSetTimeout.js new file mode 100644 index 00000000..2adf058e --- /dev/null +++ b/test/dummy/app/javascript/components/WithSetTimeout.js @@ -0,0 +1,10 @@ +var React = require("react") +module.exports = React.createClass({ + componentWillMount: function () { + setTimeout(function () {}, 1000) + clearTimeout(0) + }, + render: function () { + return I am rendered! + } +}) diff --git a/test/dummy/app/javascript/packs/server_rendering.js b/test/dummy/app/javascript/packs/server_rendering.js new file mode 100644 index 00000000..fe6fbdd1 --- /dev/null +++ b/test/dummy/app/javascript/packs/server_rendering.js @@ -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) diff --git a/test/dummy/app/views/server/show.html.erb b/test/dummy/app/views/server/show.html.erb index 67985739..168559a7 100644 --- a/test/dummy/app/views/server/show.html.erb +++ b/test/dummy/app/views/server/show.html.erb @@ -1 +1 @@ -<%= react_component "TodoList", {todos: @todos}, {prerender: true} %> +<%= react_component @component_name, {todos: @todos}, {prerender: true} %> diff --git a/test/helper_files/TodoListWithUpdates.js b/test/helper_files/TodoListWithUpdates.js new file mode 100644 index 00000000..1b923380 --- /dev/null +++ b/test/helper_files/TodoListWithUpdates.js @@ -0,0 +1,10 @@ +var React = require("react") +module.exports = React.createClass({ + render: function() { + return ( + + ) + } +}) diff --git a/test/react/rails/component_mount_test.rb b/test/react/rails/component_mount_test.rb index d603f98d..3c14a6c4 100644 --- a/test/react/rails/component_mount_test.rb +++ b/test/react/rails/component_mount_test.rb @@ -3,6 +3,7 @@ when_sprockets_available do class ComponentMountTest < ActionDispatch::IntegrationTest setup do + WebpackerHelpers.compile_if_missing @helper = React::Rails::ComponentMount.new end diff --git a/test/react/rails/controller_lifecycle_test.rb b/test/react/rails/controller_lifecycle_test.rb index 2b5c236f..983f602d 100644 --- a/test/react/rails/controller_lifecycle_test.rb +++ b/test/react/rails/controller_lifecycle_test.rb @@ -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 diff --git a/test/react/rails/pages_controller_test.rb b/test/react/rails/pages_controller_test.rb index 89a7743a..45faf15a 100644 --- a/test/react/rails/pages_controller_test.rb +++ b/test/react/rails/pages_controller_test.rb @@ -1,6 +1,10 @@ require 'test_helper' class PagesControllerTest < ActionController::TestCase + setup do + WebpackerHelpers.compile_if_missing + end + test 'renders successfully' do get :show, id: 1 assert_equal(200, response.status) diff --git a/test/react/rails/react_rails_ujs_test.rb b/test/react/rails/react_rails_ujs_test.rb index 77135068..9aefcff9 100644 --- a/test/react/rails/react_rails_ujs_test.rb +++ b/test/react/rails/react_rails_ujs_test.rb @@ -6,6 +6,7 @@ class ReactRailsUJSTest < ActionDispatch::IntegrationTest setup do Capybara.current_driver = Capybara.javascript_driver + WebpackerHelpers.compile_if_missing end test 'ujs object present on the global React object and has our methods' do diff --git a/test/react/rails/webpacker_test.rb b/test/react/rails/webpacker_test.rb index 2f278f38..1bc9b13d 100644 --- a/test/react/rails/webpacker_test.rb +++ b/test/react/rails/webpacker_test.rb @@ -1,12 +1,12 @@ require 'test_helper' - WebpackerHelpers.when_webpacker_available do class ReactRailsWebpackerTest < ActionDispatch::IntegrationTest include Capybara::DSL setup do Capybara.current_driver = Capybara.javascript_driver + WebpackerHelpers.compile end teardown do diff --git a/test/react/server_rendering/exec_js_renderer_test.rb b/test/react/server_rendering/exec_js_renderer_test.rb index 7aa4c405..f45f881e 100644 --- a/test/react/server_rendering/exec_js_renderer_test.rb +++ b/test/react/server_rendering/exec_js_renderer_test.rb @@ -5,7 +5,7 @@ var React = { createElement: function() {}, } -var ReactRailsUJS = { +this.ReactRailsUJS = { serverRender: function() { return 'serverRender was called' }, @@ -50,7 +50,6 @@ def @renderer.after_render(name, props, opts) assert_no_match(/assigned_after_render/, error.message) end - test '#after_render is called after #before_render' do def @renderer.before_render(name, props, opts) "var beforeRenderVar = 'assigned_before_render'" diff --git a/test/react/server_rendering/sprockets_renderer_test.rb b/test/react/server_rendering/sprockets_renderer_test.rb index bb954e08..f2793255 100644 --- a/test/react/server_rendering/sprockets_renderer_test.rb +++ b/test/react/server_rendering/sprockets_renderer_test.rb @@ -4,7 +4,12 @@ class SprocketsRendererTest < ActiveSupport::TestCase CALLBACKS = [:before_render, :after_render] + webpacker_compiled = false setup do + if WebpackerHelpers.available? && !webpacker_compiled + WebpackerHelpers.compile + webpacker_compiled = true + end @renderer = React::ServerRendering::SprocketsRenderer.new({}) end @@ -62,8 +67,14 @@ class SprocketsRendererTest < ActiveSupport::TestCase err = assert_raises React::ServerRendering::PrerenderError do @renderer.render("NonExistentComponent", {}, nil) end - assert_match(/ReferenceError/, err.to_s) - assert_match(/NonExistentComponent/, err.to_s, "it names the component") + + if WebpackerHelpers.available? + assert_includes err.message, "Cannot find module './NonExistentComponent'" + else + assert_match(/ReferenceError/, err.to_s) + assert_match(/NonExistentComponent/, err.to_s, "it names the component") + end + assert_match(/\n/, err.to_s, "it includes the multi-line backtrace") end @@ -77,41 +88,45 @@ class SprocketsRendererTest < ActiveSupport::TestCase assert_match(/console.error.apply\(console, \["setTimeout #{message}"\]\);$/, result) end - test '.new accepts additional code to add to the JS context' do - additional_code = File.read(File.expand_path("../../../helper_files/WithoutSprockets.js", __FILE__)) + if !WebpackerHelpers.available? + # This doesn't work with webpacker since finding components is based on filename + test '.new accepts additional code to add to the JS context' do + additional_code = File.read(File.expand_path("../../../helper_files/WithoutSprockets.js", __FILE__)) - additional_renderer = React::ServerRendering::SprocketsRenderer.new(code: additional_code) + additional_renderer = React::ServerRendering::SprocketsRenderer.new(code: additional_code) - assert_match(/drink more caffeine<\/span>/, additional_renderer.render("WithoutSprockets", {label: "drink more caffeine"}, nil)) - end + assert_match(/drink more caffeine<\/span>/, additional_renderer.render("WithoutSprockets", {label: "drink more caffeine"}, nil)) + end - test '.new accepts any filenames' do - limited_renderer = React::ServerRendering::SprocketsRenderer.new(files: ["react-server.js", "react_ujs.js", "components/Todo.js"]) - assert_match(/get a real job<\/li>/, limited_renderer.render("Todo", {todo: "get a real job"}, nil)) - err = assert_raises React::ServerRendering::PrerenderError do - limited_renderer.render("TodoList", {todos: []}, nil) + # These use cases don't apply to Webpacker since the require.context comes from a pack: + test '.new accepts any filenames' do + limited_renderer = React::ServerRendering::SprocketsRenderer.new(files: ["react-server.js", "react_ujs.js", "components/Todo.js"]) + assert_match(/get a real job<\/li>/, limited_renderer.render("Todo", {todo: "get a real job"}, nil)) + err = assert_raises React::ServerRendering::PrerenderError do + limited_renderer.render("TodoList", {todos: []}, nil) + end + assert_match(/ReferenceError/, err.to_s, "it doesnt load other files") end - assert_match(/ReferenceError/, err.to_s, "it doesnt load other files") - end - test '#render returns html when config.assets.compile is false' do - begin - legacy_rendering_files = ["react-server.js", "react_ujs.js", "components.js"] - Rails.application.config.assets.precompile += legacy_rendering_files + test '#render returns html when config.assets.compile is false' do + begin + legacy_rendering_files = ["react-server.js", "react_ujs.js", "components.js"] + Rails.application.config.assets.precompile += legacy_rendering_files - precompile_assets + precompile_assets - Rails.application.config.assets.compile = false + Rails.application.config.assets.compile = false - @renderer = React::ServerRendering::SprocketsRenderer.new(files: legacy_rendering_files) + @renderer = React::ServerRendering::SprocketsRenderer.new(files: legacy_rendering_files) - result = @renderer.render("Todo", {todo: "write tests"}, nil) - assert_match(//, result) - assert_match(/data-react-checksum/, result) - ensure - Rails.application.config.assets.compile = true + result = @renderer.render("Todo", {todo: "write tests"}, nil) + assert_match(//, result) + assert_match(/data-react-checksum/, result) + ensure + Rails.application.config.assets.compile = true - clear_precompiled_assets + clear_precompiled_assets + end end end end diff --git a/test/react/server_rendering/webpacker_manifest_container_test.rb b/test/react/server_rendering/webpacker_manifest_container_test.rb new file mode 100644 index 00000000..585ce4d2 --- /dev/null +++ b/test/react/server_rendering/webpacker_manifest_container_test.rb @@ -0,0 +1,72 @@ +require "test_helper" +require "open-uri" + +WebpackerHelpers.when_webpacker_available do + class WebpackerManifestContainerTest < ActiveSupport::TestCase + setup do + WebpackerHelpers.clear_webpacker_packs + end + + test "it loads JS from the webpacker container" do + WebpackerHelpers.compile + container = React::ServerRendering::WebpackerManifestContainer.new + js_file = container.find_asset("application.js") + # Main file: + assert_includes js_file, "ReactRailsUJS.loadContext(ctx)" + # Bundled dependencies: + assert_includes js_file, "ExportDefaultComponent" + end + + def test_it_loads_from_webpack_dev_server + webpack_dev_server = fork do + Dir.chdir("test/dummy") do + exec "RAILS_ENV=development ./bin/webpack-dev-server " + end + end + + detected_dev_server = false + 60.times do |i| + begin + # Make sure that the manifest has been updated: + Webpacker::Manifest.load("./test/dummy/public/packs/manifest.json") + webpack_manifest = Webpacker::Manifest.instance.data + example_asset_path = webpack_manifest.values.first + if example_asset_path.nil? + puts "Manifest is blank, all manifests:" + Dir.glob("./test/dummy/public/packs/*.json").each do |f| + puts f + puts File.read(f) + end + next + end + assert_includes example_asset_path, "http://localhost:8080" + # Make sure the dev server is up: + open("http://localhost:8080/application.js") + detected_dev_server = true + break + rescue StandardError => err + puts err.message + ensure + sleep 0.5 + puts i + end + end + + # If we didn't hook up with a dev server after 10s, + # fail loudly. + assert detected_dev_server + + container = React::ServerRendering::WebpackerManifestContainer.new + js_file = container.find_asset("application.js") + # Main file: + assert_includes js_file, "ReactRailsUJS.loadContext(ctx)" + # Bundled dependencies: + assert_includes js_file, "ExportDefaultComponent" + ensure + Process.kill(9, webpack_dev_server) + Process.wait + # Remove the dev-server packs: + WebpackerHelpers.clear_webpacker_packs + end + end +end diff --git a/test/server_rendered_html_test.rb b/test/server_rendered_html_test.rb index 2aa174ed..41c4af05 100644 --- a/test/server_rendered_html_test.rb +++ b/test/server_rendered_html_test.rb @@ -10,10 +10,22 @@ def wait_to_ensure_asset_pipeline_detects_changes sleep(1) end + setup do + if WebpackerHelpers.available? + WebpackerHelpers.compile + end + end + test 'react server rendering reloads jsx after changes to the jsx files' do - file_with_updates = File.expand_path('../helper_files/TodoListWithUpdates.js.jsx', __FILE__) - file_without_updates = File.expand_path('../helper_files/TodoListWithoutUpdates.js.jsx', __FILE__) - app_file = File.expand_path('../dummy/app/assets/javascripts/components/TodoList.js.jsx', __FILE__) + if WebpackerHelpers.available? + file_with_updates = File.expand_path('../helper_files/TodoListWithUpdates.js', __FILE__) + file_without_updates = File.expand_path('../helper_files/TodoListWithoutUpdates.js', __FILE__) + app_file = File.expand_path('../dummy/app/javascript/components/TodoList.js', __FILE__) + else + file_with_updates = File.expand_path('../helper_files/TodoListWithUpdates.js.jsx', __FILE__) + file_without_updates = File.expand_path('../helper_files/TodoListWithoutUpdates.js.jsx', __FILE__) + app_file = File.expand_path('../dummy/app/assets/javascripts/components/TodoList.js.jsx', __FILE__) + end FileUtils.cp app_file, file_without_updates FileUtils.touch app_file @@ -25,7 +37,11 @@ def wait_to_ensure_asset_pipeline_detects_changes FileUtils.cp file_with_updates, app_file FileUtils.touch app_file - wait_to_ensure_asset_pipeline_detects_changes + if WebpackerHelpers.available? + WebpackerHelpers.compile + else + wait_to_ensure_asset_pipeline_detects_changes + end get '/server/1' assert_match(/Updated/, response.body) @@ -37,21 +53,36 @@ def wait_to_ensure_asset_pipeline_detects_changes end end - test 'it reloads when new jsx files are added' do + test 'it reloads when new jsx files are added to the asset pipeline' do begin - get '/server/1' - refute_match(/Overwritten List/, response.body) + assert_raises(ActionView::Template::Error) { + get '/server/1?component_name=NewList' + } - # Make it alphabetically last so it will override the preceeding one: - new_file_path = File.expand_path('../dummy/app/assets/javascripts/components/ZZ_NewComponent.js.jsx', __FILE__) - File.write new_file_path, <<-JS - var TodoList = function() { return "Overwritten List" } - JS + if WebpackerHelpers.available? + new_file_path = '../dummy/app/javascript/components/NewList.js' + new_file_contents = <<-JS + var React = require("react") + module.exports = function() { return "New List" } + JS + else + new_file_path = '../dummy/app/assets/javascripts/components/ZZ_NewComponent.js.jsx' + new_file_contents = <<-JS + var NewList = function() { return "New List" } + JS + end - wait_to_ensure_asset_pipeline_detects_changes + new_file_path = File.expand_path(new_file_path, __FILE__) + File.write new_file_path, new_file_contents - get '/server/1' - assert_match(/Overwritten List/, response.body) + if WebpackerHelpers.available? + WebpackerHelpers.compile + else + wait_to_ensure_asset_pipeline_detects_changes + end + + get '/server/1?component_name=NewList' + assert_match(/New List/, response.body) ensure FileUtils.rm_rf(new_file_path) wait_to_ensure_asset_pipeline_detects_changes diff --git a/test/support/webpacker_helpers.rb b/test/support/webpacker_helpers.rb index d6b8eaa4..b95f4817 100644 --- a/test/support/webpacker_helpers.rb +++ b/test/support/webpacker_helpers.rb @@ -1,4 +1,6 @@ module WebpackerHelpers + PACKS_DIRECTORY = File.expand_path("../../dummy/public/packs", __FILE__) + module_function def available? defined?(Webpacker) @@ -6,18 +8,30 @@ def available? def when_webpacker_available if available? - clear_webpacker_packs - Dir.chdir("./test/dummy") do + yield + end + end + + def compile + return if !available? + clear_webpacker_packs + Dir.chdir("./test/dummy") do + capture_io do + Rake::Task['webpacker:compile'].reenable Rake::Task['webpacker:compile'].invoke end - # Reload cached JSON manifest: - Webpacker::Manifest.load - yield + end + # Reload cached JSON manifest: + Webpacker::Manifest.load + end + + def compile_if_missing + if !File.exist?(PACKS_DIRECTORY) + compile end end def clear_webpacker_packs - packs_directory = File.expand_path("../dummy/public/packs", __FILE__) - FileUtils.rm_rf(packs_directory) + FileUtils.rm_rf(PACKS_DIRECTORY) end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 9da299b8..a1425d8b 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -20,6 +20,8 @@ require(f) end +WebpackerHelpers.clear_webpacker_packs + Capybara.javascript_driver = :poltergeist Capybara.app = Rails.application @@ -64,16 +66,22 @@ def asset.fresh?(env); false; end def precompile_assets capture_io do - ENV['RAILS_GROUPS'] = 'assets' # required for Rails 3.2 - Rake::Task['assets:precompile'].reenable + # Changing directories is required because: + # - assets:precompile runs webpacker:compile when availabled + # - webpacker:compile depends on `./bin/webpack`, so `.` must be the app root + Dir.chdir("./test/dummy") do - if Rails::VERSION::MAJOR == 3 - Rake::Task['assets:precompile:all'].reenable - Rake::Task['assets:precompile:primary'].reenable - Rake::Task['assets:precompile:nondigest'].reenable - end + ENV['RAILS_GROUPS'] = 'assets' # required for Rails 3.2 + Rake::Task['assets:precompile'].reenable - Rake::Task['assets:precompile'].invoke + if Rails::VERSION::MAJOR == 3 + Rake::Task['assets:precompile:all'].reenable + Rake::Task['assets:precompile:primary'].reenable + Rake::Task['assets:precompile:nondigest'].reenable + end + + Rake::Task['assets:precompile'].invoke + end end if Rails.application.respond_to?(:assets_manifest)