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
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