Module: ReactOnRails::PackerUtils
- Defined in:
- lib/react_on_rails/packer_utils.rb
Overview
rubocop:disable Metrics/ModuleLength
Constant Summary collapse
- SHAKAPACKER_CONFIG_PATH =
File.join("config", "shakapacker.yml")
- GENERATE_PACKS_PATTERN =
Regex pattern to detect pack generation in hook scripts Matches both:
-
The rake task: react_on_rails:generate_packs
-
The Ruby methods: generate_packs_if_stale / generate_packs_if_needed
-
/\b(react_on_rails:generate_packs|generate_packs_if_stale|generate_packs_if_needed)\b/- SELF_GUARD_PATTERN =
Pattern to detect a real self-guard statement that exits early when SHAKAPACKER_SKIP_PRECOMPILE_HOOK is true. This avoids false positives from comments or unrelated string literals.
/ (?:^|\s) (?:exit|return) (?:\s+0)? \s+if\s+ENV\[(["'])SHAKAPACKER_SKIP_PRECOMPILE_HOOK\1\]\s*==\s*(["'])true\2 /x
Class Method Summary collapse
-
.asset_uri_from_packer(asset_name) ⇒ Object
The function doesn’t ensure that the asset exists.
-
.bundle_js_uri_from_packer(bundle_name) ⇒ Object
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.
- .check_manifest_not_cached ⇒ Object
- .current_shakapacker_environment ⇒ Object
- .dev_server_running? ⇒ Boolean
- .dev_server_url ⇒ Object
- .extract_hash_for_environment(config_data, env_name) ⇒ Object
- .extract_precompile_hook ⇒ Object
- .extract_precompile_hook_from_shakapacker_config ⇒ Object
- .extract_precompile_hook_from_yaml ⇒ Object
-
.extract_script_from_command(command) ⇒ Object
Extract the script path from an interpreter-prefixed command.
- .hook_contains_generate_packs?(hook_value) ⇒ Boolean
-
.hook_script_has_self_guard?(hook_value) ⇒ Boolean
Check if a hook script file contains the self-guard pattern that prevents duplicate execution when SHAKAPACKER_SKIP_PRECOMPILE_HOOK is set.
- .manifest_exists? ⇒ Boolean
- .nested_entries? ⇒ Boolean
- .packer_public_output_path ⇒ Object
- .packer_source_entry_path ⇒ Object
- .packer_source_path ⇒ Object
- .packer_source_path_explicit? ⇒ Boolean
- .precompile? ⇒ Boolean
- .project_root ⇒ Object
- .public_output_uri_path ⇒ Object
- .raise_nested_entries_disabled ⇒ Object
- .raise_shakapacker_version_incompatible_for_autobundling ⇒ Object
- .resolve_hook_script_path(hook_value) ⇒ Object
-
.shakapacker_precompile_hook_configured? ⇒ Boolean
Check if shakapacker.yml has a precompile hook configured This prevents react_on_rails from running generate_packs twice.
-
.shakapacker_precompile_hook_value ⇒ Object
Returns the configured precompile hook value for logging/debugging Returns nil if no hook is configured.
- .shakapacker_version ⇒ Object
- .shakapacker_version_as_array ⇒ Object
- .shakapacker_version_requirement_met?(required_version) ⇒ Boolean
- .supports_async_loading? ⇒ Boolean
- .supports_autobundling? ⇒ Boolean
- .webpack_assets_status_checker ⇒ Object
Class Method Details
.asset_uri_from_packer(asset_name) ⇒ Object
The function doesn’t ensure that the asset exists.
-
It just returns url to the asset if dev server is running
-
Otherwise it returns file path to the asset
82 83 84 85 86 87 88 |
# File 'lib/react_on_rails/packer_utils.rb', line 82 def self.asset_uri_from_packer(asset_name) if dev_server_running? "#{dev_server_url}/#{public_output_uri_path}#{asset_name}" else File.join(packer_public_output_path, asset_name).to_s end end |
.bundle_js_uri_from_packer(bundle_name) ⇒ Object
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.
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/react_on_rails/packer_utils.rb', line 53 def self.bundle_js_uri_from_packer(bundle_name) hashed_bundle_name = ::Shakapacker.manifest.lookup!(bundle_name) # Support for hashing the server-bundle and having that built # the webpack-dev-server is provided by the config value # "same_bundle_for_client_and_server" where a value of true # would mean that the bundle is created by the webpack-dev-server is_bundle_running_on_server = bundle_name == ReactOnRails.configuration.server_bundle_js_file # Check Pro RSC bundle if Pro is available if ReactOnRails::Utils.react_on_rails_pro? is_bundle_running_on_server ||= (bundle_name == ReactOnRailsPro.configuration.rsc_bundle_js_file) end if ::Shakapacker.dev_server.running? && (!is_bundle_running_on_server || ReactOnRails.configuration.same_bundle_for_client_and_server) "#{dev_server_url}#{hashed_bundle_name}" else File.(File.join("public", hashed_bundle_name)).to_s end end |
.check_manifest_not_cached ⇒ Object
118 119 120 121 122 123 124 125 126 127 128 |
# File 'lib/react_on_rails/packer_utils.rb', line 118 def self.check_manifest_not_cached return unless ::Shakapacker.config.cache_manifest? msg = <<-MSG.strip_heredoc ERROR: you have enabled cache_manifest in the #{Rails.env} env when using the ReactOnRails::TestHelper.configure_rspec_to_compile_assets helper To fix this: edit your config/shakapacker.yml file and set cache_manifest to false for test. MSG puts (msg) exit! end |
.current_shakapacker_environment ⇒ Object
329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 |
# File 'lib/react_on_rails/packer_utils.rb', line 329 def self.current_shakapacker_environment if defined?(Rails) && Rails.respond_to?(:env) env = begin Rails.env.to_s rescue StandardError nil end return env unless env.to_s.strip.empty? end rails_env = ENV.fetch("RAILS_ENV", nil) return rails_env unless rails_env.to_s.strip.empty? rack_env = ENV.fetch("RACK_ENV", nil) return rack_env unless rack_env.to_s.strip.empty? "development" end |
.dev_server_running? ⇒ Boolean
13 14 15 |
# File 'lib/react_on_rails/packer_utils.rb', line 13 def self.dev_server_running? Shakapacker.dev_server.running? end |
.dev_server_url ⇒ Object
17 18 19 |
# File 'lib/react_on_rails/packer_utils.rb', line 17 def self.dev_server_url "#{Shakapacker.dev_server.protocol}://#{Shakapacker.dev_server.host_with_port}" end |
.extract_hash_for_environment(config_data, env_name) ⇒ Object
324 325 326 327 |
# File 'lib/react_on_rails/packer_utils.rb', line 324 def self.extract_hash_for_environment(config_data, env_name) value = config_data[env_name] || config_data[env_name.to_sym] value.is_a?(Hash) ? value : {} end |
.extract_precompile_hook ⇒ Object
187 188 189 190 191 192 193 194 195 |
# File 'lib/react_on_rails/packer_utils.rb', line 187 def self.extract_precompile_hook # Prefer using Shakapacker's runtime config when available. # In bin/dev startup before Rails boots, this can raise (e.g., missing Rails.env), # so we rescue and fall back to parsing config/shakapacker.yml directly. hook_value = extract_precompile_hook_from_shakapacker_config return hook_value unless hook_value.nil? extract_precompile_hook_from_yaml end |
.extract_precompile_hook_from_shakapacker_config ⇒ Object
287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 |
# File 'lib/react_on_rails/packer_utils.rb', line 287 def self.extract_precompile_hook_from_shakapacker_config # Prefer the public API (available in Shakapacker 9.0+) return ::Shakapacker.config.precompile_hook if ::Shakapacker.config.respond_to?(:precompile_hook) # Fallback: access config data using private :data method config_data = ::Shakapacker.config.send(:data) # Try symbol keys first (Shakapacker's internal format), then fall back to string keys if config_data&.key?(:precompile_hook) config_data[:precompile_hook] elsif config_data&.key?("precompile_hook") config_data["precompile_hook"] end rescue StandardError nil end |
.extract_precompile_hook_from_yaml ⇒ Object
304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 |
# File 'lib/react_on_rails/packer_utils.rb', line 304 def self.extract_precompile_hook_from_yaml config_path = project_root.join(SHAKAPACKER_CONFIG_PATH) return nil unless config_path.file? yaml_content = ERB.new(File.read(config_path)).result config_data = YAML.safe_load(yaml_content, permitted_classes: [Symbol], aliases: true) || {} env_config = extract_hash_for_environment(config_data, current_shakapacker_environment) return env_config["precompile_hook"] if env_config.key?("precompile_hook") return env_config[:precompile_hook] if env_config.key?(:precompile_hook) default_config = extract_hash_for_environment(config_data, "default") return default_config["precompile_hook"] if default_config.key?("precompile_hook") return default_config[:precompile_hook] if default_config.key?(:precompile_hook) nil rescue StandardError nil end |
.extract_script_from_command(command) ⇒ Object
Extract the script path from an interpreter-prefixed command. e.g., “ruby bin/shakapacker-precompile-hook” -> “bin/shakapacker-precompile-hook” Returns nil if the value doesn’t look like an interpreter-prefixed command.
252 253 254 255 256 257 258 259 260 |
# File 'lib/react_on_rails/packer_utils.rb', line 252 def self.extract_script_from_command(command) parts = command.strip.split(/\s+/, 2) return nil unless parts.length == 2 interpreter = File.basename(parts[0]) return parts[1] if %w[ruby node bash sh].include?(interpreter) nil end |
.hook_contains_generate_packs?(hook_value) ⇒ Boolean
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 |
# File 'lib/react_on_rails/packer_utils.rb', line 213 def self.hook_contains_generate_packs?(hook_value) # The hook value can be either: # 1. A direct command containing the rake task # 2. A path to a script file that needs to be read return false if hook_value.blank? # Check if it's a direct command first return true if hook_value.to_s.match?(GENERATE_PACKS_PATTERN) # Check if it's a script file path script_path = resolve_hook_script_path(hook_value) return false unless script_path # Read and check script contents script_contents = File.read(script_path) script_contents.match?(GENERATE_PACKS_PATTERN) rescue StandardError # If we can't read the script, assume it doesn't contain generate_packs false end |
.hook_script_has_self_guard?(hook_value) ⇒ Boolean
Check if a hook script file contains the self-guard pattern that prevents duplicate execution when SHAKAPACKER_SKIP_PRECOMPILE_HOOK is set. Returns false for direct command hooks (non-script values).
265 266 267 268 269 270 271 272 273 274 275 |
# File 'lib/react_on_rails/packer_utils.rb', line 265 def self.hook_script_has_self_guard?(hook_value) return false if hook_value.blank? script_path = resolve_hook_script_path(hook_value) return false unless script_path script_contents = File.read(script_path) script_contents.match?(SELF_GUARD_PATTERN) rescue StandardError false end |
.manifest_exists? ⇒ Boolean
110 111 112 |
# File 'lib/react_on_rails/packer_utils.rb', line 110 def self.manifest_exists? ::Shakapacker.config.public_manifest_path.exist? end |
.nested_entries? ⇒ Boolean
102 103 104 |
# File 'lib/react_on_rails/packer_utils.rb', line 102 def self.nested_entries? ::Shakapacker.config.nested_entries? end |
.packer_public_output_path ⇒ Object
106 107 108 |
# File 'lib/react_on_rails/packer_utils.rb', line 106 def self.packer_public_output_path ::Shakapacker.config.public_output_path.to_s end |
.packer_source_entry_path ⇒ Object
98 99 100 |
# File 'lib/react_on_rails/packer_utils.rb', line 98 def self.packer_source_entry_path ::Shakapacker.config.source_entry_path end |
.packer_source_path ⇒ Object
94 95 96 |
# File 'lib/react_on_rails/packer_utils.rb', line 94 def self.packer_source_path ::Shakapacker.config.source_path end |
.packer_source_path_explicit? ⇒ Boolean
114 115 116 |
# File 'lib/react_on_rails/packer_utils.rb', line 114 def self.packer_source_path_explicit? ::Shakapacker.config.send(:data)[:source_path].present? end |
.precompile? ⇒ Boolean
90 91 92 |
# File 'lib/react_on_rails/packer_utils.rb', line 90 def self.precompile? ::Shakapacker.config.shakapacker_precompile? end |
.project_root ⇒ Object
348 349 350 351 352 353 354 355 356 357 358 |
# File 'lib/react_on_rails/packer_utils.rb', line 348 def self.project_root return Rails.root if defined?(Rails) && Rails.respond_to?(:root) && Rails.root bundle_gemfile = ENV.fetch("BUNDLE_GEMFILE", nil) if bundle_gemfile && !bundle_gemfile.strip.empty? gemfile_path = Pathname.new(bundle_gemfile). return gemfile_path.dirname if gemfile_path.file? end Pathname.new(Dir.pwd) end |
.public_output_uri_path ⇒ Object
75 76 77 |
# File 'lib/react_on_rails/packer_utils.rb', line 75 def self.public_output_uri_path "#{::Shakapacker.config.public_output_path.relative_path_from(::Shakapacker.config.public_path)}/" end |
.raise_nested_entries_disabled ⇒ Object
142 143 144 145 146 147 148 149 150 |
# File 'lib/react_on_rails/packer_utils.rb', line 142 def self.raise_nested_entries_disabled msg = <<~MSG **ERROR** ReactOnRails: `nested_entries` is configured to be disabled in shakapacker. Please update \ config/shakapacker.yml to enable nested entries. 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 |
.raise_shakapacker_version_incompatible_for_autobundling ⇒ Object
152 153 154 155 156 157 158 159 160 161 |
# File 'lib/react_on_rails/packer_utils.rb', line 152 def self.raise_shakapacker_version_incompatible_for_autobundling msg = <<~MSG **ERROR** ReactOnRails: Automated bundle generation requires Shakapacker >= #{ReactOnRails::PacksGenerator::MINIMUM_SHAKAPACKER_VERSION_FOR_AUTO_BUNDLING} (for nested_entries support). Installed version: #{ReactOnRails::PackerUtils.shakapacker_version} To fix: Upgrade Shakapacker, or set `auto_load_bundle: false` in your ReactOnRails configuration. MSG raise ReactOnRails::Error, msg end |
.resolve_hook_script_path(hook_value) ⇒ Object
234 235 236 237 238 239 240 241 242 243 244 245 246 247 |
# File 'lib/react_on_rails/packer_utils.rb', line 234 def self.resolve_hook_script_path(hook_value) return nil if hook_value.blank? hook_path = hook_value.to_s.strip return nil if hook_path.empty? # Strip interpreter prefix (e.g., "ruby bin/hook" -> "bin/hook") hook_path = extract_script_from_command(hook_path) || hook_path # Hook value might be a script path relative to project root. # project_root prefers Rails.root and otherwise derives from BUNDLE_GEMFILE or cwd. potential_path = project_root.join(hook_path) potential_path if potential_path.file? end |
.shakapacker_precompile_hook_configured? ⇒ Boolean
Check if shakapacker.yml has a precompile hook configured This prevents react_on_rails from running generate_packs twice
Returns false if detection fails for any reason (missing shakapacker, malformed config, etc.) to ensure generate_packs runs rather than being incorrectly skipped
Note: Currently checks a single hook value. Future enhancement will support hook lists to allow prepending/appending multiple commands. See related Shakapacker issue for details.
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 |
# File 'lib/react_on_rails/packer_utils.rb', line 171 def self.shakapacker_precompile_hook_configured? return false unless defined?(::Shakapacker) hook_value = extract_precompile_hook return false if hook_value.nil? hook_contains_generate_packs?(hook_value) rescue StandardError => e # Swallow errors during hook detection to fail safe - if we can't detect the hook, # we should run generate_packs rather than skip it incorrectly. # Possible errors: NoMethodError (config method missing), TypeError (unexpected data structure), # or errors from shakapacker's internal implementation changes warn "Warning: Unable to detect shakapacker precompile hook: #{e.}" if ENV["DEBUG"] false end |
.shakapacker_precompile_hook_value ⇒ Object
Returns the configured precompile hook value for logging/debugging Returns nil if no hook is configured
279 280 281 282 283 284 285 |
# File 'lib/react_on_rails/packer_utils.rb', line 279 def self.shakapacker_precompile_hook_value return nil unless defined?(::Shakapacker) extract_precompile_hook rescue StandardError nil end |
.shakapacker_version ⇒ Object
21 22 23 24 25 |
# File 'lib/react_on_rails/packer_utils.rb', line 21 def self.shakapacker_version return @shakapacker_version if defined?(@shakapacker_version) @shakapacker_version = Gem.loaded_specs["shakapacker"].version.to_s end |
.shakapacker_version_as_array ⇒ Object
27 28 29 30 31 32 33 34 |
# File 'lib/react_on_rails/packer_utils.rb', line 27 def self.shakapacker_version_as_array return @shakapacker_version_as_array if defined?(@shakapacker_version_as_array) match = shakapacker_version.match(ReactOnRails::VersionChecker::VERSION_PARTS_REGEX) # match[4] is the pre-release version, not normally a number but something like "beta.1" or `nil` @shakapacker_version_as_array = [match[1].to_i, match[2].to_i, match[3].to_i, match[4]].compact end |
.shakapacker_version_requirement_met?(required_version) ⇒ Boolean
36 37 38 39 |
# File 'lib/react_on_rails/packer_utils.rb', line 36 def self.shakapacker_version_requirement_met?(required_version) @version_checks ||= {} @version_checks[required_version] ||= Gem::Version.new(shakapacker_version) >= Gem::Version.new(required_version) end |
.supports_async_loading? ⇒ Boolean
41 42 43 |
# File 'lib/react_on_rails/packer_utils.rb', line 41 def self.supports_async_loading? shakapacker_version_requirement_met?("8.2.0") end |
.supports_autobundling? ⇒ Boolean
45 46 47 48 |
# File 'lib/react_on_rails/packer_utils.rb', line 45 def self.supports_autobundling? min_version = ReactOnRails::PacksGenerator::MINIMUM_SHAKAPACKER_VERSION_FOR_AUTO_BUNDLING ::Shakapacker.config.respond_to?(:nested_entries?) && shakapacker_version_requirement_met?(min_version) end |
.webpack_assets_status_checker ⇒ Object
130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/react_on_rails/packer_utils.rb', line 130 def self.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 |