Class: Shakapacker::Compiler

Inherits:
Object
  • Object
show all
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

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(instance) ⇒ Compiler

Creates a new compiler instance

Parameters:



10
11
12
# File 'sig/shakapacker/compiler.rbs', line 10

def initialize(instance)
  @instance = instance
end

Class Attribute Details

.doctor_hint_shownObject

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

#instanceInstance (readonly)

Returns the value of attribute instance.

Returns:



57
58
59
# File 'lib/shakapacker/compiler.rb', line 57

def instance
  @instance
end

Class Method Details

.envHash[String, String]

Additional environment variables that the compiler is being run with

Returns:

  • (Hash[String, String])


4
# File 'sig/shakapacker/compiler.rbs', line 4

def self.env: () -> Hash[String, String]

.env=Hash[String, String]

Sets additional environment variables

Parameters:

  • env (Hash[String, String])

Returns:

  • (Hash[String, String])


7
# File 'sig/shakapacker/compiler.rbs', line 7

def self.env=: (Hash[String, String] env) -> Hash[String, String]

Instance Method Details

#acquire_ipc_lockvoid

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_hookvoid

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_pathPathname

Returns:

  • (Pathname)


231
232
233
# File 'lib/shakapacker/compiler.rb', line 231

def bin_shakapacker_path
  config.root_path.join("bin/shakapacker")
end

#compileBoolean

Compiles all webpack/rspack packs

Returns:

  • (Boolean)


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

#configConfiguration

Returns the configuration object

Returns:



16
# File 'sig/shakapacker/compiler.rbs', line 16

def config: () -> Configuration

#create_lock_file_dirvoid

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)

Returns:

  • (Boolean)


25
# File 'sig/shakapacker/compiler.rbs', line 25

def fresh?: () -> bool

#lock_file_pathPathname

Returns:

  • (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?

Returns:

  • (Boolean)


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

#loggerActiveSupport::TaggedLogging

Returns the logger instance

Returns:

  • (ActiveSupport::TaggedLogging)


19
# File 'sig/shakapacker/compiler.rbs', line 19

def logger: () -> ActiveSupport::TaggedLogging

#open_lock_filevoid

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

Returns one executable path for array-form Open3, not a shell command snippet.

Returns:

  • (String, nil)


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_hookvoid

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.expand_path(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_webpackBoolean

Returns:

  • (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.expand_path(config.root_path)
    )
  rescue Errno::EACCES, Errno::ENOENT, Errno::ENOEXEC, Errno::EPERM, Errno::ENOTDIR => e
    logger.error "\nCOMPILATION FAILED:\n#{e.class}: #{e.message}"
    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

Returns:

  • (Boolean)


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)

Returns:

  • (Boolean)


28
# File 'sig/shakapacker/compiler.rbs', line 28

def stale?: () -> bool

#strategyCompilerStrategy

Returns the compiler strategy

Returns:



22
# File 'sig/shakapacker/compiler.rbs', line 22

def strategy: () -> CompilerStrategy

#validate_precompile_hook(hook_command) ⇒ Hash[Symbol, untyped]

Parameters:

  • hook_command (String)

Returns:

  • (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.message}. 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_completevoid

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_envHash[String, String]

Returns:

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