@@ -166,5 +166,78 @@ def self.raise_shakapacker_version_incompatible_for_basic_pack_generation
166166
167167 raise ReactOnRails ::Error , msg
168168 end
169+
170+ # Check if shakapacker.yml has a precompile hook configured
171+ # This prevents react_on_rails from running generate_packs twice
172+ #
173+ # Returns false if detection fails for any reason (missing shakapacker, malformed config, etc.)
174+ # to ensure generate_packs runs rather than being incorrectly skipped
175+ #
176+ # Note: Currently checks a single hook value. Future enhancement will support hook lists
177+ # to allow prepending/appending multiple commands. See related Shakapacker issue for details.
178+ def self . shakapacker_precompile_hook_configured?
179+ return false unless defined? ( ::Shakapacker )
180+
181+ hook_value = extract_precompile_hook
182+ return false if hook_value . nil?
183+
184+ hook_contains_generate_packs? ( hook_value )
185+ rescue StandardError => e
186+ # Swallow errors during hook detection to fail safe - if we can't detect the hook,
187+ # we should run generate_packs rather than skip it incorrectly.
188+ # Possible errors: NoMethodError (config method missing), TypeError (unexpected data structure),
189+ # or errors from shakapacker's internal implementation changes
190+ warn "Warning: Unable to detect shakapacker precompile hook: #{ e . message } " if ENV [ "DEBUG" ]
191+ false
192+ end
193+
194+ def self . extract_precompile_hook
195+ # Access config data using private :data method since there's no public API
196+ # to access the raw configuration hash needed for hook detection
197+ config_data = ::Shakapacker . config . send ( :data )
198+
199+ # Try symbol keys first (Shakapacker's internal format), then fall back to string keys
200+ # Note: Currently only one hook value is supported, but this will change to support lists
201+ config_data &.dig ( :hooks , :precompile ) || config_data &.dig ( "hooks" , "precompile" )
202+ end
203+
204+ def self . hook_contains_generate_packs? ( hook_value )
205+ # The hook value can be either:
206+ # 1. A direct command containing the rake task
207+ # 2. A path to a script file that needs to be read
208+ return false if hook_value . blank?
209+
210+ # Check if it's a direct command first
211+ return true if hook_value . to_s . match? ( /\b react_on_rails:generate_packs\b / )
212+
213+ # Check if it's a script file path
214+ script_path = resolve_hook_script_path ( hook_value )
215+ return false unless script_path && File . exist? ( script_path )
216+
217+ # Read and check script contents
218+ script_contents = File . read ( script_path )
219+ script_contents . match? ( /\b react_on_rails:generate_packs\b / )
220+ rescue StandardError
221+ # If we can't read the script, assume it doesn't contain generate_packs
222+ false
223+ end
224+
225+ def self . resolve_hook_script_path ( hook_value )
226+ # Hook value might be a script path relative to Rails root
227+ return nil unless defined? ( Rails ) && Rails . respond_to? ( :root )
228+
229+ potential_path = Rails . root . join ( hook_value . to_s . strip )
230+ potential_path if potential_path . file?
231+ end
232+
233+ # Returns the configured precompile hook value for logging/debugging
234+ # Returns nil if no hook is configured
235+ def self . shakapacker_precompile_hook_value
236+ return nil unless defined? ( ::Shakapacker )
237+
238+ extract_precompile_hook
239+ rescue StandardError
240+ nil
241+ end
169242 end
170243end
0 commit comments