Class: Shakapacker::Configuration

Inherits:
Object
  • Object
show all
Defined in:
lib/shakapacker/configuration.rb,
sig/shakapacker/configuration.rbs

Overview

Configuration management for Shakapacker

Loads and provides access to settings from config/shakapacker.yml

Constant Summary collapse

SHAKAPACKER_NODE_FLAGS =

Shared Ruby source of truth for Shakapacker-owned Node flags; Runner aliases this constant.

%w[--debug-shakapacker --trace-deprecation --no-deprecation].freeze

Class Attribute Summary collapse

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(root_path:, config_path:, env:, bundler_override: nil) ⇒ Object

Creates a new configuration instance



97
98
99
100
101
102
# File 'lib/shakapacker/configuration.rb', line 97

def initialize(root_path:, config_path:, env:, bundler_override: nil)
  @root_path = root_path
  @env = env
  @config_path = config_path
  @bundler_override = bundler_override
end

Class Attribute Details

.installingBoolean

Flag indicating whether Shakapacker is currently being installed

Returns:

  • (Boolean)


70
71
72
# File 'lib/shakapacker/configuration.rb', line 70

def installing
  @installing
end

Instance Attribute Details

#bundler_overrideString? (readonly)

Override for the assets bundler (set via CLI flag)

Returns:

  • (String, nil)


88
89
90
# File 'lib/shakapacker/configuration.rb', line 88

def bundler_override
  @bundler_override
end

#config_pathPathname (readonly)

The path to the shakapacker.yml configuration file

Returns:

  • (Pathname)


79
80
81
# File 'lib/shakapacker/configuration.rb', line 79

def config_path
  @config_path
end

#envActiveSupport::StringInquirer (readonly)

The current Rails environment

Returns:

  • (ActiveSupport::StringInquirer)


83
84
85
# File 'lib/shakapacker/configuration.rb', line 83

def env
  @env
end

#root_pathPathname (readonly)

The application root path

Returns:

  • (Pathname)


75
76
77
# File 'lib/shakapacker/configuration.rb', line 75

def root_path
  @root_path
end

Instance Method Details

#additional_pathsArray[String]

Returns additional paths to include in compilation

Returns:

  • (Array[String])


174
175
176
# File 'lib/shakapacker/configuration.rb', line 174

def additional_paths
  fetch(:additional_paths)
end

#asset_hostString?

Returns the asset host URL for serving assets

Returns:

  • (String, nil)


661
662
663
664
665
666
# File 'lib/shakapacker/configuration.rb', line 661

def asset_host
  ENV.fetch(
    "SHAKAPACKER_ASSET_HOST",
    fetch(:asset_host) || ActionController::Base.helpers.compute_asset_host
  )
end

#assets_bundlerString

Returns the assets bundler to use (webpack or rspack)

Returns:

  • (String)


311
312
313
314
315
316
317
318
319
320
# File 'lib/shakapacker/configuration.rb', line 311

def assets_bundler
  # CLI --bundler flag takes highest precedence
  return @bundler_override if @bundler_override

  # Show deprecation warning if using old 'bundler' key
  if data.has_key?(:bundler) && !data.has_key?(:assets_bundler)
    $stderr.puts "⚠️  DEPRECATION WARNING: The 'bundler' configuration option is deprecated. Please use 'assets_bundler' instead to avoid confusion with Ruby's Bundler gem manager."
  end
  ENV["SHAKAPACKER_ASSETS_BUNDLER"] || fetch(:assets_bundler) || fetch(:bundler) || "webpack"
end

#assets_bundler_config_pathString

Returns the path to the bundler configuration directory

Returns:

  • (String)


457
458
459
460
461
462
463
# File 'lib/shakapacker/configuration.rb', line 457

def assets_bundler_config_path
  custom_path = fetch(:assets_bundler_config_path)
  return custom_path if custom_path

  # Default paths based on bundler type
  rspack? ? "config/rspack" : "config/webpack"
end

#bundlerString

Deprecated alias for assets_bundler

Returns:

  • (String)


327
328
329
# File 'lib/shakapacker/configuration.rb', line 327

def bundler
  assets_bundler
end

#cache_manifest?Boolean

Returns whether manifest caching is enabled

Returns:

  • (Boolean)


248
249
250
# File 'lib/shakapacker/configuration.rb', line 248

