Class: Shakapacker::Compiler
- Inherits:
-
Object
- Object
- Shakapacker::Compiler
- Defined in:
- sig/shakapacker/compiler.rbs,
lib/shakapacker/compiler.rb
Overview
Compiler for compiling assets with webpack/rspack
Constant Summary collapse
- DOCTOR_HINT_MUTEX =
Mutex.new
Class Attribute Summary collapse
-
.doctor_hint_shown ⇒ Object
Returns the value of attribute doctor_hint_shown.
Instance Attribute Summary collapse
-
#instance ⇒ Instance
readonly
Returns the value of attribute instance.
Class Method Summary collapse
-
.env ⇒ Hash[String, String]
Additional environment variables that the compiler is being run with.
-
.env= ⇒ Hash[String, String]
Sets additional environment variables.
Instance Method Summary collapse
- #acquire_ipc_lock ⇒ void
-
#after_compile_hook ⇒ void
Runs the post-compile hook.
- #bin_shakapacker_path ⇒ Pathname
-
#compile ⇒ Boolean
Compiles all webpack/rspack packs.
-
#config ⇒ Configuration
Returns the configuration object.
- #create_lock_file_dir ⇒ void
-
#fresh? ⇒ Boolean
Returns whether assets are fresh (up-to-date).
-
#initialize(instance) ⇒ Compiler
constructor
Creates a new compiler instance.
- #lock_file_path ⇒ Pathname
- #locked? ⇒ Boolean (also: #compiling?)
-
#logger ⇒ ActiveSupport::TaggedLogging
Returns the logger instance.
- #open_lock_file ⇒ void
-
#optional_ruby_runner ⇒ String?
Returns one executable path for array-form Open3, not a shell command snippet.
- #run_precompile_hook ⇒ void
- #run_webpack ⇒ Boolean
-
#should_run_precompile_hook? ⇒ Boolean
Returns true if precompile hook should run, false if disabled or skipped via ENV.
-
#stale? ⇒ Boolean
Returns whether assets are stale (need recompilation).
-
#strategy ⇒ CompilerStrategy
Returns the compiler strategy.
- #validate_precompile_hook(hook_command) ⇒ Hash[Symbol, untyped]
- #wait_for_compilation_to_complete ⇒ void
- #webpack_env ⇒ Hash[String, String]
Constructor Details
#initialize(instance) ⇒ Compiler
Creates a new compiler instance
10 11 12 |
# File 'sig/shakapacker/compiler.rbs', line 10 def initialize(instance) @instance = instance end |
Class Attribute Details
.doctor_hint_shown ⇒ Object
Returns the value of attribute doctor_hint_shown.
20 21 22 |
# File 'lib/shakapacker/compiler.rb', line 20 def doctor_hint_shown @doctor_hint_shown end |
Instance Attribute Details
#instance ⇒ Instance (readonly)
Returns the value of attribute instance.
57 58 59 |
# File 'lib/shakapacker/compiler.rb', line 57 def instance @instance end |
Class Method Details
.env ⇒ Hash[String, String]
Additional environment variables that the compiler is being run with
4 |
# File 'sig/shakapacker/compiler.rbs', line 4
def self.env: () -> Hash[String, String]
|
.env= ⇒ Hash[String, String]
Sets additional environment variables
7 |
# File 'sig/shakapacker/compiler.rbs', line 7
def self.env=: (Hash[String, String] env) -> Hash[String, String]
|
Instance Method Details
#acquire_ipc_lock ⇒ void
This method returns an undefined value.
59 60 61 62 63 64 |
# File 'lib/shakapacker/compiler.rb', line 59 def acquire_ipc_lock open_lock_file do |lf| lf.flock(File::LOCK_EX) yield if block_given? end end |
#after_compile_hook ⇒ void
This method returns an undefined value.
Runs the post-compile hook
31 |
# File 'sig/shakapacker/compiler.rbs', line 31
def after_compile_hook: () -> void
|
#bin_shakapacker_path ⇒ Pathname
231 232 233 |
# File 'lib/shakapacker/compiler.rb', line 231 def bin_shakapacker_path config.root_path.join("bin/shakapacker") end |
#compile ⇒ Boolean
Compiles all webpack/rspack packs
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
# File 'sig/shakapacker/compiler.rbs', line 13 def compile unless stale? logger.debug "Everything's up-to-date. Nothing to do" return true end if compiling? wait_for_compilation_to_complete true else acquire_ipc_lock do run_precompile_hook if should_run_precompile_hook? spawn_failed = false begin success = run_webpack rescue SpawnFailure spawn_failed = true success = false end after_compile_hook unless spawn_failed success end end end |
#config ⇒ Configuration
Returns the configuration object
16 |
# File 'sig/shakapacker/compiler.rbs', line 16
def config: () -> Configuration
|
#create_lock_file_dir ⇒ void
This method returns an undefined value.
87 88 89 90 |
# File 'lib/shakapacker/compiler.rb', line 87 def create_lock_file_dir dirname = File.dirname(lock_file_path) FileUtils.mkdir_p(dirname) end |
#fresh? ⇒ Boolean
Returns whether assets are fresh (up-to-date)
25 |
# File 'sig/shakapacker/compiler.rbs', line 25
def fresh?: () -> bool
|
#lock_file_path ⇒ Pathname
92 93 94 |
# File 'lib/shakapacker/compiler.rb', line 92 def lock_file_path config.root_path.join("tmp/shakapacker.lock") end |
#locked? ⇒ Boolean Also known as: compiling?
66 67 68 69 70 |
# File 'lib/shakapacker/compiler.rb', line 66 def locked? open_lock_file do |lf| lf.flock(File::LOCK_EX | File::LOCK_NB) != 0 end end |
#logger ⇒ ActiveSupport::TaggedLogging
Returns the logger instance
19 |
# File 'sig/shakapacker/compiler.rbs', line 19
def logger: () -> ActiveSupport::TaggedLogging
|
#open_lock_file ⇒ void
This method returns an undefined value.
79 80 81 82 83 84 85 |
# File 'lib/shakapacker/compiler.rb', line 79 def open_lock_file create_lock_file_dir unless File.exist?(lock_file_path) File.open(lock_file_path, File::CREAT) do |lf| return yield lf end end |
#optional_ruby_runner ⇒ String?
Returns one executable path for array-form Open3, not a shell command snippet.
97 98 99 100 |
# File 'lib/shakapacker/compiler.rb', line 97 def optional_ruby_runner first_line = File.readlines(bin_shakapacker_path).first&.chomp || "" /ruby/.match?(first_line) ? RbConfig.ruby : "" end |
#run_precompile_hook ⇒ void
This method returns an undefined value.
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/shakapacker/compiler.rb', line 109 def run_precompile_hook hook_command = config.precompile_hook hook_spec = validate_precompile_hook(hook_command) logger.info "Running precompile hook: #{hook_command}" runtime_env = webpack_env.merge(hook_spec[:env]) stdout, stderr, status = Open3.capture3( runtime_env, hook_spec[:executable], *hook_spec[:args], chdir: File.(config.root_path) ) if status.success? logger.info "Precompile hook completed successfully" logger.info stdout unless stdout.empty? logger.warn stderr unless stderr.empty? else non_empty_streams = [stdout, stderr].delete_if(&:empty?) logger.error "\nPRECOMPILE HOOK FAILED:\nEXIT STATUS: #{status.exitstatus}\nCOMMAND: #{hook_command}\nOUTPUTS:\n#{non_empty_streams.join("\n\n")}" logger.error "\nTo fix this:" logger.error " 1. Check that the hook script exists and is executable" logger.error " 2. Test the hook command manually: #{hook_command}" logger.error " 3. Review the error output above for details" logger.error " 4. You can disable the hook temporarily by commenting out 'precompile_hook' in shakapacker.yml" raise "Precompile hook '#{hook_command}' failed with exit status #{status.exitstatus}" end end |
#run_webpack ⇒ Boolean
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 |
# File 'lib/shakapacker/compiler.rb', line 187 def run_webpack logger.info "Compiling..." # Fetch compile flags before the spawn rescue so configuration errors stay explicit. compile_flags = config.webpack_compile_flags begin command = shakapacker_command(compile_flags) stdout, stderr, status = Open3.capture3( webpack_env, *command, chdir: File.(config.root_path) ) rescue Errno::EACCES, Errno::ENOENT, Errno::ENOEXEC, Errno::EPERM, Errno::ENOTDIR => e logger.error "\nCOMPILATION FAILED:\n#{e.class}: #{e.}" show_doctor_hint_once raise SpawnFailure end if status.success? logger.info "Compiled all packs in #{config.public_output_path}" logger.warn "#{stderr}" unless stderr.empty? if config.webpack_compile_output? logger.info stdout end else non_empty_streams = [stdout, stderr].delete_if(&:empty?) logger.error "\nCOMPILATION FAILED:\nEXIT STATUS: #{status}\nOUTPUTS:\n#{non_empty_streams.join("\n\n")}" show_doctor_hint_once end status.success? end |
#should_run_precompile_hook? ⇒ Boolean
Returns true if precompile hook should run, false if disabled or skipped via ENV
56 57 58 59 60 61 |
# File 'sig/shakapacker/compiler.rbs', line 56 def should_run_precompile_hook? return false unless config.precompile_hook return false if ENV["SHAKAPACKER_SKIP_PRECOMPILE_HOOK"] == "true" true end |
#stale? ⇒ Boolean
Returns whether assets are stale (need recompilation)
28 |
# File 'sig/shakapacker/compiler.rbs', line 28
def stale?: () -> bool
|
#strategy ⇒ CompilerStrategy
Returns the compiler strategy
22 |
# File 'sig/shakapacker/compiler.rbs', line 22
def strategy: () -> CompilerStrategy
|
#validate_precompile_hook(hook_command) ⇒ Hash[Symbol, untyped]
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 |
# File 'lib/shakapacker/compiler.rb', line 139 def validate_precompile_hook(hook_command) hook_tokens = begin Shellwords.shellsplit(hook_command) rescue ArgumentError => e raise "Shakapacker configuration error: Invalid precompile_hook command syntax: #{e.}. Check for unmatched quotes in: #{hook_command}" end env_assignments = {} while hook_tokens.first&.match?(/\A[A-Za-z_][A-Za-z0-9_]*=/) key, value = hook_tokens.shift.split("=", 2) env_assignments[key] = value end executable = hook_tokens.shift if executable.nil? || executable.empty? raise "Shakapacker configuration error: precompile_hook must include an executable command. Got: #{hook_command}" end executable_path = config.root_path.join(executable) # Security: Resolve symlinks and verify the hook points to a file within the project # This prevents symlink bypass attacks and path traversal attacks begin resolved_path = executable_path.realpath resolved_root = config.root_path.realpath rescue Errno::ENOENT # If file doesn't exist, use cleanpath for basic validation resolved_path = executable_path.cleanpath resolved_root = config.root_path.cleanpath end # Verify path is within project root with proper separator check # Using File::SEPARATOR prevents partial path matches (e.g., /project vs /project-evil) unless resolved_path.to_s.start_with?(resolved_root.to_s + File::SEPARATOR) raise "Security Error: precompile_hook must reference a script within the project root. " \ "Got: #{hook_command} (resolved to: #{resolved_path})" end # Warn if the executable doesn't exist within the project unless File.exist?(executable_path) logger.warn "⚠️ Warning: precompile_hook executable not found: #{executable_path}" logger.warn " The hook command is configured but the script does not exist within the project root." logger.warn " Please ensure the script exists or remove 'precompile_hook' from your shakapacker.yml configuration." end { env: env_assignments, executable: executable, args: hook_tokens } end |
#wait_for_compilation_to_complete ⇒ void
This method returns an undefined value.
74 75 76 77 |
# File 'lib/shakapacker/compiler.rb', line 74 def wait_for_compilation_to_complete logger.info "Waiting for the compilation to complete..." acquire_ipc_lock end |
#webpack_env ⇒ Hash[String, String]
222 223 224 225 226 227 228 229 |
# File 'lib/shakapacker/compiler.rb', line 222 def webpack_env return env unless defined?(ActionController::Base) env.merge( "SHAKAPACKER_ASSET_HOST" => instance.config.asset_host, "SHAKAPACKER_CONFIG" => instance.config.config_path.to_s ) end |