Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

# Shakapacker precompile hook for React on Rails Pro test dummy app
#
# This script loads the shared test helper implementation.
# For production apps, use the generator template which includes a standalone implementation.

# Find the gem root directory (four levels up from react_on_rails_pro/spec/dummy/bin)
gem_root = File.expand_path("../../../..", __dir__)
shared_hook = File.join(gem_root, "spec", "support", "shakapacker_precompile_hook_shared.rb")

unless File.exist?(shared_hook)
warn "❌ Error: Shared precompile hook not found at #{shared_hook}"
exit 1
end

# Load and execute the shared hook
load shared_hook
5 changes: 5 additions & 0 deletions react_on_rails_pro/spec/dummy/config/shakapacker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ default: &default
# Reload manifest.json on all requests so we reload latest compiled packs
cache_manifest: false

# Hook to run before webpack compilation (e.g., for generating dynamic entry points)
# SECURITY: Only reference trusted scripts within your project. Ensure the hook path
# points to a file within the project root that you control.
precompile_hook: 'bin/shakapacker-precompile-hook'

# Extract and emit a css file
extract_css: true

Expand Down
106 changes: 12 additions & 94 deletions spec/dummy/bin/shakapacker-precompile-hook
Original file line number Diff line number Diff line change
@@ -1,101 +1,19 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

# Shakapacker precompile hook
# This script runs before Shakapacker compilation in both development and production.
# See: https://github.com/shakacode/shakapacker/blob/main/docs/precompile_hook.md
# Shakapacker precompile hook for React on Rails test dummy app
#
# This script loads the shared test helper implementation.
# For production apps, use the generator template which includes a standalone implementation.

require "fileutils"
# Find the gem root directory (three levels up from spec/dummy/bin)
gem_root = File.expand_path("../../..", __dir__)
shared_hook = File.join(gem_root, "spec", "support", "shakapacker_precompile_hook_shared.rb")

# Find Rails root by walking upward looking for config/environment.rb
def find_rails_root
dir = Dir.pwd
loop do
return dir if File.exist?(File.join(dir, "config", "environment.rb"))

parent = File.dirname(dir)
return nil if parent == dir # Reached filesystem root

dir = parent
end
end

# Build ReScript if needed
def build_rescript_if_needed
# Check for both old (bsconfig.json) and new (rescript.json) config files
return unless File.exist?("bsconfig.json") || File.exist?("rescript.json")

puts "🔧 Building ReScript..."

# Cross-platform package manager detection
yarn_available = system("yarn", "--version", out: File::NULL, err: File::NULL)
npm_available = system("npm", "--version", out: File::NULL, err: File::NULL)

success = if yarn_available
system("yarn", "build:rescript")
elsif npm_available
system("npm", "run", "build:rescript")
else
warn "⚠️ Warning: Neither yarn nor npm found. Skipping ReScript build."
return
end

if success
puts "✅ ReScript build completed successfully"
else
warn "❌ ReScript build failed"
exit 1
end
end

# Generate React on Rails packs if needed
# rubocop:disable Metrics/CyclomaticComplexity
def generate_packs_if_needed
# Find Rails root directory
rails_root = find_rails_root
return unless rails_root

# Check if React on Rails initializer exists
initializer_path = File.join(rails_root, "config", "initializers", "react_on_rails.rb")
return unless File.exist?(initializer_path)