def cache_manifest?
  fetch(:cache_manifest)
end

#cache_pathPathname

Returns the absolute path to the compilation cache directory

Returns:

  • (Pathname)


258
259
260
# File 'lib/shakapacker/configuration.rb', line 258

def cache_path
  root_path.join(fetch(:cache_path))
end

#compile?Boolean

Returns whether automatic compilation is enabled

Returns:

  • (Boolean)


119
120
121
# File 'lib/shakapacker/configuration.rb', line 119

def compile?
  fetch(:compile)
end

#compiler_strategyString

Returns the compiler strategy for determining staleness

Returns:

  • (String)


297
298
299
# File 'lib/shakapacker/configuration.rb', line 297

def compiler_strategy
  fetch(:compiler_strategy)
end

#css_modules_export_modeString

Returns the CSS Modules export mode configuration

Controls how CSS Module class names are exported in JavaScript:

  • "named" (default): Use named exports with camelCase conversion (v9 behavior)
  • "default": Use default export with both original and camelCase names (v8 behavior)

Returns:

  • (String)

    "named" or "default"

Raises:

  • (ArgumentError)

    if an invalid value is configured



434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
# File 'lib/shakapacker/configuration.rb', line 434

def css_modules_export_mode
  @css_modules_export_mode ||= begin
    mode = fetch(:css_modules_export_mode) || "named"

    # Validate the configuration value
    valid_modes = ["named", "default"]
    unless valid_modes.include?(mode)
      raise ArgumentError,
        "Invalid css_modules_export_mode: '#{mode}'. " \
        "Valid values are: #{valid_modes.map { |m| "'#{m}'" }.join(', ')}. " \
        "See https://github.com/shakacode/shakapacker/blob/main/docs/css-modules-export-mode.md"
    end

    mode
  end
end

#dataHash[Symbol, untyped]

Returns the raw configuration data hash

Returns:

  • (Hash[Symbol, untyped])


480
481
482
# File 'lib/shakapacker/configuration.rb', line 480

def data
  @data ||= load.freeze
end

#default_javascript_transpilerString

Returns:

  • (String)


496
497
498
499
# File 'lib/shakapacker/configuration.rb', line 496

def default_javascript_transpiler
  # RSpack has built-in SWC support, use it by default
  rspack? ? "swc" : "babel"
end

#defaultsHashWithIndifferentAccess

Returns:

  • (HashWithIndifferentAccess)


758
759
760
761
762
763
764
765
766
767
768
769
770
771
# File 'lib/shakapacker/configuration.rb', line 758

def defaults
  @defaults ||= begin
    path = File.expand_path("../../install/config/shakapacker.yml", __FILE__)
    config = begin
      YAML.load_file(path, aliases: true)
    rescue ArgumentError
      YAML.load_file(path)
    end
    # Load defaults from bundled shakapacker.yml (always has all environments)
    # Note: This differs from load() which reads user's config and may be missing environments
    # Fallback to production ensures staging and other custom envs get production-like defaults
    HashWithIndifferentAccess.new(config[env] || config["production"])
  end
end

#dev_serverHash[String | Symbol, untyped]

Returns the dev server configuration hash

Returns:

  • (Hash[String | Symbol, untyped])


109
110
111
# File 'lib/shakapacker/configuration.rb', line 109

def dev_server
  fetch(:dev_server)
end

#early_hintsBoolean

Returns whether HTTP/2 Early Hints are enabled

Returns:

  • (Boolean)


684
685
686
# File 'lib/shakapacker/configuration.rb', line 684

def early_hints
  fetch(:early_hints)
end

#ensure_consistent_versioning?Boolean

Returns whether consistent versioning check is enabled

Returns:

  • (Boolean)


138
139
140
# File 'lib/shakapacker/configuration.rb', line 138

def ensure_consistent_versioning?
  fetch(:ensure_consistent_versioning)
end

#fetch(key) ⇒ Object

Fetches a configuration value

Parameters:

  • key (Symbol)

Returns:

  • (Object)


647
648
649
# File 'lib/shakapacker/configuration.rb', line 647

def fetch(key)
  data.fetch(key, defaults[key])
end

#integritybool, Hash[String | Symbol, untyped]

Returns whether subresource integrity (SRI) is enabled

Returns:

  • (bool, Hash[String | Symbol, untyped])


