diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 0ea114e7b4..caa488ac36 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -95,6 +95,9 @@ jobs: fi - name: Increase the amount of inotify watchers run: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p + - name: Set packer version environment variable + run: | + echo "CI_PACKER_VERSION=${{ matrix.versions }}" >> $GITHUB_ENV - name: Main CI if: steps.changed-files.outputs.any_changed == 'true' run: bundle exec rake run_rspec:${{ matrix.versions == 'oldest' && 'web' || 'shaka' }}packer_examples diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3c0489f78d..ea84f1cd70 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -190,6 +190,9 @@ jobs: git config user.name "Your Name" git commit -am "stop generators from complaining about uncommitted code" - run: cd spec/dummy && bundle info shakapacker + - name: Set packer version environment variable + run: | + echo "CI_PACKER_VERSION=${{ matrix.versions }}" >> $GITHUB_ENV - name: Main CI run: bundle exec rake run_rspec:all_dummy - name: Store test results diff --git a/.github/workflows/rspec-package-specs.yml b/.github/workflows/rspec-package-specs.yml index e0b5113082..67010526d0 100644 --- a/.github/workflows/rspec-package-specs.yml +++ b/.github/workflows/rspec-package-specs.yml @@ -49,7 +49,7 @@ jobs: git commit -am "stop generators from complaining about uncommitted code" - name: Set packer version environment variable run: | - echo "CI_PACKER_VERSION=${{ matrix.versions == 'oldest' && 'old' || 'new' }}" >> $GITHUB_ENV + echo "CI_PACKER_VERSION=${{ matrix.versions }}" >> $GITHUB_ENV - name: Run rspec tests run: bundle exec rspec spec/react_on_rails - name: Store test results diff --git a/CHANGELOG.md b/CHANGELOG.md index 14410aa353..c94aba1daf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,14 @@ After a release, please make sure to run `bundle exec rake update_changelog`. Th Changes since the last non-beta release. +#### Added + +- Configuration option `generated_component_packs_loading_strategy` to control how generated component packs are loaded. It supports `sync`, `async`, and `defer` strategies. [PR 1712](https://github.com/shakacode/react_on_rails/pull/1712) by [AbanoubGhadban](https://github.com/AbanoubGhadban). + +### Removed (Breaking Changes) + +- Deprecated `defer_generated_component_packs` configuration option. You should use `generated_component_packs_loading_strategy` instead. [PR 1712](https://github.com/shakacode/react_on_rails/pull/1712) by [AbanoubGhadban](https://github.com/AbanoubGhadban). + ### [15.0.0-alpha.2] - 2025-03-07 See [Release Notes](docs/release-notes/15.0.0.md) for full details. diff --git a/Gemfile.development_dependencies b/Gemfile.development_dependencies index ea7f695b9a..1b4814c94a 100644 --- a/Gemfile.development_dependencies +++ b/Gemfile.development_dependencies @@ -1,6 +1,6 @@ # frozen_string_literal: true -gem "shakapacker", "8.0.0" +gem "shakapacker", "8.2.0" gem "bootsnap", require: false gem "rails", "~> 7.1" diff --git a/Gemfile.lock b/Gemfile.lock index 98b5df8bc3..2bfef9bff4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -336,8 +336,8 @@ GEM rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) - semantic_range (3.0.0) - shakapacker (8.0.0) + semantic_range (3.1.0) + shakapacker (8.2.0) activesupport (>= 5.2) package_json rack-proxy (>= 0.6.1) @@ -431,7 +431,7 @@ DEPENDENCIES scss_lint sdoc selenium-webdriver (= 4.9.0) - shakapacker (= 8.0.0) + shakapacker (= 8.2.0) spring (~> 4.0) sprockets (~> 4.0) sqlite3 (~> 1.6) diff --git a/docs/release-notes/15.0.0.md b/docs/release-notes/15.0.0.md index 216162940c..50722bcfde 100644 --- a/docs/release-notes/15.0.0.md +++ b/docs/release-notes/15.0.0.md @@ -21,15 +21,27 @@ Major improvements to component and store hydration: - Can use `async` scripts in the page with no fear of race condition - No need to use `defer` anymore +### Enhanced Script Loading Strategies + +- New configuration option `generated_component_packs_loading_strategy` replaces `defer_generated_component_packs` +- Supports three loading strategies: + - `:async` - Loads scripts asynchronously (default for Shakapacker ≥ 8.2.0) + - `:defer` - Defers script execution until after page load (doesn't work well with Streamed HTML as it will wait for the full page load before hydrating the components) + - `:sync` - Loads scripts synchronously (default for Shakapacker < 8.2.0) (better to upgrade to Shakapacker 8.2.0 and use `:async` strategy) +- Improves page performance by optimizing how component packs are loaded + ## Breaking Changes ### Component Hydration Changes -- The `defer_generated_component_packs` and `force_load` configurations now default to `false` and `true` respectively. This means components will hydrate early without waiting for the full page load. This improves performance by eliminating unnecessary delays in hydration. +- The `defer_generated_component_packs` configuration has been deprecated. Use `generated_component_packs_loading_strategy` instead. +- The `generated_component_packs_loading_strategy` defaults to `:async` for Shakapacker ≥ 8.2.0 and `:sync` for Shakapacker < 8.2.0. +- The `force_load` configuration now defaults to `true`. +- The new default values of `generated_component_packs_loading_strategy: :async` and `force_load: true` work together to optimize component hydration. Components now hydrate as soon as their code and server-rendered HTML are available, without waiting for the full page to load. This parallel processing significantly improves time-to-interactive by eliminating the traditional waterfall of waiting for page load before beginning hydration (It's critical for streamed HTML). - The previous need for deferring scripts to prevent race conditions has been eliminated due to improved hydration handling. Making scripts not defer is critical to execute the hydration scripts early before the page is fully loaded. - The `force_load` configuration makes `react-on-rails` hydrate components immediately as soon as their server-rendered HTML reaches the client, without waiting for the full page load. - - If you want to keep the previous behavior, you can set `defer_generated_component_packs: true` or `force_load: false` in your `config/initializers/react_on_rails.rb` file. + - If you want to keep the previous behavior, you can set `generated_component_packs_loading_strategy: :defer` or `force_load: false` in your `config/initializers/react_on_rails.rb` file. - You can also keep it for individual components by passing `force_load: false` to `react_component` or `stream_react_component`. - Redux store now supports `force_load` option, which defaults to `config.force_load` (and so to `true` if that isn't set). If `true`, the Redux store will hydrate immediately as soon as its server-side data reaches the client. - You can override this behavior for individual Redux stores by calling the `redux_store` helper with `force_load: false`, same as `react_component`. @@ -50,6 +62,12 @@ Major improvements to component and store hydration: - If you call it in a `turbolinks:load` listener to work around the issue documented in [Turbolinks](https://www.shakacode.com/react-on-rails/docs/rails/turbolinks/#async-script-loading), the listener can be safely removed. +### Script Loading Strategy Migration + +- If you were previously using `defer_generated_component_packs: true`, use `generated_component_packs_loading_strategy: :defer` instead +- If you were previously using `defer_generated_component_packs: false`, use `generated_component_packs_loading_strategy: :sync` instead +- For optimal performance with Shakapacker ≥ 8.2.0, consider using `generated_component_packs_loading_strategy: :async` + ## Store Dependencies for Components When using Redux stores with multiple components, you need to explicitly declare store dependencies to optimize hydration. Here's how: diff --git a/lib/react_on_rails/configuration.rb b/lib/react_on_rails/configuration.rb index 3226f3a136..48e81fdbae 100644 --- a/lib/react_on_rails/configuration.rb +++ b/lib/react_on_rails/configuration.rb @@ -49,7 +49,8 @@ def self.configuration # Maximum time in milliseconds to wait for client-side component registration after page load. # If exceeded, an error will be thrown for server-side rendered components not registered on the client. # Set to 0 to disable the timeout and wait indefinitely for component registration. - component_registry_timeout: DEFAULT_COMPONENT_REGISTRY_TIMEOUT + component_registry_timeout: DEFAULT_COMPONENT_REGISTRY_TIMEOUT, + generated_component_packs_loading_strategy: nil ) end @@ -60,23 +61,23 @@ class Configuration :generated_assets_dirs, :generated_assets_dir, :components_subdirectory, :webpack_generated_files, :rendering_extension, :build_test_command, :build_production_command, :i18n_dir, :i18n_yml_dir, :i18n_output_format, - :i18n_yml_safe_load_options, + :i18n_yml_safe_load_options, :defer_generated_component_packs, :server_render_method, :random_dom_id, :auto_load_bundle, :same_bundle_for_client_and_server, :rendering_props_extension, :make_generated_server_bundle_the_entrypoint, - :defer_generated_component_packs, :force_load, :rsc_bundle_js_file, + :generated_component_packs_loading_strategy, :force_load, :rsc_bundle_js_file, :react_client_manifest_file, :component_registry_timeout # rubocop:disable Metrics/AbcSize def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender: nil, replay_console: nil, make_generated_server_bundle_the_entrypoint: nil, - trace: nil, development_mode: nil, + trace: nil, development_mode: nil, defer_generated_component_packs: nil, logging_on_server: nil, server_renderer_pool_size: nil, server_renderer_timeout: nil, raise_on_prerender_error: true, skip_display_none: nil, generated_assets_dirs: nil, generated_assets_dir: nil, webpack_generated_files: nil, rendering_extension: nil, build_test_command: nil, - build_production_command: nil, defer_generated_component_packs: nil, + build_production_command: nil, generated_component_packs_loading_strategy: nil, same_bundle_for_client_and_server: nil, i18n_dir: nil, i18n_yml_dir: nil, i18n_output_format: nil, i18n_yml_safe_load_options: nil, random_dom_id: nil, server_render_method: nil, rendering_props_extension: nil, @@ -124,6 +125,7 @@ def initialize(node_modules_location: nil, server_bundle_js_file: nil, prerender self.make_generated_server_bundle_the_entrypoint = make_generated_server_bundle_the_entrypoint self.defer_generated_component_packs = defer_generated_component_packs self.force_load = force_load + self.generated_component_packs_loading_strategy = generated_component_packs_loading_strategy end # rubocop:enable Metrics/AbcSize @@ -139,6 +141,7 @@ def setup_config_values # check_deprecated_settings adjust_precompile_task check_component_registry_timeout + validate_generated_component_packs_loading_strategy end private @@ -151,6 +154,42 @@ def check_component_registry_timeout raise ReactOnRails::Error, "component_registry_timeout must be a positive integer" end + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + def validate_generated_component_packs_loading_strategy + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + + if defer_generated_component_packs + if %i[async sync].include?(generated_component_packs_loading_strategy) + Rails.logger.warn "**WARNING** ReactOnRails: config.defer_generated_component_packs is " \ + "superseded by config.generated_component_packs_loading_strategy" + else + Rails.logger.warn "[DEPRECATION] ReactOnRails: Use config." \ + "generated_component_packs_loading_strategy = :defer rather than " \ + "defer_generated_component_packs" + self.generated_component_packs_loading_strategy ||= :defer + end + end + + msg = <<~MSG + ReactOnRails: Your current version of #{ReactOnRails::PackerUtils.packer_type.upcase_first} \ + does not support async script loading, which may cause performance issues. Please either: + 1. Use :sync or :defer loading strategy instead of :async + 2. Upgrade to Shakapacker v8.2.0 or above to enable async script loading + MSG + if PackerUtils.shakapacker_version_requirement_met?([8, 2, 0]) + self.generated_component_packs_loading_strategy ||= :async + elsif generated_component_packs_loading_strategy.nil? + Rails.logger.warn("**WARNING** #{msg}") + self.generated_component_packs_loading_strategy = :sync + elsif generated_component_packs_loading_strategy == :async + raise ReactOnRails::Error, "**ERROR** #{msg}" + end + + return if %i[async defer sync].include?(generated_component_packs_loading_strategy) + + raise ReactOnRails::Error, "generated_component_packs_loading_strategy must be either :async, :defer, or :sync" + end + def check_autobundling_requirements raise_missing_components_subdirectory if auto_load_bundle && !components_subdirectory.present? return unless components_subdirectory.present? diff --git a/lib/react_on_rails/controller.rb b/lib/react_on_rails/controller.rb index bf0a08a239..440f808ed6 100644 --- a/lib/react_on_rails/controller.rb +++ b/lib/react_on_rails/controller.rb @@ -12,9 +12,11 @@ module Controller # # Be sure to include view helper `redux_store_hydration_data` at the end of your layout or view # or else there will be no client side hydration of your stores. - def redux_store(store_name, props: {}) + def redux_store(store_name, props: {}, force_load: nil) + force_load = ReactOnRails.configuration.force_load if force_load.nil? redux_store_data = { store_name: store_name, - props: props } + props: props, + force_load: force_load } @registered_stores_defer_render ||= [] @registered_stores_defer_render << redux_store_data end diff --git a/lib/react_on_rails/helper.rb b/lib/react_on_rails/helper.rb index a2f7116a2d..6534fede9c 100644 --- a/lib/react_on_rails/helper.rb +++ b/lib/react_on_rails/helper.rb @@ -422,8 +422,13 @@ def load_pack_for_generated_component(react_component_name, render_options) is_component_pack_present = File.exist?(generated_components_pack_path(react_component_name)) raise_missing_autoloaded_bundle(react_component_name) unless is_component_pack_present end - append_javascript_pack_tag("generated/#{react_component_name}", - defer: ReactOnRails.configuration.defer_generated_component_packs) + + options = { defer: ReactOnRails.configuration.generated_component_packs_loading_strategy == :defer } + # Old versions of Shakapacker don't support async script tags. + # ReactOnRails.configure already validates if async loading is supported by the installed Shakapacker version. + # Therefore, we only need to pass the async option if the loading strategy is explicitly set to :async + options[:async] = true if ReactOnRails.configuration.generated_component_packs_loading_strategy == :async + append_javascript_pack_tag("generated/#{react_component_name}", **options) append_stylesheet_pack_tag("generated/#{react_component_name}") end diff --git a/spec/dummy/Gemfile.lock b/spec/dummy/Gemfile.lock index b4ed7eb777..cc1191c13d 100644 --- a/spec/dummy/Gemfile.lock +++ b/spec/dummy/Gemfile.lock @@ -331,8 +331,8 @@ GEM rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) - semantic_range (3.0.0) - shakapacker (8.0.0) + semantic_range (3.1.0) + shakapacker (8.2.0) activesupport (>= 5.2) package_json rack-proxy (>= 0.6.1) @@ -423,7 +423,7 @@ DEPENDENCIES scss_lint sdoc selenium-webdriver (= 4.9.0) - shakapacker (= 8.0.0) + shakapacker (= 8.2.0) spring (~> 4.0) sprockets (~> 4.0) sqlite3 (~> 1.6) diff --git a/spec/dummy/package.json b/spec/dummy/package.json index b7a3688a5c..f15534d533 100644 --- a/spec/dummy/package.json +++ b/spec/dummy/package.json @@ -50,7 +50,7 @@ "sass": "^1.43.4", "sass-loader": "^12.3.0", "sass-resources-loader": "^2.1.0", - "shakapacker": "8.0.0", + "shakapacker": "8.2.0", "style-loader": "^3.3.1", "terser-webpack-plugin": "5.3.1", "url-loader": "^4.0.0", diff --git a/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb b/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb index 5d247de483..b1e9f7d29b 100644 --- a/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb +++ b/spec/dummy/spec/helpers/react_on_rails_helper_spec.rb @@ -50,10 +50,60 @@ class PlainReactOnRailsHelper allow(helper).to receive(:append_javascript_pack_tag) allow(helper).to receive(:append_stylesheet_pack_tag) expect { helper.load_pack_for_generated_component("component_name", render_options) }.not_to raise_error - expect(helper).to have_received(:append_javascript_pack_tag).with("generated/component_name", { defer: false }) + + if ENV["CI_PACKER_VERSION"] == "oldest" + expect(helper).to have_received(:append_javascript_pack_tag).with("generated/component_name", { defer: false }) + else + expect(helper).to have_received(:append_javascript_pack_tag) + .with("generated/component_name", { defer: false, async: true }) + end expect(helper).to have_received(:append_stylesheet_pack_tag).with("generated/component_name") end + context "when async loading is enabled" do + before do + allow(ReactOnRails.configuration).to receive(:generated_component_packs_loading_strategy).and_return(:async) + end + + it "appends the async attribute to the script tag" do + original_append_javascript_pack_tag = helper.method(:append_javascript_pack_tag) + begin + # Temporarily redefine append_javascript_pack_tag to handle the async keyword argument. + # This is needed because older versions of Shakapacker (which may be used during testing) + # don't support async loading, but we still want to test that the async option is passed + # correctly when enabled. + def helper.append_javascript_pack_tag(name, **options) + original_append_javascript_pack_tag.call(name, **options) + end + + allow(helper).to receive(:append_javascript_pack_tag) + allow(helper).to receive(:append_stylesheet_pack_tag) + expect { helper.load_pack_for_generated_component("component_name", render_options) }.not_to raise_error + expect(helper).to have_received(:append_javascript_pack_tag).with( + "generated/component_name", + { defer: false, async: true } + ) + expect(helper).to have_received(:append_stylesheet_pack_tag).with("generated/component_name") + ensure + helper.define_singleton_method(:append_javascript_pack_tag, original_append_javascript_pack_tag) + end + end + end + + context "when defer loading is enabled" do + before do + allow(ReactOnRails.configuration).to receive(:generated_component_packs_loading_strategy).and_return(:defer) + end + + it "appends the defer attribute to the script tag" do + allow(helper).to receive(:append_javascript_pack_tag) + allow(helper).to receive(:append_stylesheet_pack_tag) + expect { helper.load_pack_for_generated_component("component_name", render_options) }.not_to raise_error + expect(helper).to have_received(:append_javascript_pack_tag).with("generated/component_name", { defer: true }) + expect(helper).to have_received(:append_stylesheet_pack_tag).with("generated/component_name") + end + end + it "throws an error in development if generated component isn't found" do allow(Rails.env).to receive(:development?).and_return(true) expect { helper.load_pack_for_generated_component("nonexisting_component", render_options) } diff --git a/spec/dummy/yarn.lock b/spec/dummy/yarn.lock index 19bdf1c192..71996f4e24 100644 --- a/spec/dummy/yarn.lock +++ b/spec/dummy/yarn.lock @@ -6068,10 +6068,10 @@ sha.js@^2.4.0, sha.js@^2.4.8: inherits "^2.0.1" safe-buffer "^5.0.1" -shakapacker@8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/shakapacker/-/shakapacker-8.0.0.tgz#f29537c19078af7318758c92e7a1bca4cee96bdd" - integrity sha512-HCdpITzIKXzGEyUWQhKzPbpwwOsgTamaPH+0kXdhM59VQxZ3NWnT5cL3DlJdAT3sGsWCJskEl3eMkQlnh9DjhA== +shakapacker@8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/shakapacker/-/shakapacker-8.2.0.tgz#c7bed87b8be2ae565cfe616f68552be545c77e14" + integrity sha512-Ct7BFqJVnKbxdqCzG+ja7Q6LPt/PlB7sSVBfG5jsAvmVCADM05cuoNwEgYNjFGKbDzHAxUqy5XgoI9Y030+JKQ== dependencies: js-yaml "^4.1.0" path-complete-extname "^1.0.0" diff --git a/spec/react_on_rails/configuration_spec.rb b/spec/react_on_rails/configuration_spec.rb index 84d9784220..1c53b8041d 100644 --- a/spec/react_on_rails/configuration_spec.rb +++ b/spec/react_on_rails/configuration_spec.rb @@ -265,7 +265,7 @@ module ReactOnRails end expect(ReactOnRails::PackerUtils).to have_received(:using_packer?).thrice - expect(ReactOnRails::PackerUtils).to have_received(:shakapacker_version_requirement_met?) + expect(ReactOnRails::PackerUtils).to have_received(:shakapacker_version_requirement_met?).twice expect(ReactOnRails::PackerUtils).to have_received(:nested_entries?) end @@ -277,6 +277,95 @@ module ReactOnRails expect(ReactOnRails.configuration.random_dom_id).to be(true) end + + describe ".generated_component_packs_loading_strategy" do + context "when using Shakapacker >= 8.2.0" do + before do + allow(ReactOnRails::PackerUtils).to receive(:shakapacker_version_requirement_met?) + .with([8, 2, 0]).and_return(true) + end + + it "defaults to :async" do + ReactOnRails.configure {} # rubocop:disable Lint/EmptyBlock + expect(ReactOnRails.configuration.generated_component_packs_loading_strategy).to eq(:async) + end + + it "accepts :async value" do + expect do + ReactOnRails.configure do |config| + config.generated_component_packs_loading_strategy = :async + end + end.not_to raise_error + expect(ReactOnRails.configuration.generated_component_packs_loading_strategy).to eq(:async) + end + + it "accepts :defer value" do + expect do + ReactOnRails.configure do |config| + config.generated_component_packs_loading_strategy = :defer + end + end.not_to raise_error + expect(ReactOnRails.configuration.generated_component_packs_loading_strategy).to eq(:defer) + end + + it "accepts :sync value" do + expect do + ReactOnRails.configure do |config| + config.generated_component_packs_loading_strategy = :sync + end + end.not_to raise_error + expect(ReactOnRails.configuration.generated_component_packs_loading_strategy).to eq(:sync) + end + + it "raises error for invalid values" do + expect do + ReactOnRails.configure do |config| + config.generated_component_packs_loading_strategy = :invalid + end + end.to raise_error(ReactOnRails::Error, /must be either :async, :defer, or :sync/) + end + end + + context "when using Shakapacker < 8.2.0" do + before do + allow(ReactOnRails::PackerUtils).to receive(:shakapacker_version_requirement_met?) + .with([8, 2, 0]).and_return(false) + allow(Rails.logger).to receive(:warn) + end + + it "defaults to :sync and logs a warning" do + ReactOnRails.configure {} # rubocop:disable Lint/EmptyBlock + expect(ReactOnRails.configuration.generated_component_packs_loading_strategy).to eq(:sync) + expect(Rails.logger).to have_received(:warn).with(/does not support async script loading/) + end + + it "accepts :defer value" do + expect do + ReactOnRails.configure do |config| + config.generated_component_packs_loading_strategy = :defer + end + end.not_to raise_error + expect(ReactOnRails.configuration.generated_component_packs_loading_strategy).to eq(:defer) + end + + it "accepts :sync value" do + expect do + ReactOnRails.configure do |config| + config.generated_component_packs_loading_strategy = :sync + end + end.not_to raise_error + expect(ReactOnRails.configuration.generated_component_packs_loading_strategy).to eq(:sync) + end + + it "raises error for :async value" do + expect do + ReactOnRails.configure do |config| + config.generated_component_packs_loading_strategy = :async + end + end.to raise_error(ReactOnRails::Error, /does not support async script loading/) + end + end + end end end diff --git a/spec/react_on_rails/utils_spec.rb b/spec/react_on_rails/utils_spec.rb index 087306eaeb..98fe24a500 100644 --- a/spec/react_on_rails/utils_spec.rb +++ b/spec/react_on_rails/utils_spec.rb @@ -10,9 +10,9 @@ module ReactOnRails # If rspec tests are run locally, we want to test both packers. # If rspec tests are run in CI, we want to test the packer specified in the CI_PACKER_VERSION environment variable. # Check script/convert and .github/workflows/rspec-package-specs.yml for more details. - packers_to_test = if ENV["CI_PACKER_VERSION"] == "old" + packers_to_test = if ENV["CI_PACKER_VERSION"] == "oldest" ["webpacker"] - elsif ENV["CI_PACKER_VERSION"] == "new" + elsif ENV["CI_PACKER_VERSION"] == "newest" ["shakapacker"] else %w[shakapacker webpacker]