# Check if auto-pack generation is configured (match actual config assignments, not comments)
config_file = File.read(initializer_path)
# Match uncommented configuration lines only (lines not starting with #)
has_auto_load = config_file =~ /^\s*(?!#).*config\.auto_load_bundle\s*=/
has_components_subdir = config_file =~ /^\s*(?!#).*config\.components_subdirectory\s*=/
return unless has_auto_load || has_components_subdir

puts "📦 Generating React on Rails packs..."

# Cross-platform bundle availability check
bundle_available = system("bundle", "--version", out: File::NULL, err: File::NULL)
return unless bundle_available

# Check if rake task exists (use array form for security)
task_list = IO.popen(["bundle", "exec", "rails", "-T"], err: [:child, :out], &:read)
return unless task_list.include?("react_on_rails:generate_packs")

# Use array form for better cross-platform support
success = system("bundle", "exec", "rails", "react_on_rails:generate_packs")

if success
puts "✅ Pack generation completed successfully"
else
warn "❌ Pack generation failed"
exit 1
end
end
# rubocop:enable Metrics/CyclomaticComplexity

# Main execution
begin
build_rescript_if_needed
generate_packs_if_needed

exit 0
rescue StandardError => e
warn "❌ Precompile hook failed: #{e.message}"
warn e.backtrace.join("\n")
unless File.exist?(shared_hook)
warn "❌ Error: Shared precompile hook not found at #{shared_hook}"
exit 1
end

# Load and execute the shared hook
load shared_hook
Comment on lines +9 to +19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Shared precompile hook is never executed (bin script only loads it).

This script currently just loads spec/support/shakapacker_precompile_hook_shared.rb and exits. Because the shared file only runs its main block when __FILE__ == $PROGRAM_NAME, loading it from this bin script does not trigger build_rescript_if_needed or generate_packs_if_needed, so the Shakapacker precompile hook becomes a no-op.

Explicitly invoke the shared methods after loading the file so ReScript builds and pack generation actually run.

Suggested fix:

 gem_root = File.expand_path("../../..", __dir__)
 shared_hook = File.join(gem_root, "spec", "support", "shakapacker_precompile_hook_shared.rb")

 unless File.exist?(shared_hook)
   warn "❌ Error: Shared precompile hook not found at #{shared_hook}"
   exit 1
 end

 # Load and execute the shared hook
-load shared_hook
+load shared_hook
+
+# Run shared precompile steps
+build_rescript_if_needed
+generate_packs_if_needed

I'll verify the review comment by inspecting the relevant files in the codebase.


🏁 Script executed:

#!/bin/bash
# Inspect the bin script
echo "=== BIN SCRIPT ==="
cat spec/dummy/bin/shakapacker-precompile-hook

echo -e "\n=== SHARED HOOK FILE ==="
cat spec/support/shakapacker_precompile_hook_shared.rb

Length of output: 4345


Shared precompile hook is never executed (bin script only loads it).

This script currently just loads spec/support/shakapacker_precompile_hook_shared.rb and exits. Because the shared file only runs its main block when __FILE__ == $PROGRAM_NAME, loading it from this bin script does not trigger build_rescript_if_needed or generate_packs_if_needed, so the Shakapacker precompile hook becomes a no-op.

Explicitly invoke the shared methods after loading the file so ReScript builds and pack generation actually run.

Suggested fix:

 gem_root = File.expand_path("../../..", __dir__)
 shared_hook = File.join(gem_root, "spec", "support", "shakapacker_precompile_hook_shared.rb")

 unless File.exist?(shared_hook)
   warn "❌ Error: Shared precompile hook not found at #{shared_hook}"
   exit 1
 end

 # Load and execute the shared hook
-load shared_hook
+load shared_hook
+
+# Run shared precompile steps
+build_rescript_if_needed
+generate_packs_if_needed
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Find the gem root directory (three levels up from spec/dummy/bin)
gem_root = File.expand_path("../../..", __dir__)
shared_hook = File.join(gem_root, "spec", "support", "shakapacker_precompile_hook_shared.rb")
# Find Rails root by walking upward looking for config/environment.rb
def find_rails_root
dir = Dir.pwd
loop do
return dir if File.exist?(File.join(dir, "config", "environment.rb"))
parent = File.dirname(dir)
return nil if parent == dir # Reached filesystem root
dir = parent
end
end
# Build ReScript if needed
def build_rescript_if_needed
# Check for both old (bsconfig.json) and new (rescript.json) config files
return unless File.exist?("bsconfig.json") || File.exist?("rescript.json")
puts "🔧 Building ReScript..."
# Cross-platform package manager detection
yarn_available = system("yarn", "--version", out: File::NULL, err: File::NULL)
npm_available = system("npm", "--version", out: File::NULL, err: File::NULL)
success = if yarn_available
system("yarn", "build:rescript")
elsif npm_available
system("npm", "run", "build:rescript")
else
warn "⚠️ Warning: Neither yarn nor npm found. Skipping ReScript build."
return
end
if success
puts "✅ ReScript build completed successfully"
else
warn "❌ ReScript build failed"
exit 1
end
end
# Generate React on Rails packs if needed
# rubocop:disable Metrics/CyclomaticComplexity
def generate_packs_if_needed
# Find Rails root directory
rails_root = find_rails_root
return unless rails_root
# Check if React on Rails initializer exists
initializer_path = File.join(rails_root, "config", "initializers", "react_on_rails.rb")
return unless File.exist?(initializer_path)
# Check if auto-pack generation is configured (match actual config assignments, not comments)
config_file = File.read(initializer_path)
# Match uncommented configuration lines only (lines not starting with #)
has_auto_load = config_file =~ /^\s*(?!#).*config\.auto_load_bundle\s*=/
has_components_subdir = config_file =~ /^\s*(?!#).*config\.components_subdirectory\s*=/
return unless has_auto_load || has_components_subdir
puts "📦 Generating React on Rails packs..."
# Cross-platform bundle availability check
bundle_available = system("bundle", "--version", out: File::NULL, err: File::NULL)
return unless bundle_available
# Check if rake task exists (use array form for security)
task_list = IO.popen(["bundle", "exec", "rails", "-T"], err: [:child, :out], &:read)
return unless task_list.include?("react_on_rails:generate_packs")
# Use array form for better cross-platform support
success = system("bundle", "exec", "rails", "react_on_rails:generate_packs")
if success
puts "✅ Pack generation completed successfully"
else
warn "❌ Pack generation failed"
exit 1
end
end
# rubocop:enable Metrics/CyclomaticComplexity
# Main execution
begin
build_rescript_if_needed
generate_packs_if_needed
exit 0
rescue StandardError => e
warn "❌ Precompile hook failed: #{e.message}"
warn e.backtrace.join("\n")
unless File.exist?(shared_hook)
warn "❌ Error: Shared precompile hook not found at #{shared_hook}"
exit 1
end
# Load and execute the shared hook
load shared_hook
# Find the gem root directory (three levels up from spec/dummy/bin)
gem_root = File.expand_path("../../..", __dir__)
shared_hook = File.join(gem_root, "spec", "support", "shakapacker_precompile_hook_shared.rb")
unless File.exist?(shared_hook)
warn "❌ Error: Shared precompile hook not found at #{shared_hook}"
exit 1
end
# Load and execute the shared hook
load shared_hook
# Run shared precompile steps
build_rescript_if_needed
generate_packs_if_needed
🤖 Prompt for AI Agents
In spec/dummy/bin/shakapacker-precompile-hook around lines 9 to 19, the script
only loads spec/support/shakapacker_precompile_hook_shared.rb so its main block
(guarded by __FILE__ == $PROGRAM_NAME) never runs; after loading the shared file
explicitly call the methods that perform the work (e.g. build_rescript_if_needed
and generate_packs_if_needed), check their return values or rescue exceptions to
surface failures, log or warn on error and exit with non‑zero status when those
calls fail so the precompile hook actually performs ReScript builds and pack
generation.

119 changes: 119 additions & 0 deletions spec/support/shakapacker_precompile_hook_shared.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

# Shakapacker precompile hook for React on Rails - Shared Implementation
#
# This is the shared implementation used by both test dummy apps:
# - spec/dummy/bin/shakapacker-precompile-hook
# - react_on_rails_pro/spec/dummy/bin/shakapacker-precompile-hook
#
# This script runs before webpack compilation to:
# 1. Build ReScript files (if configured)
# 2. Generate pack files for auto-bundled components
#
# See: https://github.com/shakacode/shakapacker/blob/main/docs/precompile_hook.md

require "fileutils"
require "json"

# Find Rails root by walking upward looking for config/environment.rb
def find_rails_root
dir = Dir.pwd
while dir != "/"
return dir if File.exist?(File.join(dir, "config", "environment.rb"))

dir = File.dirname(dir)
end
nil
end

# Build ReScript if needed
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
def build_rescript_if_needed
rails_root = find_rails_root
unless rails_root
warn "⚠️ Warning: Could not find Rails root. Skipping ReScript build."
return
end

# Check for both old (bsconfig.json) and new (rescript.json) config files
return unless File.exist?(File.join(rails_root, "bsconfig.json")) ||
File.exist?(File.join(rails_root, "rescript.json"))

puts "🔧 Building ReScript..."

# Validate that build:rescript script exists in package.json
package_json_path = File.join(rails_root, "package.json")
unless File.exist?(package_json_path)
warn "❌ Error: ReScript config found but package.json not found"
warn " ReScript requires a package.json with a build:rescript script"
exit 1
end

package_json = JSON.parse(File.read(package_json_path))
unless package_json.dig("scripts", "build:rescript")
warn "❌ Error: ReScript config found but no build:rescript script in package.json"
warn " Add this to your package.json scripts section:"
warn ' "build:rescript": "rescript build"'
exit 1
end

Dir.chdir(rails_root) do
# Cross-platform package manager detection
if system("which yarn > /dev/null 2>&1")
system("yarn", "build:rescript", exception: true)
elsif system("which npm > /dev/null 2>&1")
system("npm", "run", "build:rescript", exception: true)
else
warn "❌ Error: Neither yarn nor npm found but ReScript build required"
warn " Install yarn or npm to build ReScript files"
exit 1
end

puts "✅ ReScript build completed successfully"
end
rescue JSON::ParserError => e
warn "❌ Error: Invalid package.json: #{e.message}"
exit 1
rescue StandardError => e
warn "❌ ReScript build failed: #{e.message}"
exit 1
end
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity

# Generate React on Rails packs if needed
def generate_packs_if_needed
rails_root = find_rails_root
return unless rails_root

initializer_path = File.join(rails_root, "config", "initializers", "react_on_rails.rb")
return unless File.exist?(initializer_path)

# Check if auto-pack generation is configured
# Match config lines that aren't commented out and allow flexible spacing
initializer_content = File.read(initializer_path)
return unless initializer_content.match?(/^\s*(?!#).*config\.auto_load_bundle\s*=/) ||
initializer_content.match?(/^\s*(?!#).*config\.components_subdirectory\s*=/)

puts "📦 Generating React on Rails packs..."

Dir.chdir(rails_root) do
# Skip validation during precompile hook execution
ENV["REACT_ON_RAILS_SKIP_VALIDATION"] = "true"

# Run pack generation
system("bundle", "exec", "rails", "react_on_rails:generate_packs", exception: true)
puts "✅ Pack generation completed successfully"
end
rescue Errno::ENOENT => e
warn "⚠️ Warning: #{e.message}"
rescue StandardError => e
warn "❌ Pack generation failed: #{e.message}"
exit 1
end

# Main execution (only if run directly, not when required)
if __FILE__ == $PROGRAM_NAME
build_rescript_if_needed
generate_packs_if_needed
end
Loading