diff --git a/CHANGELOG.md b/CHANGELOG.md index 153ff0dcd..aa8bc8b47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ Changes since last non-beta release. - Upgraded the example test app in `spec/dummy` to React 18. [PR 1463](https://github.com/shakacode/react_on_rails/pull/1463) by [alexeyr](https://github.com/alexeyr). +- Added file-system-based automatic bundle generation feature. [PR 1455](https://github.com/shakacode/react_on_rails/pull/1455) by [pulkitkkr](https://github.com/pulkitkkr). + #### Fixed - Correctly unmount roots under React 18. [PR 1466](https://github.com/shakacode/react_on_rails/pull/1466) by [alexeyr](https://github.com/alexeyr). diff --git a/README.md b/README.md index 0b9829bcd..3dc215f9b 100644 --- a/README.md +++ b/README.md @@ -49,8 +49,9 @@ To provide a high performance framework for integrating Ruby on Rails with React Given that `rails/webpacker` gem already provides basic React integration, why would you use "React on Rails"? 1. Easy passing of props directly from your Rails view to your React components rather than having your Rails view load and then make a separate request to your API. -1. Tight integration with [shakapacker](https://github.com/shakacode/shakapacker) (or it's predecessor [rails/webpacker](https://github.com/rails/webpacker)). +Tight integration with [shakapacker](https://github.com/shakacode/shakapacker) (or it's predecessor [rails/webpacker](https://github.com/rails/webpacker)). 1. Server-Side Rendering (SSR), often used for SEO crawler indexing and UX performance. +1. [Automated optimized entry-point creation and bundle inclusion when placing a component on a page. With this feature, you no longer need to configure `javascript_pack_tags` and `stylesheet_pack_tags` on your layouts based on what’s shown. “It just works!”](https://www.shakacode.com/react-on-rails/docs/guides/file-system-based-automated-bundle-generation.md) 1. [Redux](https://github.com/reactjs/redux) and [React Router](https://github.com/ReactTraining/react-router#readme) integration with server-side-rendering. 1. [Internationalization (I18n) and (localization)](https://www.shakacode.com/react-on-rails/docs/guides/i18n) 1. A supportive community. This [web search shows how live public sites are using React on Rails](https://publicwww.com/websites/%22react-on-rails%22++-undeveloped.com+depth%3Aall/). diff --git a/docs/api/view-helpers-api.md b/docs/api/view-helpers-api.md index 6c0edbeec..c23431e42 100644 --- a/docs/api/view-helpers-api.md +++ b/docs/api/view-helpers-api.md @@ -1,7 +1,7 @@ # View and Controller Helpers ## View Helpers API -Once the bundled files have been generated in your `app/assets/webpack` folder and you have registered your components, you will want to render these components on your Rails views using the included helper method, `react_component`. +Once the bundled files have been generated in your `app/assets/webpack` folder and you have registered your components, you will want to render these components on your Rails views using the included helper method, [`react_component`](https://www.shakacode.com/react-on-rails/docs/api/view-helpers-api/#react_component). ------------ @@ -27,6 +27,7 @@ Uncommonly used options: - **general options:** - **props:** Ruby Hash which contains the properties to pass to the react object, or a JSON string. If you pass a string, we'll escape it for you. - **prerender:** enable server-side rendering of a component. Set to false when debugging! + - **auto_load_bundle:** will automatically load the bundle for component by calling `append_javascript_pack_tag` and `append_stylesheet_pack_tag` under the hood. - **id:** Id for the div, will be used to attach the React component. This will get assigned automatically if you do not provide an id. Must be unique. - **html_options:** Any other HTML options get placed on the added div for the component. For example, you can set a class (or inline style) on the outer div so that it behaves like a span, with the styling of `display:inline-block`. You may also use an option of `tag: "span"` to replace the use of the default DIV tag to be a SPAN tag. - **trace:** set to true to print additional debugging information in the browser. Defaults to true for development, off otherwise. Only on the **client side** will you will see the `railsContext` and your props. diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md index e6ac570d2..3b2bf32ac 100644 --- a/docs/guides/configuration.md +++ b/docs/guides/configuration.md @@ -151,6 +151,19 @@ ReactOnRails.configure do |config| config.server_renderer_pool_size = 1 # increase if you're on JRuby config.server_renderer_timeout = 20 # seconds + ################################################################################ + ################################################################################ + # FILE SYSTEM BASED COMPONENT REGISTRY + ################################################################################ + # components_subdirectory is the name of the subdirectory matched to detect and register components automatically + # The default is nil. You can enable the feature by updating it in the next line. + config.components_subdirectory = "ror_components" + + # For automated component registry, `render_component` view helper method tries to load bundle for component from + # generated directory. default is false, you can pass option at the time of individual usage or update the default + # in the following line + config.auto_load_bundle = false + ################################################################################ # I18N OPTIONS ################################################################################ diff --git a/docs/guides/file-system-based-automated-bundle-generation.md b/docs/guides/file-system-based-automated-bundle-generation.md new file mode 100644 index 000000000..875080b87 --- /dev/null +++ b/docs/guides/file-system-based-automated-bundle-generation.md @@ -0,0 +1,188 @@ +# File-System-Based Automated Bundle Generation + +To use the automated bundle generation feature introduced in React on Rails v13.1.0, please upgrade to use [Shakapacker v6.5.1](https://github.com/shakacode/shakapacker/tree/v6.5.1) at least. If you are currently using webpacker, please follow the migration steps available [here](https://github.com/shakacode/shakapacker/blob/master/docs/v6_upgrade.md). + +## Configuration + +### Enable nested_entries for Shakapacker +To use the automated bundle generation feature, set nested_entries: true in the webpacker.yml file like this. The generated files will go in a nested directory. + +```yml +default: + ... + nested_entries: true +``` + +For more details, see [Configuration and Code](https://github.com/shakacode/shakapacker#configuration-and-code) section in [shakapacker](https://github.com/shakacode/shakapacker/). + +### Configure Components Subdirectory +`components_subdirectory` is the name of the matched directories containing components that will be automatically registered for use by the view helpers. +For example, configure `config/initializers/react_on_rails` to set the name for `components_subdirectory`.· + +```rb +config.components_subdirectory = "ror_components" +``` + +Now all React components inside the directories called `ror_components` will automatically be registered for usage with [`react_component`](https://www.shakacode.com/react-on-rails/docs/api/view-helpers-api/#react_component) and [`react_component_hash`](https://www.shakacode.com/react-on-rails/docs/api/view-helpers-api/#react_component_hash) helper methods provided by React on Rails. + +### Configure `auto_load_bundle` Option + +For automated component registry, [`react_component`](https://www.shakacode.com/react-on-rails/docs/api/view-helpers-api/#react_component) and [`react_component_hash`](https://www.shakacode.com/react-on-rails/docs/api/view-helpers-api/#react_component_hash) view helper method tries to load generated bundle for component from the generated directory automatically per `auto_load_bundle` option. `auto_load_bundle` option in `config/initializers/react_on_rails` configures the default value that will be passed to component helpers. The default is `false`, and the parameter can be passed explicitly for each call. + +You can change the value in `config/initializers/react_on_rails` by updating it as follows: + +```rb +config.auto_load_bundle = true +``` + +### Update `.gitignore` file +React on Rails automatically generates pack files for components to be registered in the `packs/generated` directory. To avoid committing generated files into the version control system, please update `.gitignore` to have + +```gitignore +# Generated React on Rails packs +app/javascript/packs/generated +``` + +*Note: the directory might be different depending on the `source_entry_path` in `config/webpacker.yml`.* + +## Usage + +### Basic usage + +#### Background +If the `webpacker.yml` file is configured as instructed [here](https://github.com/shakacode/shakapacker#configuration-and-code), with the following configurations + +```yml +default: &default + source_path: app/javascript + source_entry_path: packs + public_root_path: public + public_output_path: packs + nested_entries: true +# And more +``` + +the directory structure will look like this +``` +app/javascript: + └── packs: # sets up webpack entries + │ └── application.js # references FooComponentOne.jsx, BarComponentOne.jsx and BarComponentTwo.jsx in `../src` + └── src: # any directory name is fine. Referenced files need to be under source_path + │ └── Foo + │ │ └── ... + │ │ └── FooComponentOne.jsx + │ └── Bar + │ │ └── ... + │ │ └── BarComponentOne.jsx + │ │ └── BarComponentTwo.jsx + └── stylesheets: + │ └── my_styles.css + └── images: + └── logo.svg +``` + +Previously, many applications would use one pack (webpack entrypoint) for many components. In this example, the`application.js` file manually registers server components, `FooComponentOne`, `BarComponentOne` and `BarComponentTwo`. + +```jsx +import ReactOnRails from 'react-on-rails'; +import FooComponentOne from '../src/Foo/FooComponentOne'; +import BarComponentOne from '../src/Foo/BarComponentOne'; +import BarComponentTwo from '../src/Foo/BarComponentTwo'; + +ReactOnRails.register({ FooComponentOne, BarComponentOne, BarComponentTwo }); +``` + +Your layout would contain: + +```erb + <%= javascript_pack_tag 'application' %> + <%= stylesheet_pack_tag 'application' %> +``` + + +Suppose, you want to use bundle splitting to minimize unnecessary javascript loaded on each page, You would put each of your components in the `packs` directory. +``` +app/javascript: + └── packs: # sets up webpack entries + │ └── FooComponentOne.jsx # Internally uses ReactOnRails.register + │ └── BarComponentOne.jsx # Internally uses ReactOnRails.register + │ └── BarComponentTwo.jsx # Internally uses ReactOnRails.register + └── src: # any directory name is fine. Referenced files need to be under source_path + │ └── Foo + │ │ └── ... + │ └── Bar + │ │ └── ... + └── stylesheets: + │ └── my_styles.css + └── images: + └── logo.svg +``` + +The tricky part is to figure out which bundles to load on any Rails view. [Shakapacker's `append_stylesheet_pack_tag` and `append_javascript_pack_tag` view helpers](https://github.com/shakacode/shakapacker#view-helper-append_javascript_pack_tag-and-append_stylesheet_pack_tag) enables Rails views to specify needed bundles for use by layout's call to `javascript_pack_tag` and `stylesheet_pack_tag`. + +#### Solution + +File-system-based automated pack generation simplifies this process with a new option for the view helpers. The steps to use it in this example are: + +1. Remove parameters passed directly to `javascript_pack_tag` and `stylesheet_pack_tag`. +2. Remove parameters passed directly to `append_javascript_pack_tag` and `append_stylesheet_pack_tag`. + +Your layout would now contain: + +```erb + <%= javascript_pack_tag %> + <%= stylesheet_pack_tag %> +``` + +3. Create a directory structure as mentioned below: + +``` +app/javascript: + └── packs + └── src: + │ └── Foo + │ │ └── ... + │ │ └── ror_components # configured as `components_subdirectory` + │ │ └── FooComponentOne.jsx + │ └── Bar + │ │ └── ... + │ │ └── ror_components # configured as `components_subdirectory` + │ │ │ └── BarComponentOne.jsx + │ │ │ └── BarComponentTwo.jsx +``` + +4. You no longer need to register these React components nor directly add their bundles. For example you can have a Rails view using three components: + +```erb + <%= react_component("FooComponentOne", {}, auto_load_bundle: true) %> + <%= react_component("BarComponentOne", {}, auto_load_bundle: true) %> + <%= react_component("BarComponentTwo", {}, auto_load_bundle: true) %> +``` + +If `FooComponentOne` uses multiple HTML strings for server rendering, the [`react_component_hash`](https://www.shakacode.com/react-on-rails/docs/api/view-helpers-api/#react_component_hash) view helper can be used on the Rails view, as illustrated below. + +```erb +<% foo_component_one_data = react_component_hash("FooComponentOne", + prerender: true, + auto_load_bundle: true + props: {} + ) %> +<% content_for :title do %> + <%= foo_component_one_data['title'] %> +<% end %> +<%= foo_component_one_data["componentHtml"] %> +``` + +The default value of the `auto_load_bundle` parameter can be specified by setting `config.auto_load_bundle` in `config/initializers/react_on_rails.rb` and thus removed from each call to `react_component`. + +### Server Rendering and Client Rendering Components + +If server rendering is enabled, the component will be registered for usage both in server and client rendering. In order to have separate definitions for client and server rendering, name the component files as `ComponentName.server.jsx` and `ComponentName.client.jsx`. The `ComponentName.server.jsx` file will be used for server rendering and the `ComponentName.client.jsx` file for client rendering. If you don't want the component rendered on the server, you should only have the `ComponentName.client.jsx` file. + +*Note: If specifying separate definitions for client and server rendering, please make sure to delete the generalized `ComponentName.jsx` file.* + +### Using Automated Bundle Generation Feature with already defined packs + +To use the Automated Bundle Generation feature with already defined packs, `config/initializers/react_on_rails` should explicitly be configured with `config.auto_load_bundle = false` and you can explicitly pass `auto_load_bundle` option in [`react_component`](https://www.shakacode.com/react-on-rails/docs/api/view-helpers-api/#react_component) and [`react_component_hash`](https://www.shakacode.com/react-on-rails/docs/api/view-helpers-api/#react_component_hash) for the components using this feature. + + diff --git a/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb b/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb index af51b3980..17fca3ce9 100644 --- a/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb +++ b/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb @@ -42,4 +42,17 @@ # React components. # config.server_bundle_js_file = "server-bundle.js" + + ################################################################################ + ################################################################################ + # FILE SYSTEM BASED COMPONENT REGISTRY + ################################################################################ + # `components_subdirectory` is the name of the matching directories that contain automatically registered components + # for use in the Rails views. The default is nil, you can enable the feature by updating it in the next line. + # config.components_subdirectory = "ror_components" + # + # For automated component registry, `render_component` view helper method tries to load bundle for component from + # generated directory. default is false, you can pass option at the time of individual usage or update the default + # in the following line + config.auto_load_bundle = false end diff --git a/lib/react_on_rails.rb b/lib/react_on_rails.rb index 0af65434c..347534a4d 100644 --- a/lib/react_on_rails.rb +++ b/lib/react_on_rails.rb @@ -19,6 +19,7 @@ require "react_on_rails/git_utils" require "react_on_rails/utils" require "react_on_rails/webpacker_utils" +require "react_on_rails/packs_generator" require "react_on_rails/test_helper/webpack_assets_compiler" require "react_on_rails/test_helper/webpack_assets_status_checker" require "react_on_rails/test_helper/ensure_assets_compiled" diff --git a/lib/react_on_rails/configuration.rb b/lib/react_on_rails/configuration.rb index 5fd03fac8..f21748d77 100644 --- a/lib/react_on_rails/configuration.rb +++ b/lib/react_on_rails/configuration.rb @@ -7,6 +7,7 @@ def self.configure end DEFAULT_GENERATED_ASSETS_DIR = File.join(%w[public webpack], Rails.env).freeze + DEFAULT_COMPONENTS_SUBDIRECTORY = nil DEFAULT_SERVER_RENDER_TIMEOUT = 20 DEFAULT_POOL_SIZE = 1 DEFAULT_RANDOM_DOM_ID = true # for backwards compatability @@ -19,6 +20,7 @@ def self.configuration generated_assets_dir: "", server_bundle_js_file: "", prerender: false, + auto_load_bundle: false, replay_console: true, logging_on_server: true, raise_on_prerender_error: Rails.env.development?, @@ -36,7 +38,8 @@ def self.configuration build_production_command: "", random_dom_id: DEFAULT_RANDOM_DOM_ID, same_bundle_for_client_and_server: false, - i18n_output_format: nil + i18n_output_format: nil, + components_subdirectory: DEFAULT_COMPONENTS_SUBDIRECTORY ) end @@ -49,8 +52,8 @@ class Configuration :webpack_generated_files, :rendering_extension, :build_test_command, :build_production_command, :i18n_dir, :i18n_yml_dir, :i18n_output_format, - :server_render_method, :random_dom_id, - :same_bundle_for_client_and_server, :rendering_props_extension + :server_render_method, :random_dom_id, :auto_load_bundle, + :same_bundle_for_client_and_server, :rendering_props_extension, :components_subdirectory # rubocop:disable Metrics/AbcSize def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender: nil, @@ -64,7 +67,8 @@ def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender build_production_command: nil, same_bundle_for_client_and_server: nil, i18n_dir: nil, i18n_yml_dir: nil, i18n_output_format: nil, - random_dom_id: nil, server_render_method: nil, rendering_props_extension: nil) + random_dom_id: nil, server_render_method: nil, rendering_props_extension: nil, + components_subdirectory: nil, auto_load_bundle: nil) self.node_modules_location = node_modules_location.present? ? node_modules_location : Rails.root self.generated_assets_dirs = generated_assets_dirs self.generated_assets_dir = generated_assets_dir @@ -98,6 +102,8 @@ def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender self.rendering_extension = rendering_extension self.server_render_method = server_render_method + self.components_subdirectory = components_subdirectory + self.auto_load_bundle = auto_load_bundle end # rubocop:enable Metrics/AbcSize @@ -125,6 +131,7 @@ def adjust_precompile_task ENV["WEBPACKER_PRECOMPILE"] = "false" precompile_tasks = lambda { + Rake::Task["react_on_rails:generate_packs"].invoke Rake::Task["react_on_rails:assets:webpack"].invoke puts "Invoking task webpacker:clean from React on Rails" diff --git a/lib/react_on_rails/helper.rb b/lib/react_on_rails/helper.rb index 60b37da33..6e8bc8f0e 100644 --- a/lib/react_on_rails/helper.rb +++ b/lib/react_on_rails/helper.rb @@ -57,6 +57,7 @@ def react_component(component_name, options = {}) internal_result = internal_react_component(component_name, options) server_rendered_html = internal_result[:result]["html"] console_script = internal_result[:result]["consoleReplayScript"] + render_options = internal_result[:render_options] case server_rendered_html when String @@ -64,7 +65,7 @@ def react_component(component_name, options = {}) server_rendered_html: server_rendered_html, component_specification_tag: internal_result[:tag], console_script: console_script, - render_options: internal_result[:render_options] + render_options: render_options ) when Hash msg = <<~MSG @@ -90,6 +91,26 @@ def react_component(component_name, options = {}) end end + def load_pack_for_component(component_name) + component_pack_file = generated_components_pack(component_name) + is_component_pack_present = File.exist?("#{component_pack_file}.jsx") + is_development = ENV["RAILS_ENV"] == "development" + + if is_development && !is_component_pack_present + ReactOnRails::PacksGenerator.generate + raise_generated_missing_pack_warning(component_name) + end + + ReactOnRails::PacksGenerator.raise_nested_enteries_disabled unless ReactOnRails::WebpackerUtils.nested_entries? + + append_javascript_pack_tag "generated/#{component_name}" + append_stylesheet_pack_tag "generated/#{component_name}" + end + + def generated_components_pack(component_name) + "#{ReactOnRails::WebpackerUtils.webpacker_source_entry_path}/generated/#{component_name}" + end + # react_component_hash is used to return multiple HTML strings for server rendering, such as for # adding meta-tags to a page. # It is exactly like react_component except for the following: @@ -112,6 +133,7 @@ def react_component_hash(component_name, options = {}) internal_result = internal_react_component(component_name, options) server_rendered_html = internal_result[:result]["html"] console_script = internal_result[:result]["consoleReplayScript"] + render_options = internal_result[:render_options] if server_rendered_html.is_a?(String) && internal_result[:result]["hasErrors"] server_rendered_html = { COMPONENT_HTML_KEY => internal_result[:result]["html"] } @@ -122,7 +144,7 @@ def react_component_hash(component_name, options = {}) server_rendered_html: server_rendered_html, component_specification_tag: internal_result[:tag], console_script: console_script, - render_options: internal_result[:render_options] + render_options: render_options ) else msg = <<~MSG @@ -425,6 +447,8 @@ def internal_react_component(react_component_name, options = {}) # Create the HTML rendering part result = server_rendered_react_component(render_options) + load_pack_for_component react_component_name if render_options.auto_load_bundle + { render_options: render_options, tag: component_specification_tag, @@ -528,6 +552,17 @@ def initialize_redux_stores result end + def raise_generated_missing_pack_warning(component_name) + msg = <<~MSG + **ERROR** ReactOnRails: Generated missing pack for Component: #{component_name}. Please refresh the webpage \ + once webpack has finished generating the bundles. If the problem persists + 1. Verify `components_subdirectory` is configured in `config/initializers/react_on_rails`. + 2. Component: #{component_name} is placed inside the configured `components_subdirectory`. + MSG + + raise ReactOnRails::Error, msg + end + def replay_console_option(val) val.nil? ? ReactOnRails.configuration.replay_console : val end diff --git a/lib/react_on_rails/packs_generator.rb b/lib/react_on_rails/packs_generator.rb new file mode 100644 index 000000000..a3ccb8217 --- /dev/null +++ b/lib/react_on_rails/packs_generator.rb @@ -0,0 +1,298 @@ +# frozen_string_literal: true + +require "fileutils" + +module ReactOnRails + # rubocop:disable Metrics/ClassLength + class PacksGenerator + CONTAINS_CLIENT_OR_SERVER_REGEX = /\.(server|client)($|\.)/.freeze + MINIMUM_SHAKAPACKER_MAJOR_VERSION = 6 + MINIMUM_SHAKAPACKER_MINOR_VERSION = 5 + MINIMUM_SHAKAPACKER_PATCH_VERSION = 1 + + def self.generate + packs_generator = PacksGenerator.new + packs_generator.verify_setup_and_generate_packs + end + + def self.raise_nested_enteries_disabled + packs_generator = PacksGenerator.new + packs_generator.raise_nested_enteries_disabled + end + + def verify_setup_and_generate_packs + return unless components_subdirectory.present? + + raise_webpacker_not_installed unless ReactOnRails::WebpackerUtils.using_webpacker? + raise_shakapacker_version_incompatible unless shackapacker_version_requirement_met? + raise_nested_enteries_disabled unless ReactOnRails::WebpackerUtils.nested_entries? + + is_generated_directory_present = Dir.exist?(generated_packs_directory_path) + + return if is_generated_directory_present && webpack_assets_status_checker.stale_generated_component_packs.empty? + + clean_generated_packs_directory + generate_packs + end + + def raise_nested_enteries_disabled + msg = <<~MSG + **ERROR** ReactOnRails: `nested_entries` is configured to be disabled in shakapacker. Please update \ + webpacker.yml to enable nested enteries. for more information read + https://www.shakacode.com/react-on-rails/docs/guides/file-system-based-automated-bundle-generation.md#enable-nested_entries-for-shakapacker + MSG + + raise ReactOnRails::Error, msg + end + + private + + def generate_packs + common_component_to_path.each_value { |component_path| create_pack(component_path) } + client_component_to_path.each_value { |component_path| create_pack(component_path) } + + create_server_pack if ReactOnRails.configuration.server_bundle_js_file.present? + end + + def create_pack(file_path) + output_path = generated_pack_path(file_path) + content = pack_file_contents(file_path) + + File.write(output_path, content) + + puts(Rainbow("Generated Packs: #{output_path}").yellow) + end + + def pack_file_contents(file_path) + registered_component_name = component_name(file_path) + <<~FILE_CONTENT + import ReactOnRails from 'react-on-rails'; + import #{registered_component_name} from '#{relative_component_path_from_generated_pack(file_path)}'; + + ReactOnRails.register({#{registered_component_name}}); + FILE_CONTENT + end + + def create_server_pack + File.write(generated_server_bundle_file_path, generated_server_pack_file_content) + + add_generated_pack_to_server_bundle + puts(Rainbow("Generated Server Bundle: #{generated_server_bundle_file_path}").orange) + end + + def generated_server_pack_file_content + common_components_for_server_bundle = common_component_to_path.delete_if { |k| server_component_to_path.key?(k) } + component_for_server_registration_to_path = common_components_for_server_bundle.merge(server_component_to_path) + + server_component_imports = component_for_server_registration_to_path.map do |name, component_path| + "import #{name} from '#{relative_path(generated_server_bundle_file_path, component_path)}';" + end + + components_to_register = component_for_server_registration_to_path.keys + + <<~FILE_CONTENT + import ReactOnRails from 'react-on-rails'; + + #{server_component_imports.join("\n")} + + ReactOnRails.register({#{components_to_register.join(",\n")}}); + FILE_CONTENT + end + + def add_generated_pack_to_server_bundle + relative_path_to_generated_server_bundle = relative_path(defined_server_bundle_file_path, + generated_server_bundle_file_path) + content = <<~FILE_CONTENT + import "./#{relative_path_to_generated_server_bundle}"\n + FILE_CONTENT + + prepend_to_file_if_not_present(defined_server_bundle_file_path, content) + end + + def generated_server_bundle_file_path + file_ext = File.extname(defined_server_bundle_file_path) + generated_server_bundle_file_path = defined_server_bundle_file_path.sub(file_ext, "-generated#{file_ext}") + generated_server_bundle_file_name = component_name(generated_server_bundle_file_path) + source_entry_path = ReactOnRails::WebpackerUtils.webpacker_source_entry_path + + "#{source_entry_path}/#{generated_server_bundle_file_name}#{file_ext}" + end + + def clean_generated_packs_directory + FileUtils.rm_rf(generated_packs_directory_path) + FileUtils.mkdir_p(generated_packs_directory_path) + end + + def defined_server_bundle_file_path + ReactOnRails::Utils.server_bundle_js_file_path + end + + def generated_packs_directory_path + source_entry_path = ReactOnRails::WebpackerUtils.webpacker_source_entry_path + + "#{source_entry_path}/generated" + end + + def relative_component_path_from_generated_pack(ror_component_path) + component_file_pathname = Pathname.new(ror_component_path) + component_generated_pack_path = generated_pack_path(ror_component_path) + generated_pack_pathname = Pathname.new(component_generated_pack_path) + + relative_path(generated_pack_pathname, component_file_pathname) + end + + def relative_path(from, to) + from_path = Pathname.new(from) + to_path = Pathname.new(to) + + relative_path = to_path.relative_path_from(from_path) + relative_path.sub("../", "") + end + + def generated_pack_path(file_path) + "#{generated_packs_directory_path}/#{component_name(file_path)}.jsx" + end + + def component_name(file_path) + basename = File.basename(file_path, File.extname(file_path)) + + basename.sub(CONTAINS_CLIENT_OR_SERVER_REGEX, "") + end + + def component_name_to_path(paths) + paths.to_h { |path| [component_name(path), path] } + end + + def common_component_to_path + common_components_paths = Dir.glob("#{components_search_path}/*").reject do |f| + CONTAINS_CLIENT_OR_SERVER_REGEX.match?(f) + end + component_name_to_path(common_components_paths) + end + + def client_component_to_path + client_render_components_paths = Dir.glob("#{components_search_path}/*.client.*") + client_specific_components = component_name_to_path(client_render_components_paths) + + duplicate_components = common_component_to_path.slice(*client_specific_components.keys) + duplicate_components.each_key { |component| raise_client_component_overrides_common(component) } + + client_specific_components + end + + def server_component_to_path + server_render_components_paths = Dir.glob("#{components_search_path}/*.server.*") + server_specific_components = component_name_to_path(server_render_components_paths) + + duplicate_components = common_component_to_path.slice(*server_specific_components.keys) + duplicate_components.each_key { |component| raise_server_component_overrides_common(component) } + + server_specific_components.each_key do |k| + raise_missing_client_component(k) unless client_component_to_path.key?(k) + end + + server_specific_components + end + + def components_search_path + source_path = ReactOnRails::WebpackerUtils.webpacker_source_path + + "#{source_path}/**/#{components_subdirectory}" + end + + def components_subdirectory + ReactOnRails.configuration.components_subdirectory + end + + def webpack_assets_status_checker + source_path = ReactOnRails::Utils.source_path + generated_assets_full_path = ReactOnRails::Utils.generated_assets_full_path + webpack_generated_files = ReactOnRails.configuration.webpack_generated_files + + @webpack_assets_status_checker ||= ReactOnRails::TestHelper::WebpackAssetsStatusChecker.new( + source_path: source_path, + generated_assets_full_path: generated_assets_full_path, + webpack_generated_files: webpack_generated_files + ) + end + + def raise_client_component_overrides_common(component_name) + msg = <<~MSG + **ERROR** ReactOnRails: client specific definition for Component '#{component_name}' overrides the \ + common definition. Please delete the common definition and have separate server and client files. For more \ + information, please see https://www.shakacode.com/react-on-rails/docs/guides/file-system-based-automated-bundle-generation.md + MSG + + raise ReactOnRails::Error, msg + end + + def raise_server_component_overrides_common(component_name) + msg = <<~MSG + **ERROR** ReactOnRails: server specific definition for Component '#{component_name}' overrides the \ + common definition. Please delete the common definition and have separate server and client files. For more \ + information, please see https://www.shakacode.com/react-on-rails/docs/guides/file-system-based-automated-bundle-generation.md + MSG + + raise ReactOnRails::Error, msg + end + + def raise_missing_client_component(component_name) + msg = <<~MSG + **ERROR** ReactOnRails: Component '#{component_name}' is missing a client specific file. For more \ + information, please see https://www.shakacode.com/react-on-rails/docs/guides/file-system-based-automated-bundle-generation.md + MSG + + raise ReactOnRails::Error, msg + end + + def raise_shakapacker_version_incompatible + msg = <<~MSG + **ERROR** ReactOnRails: Please upgrade Shakapacker to version #{minimum_required_shakapacker_version} or \ + above to use the automated bundle generation feature. The currently installed version is \ + #{ReactOnRails::WebpackerUtils.shakapacker_version}. + MSG + + raise ReactOnRails::Error, msg + end + + def raise_webpacker_not_installed + msg = <<~MSG + **ERROR** ReactOnRails: Missing Shakapacker gem. Please upgrade to use Shakapacker \ + #{minimum_required_shakapacker_version} or above to use the \ + automated bundle generation feature. + MSG + + raise ReactOnRails::Error, msg + end + + def shakapacker_major_minor_version + shakapacker_version = ReactOnRails::WebpackerUtils.shakapacker_version + match = shakapacker_version.match(ReactOnRails::VersionChecker::MAJOR_MINOR_PATCH_VERSION_REGEX) + + [match[1].to_i, match[2].to_i, match[3].to_i] + end + + def shackapacker_version_requirement_met? + major = shakapacker_major_minor_version[0] + minor = shakapacker_major_minor_version[1] + patch = shakapacker_major_minor_version[2] + + major >= MINIMUM_SHAKAPACKER_MAJOR_VERSION && minor >= MINIMUM_SHAKAPACKER_MINOR_VERSION && + patch >= MINIMUM_SHAKAPACKER_PATCH_VERSION + end + + def minimum_required_shakapacker_version + "#{MINIMUM_SHAKAPACKER_MAJOR_VERSION}.#{MINIMUM_SHAKAPACKER_MINOR_VERSION}.#{MINIMUM_SHAKAPACKER_PATCH_VERSION}" + end + + def prepend_to_file_if_not_present(file, text_to_prepend) + file_content = File.read(file) + + return if file_content.include?(text_to_prepend) + + content_with_prepended_text = text_to_prepend + file_content + File.write(file, content_with_prepended_text) + end + end + # rubocop:enable Metrics/ClassLength +end diff --git a/lib/react_on_rails/react_component/render_options.rb b/lib/react_on_rails/react_component/render_options.rb index cc025ef21..3b12bb93c 100644 --- a/lib/react_on_rails/react_component/render_options.rb +++ b/lib/react_on_rails/react_component/render_options.rb @@ -71,6 +71,10 @@ def prerender retrieve_configuration_value_for(:prerender) end + def auto_load_bundle + retrieve_configuration_value_for(:auto_load_bundle) + end + def trace retrieve_configuration_value_for(:trace) end diff --git a/lib/react_on_rails/test_helper/ensure_assets_compiled.rb b/lib/react_on_rails/test_helper/ensure_assets_compiled.rb index 3c38ce422..ad36427a0 100644 --- a/lib/react_on_rails/test_helper/ensure_assets_compiled.rb +++ b/lib/react_on_rails/test_helper/ensure_assets_compiled.rb @@ -37,6 +37,8 @@ def call # All done if no stale files! return if stale_gen_files.empty? + ReactOnRails::PacksGenerator.generate + # Inform the developer that we're ensuring gen assets are ready. puts_start_compile_check_message(stale_gen_files) diff --git a/lib/react_on_rails/test_helper/webpack_assets_status_checker.rb b/lib/react_on_rails/test_helper/webpack_assets_status_checker.rb index 1074d722e..bd41f18cf 100644 --- a/lib/react_on_rails/test_helper/webpack_assets_status_checker.rb +++ b/lib/react_on_rails/test_helper/webpack_assets_status_checker.rb @@ -26,12 +26,20 @@ def initialize( end def stale_generated_webpack_files + stale_generated_files(client_files) + end + + def stale_generated_component_packs + stale_generated_files(component_pack_files) + end + + def stale_generated_files(files) manifest_needed = ReactOnRails::WebpackerUtils.using_webpacker? && !ReactOnRails::WebpackerUtils.manifest_exists? return ["manifest.json"] if manifest_needed - most_recent_mtime = find_most_recent_mtime + most_recent_mtime = find_most_recent_mtime(files) all_compiled_assets.each_with_object([]) do |webpack_generated_file, stale_gen_list| if !File.exist?(webpack_generated_file) || File.mtime(webpack_generated_file) < most_recent_mtime @@ -43,8 +51,8 @@ def stale_generated_webpack_files private - def find_most_recent_mtime - client_files.reduce(1.year.ago) do |newest_time, file| + def find_most_recent_mtime(files) + files.reduce(1.year.ago) do |newest_time, file| mt = File.mtime(file) mt > newest_time ? mt : newest_time end @@ -81,6 +89,14 @@ def client_files @client_files ||= make_file_list(make_globs(source_path)).to_ary end + def component_pack_files + make_file_list(make_globs(components_search_path)).to_ary + end + + def components_search_path + "#{source_path}/**/#{ReactOnRails.configuration.components_subdirectory}" + end + def make_globs(dirs) Array(dirs).map { |dir| File.join(dir, "**", "*") } end diff --git a/lib/react_on_rails/webpacker_utils.rb b/lib/react_on_rails/webpacker_utils.rb index 19133a615..fe9ff59d9 100644 --- a/lib/react_on_rails/webpacker_utils.rb +++ b/lib/react_on_rails/webpacker_utils.rb @@ -17,6 +17,12 @@ def self.dev_server_running? Webpacker.dev_server.running? end + def self.shakapacker_version + return nil unless ReactOnRails::Utils.gem_available?("shakapacker") + + Gem.loaded_specs["shakapacker"].version.to_s + end + # This returns either a URL for the webpack-dev-server, non-server bundle or # the hashed server bundle if using the same bundle for the client. # Otherwise returns a file path. @@ -45,6 +51,14 @@ def self.webpacker_source_path Webpacker.config.source_path end + def self.webpacker_source_entry_path + Webpacker.config.source_entry_path + end + + def self.nested_entries? + Webpacker.config.nested_entries? + end + def self.webpacker_public_output_path # Webpacker has the full absolute path of webpacker output files in a Pathname Webpacker.config.public_output_path.to_s diff --git a/lib/tasks/generate_packs.rake b/lib/tasks/generate_packs.rake new file mode 100644 index 000000000..69daa3bd0 --- /dev/null +++ b/lib/tasks/generate_packs.rake @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +namespace :react_on_rails do + desc <<~DESC + If there is a file inside any directory matching config.components_subdirectory, this command generates corresponding packs. + DESC + + task generate_packs: :environment do + ReactOnRails::PacksGenerator.generate + end +end diff --git a/spec/react_on_rails/configuration_spec.rb b/spec/react_on_rails/configuration_spec.rb index fd034ca49..f5a9ada0a 100644 --- a/spec/react_on_rails/configuration_spec.rb +++ b/spec/react_on_rails/configuration_spec.rb @@ -186,6 +186,22 @@ module ReactOnRails expect(ReactOnRails.configuration.random_dom_id).to eq(false) end + it "changes the configuration of the gem, such as setting the auto_load_bundle option to false" do + ReactOnRails.configure do |config| + config.auto_load_bundle = false + end + + expect(ReactOnRails.configuration.auto_load_bundle).to eq(false) + end + + it "changes the configuration of the gem, such as setting the auto_load_bundle option to true" do + ReactOnRails.configure do |config| + config.auto_load_bundle = true + end + + expect(ReactOnRails.configuration.auto_load_bundle).to eq(true) + end + it "has a default configuration of the gem" do # rubocop:disable Lint/EmptyBlock ReactOnRails.configure do |_config| diff --git a/spec/react_on_rails/fixtures/automated_packs_generation/components/ComponentWithClientAndCommon/ror_components/ComponentWithClientAndCommon.client.jsx b/spec/react_on_rails/fixtures/automated_packs_generation/components/ComponentWithClientAndCommon/ror_components/ComponentWithClientAndCommon.client.jsx new file mode 100644 index 000000000..7ce1266fa --- /dev/null +++ b/spec/react_on_rails/fixtures/automated_packs_generation/components/ComponentWithClientAndCommon/ror_components/ComponentWithClientAndCommon.client.jsx @@ -0,0 +1 @@ +// Empty Test Component diff --git a/spec/react_on_rails/fixtures/automated_packs_generation/components/ComponentWithClientAndCommon/ror_components/ComponentWithClientAndCommon.jsx b/spec/react_on_rails/fixtures/automated_packs_generation/components/ComponentWithClientAndCommon/ror_components/ComponentWithClientAndCommon.jsx new file mode 100644 index 000000000..7ce1266fa --- /dev/null +++ b/spec/react_on_rails/fixtures/automated_packs_generation/components/ComponentWithClientAndCommon/ror_components/ComponentWithClientAndCommon.jsx @@ -0,0 +1 @@ +// Empty Test Component diff --git a/spec/react_on_rails/fixtures/automated_packs_generation/components/ComponentWithClientOnly/ror_components/ComponentWithClientOnly.client.jsx b/spec/react_on_rails/fixtures/automated_packs_generation/components/ComponentWithClientOnly/ror_components/ComponentWithClientOnly.client.jsx new file mode 100644 index 000000000..7ce1266fa --- /dev/null +++ b/spec/react_on_rails/fixtures/automated_packs_generation/components/ComponentWithClientOnly/ror_components/ComponentWithClientOnly.client.jsx @@ -0,0 +1 @@ +// Empty Test Component diff --git a/spec/react_on_rails/fixtures/automated_packs_generation/components/ComponentWithCommonClientAndServer/ror_components/ComponentWithCommonClientAndServer.client.jsx b/spec/react_on_rails/fixtures/automated_packs_generation/components/ComponentWithCommonClientAndServer/ror_components/ComponentWithCommonClientAndServer.client.jsx new file mode 100644 index 000000000..7ce1266fa --- /dev/null +++ b/spec/react_on_rails/fixtures/automated_packs_generation/components/ComponentWithCommonClientAndServer/ror_components/ComponentWithCommonClientAndServer.client.jsx @@ -0,0 +1 @@ +// Empty Test Component diff --git a/spec/react_on_rails/fixtures/automated_packs_generation/components/ComponentWithCommonClientAndServer/ror_components/ComponentWithCommonClientAndServer.jsx b/spec/react_on_rails/fixtures/automated_packs_generation/components/ComponentWithCommonClientAndServer/ror_components/ComponentWithCommonClientAndServer.jsx new file mode 100644 index 000000000..7ce1266fa --- /dev/null +++ b/spec/react_on_rails/fixtures/automated_packs_generation/components/ComponentWithCommonClientAndServer/ror_components/ComponentWithCommonClientAndServer.jsx @@ -0,0 +1 @@ +// Empty Test Component diff --git a/spec/react_on_rails/fixtures/automated_packs_generation/components/ComponentWithCommonClientAndServer/ror_components/ComponentWithCommonClientAndServer.server.jsx b/spec/react_on_rails/fixtures/automated_packs_generation/components/ComponentWithCommonClientAndServer/ror_components/ComponentWithCommonClientAndServer.server.jsx new file mode 100644 index 000000000..7ce1266fa --- /dev/null +++ b/spec/react_on_rails/fixtures/automated_packs_generation/components/ComponentWithCommonClientAndServer/ror_components/ComponentWithCommonClientAndServer.server.jsx @@ -0,0 +1 @@ +// Empty Test Component diff --git a/spec/react_on_rails/fixtures/automated_packs_generation/components/ComponentWithCommonOnly/ror_components/ComponentWithCommonOnly.jsx b/spec/react_on_rails/fixtures/automated_packs_generation/components/ComponentWithCommonOnly/ror_components/ComponentWithCommonOnly.jsx new file mode 100644 index 000000000..7ce1266fa --- /dev/null +++ b/spec/react_on_rails/fixtures/automated_packs_generation/components/ComponentWithCommonOnly/ror_components/ComponentWithCommonOnly.jsx @@ -0,0 +1 @@ +// Empty Test Component diff --git a/spec/react_on_rails/fixtures/automated_packs_generation/components/ComponentWithServerAndCommon/ror_components/ComponentWithServerAndCommon.jsx b/spec/react_on_rails/fixtures/automated_packs_generation/components/ComponentWithServerAndCommon/ror_components/ComponentWithServerAndCommon.jsx new file mode 100644 index 000000000..7ce1266fa --- /dev/null +++ b/spec/react_on_rails/fixtures/automated_packs_generation/components/ComponentWithServerAndCommon/ror_components/ComponentWithServerAndCommon.jsx @@ -0,0 +1 @@ +// Empty Test Component diff --git a/spec/react_on_rails/fixtures/automated_packs_generation/components/ComponentWithServerAndCommon/ror_components/ComponentWithServerAndCommon.server.jsx b/spec/react_on_rails/fixtures/automated_packs_generation/components/ComponentWithServerAndCommon/ror_components/ComponentWithServerAndCommon.server.jsx new file mode 100644 index 000000000..7ce1266fa --- /dev/null +++ b/spec/react_on_rails/fixtures/automated_packs_generation/components/ComponentWithServerAndCommon/ror_components/ComponentWithServerAndCommon.server.jsx @@ -0,0 +1 @@ +// Empty Test Component diff --git a/spec/react_on_rails/fixtures/automated_packs_generation/components/ComponentWithServerOnly/ror_components/ComponentWithServerOnly.server.jsx b/spec/react_on_rails/fixtures/automated_packs_generation/components/ComponentWithServerOnly/ror_components/ComponentWithServerOnly.server.jsx new file mode 100644 index 000000000..7ce1266fa --- /dev/null +++ b/spec/react_on_rails/fixtures/automated_packs_generation/components/ComponentWithServerOnly/ror_components/ComponentWithServerOnly.server.jsx @@ -0,0 +1 @@ +// Empty Test Component diff --git a/spec/react_on_rails/fixtures/automated_packs_generation/packs/server-bundle.js b/spec/react_on_rails/fixtures/automated_packs_generation/packs/server-bundle.js new file mode 100644 index 000000000..e69de29bb diff --git a/spec/react_on_rails/packs_generator_spec.rb b/spec/react_on_rails/packs_generator_spec.rb new file mode 100644 index 000000000..49cdfce16 --- /dev/null +++ b/spec/react_on_rails/packs_generator_spec.rb @@ -0,0 +1,271 @@ +# frozen_string_literal: true + +require_relative "spec_helper" + +# rubocop:disable Metrics/ModuleLength +module ReactOnRails + # rubocop:disable Metrics/BlockLength + describe PacksGenerator do + let(:webpacker_source_path) { File.expand_path("fixtures/automated_packs_generation", __dir__) } + let(:webpacker_source_entry_path) { File.expand_path("fixtures/automated_packs_generation/packs", __dir__) } + let(:generated_directory) { File.expand_path("fixtures/automated_packs_generation/packs/generated", __dir__) } + let(:server_bundle_js_file) { "server-bundle.js" } + let(:server_bundle_js_file_path) do + File.expand_path("fixtures/automated_packs_generation/packs/#{server_bundle_js_file}", __dir__) + end + let(:generated_assets_full_path) do + File.expand_path("fixtures/automated_packs_generation/packs", __dir__) + end + let(:webpack_generated_files) { %w[manifest.json] } + + before do + ReactOnRails.configuration.server_bundle_js_file = server_bundle_js_file + ReactOnRails.configuration.components_subdirectory = "ror_components" + ReactOnRails.configuration.webpack_generated_files = webpack_generated_files + + allow(ReactOnRails::WebpackerUtils).to receive(:manifest_exists?).and_return(true) + allow(ReactOnRails::WebpackerUtils).to receive(:using_webpacker?).and_return(true) + allow(ReactOnRails::WebpackerUtils).to receive(:nested_entries?).and_return(true) + allow(ReactOnRails::WebpackerUtils).to receive(:webpacker_source_entry_path) + .and_return(webpacker_source_entry_path) + allow(ReactOnRails::WebpackerUtils).to receive(:shakapacker_version).and_return("6.5.1") + allow(ReactOnRails::Utils).to receive(:generated_assets_full_path).and_return(generated_assets_full_path) + allow(ReactOnRails::Utils).to receive(:server_bundle_js_file_path).and_return(server_bundle_js_file_path) + end + + after do + ReactOnRails.configuration.server_bundle_js_file = nil + ReactOnRails.configuration.components_subdirectory = nil + + FileUtils.rm_rf "#{webpacker_source_entry_path}/generated" + FileUtils.rm_rf generated_server_bundle_file_path + File.truncate("#{webpacker_source_entry_path}/#{server_bundle_js_file}", 0) + end + + context "when webpacker is not installed" do + before do + allow(ReactOnRails::WebpackerUtils).to receive(:using_webpacker?).and_return(false) + end + + it "raises an error" do + msg = <<~MSG + **ERROR** ReactOnRails: Missing Shakapacker gem. Please upgrade to use Shakapacker \ + 6.5.1 or above to use the \ + automated bundle generation feature. + MSG + + expect { described_class.generate }.to raise_error(ReactOnRails::Error, msg) + end + end + + context "when shakapacker version requirements not met" do + before do + allow(ReactOnRails::WebpackerUtils).to receive(:shakapacker_version).and_return("6.5.0") + end + + after do + allow(ReactOnRails::WebpackerUtils).to receive(:shakapacker_version).and_return("6.5.1") + end + + it "raises an error" do + msg = <<~MSG + **ERROR** ReactOnRails: Please upgrade Shakapacker to version 6.5.1 or \ + above to use the automated bundle generation feature. The currently installed version is \ + 6.5.0. + MSG + + expect { described_class.generate }.to raise_error(ReactOnRails::Error, msg) + end + end + + context "when nested enteries not enabled" do + before do + allow(ReactOnRails::WebpackerUtils).to receive(:nested_entries?).and_return(false) + end + + after do + allow(ReactOnRails::WebpackerUtils).to receive(:nested_entries?).and_return(true) + end + + it "raises an error" do + msg = <<~MSG + **ERROR** ReactOnRails: `nested_entries` is configured to be disabled in shakapacker. Please update \ + webpacker.yml to enable nested enteries. for more information read + https://www.shakacode.com/react-on-rails/docs/guides/file-system-based-automated-bundle-generation.md#enable-nested_entries-for-shakapacker + MSG + + expect { described_class.generate }.to raise_error(ReactOnRails::Error, msg) + end + end + + context "when component with common file only" do + let(:component_name) { "ComponentWithCommonOnly" } + let(:component_pack) { "#{generated_directory}/#{component_name}.jsx" } + + before do + stub_webpacker_source_path(component_name: component_name, + webpacker_source_path: webpacker_source_path) + described_class.generate + end + + it "creates generated pack directory" do + expect(Pathname.new(generated_directory)).to be_directory + end + + it "creates generated server bundle file" do + expect(File.exist?(generated_server_bundle_file_path)).to eq(true) + end + + it "creates pack for ComponentWithCommonOnly" do + expect(File.exist?(component_pack)).to eq(true) + end + + it "imports generated server bundle to original server bundle" do + server_bundle_file_path = "#{webpacker_source_entry_path}/#{server_bundle_js_file}" + server_bundle_content = File.read(server_bundle_file_path) + + expect(server_bundle_content).to include("import \"./server-bundle-generated.js\"") + end + + it "generated pack for ComponentWithCommonOnly uses common file for pack" do + pack_content = File.read(component_pack) + + expect(pack_content).to include("#{component_name}.jsx") + expect(pack_content).not_to include("#{component_name}.client.jsx") + expect(pack_content).not_to include("#{component_name}.server.jsx") + end + + it "generated server bundle uses common file" do + generated_server_bundle_content = File.read(generated_server_bundle_file_path) + + expect(generated_server_bundle_content).to include("#{component_name}.jsx") + expect(generated_server_bundle_content).not_to include("#{component_name}.client.jsx") + expect(generated_server_bundle_content).not_to include("#{component_name}.server.jsx") + end + end + + context "when component with client and common File" do + let(:component_name) { "ComponentWithClientAndCommon" } + let(:component_pack) { "#{generated_directory}/#{component_name}.jsx" } + + before do + stub_webpacker_source_path(component_name: component_name, + webpacker_source_path: webpacker_source_path) + end + + it "raises an error for definition override" do + msg = <<~MSG + **ERROR** ReactOnRails: client specific definition for Component '#{component_name}' overrides the \ + common definition. Please delete the common definition and have separate server and client files. For more \ + information, please see https://www.shakacode.com/react-on-rails/docs/guides/file-system-based-automated-bundle-generation.md + MSG + + expect { described_class.generate }.to raise_error(ReactOnRails::Error, msg) + end + end + + context "when component with server and common file" do + let(:component_name) { "ComponentWithServerAndCommon" } + let(:component_pack) { "#{generated_directory}/#{component_name}.jsx" } + + before do + allow(ReactOnRails::WebpackerUtils).to receive(:webpacker_source_path) + .and_return("#{webpacker_source_path}/components/#{component_name}") + end + + it "raises an error for definition override" do + msg = <<~MSG + **ERROR** ReactOnRails: server specific definition for Component '#{component_name}' overrides the \ + common definition. Please delete the common definition and have separate server and client files. For more \ + information, please see https://www.shakacode.com/react-on-rails/docs/guides/file-system-based-automated-bundle-generation.md + MSG + + expect { described_class.generate }.to raise_error(ReactOnRails::Error, msg) + end + end + + context "when component with server, client and common file" do + let(:component_name) { "ComponentWithCommonClientAndServer" } + let(:component_pack) { "#{generated_directory}/#{component_name}.jsx" } + + before do + stub_webpacker_source_path(component_name: component_name, + webpacker_source_path: webpacker_source_path) + end + + it "raises an error for definition override" do + msg = /Please delete the common definition and have separate server and client files/ + expect { described_class.generate }.to raise_error(ReactOnRails::Error, msg) + end + end + + context "when component with server only" do + let(:component_name) { "ComponentWithServerOnly" } + let(:component_pack) { "#{generated_directory}/#{component_name}.jsx" } + + before do + stub_webpacker_source_path(component_name: component_name, + webpacker_source_path: webpacker_source_path) + end + + it "raises missing client file error" do + msg = <<~MSG + **ERROR** ReactOnRails: Component '#{component_name}' is missing a client specific file. For more \ + information, please see https://www.shakacode.com/react-on-rails/docs/guides/file-system-based-automated-bundle-generation.md + MSG + + expect { described_class.generate }.to raise_error(ReactOnRails::Error, msg) + end + end + + context "when component with client only" do + let(:component_name) { "ComponentWithClientOnly" } + let(:component_pack) { "#{generated_directory}/#{component_name}.jsx" } + + before do + stub_webpacker_source_path(component_name: component_name, + webpacker_source_path: webpacker_source_path) + described_class.generate + end + + it "creates generated pack directory" do + expect(Pathname.new(generated_directory)).to be_directory + end + + it "creates generated server bundle file" do + expect(File.exist?(generated_server_bundle_file_path)).to eq(true) + end + + it "creates pack for ComponentWithClientOnly" do + expect(File.exist?(component_pack)).to eq(true) + end + + it "generated pack for ComponentWithClientOnly uses client file for pack" do + pack_content = File.read(component_pack) + + expect(pack_content).to include("#{component_name}.client.jsx") + expect(pack_content).not_to include("#{component_name}.jsx") + expect(pack_content).not_to include("#{component_name}.server.jsx") + end + + it "generated server bundle do not have ComponentWithClientOnly registered" do + generated_server_bundle_content = File.read(generated_server_bundle_file_path) + + expect(generated_server_bundle_content).not_to include("#{component_name}.jsx") + expect(generated_server_bundle_content).not_to include("#{component_name}.client.jsx") + expect(generated_server_bundle_content).not_to include("#{component_name}.server.jsx") + end + end + + def generated_server_bundle_file_path + "#{webpacker_source_entry_path}/server-bundle-generated.js" + end + + def stub_webpacker_source_path(webpacker_source_path:, component_name:) + allow(ReactOnRails::WebpackerUtils).to receive(:webpacker_source_path) + .and_return("#{webpacker_source_path}/components/#{component_name}") + end + end + # rubocop:enable Metrics/BlockLength +end +# rubocop:enable Metrics/ModuleLength