674
675
676
# File 'lib/shakapacker/configuration.rb', line 674

def integrity
  fetch(:integrity)
end

#javascript_transpilerString

Returns the JavaScript transpiler to use (babel, swc, or esbuild)

Returns:

  • (String)


373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
# File 'lib/shakapacker/configuration.rb', line 373

def javascript_transpiler
  return @javascript_transpiler if defined?(@javascript_transpiler)

  @javascript_transpiler =
    begin
      javascript_transpiler_configured = config_value_configured?(:javascript_transpiler)
      webpack_loader_configured = config_value_present?(:webpack_loader)

      # Show deprecation warning if using old 'webpack_loader' key
      if webpack_loader_configured && !javascript_transpiler_configured
        $stderr.puts "⚠️  DEPRECATION WARNING: The 'webpack_loader' configuration option is deprecated. Please use 'javascript_transpiler' instead as it better reflects its purpose of configuring JavaScript transpilation regardless of the bundler used."
      end

      # Use explicit config if set, otherwise default based on bundler
      current_javascript_transpiler =
        if javascript_transpiler_configured
          fetch(:javascript_transpiler)
        elsif data.has_key?(:javascript_transpiler)
          defaults[:javascript_transpiler]
        else
          fetch(:javascript_transpiler)
        end

      transpiler =
        if webpack_loader_configured && !javascript_transpiler_configured
          fetch(:webpack_loader) || default_javascript_transpiler
        else
          current_javascript_transpiler || fetch(:webpack_loader) || default_javascript_transpiler
        end

      if !javascript_transpiler_configured &&
          !webpack_loader_configured &&
          implicit_swc_to_babel_fallback?(transpiler)
        $stderr.puts IMPLICIT_SWC_BABEL_FALLBACK_WARNING
        transpiler = "babel"
      end

      # Validate transpiler configuration
      validate_transpiler_configuration(transpiler) unless self.class.installing

      transpiler
    end
end

#loadHash[Symbol, untyped]

Returns:

  • (Hash[Symbol, untyped])


721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
# File 'lib/shakapacker/configuration.rb', line 721

def load
  config = begin
    YAML.load_file(config_path.to_s, aliases: true)
  rescue ArgumentError
    YAML.load_file(config_path.to_s)
  end

  # Try to find environment-specific configuration with fallback
  # Fallback order: requested env → production
  if config[env]
    env_config = config[env]
  elsif config["production"]
    log_fallback(env, "production")
    env_config = config["production"]
  else
    # No suitable configuration found - rely on bundled defaults
    log_fallback(env, "none (will use bundled defaults)")
    env_config = nil
  end

  symbolized_config = env_config&.deep_symbolize_keys || {}

  return symbolized_config
rescue Errno::ENOENT => e
  if self.class.installing
    {}
  else
    raise "Shakapacker configuration file not found #{config_path}. " \
          "Please run rails shakapacker:install " \
          "Error: #{e.message}"
  end
rescue Psych::SyntaxError => e
  raise "YAML syntax error occurred while parsing #{config_path}. " \
        "Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \
        "Error: #{e.message}"
end

#log_fallback(requested_env, fallback_env) ⇒ void

This method returns an undefined value.

Parameters:

  • requested_env (String)
  • fallback_env (String)


779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
# File 'lib/shakapacker/configuration.rb', line 779

def log_fallback(requested_env, fallback_env)
  message = "Shakapacker environment '#{requested_env}' not found in #{config_path}, " \
            "falling back to '#{fallback_env}'"

  # Try to use the logger if available, otherwise fall back to stdout
  begin
    if Shakapacker.respond_to?(:logger) && Shakapacker.logger
      Shakapacker.logger.info(message)
    else
      puts message
    end
  rescue NameError, NoMethodError
    # If logger access fails (e.g., circular dependency in standalone runner context),
    # fall back to stdout so the message still gets displayed
    puts message
  end
end

#manifest_pathPathname

Returns the absolute path to the manifest.json file

Returns:

  • (Pathname)


193
194
195
196
197
198
199
# File 'lib/shakapacker/configuration.rb', line 193

def manifest_path
  if data.has_key?(:manifest_path)
    root_path.join(fetch(:manifest_path))
  else
    public_output_path.join("manifest.json")
  end
end

#nested_entries?Boolean

Returns whether nested entries are enabled

