lib/sass/compiler/host.rb



# frozen_string_literal: true

require_relative 'host/function_registry'
require_relative 'host/importer_registry'
require_relative 'host/logger_registry'
require_relative 'host/protofier'
require_relative 'host/structifier'

module Sass
  class Compiler
    # The {Host} class.
    #
    # It communicates with {Dispatcher} and handles the host logic.
    class Host
      def initialize(channel)
        @channel = channel
      end

      def compile_request(path:,
                          source:,
                          importer:,
                          load_paths:,
                          syntax:,
                          url:,
                          charset:,
                          source_map:,
                          source_map_include_sources:,
                          style:,
                          functions:,
                          importers:,
                          alert_ascii:,
                          alert_color:,
                          fatal_deprecations:,
                          future_deprecations:,
                          logger:,
                          quiet_deps:,
                          silence_deprecations:,
                          verbose:)
        alert_color = Exception.to_tty? if alert_color.nil?

        @function_registry = FunctionRegistry.new(functions, alert_color:)
        @importer_registry = ImporterRegistry.new(importers, load_paths, alert_color:)
        @logger_registry = LoggerRegistry.new(logger)

        compile_request = EmbeddedProtocol::InboundMessage::CompileRequest.new(
          string: unless source.nil?
                    EmbeddedProtocol::InboundMessage::CompileRequest::StringInput.new(
                      source: source.to_str,
                      url: url&.to_s,
                      syntax: @importer_registry.syntax_to_proto(syntax),
                      importer: (@importer_registry.register(importer) unless importer.nil?)
                    )
                  end,
          path: (File.absolute_path(path) unless path.nil?),
          style: case style&.to_sym
                 when :expanded
                   EmbeddedProtocol::OutputStyle::EXPANDED
                 when :compressed
                   EmbeddedProtocol::OutputStyle::COMPRESSED
                 else
                   raise ArgumentError, 'style must be one of :expanded, :compressed'
                 end,
          charset:,
          source_map:,
          source_map_include_sources:,
          importers: @importer_registry.importers,
          global_functions: @function_registry.global_functions,
          alert_ascii:,
          alert_color:,
          fatal_deprecation: fatal_deprecations.map(&:to_s),
          future_deprecation: future_deprecations.map(&:to_s),
          quiet_deps:,
          silent: logger == Logger.silent,
          silence_deprecation: silence_deprecations.map(&:to_s),
          verbose:
        )

        compile_response = await do
          send_message(compile_request:)
        end

        oneof = compile_response.result
        result = compile_response.public_send(oneof)
        case oneof
        when :failure
          raise CompileError.new(
            result.message,
            result.formatted == '' ? nil : result.formatted,
            result.stack_trace == '' ? nil : result.stack_trace,
            result.span.nil? ? nil : Logger::SourceSpan.new(result.span),
            compile_response.loaded_urls.to_a
          )
        when :success
          CompileResult.new(
            result.css,
            result.source_map == '' ? nil : result.source_map,
            compile_response.loaded_urls.to_a
          )
        else
          raise ArgumentError, "Unknown CompileResponse.result #{result}"
        end
      end

      def version_request
        version_response = await0 do
          send_message0(version_request: EmbeddedProtocol::InboundMessage::VersionRequest.new(
            id:
          ))
        end

        info = [
          version_response.implementation_name,
          version_response.implementation_version,
          '(Sass Compiler)'
        ]

        case version_response.implementation_name
        when 'dart-sass'
          info << (CLI::COMMAND.first == 'node' ? '[JavaScript]' : '[Dart]')
        end

        info
      end

      def compile_response(message)
        @result = message
        @queue.close
      end

      def version_response(message)
        @result = message
        @queue.close
      end

      def error(message)
        case message
        when EmbeddedProtocol::ProtocolError
          raise Errno::EPROTO, message.message
        else
          @error ||= message
          @queue.close
        end
      end

      def log_event(message)
        @logger_registry.log(message)
      end

      def canonicalize_request(message)
        send_message(canonicalize_response: @importer_registry.canonicalize(message))
      end

      def import_request(message)
        send_message(import_response: @importer_registry.import(message))
      end

      def file_import_request(message)
        send_message(file_import_response: @importer_registry.file_import(message))
      end

      def function_call_request(message)
        send_message(function_call_response: @function_registry.function_call(message))
      end

      def receive_proto(proto)
        @queue.push(proto)
      end

      private

      def await0
        listen do
          yield

          @queue.pop
        end
      end

      def await
        listen do
          yield

          while (proto = @queue.pop)
            outbound_message = EmbeddedProtocol::OutboundMessage.decode(proto)
            oneof = outbound_message.message
            message = outbound_message.public_send(oneof)
            public_send(oneof, message)
          end
        rescue Exception => e # rubocop:disable Lint/RescueException
          @stream.error(e)
          raise
        end
      end

      def listen
        @queue = Queue.new
        @stream = @channel.stream(self)

        yield

        raise @error if @error

        @result
      ensure
        @stream&.close
        @queue&.close
      end

      def id
        @stream.id
      end

      def send_message0(...)
        inbound_message = EmbeddedProtocol::InboundMessage.new(...)
        @stream.send_proto(0, EmbeddedProtocol::InboundMessage.encode(inbound_message))
      end

      def send_message(...)
        inbound_message = EmbeddedProtocol::InboundMessage.new(...)
        @stream.send_proto(id, EmbeddedProtocol::InboundMessage.encode(inbound_message))
      end
    end

    private_constant :Host
  end
end