lib/svelte_on_rails/configuration.rb



require "yaml"

module SvelteOnRails
  class Configuration

    def self.instance
      @instance ||= new
    end

    attr_accessor :configs

    def initialize

      @configs = redis_cache_store_configs

      return unless defined?(Rails.root)

      config_path = Rails.root.join("config", "svelte_on_rails.yml")
      return unless File.exist?(config_path)

      environments = Dir[Rails.root.join('config', 'environments', '*.rb')]
                       .map { |file| File.basename(file, '.rb') }

      configs_base = YAML.load_file(config_path)

      @configs = @configs.deep_merge(configs_base.reject { |k, _| environments.include?(k) })

      @configs = @configs.deep_merge(configs_base[Rails.env] || {})

      @configs = @configs.symbolize_keys

      if @configs[:redis_cache_store]
        if @configs[:redis_cache_store]['expires_in'].is_a?(String)
          @configs[:redis_cache_store]['expires_in'] = parse_duration(
            @configs[:redis_cache_store]['expires_in']
          )
        end
      end

      if defined? Redis
        @redis_instance = Redis.new(url: redis_cache_store[:url])
      end

    end

    def redis_cache_store
      (@configs[:redis_cache_store] || {}).with_indifferent_access
    end

    def redis_instance
      @redis_instance
    end

    def watch_changes?
      @configs[:watch_changes] == true
    end

    def rails_root(root_url = nil)
      root_url || Rails.root
    end

    def frontend_folder
      Pathname.new(@configs[:frontend_folder].to_s)
    end

    def frontend_folder_full
      rails_root.join(@configs[:frontend_folder].to_s)
    end

    def components_folder
      Pathname.new(@configs[:components_folder].to_s)
    end

    def components_folder_full
      Pathname.new(frontend_folder_full).join(components_folder.to_s)
    end

    def ssr_manifest
      file = rails_root.join('public', 'vite-ssr', 'manifest.json')

      if watch_changes?
        begin
          JSON.parse(File.read(file))
        rescue
          raise "ERROR: Could not read public/vite-ssr/manifest.json."
        end
      else
        @manifest ||= JSON.parse(File.read(file))
      end

    end

    def ssr
      rss = @configs[:ssr]
      if rss == false || rss == :auto
        rss
      else
        true
      end
    end

    def non_ssr_request_header
      rss = @configs[:non_ssr_request_header]
      if rss.present?
        rss
      else
        'X-Turbo-Request-ID'
      end
    end

    def client_dist_folder(app_root = nil)
      if Rails.env.development?
        rails_root(app_root).join('public', 'vite')
      else
        rails_root(app_root).join('public', 'vite')
      end
    end

    def ssr_dist_folder(app_root = nil)
      rails_root(app_root).join('public', 'vite-ssr')
    end

    def system_type
      :vite_rails
    end

    def node_bin_path
      @node_bin_path ||= begin
                           n = ENV['SVELTE_ON_RAILS_NODE_BIN'] || 'node'

                           if n == 'node'
                             nvm_installed = false
                             nvm_dir = ENV['NVM_DIR'] || File.expand_path('~/.nvm')
                             nvm_installed ||= File.exist?("#{nvm_dir}/nvm.sh")

                             if nvm_installed
                               # Check for .nvmrc in the project root
                               project_root = rails_root || Dir.pwd
                               nvmrc_path = File.join(project_root, '.nvmrc')
                               use_nvmrc = File.exist?(nvmrc_path)

                               if use_nvmrc
                                 # Read the version from .nvmrc
                                 node_version = File.read(nvmrc_path).strip
                                 # Ensure NVM is sourced and use the version from .nvmrc
                                 command = "[ -s \"#{nvm_dir}/nvm.sh\" ] && . \"#{nvm_dir}/nvm.sh\" && nvm use #{node_version} > /dev/null 2>&1 && nvm which #{node_version}"
                               else
                                 # Fallback to current NVM version
                                 command = "[ -s \"#{nvm_dir}/nvm.sh\" ] && . \"#{nvm_dir}/nvm.sh\" && nvm which current"
                               end

                               node_path, status = Open3.capture2("bash -lc '#{command}'")

                               # Only update n if the command succeeded and output is non-empty
                               n = node_path.strip if status.success? && !node_path.strip.empty?
                             end
                           end

                           # Validate node_bin
                           unless n && !n.empty? && system("#{n} --version > /dev/null 2>&1")
                             raise "Node.js not found at '#{n || 'unknown'}'. Please configure SVELTE_ON_RAILS_NODE_BIN (e.g., to ~/.nvm/versions/node/vX.Y.Z/bin/node) or ensure 'node' is in the PATH. If using NVM, run `nvm alias default <version>` to set a default version or ensure a valid .nvmrc file is present."
                           end

                           n
                         rescue StandardError => e
                           raise "Failed to detect Node.js binary: «#{e.message}». Ensure Node.js is installed and accessible, or set SVELTE_ON_RAILS_NODE_BIN. If using .nvmrc, ensure the specified version is installed via NVM."
                         end
    end

    private

    def redis_cache_store_configs
      if defined?(Rails.application) && Rails.application.config.cache_store.is_a?(Array) && Rails.application.config.cache_store.first == :redis_cache_store
        { 'redis_cache_store' => Rails.application.config.cache_store.second.stringify_keys }
      else
        {}
      end
    end

    def parse_duration(string)
      # Extract number and unit
      match = string.match(/^(\d+)\.(\w+)$/)
      raise ArgumentError, "Invalid duration format: #{string}" unless match

      number, unit = match[1].to_i, match[2]
      # Ensure the unit is a valid ActiveSupport duration method
      valid_units = %w[seconds minutes hours days weeks months years]
      raise ArgumentError, "Invalid unit: #{unit} (valid: #{valid_units})" unless valid_units.include?(unit)
      ActiveSupport::Duration.build(number.send(unit))
    end

  end
end