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)


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

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)


313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'lib/shakapacker/configuration.rb', line 313

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

  resolved = ENV["SHAKAPACKER_ASSETS_BUNDLER"] || fetch(:assets_bundler) || "webpack"

  # The legacy 'bundler' key is never read, so warn that its value is ignored
  if data.has_key?(:bundler) && !data.has_key?(:assets_bundler)
    legacy_value = data[:bundler].to_s.strip
    described_value = legacy_value.empty? ? "empty" : legacy_value
    # An empty SHAKAPACKER_ASSETS_BUNDLER resolves to an empty string; name that
    # state rather than printing "using ''". Doctor reports it as its own issue.
    described_resolved = resolved.to_s.empty? ? "no bundler (SHAKAPACKER_ASSETS_BUNDLER is set but empty)" : "'#{resolved}'"
    $stderr.puts "⚠️  DEPRECATION WARNING: The 'bundler' configuration option is no longer supported and its value (#{described_value}) is IGNORED. Shakapacker is using #{described_resolved}. Set 'assets_bundler' in your shakapacker.yml instead — it was renamed to avoid confusion with Ruby's Bundler gem manager."
  end

  resolved
end

#assets_bundler_config_pathString

Returns the path to the bundler configuration directory

Returns:

  • (String)


467
468
469
470
471
472
473
# File 'lib/shakapacker/configuration.rb', line 467

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)


337
338
339
# File 'lib/shakapacker/configuration.rb', line 337

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



444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
# File 'lib/shakapacker/configuration.rb', line 444

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


490
491
492
# File 'lib/shakapacker/configuration.rb', line 490

def data
  @data ||= load.freeze
end

#default_javascript_transpilerString

Returns:

  • (String)


506
507
508
509
# File 'lib/shakapacker/configuration.rb', line 506

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

#defaultsHashWithIndifferentAccess

Returns:

  • (HashWithIndifferentAccess)


768
769
770
771
772
773
774
775
776
777
778
779
780
781
# File 'lib/shakapacker/configuration.rb', line 768

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)


694
695
696
# File 'lib/shakapacker/configuration.rb', line 694

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)


657
658
659
# File 'lib/shakapacker/configuration.rb', line 657

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


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

def integrity
  fetch(:integrity)
end

#javascript_transpilerString

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

Returns:

  • (String)


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
416
417
418
419
420
421
422
423
424
425
# File 'lib/shakapacker/configuration.rb', line 383

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


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
757
758
759
760
761
762
763
764
765
766
# File 'lib/shakapacker/configuration.rb', line 731

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)


789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
# File 'lib/shakapacker/configuration.rb', line 789

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)


361
362
363
364
365
366
367
368
369
370
# File 'lib/shakapacker/configuration.rb', line 361

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)


783
784
785
786
787
# File 'lib/shakapacker/configuration.rb', line 783

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

  path
end

#resolve_paths_for_comparison[String, String]

Returns:

  • ([String, String])


716
717
718
719
720
721
722
723
724
725
726
727
728
729
# File 'lib/shakapacker/configuration.rb', line 716

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)


344
345
346
# File 'lib/shakapacker/configuration.rb', line 344

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.



699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
# File 'lib/shakapacker/configuration.rb', line 699

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)


601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
# File 'lib/shakapacker/configuration.rb', line 601

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)


632
633
634
635
636
637
638
639
640
641
642
643
644
645
# File 'lib/shakapacker/configuration.rb', line 632

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)


351
352
353
# File 'lib/shakapacker/configuration.rb', line 351

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)


432
433
434
# File 'lib/shakapacker/configuration.rb', line 432

def webpack_loader
  javascript_transpiler
end