Returns:

  • (Boolean)


129
130
131
# File 'lib/shakapacker/configuration.rb', line 129

def nested_entries?
  fetch(:nested_entries)
end

#precompile_hookString?

Returns the precompile hook command to run after compilation

Returns:

  • (String, nil)


351
352
353
354
355
356
357
358
359
360
# File 'lib/shakapacker/configuration.rb', line 351

def precompile_hook
  hook = fetch(:precompile_hook)
  return nil if hook.nil? || (hook.is_a?(String) && hook.strip.empty?)

  unless hook.is_a?(String)
    raise "Shakapacker configuration error: precompile_hook must be a string, got #{hook.class}"
  end

  hook.strip
end

#private_output_pathPathname?

Returns the absolute path to the private output directory

Returns:

  • (Pathname, nil)


225
226
227
228
229
230
# File 'lib/shakapacker/configuration.rb', line 225

def private_output_path
  private_path = fetch(:private_output_path)
  return nil if private_path.blank?
  validate_output_paths!
  root_path.join(private_path)
end

#public_manifest_pathPathname

Alias for manifest_path

Returns:

  • (Pathname)


205
206
207
# File 'lib/shakapacker/configuration.rb', line 205

def public_manifest_path
  manifest_path
end

#public_output_pathPathname

Returns the absolute path to the public output directory

Returns:

  • (Pathname)


238
239
240
# File 'lib/shakapacker/configuration.rb', line 238

def public_output_path
  public_path.join(fetch(:public_output_path))
end

#public_pathPathname

Returns the absolute path to the public root directory

Returns:

  • (Pathname)


215
216
217
# File 'lib/shakapacker/configuration.rb', line 215

def public_path
  root_path.join(fetch(:public_root_path))
end

#relative_path(path) ⇒ String

Parameters:

  • path (String)

Returns:

  • (String)


773
774
775
776
777
# File 'lib/shakapacker/configuration.rb', line 773

def relative_path(path)
  return ".#{path}" if path.start_with?("/")

  path
end

#resolve_paths_for_comparison[String, String]

Returns:

  • ([String, String])


706
707
708
709
710
711
712
713
714
715
716
717
718
719
# File 'lib/shakapacker/configuration.rb', line 706

def resolve_paths_for_comparison
  private_full_path = root_path.join(fetch(:private_output_path))
  public_full_path = root_path.join(fetch(:public_root_path), fetch(:public_output_path))

  # Create directories if they don't exist (for testing)
  private_full_path.mkpath unless private_full_path.exist?
  public_full_path.mkpath unless public_full_path.exist?

  # Use realpath to resolve symbolic links and relative paths
  [private_full_path.realpath.to_s, public_full_path.realpath.to_s]
rescue Errno::ENOENT
  # If paths don't exist yet, fall back to cleanpath for comparison
  [private_full_path.cleanpath.to_s, public_full_path.cleanpath.to_s]
end

#rspack?Boolean

Returns whether rspack is the configured bundler

Returns:

  • (Boolean)


334
335
336
# File 'lib/shakapacker/configuration.rb', line 334

def rspack?
  assets_bundler == "rspack"
end

#shakapacker_precompile?Boolean

Returns whether Shakapacker should precompile assets

Returns:

  • (Boolean)


150
151
152
153
154
155
156
157
# File 'lib/shakapacker/configuration.rb', line 150

def shakapacker_precompile?
  # ENV of false takes precedence
  return false if %w(no false n f).include?(ENV["SHAKAPACKER_PRECOMPILE"])
  return true if %w(yes true y t).include?(ENV["SHAKAPACKER_PRECOMPILE"])

  return false unless config_path.exist?
  fetch(:shakapacker_precompile)
end

#source_entry_pathPathname

Returns the absolute path to the source entry directory

Returns:

  • (Pathname)


183
184
185
# File 'lib/shakapacker/configuration.rb', line 183

def source_entry_path
  source_path.join(relative_path(fetch(:source_entry_path)))
end

#source_pathPathname

Returns the absolute path to the source directory

Returns:

  • (Pathname)


164
165
166
# File 'lib/shakapacker/configuration.rb', line 164

def source_path
  root_path.join(fetch(:source_path))
end

#validate_output_paths!void

This method returns an undefined value.



689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
# File 'lib/shakapacker/configuration.rb', line 689

def validate_output_paths!
  # Skip validation if already validated to avoid redundant checks
  return if @validated_output_paths
  @validated_output_paths = true

  # Only validate when both paths are configured
  return unless fetch(:private_output_path) && fetch(:public_output_path)

  private_path_str, public_path_str = resolve_paths_for_comparison

  if private_path_str == public_path_str
    raise "Shakapacker configuration error: private_output_path and public_output_path must be different. " \
          "Both paths resolve to '#{private_path_str}'. " \
          "The private_output_path is for server-side bundles (e.g., SSR) that should not be served publicly."
  end
end

#validate_transpiler_configuration(transpiler) ⇒ void

This method returns an undefined value.

Parameters:

  • transpiler (String)


591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
# File 'lib/shakapacker/configuration.rb', line 591

def validate_transpiler_configuration(transpiler)
  return unless ENV["NODE_ENV"] != "test" # Skip validation in test environment

  # Skip validation if transpiler is set to 'none' (custom webpack config)
  return if transpiler == "none"

  all_deps = declared_package_dependencies
  return if all_deps.empty?

  # Check for transpiler mismatch
  has_babel = all_deps.keys.any? { |pkg| pkg.start_with?("@babel/", "babel-") }
  has_swc = all_deps.keys.any? { |pkg| pkg.include?("swc") }
  has_esbuild = all_deps.keys.any? { |pkg| pkg.include?("esbuild") }

  case transpiler
  when "babel"
    if !has_babel && has_swc
      warn_transpiler_mismatch("Babel", "SWC packages found but Babel is configured")
    end
  when "swc"
    if !has_swc && has_babel
      warn_transpiler_mismatch("SWC", "Babel packages found but SWC is configured")
    end
  when "esbuild"
    if !has_esbuild && (has_babel || has_swc)
      other = has_babel ? "Babel" : "SWC"
      warn_transpiler_mismatch("esbuild", "#{other} packages found but esbuild is configured")
    end
  end
end

#warn_transpiler_mismatch(configured, message) ⇒ void

This method returns an undefined value.

Parameters:

  • configured (String)
  • message (String)


622
623
624
625
626
627
628
629
630
631
632
633
634
635
# File 'lib/shakapacker/configuration.rb', line 622

def warn_transpiler_mismatch(configured, message)
  $stderr.puts <<~WARNING
    ⚠️  Transpiler Configuration Mismatch Detected:
       #{message}
       Configured transpiler: #{configured}
    #{'   '}
       This might cause unexpected behavior or build failures.
    #{'   '}
       To fix this:
       1. Run 'rails shakapacker:migrate_to_swc' to migrate to SWC (recommended for 20x faster builds)
       2. Or install the correct packages for #{configured}
       3. Or update your shakapacker.yml to match your installed packages
  WARNING
end

#webpack?Boolean

Returns whether webpack is the configured bundler

Returns:

  • (Boolean)


341
342
343
# File 'lib/shakapacker/configuration.rb', line 341

def webpack?
  assets_bundler == "webpack"
end

#webpack_compile_flagsArray[String]

Returns extra command-line flags passed to the webpack/rspack compile command

Returns:

  • (Array[String])


274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/shakapacker/configuration.rb', line 274

def webpack_compile_flags
  flags = fetch(:webpack_compile_flags)
  return [] if flags.nil?

  valid_flags = flags.is_a?(Array) && flags.all? { |flag| valid_webpack_compile_flag?(flag) }

  unless valid_flags
    disallowed_flags = DISALLOWED_WEBPACK_COMPILE_FLAGS.join(", ")
    raise "Shakapacker configuration error: compile flags (webpack_compile_flags) must be an array of " \
          "non-empty strings and must not include \"--\" or Shakapacker-specific " \
          "wrapper/short-circuit/watch/managed flags (#{disallowed_flags})"
  end

  flags
end

#webpack_compile_output?Boolean

Returns whether webpack/rspack compilation output should be shown

Returns:

  • (Boolean)


267
268
269
# File 'lib/shakapacker/configuration.rb', line 267

def webpack_compile_output?
  fetch(:webpack_compile_output)
end

#webpack_loaderString

Deprecated alias for javascript_transpiler

Returns:

  • (String)


422
423
424
# File 'lib/shakapacker/configuration.rb', line 422

def webpack_loader
  javascript_transpiler
end