This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
class Compiler
|
||||
# The {Channel} class.
|
||||
#
|
||||
# It manages the lifecycle of {Dispatcher}.
|
||||
class Channel
|
||||
def initialize(*args, **kwargs, &block)
|
||||
@args = args
|
||||
@kwargs = kwargs
|
||||
@block = block
|
||||
@dispatcher = Dispatcher.new(*@args, **@kwargs, &@block)
|
||||
@mutex = Mutex.new
|
||||
end
|
||||
|
||||
def close
|
||||
@mutex.synchronize do
|
||||
unless @dispatcher.nil?
|
||||
@dispatcher.close
|
||||
@dispatcher = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def closed?
|
||||
@mutex.synchronize do
|
||||
@dispatcher.nil?
|
||||
end
|
||||
end
|
||||
|
||||
def stream(session)
|
||||
@mutex.synchronize do
|
||||
raise IOError, 'closed compiler' if @dispatcher.nil?
|
||||
|
||||
Stream.new(@dispatcher, session)
|
||||
rescue Errno::EBUSY
|
||||
@dispatcher = Dispatcher.new(*@args, **@kwargs, &@block)
|
||||
Stream.new(@dispatcher, session)
|
||||
end
|
||||
end
|
||||
|
||||
# The {Stream} between {Dispatcher} and {Session}.
|
||||
class Stream
|
||||
attr_reader :id
|
||||
|
||||
def initialize(dispatcher, session)
|
||||
@dispatcher = dispatcher
|
||||
@id = @dispatcher.subscribe(session)
|
||||
end
|
||||
|
||||
def close
|
||||
@dispatcher.unsubscribe(@id)
|
||||
end
|
||||
|
||||
def error(...)
|
||||
@dispatcher.error(...)
|
||||
end
|
||||
|
||||
def send_proto(...)
|
||||
@dispatcher.send_proto(...)
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :Stream
|
||||
end
|
||||
|
||||
private_constant :Channel
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,90 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'open3'
|
||||
|
||||
require 'sass/cli'
|
||||
|
||||
module Sass
|
||||
class Compiler
|
||||
# The stdio based {Connection} between the {Dispatcher} and the compiler.
|
||||
#
|
||||
# It runs the `sass --embedded` command.
|
||||
class Connection
|
||||
def initialize
|
||||
@mutex = Mutex.new
|
||||
@stdin, @stdout, @stderr, @wait_thread = Open3.popen3(*CLI::COMMAND, '--embedded', chdir: __dir__)
|
||||
|
||||
@stdin.binmode
|
||||
|
||||
# # https://dart.dev/tools/dart-devtools
|
||||
# if %w[dart dartvm].include?(File.basename(CLI::COMMAND.first, '.exe')) &&
|
||||
# %w[--enable-vm-service --observe].intersect?(CLI::COMMAND.map { |argument| argument.partition('=').first })
|
||||
# Kernel.warn(@stdout.readline, uplevel: 0)
|
||||
# Kernel.warn(@stdout.readline, uplevel: 0)
|
||||
# end
|
||||
|
||||
@stdout.binmode
|
||||
|
||||
@wait_thread.name = "sass-embedded-process-waiter-#{id}"
|
||||
end
|
||||
|
||||
def id
|
||||
@wait_thread.pid
|
||||
end
|
||||
|
||||
def listen(dispatcher)
|
||||
Thread.new do
|
||||
Thread.current.name = "sass-embedded-process-stdout-poller-#{id}"
|
||||
loop do
|
||||
length = Varint.read(@stdout)
|
||||
id = Varint.read(@stdout)
|
||||
proto = @stdout.read(length - Varint.length(id))
|
||||
dispatcher.receive_proto(id, proto)
|
||||
end
|
||||
rescue IOError, Errno::EBADF, Errno::EPROTO => e
|
||||
dispatcher.error(e)
|
||||
@mutex.synchronize do
|
||||
@stdout.close
|
||||
end
|
||||
end
|
||||
|
||||
Thread.new do
|
||||
Thread.current.name = "sass-embedded-process-stderr-poller-#{id}"
|
||||
loop do
|
||||
Kernel.warn(@stderr.readline, uplevel: 0)
|
||||
end
|
||||
rescue IOError, Errno::EBADF
|
||||
@mutex.synchronize do
|
||||
@stderr.close
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def close
|
||||
@mutex.synchronize do
|
||||
@stdin.close
|
||||
@wait_thread.join
|
||||
@stdout.close
|
||||
@stderr.close
|
||||
end
|
||||
end
|
||||
|
||||
def closed?
|
||||
@mutex.synchronize do
|
||||
@stdin.closed? && !@wait_thread.alive?
|
||||
end
|
||||
end
|
||||
|
||||
def write(id, proto)
|
||||
buffer = []
|
||||
Varint.write(buffer, Varint.length(id) + proto.length)
|
||||
Varint.write(buffer, id)
|
||||
@mutex.synchronize do
|
||||
@stdin.write(buffer.pack('C*'), proto)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :Connection
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,142 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
class Compiler
|
||||
# The {Dispatcher} class.
|
||||
#
|
||||
# It dispatches messages between multiple instances of {Session} and a single {Connection} to the compiler.
|
||||
class Dispatcher
|
||||
def initialize(idle_timeout: 0)
|
||||
@id = 1
|
||||
@observers = {}.compare_by_identity
|
||||
@mutex = Mutex.new
|
||||
@connection = Connection.new
|
||||
@connection.listen(self)
|
||||
ForkTracker.add(self)
|
||||
|
||||
return unless idle_timeout.positive?
|
||||
|
||||
@last_accessed_time = current_time
|
||||
Thread.new do
|
||||
Thread.current.name = "sass-embedded-connection-reaper-#{@connection.id}"
|
||||
duration = idle_timeout
|
||||
loop do
|
||||
sleep(duration.negative? ? idle_timeout : duration)
|
||||
break if @mutex.synchronize do
|
||||
raise Errno::EBUSY if _closed?
|
||||
|
||||
duration = idle_timeout - (current_time - @last_accessed_time)
|
||||
duration.negative? && _idle? && _close
|
||||
end
|
||||
end
|
||||
close
|
||||
rescue Errno::EBUSY
|
||||
# do nothing
|
||||
end
|
||||
end
|
||||
|
||||
def subscribe(observer)
|
||||
@mutex.synchronize do
|
||||
raise Errno::EBUSY if _closed?
|
||||
|
||||
id = @id
|
||||
@id = id.next
|
||||
@observers[id] = observer
|
||||
id
|
||||
end
|
||||
end
|
||||
|
||||
def unsubscribe(id)
|
||||
@mutex.synchronize do
|
||||
@observers.delete(id)
|
||||
|
||||
return unless @observers.empty?
|
||||
|
||||
if _closed?
|
||||
Thread.new do
|
||||
close
|
||||
end
|
||||
else
|
||||
_idle
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def close
|
||||
@mutex.synchronize do
|
||||
_close
|
||||
end
|
||||
@connection.close
|
||||
ForkTracker.delete(self)
|
||||
end
|
||||
|
||||
def closed?
|
||||
@connection.closed?
|
||||
end
|
||||
|
||||
def error(error)
|
||||
observers = @mutex.synchronize do
|
||||
_close
|
||||
@observers.values
|
||||
end
|
||||
|
||||
if observers.empty?
|
||||
close
|
||||
else
|
||||
observers.each do |observer|
|
||||
observer.error(error)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def receive_proto(id, proto)
|
||||
case id
|
||||
when 1...0xffffffff
|
||||
@mutex.synchronize { @observers[id] }&.receive_proto(proto)
|
||||
when 0
|
||||
outbound_message = EmbeddedProtocol::OutboundMessage.decode(proto)
|
||||
oneof = outbound_message.message
|
||||
message = outbound_message.public_send(oneof)
|
||||
@mutex.synchronize { @observers[message.id] }&.public_send(oneof, message)
|
||||
when 0xffffffff
|
||||
outbound_message = EmbeddedProtocol::OutboundMessage.decode(proto)
|
||||
oneof = outbound_message.message
|
||||
message = outbound_message.public_send(oneof)
|
||||
raise Errno::EPROTO, message.message
|
||||
else
|
||||
raise Errno::EPROTO
|
||||
end
|
||||
end
|
||||
|
||||
def send_proto(...)
|
||||
@connection.write(...)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def current_time
|
||||
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||
end
|
||||
|
||||
def _close
|
||||
@id = 0xffffffff
|
||||
end
|
||||
|
||||
def _closed?
|
||||
@id == 0xffffffff
|
||||
end
|
||||
|
||||
def _idle
|
||||
@last_accessed_time = current_time if defined?(@last_accessed_time)
|
||||
|
||||
@id = 1
|
||||
end
|
||||
|
||||
def _idle?
|
||||
@id == 1
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :Dispatcher
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,233 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'session/function_registry'
|
||||
require_relative 'session/importer_registry'
|
||||
require_relative 'session/logger_registry'
|
||||
require_relative 'session/path'
|
||||
require_relative 'session/protofier'
|
||||
require_relative 'session/stack_trace'
|
||||
require_relative 'session/struct'
|
||||
|
||||
module Sass
|
||||
class Compiler
|
||||
# The {Session} class.
|
||||
#
|
||||
# It communicates with {Dispatcher} and handles the host logic.
|
||||
class Session
|
||||
attr_writer :backtrace
|
||||
|
||||
def initialize(channel)
|
||||
@channel = channel
|
||||
@backtrace = nil
|
||||
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, session: self)
|
||||
@importer_registry = ImporterRegistry.new(importers, load_paths, session: self)
|
||||
@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
|
||||
compile_error = CompileError.new(
|
||||
result.message,
|
||||
result.formatted == '' ? nil : StackTrace.pretty_formatted!(+result.formatted, result.stack_trace),
|
||||
result.stack_trace == '' ? nil : result.stack_trace,
|
||||
result.span.nil? ? nil : Logger::SourceSpan.new(result.span),
|
||||
compile_response.loaded_urls.to_a
|
||||
)
|
||||
compile_error.set_backtrace(@backtrace) unless @backtrace.nil?
|
||||
raise compile_error
|
||||
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 << (File.basename(CLI::COMMAND.first, '.exe') == '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 :Session
|
||||
end
|
||||
end
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
class Compiler
|
||||
class Session
|
||||
# The {FunctionRegistry} class.
|
||||
#
|
||||
# It stores sass custom functions and handles function calls.
|
||||
class FunctionRegistry
|
||||
attr_reader :compile_context, :global_functions
|
||||
|
||||
def initialize(functions, session:)
|
||||
@compile_context = Object.new
|
||||
@global_functions = functions.keys.map!(&:to_s)
|
||||
@functions_by_name = functions.transform_keys do |signature|
|
||||
signature = signature.to_s
|
||||
index = signature.index('(')
|
||||
if index
|
||||
signature.slice(0, index)
|
||||
else
|
||||
signature
|
||||
end
|
||||
end
|
||||
|
||||
@id = 0
|
||||
@functions_by_id = {}.compare_by_identity
|
||||
@ids_by_function = {}.compare_by_identity
|
||||
|
||||
@session = session
|
||||
end
|
||||
|
||||
def register(function)
|
||||
@ids_by_function.fetch(function) do |fn|
|
||||
id = @id
|
||||
@id = id.next
|
||||
|
||||
@functions_by_id[id] = fn
|
||||
@ids_by_function[fn] = id
|
||||
end
|
||||
end
|
||||
|
||||
def function_call(function_call_request)
|
||||
oneof = function_call_request.identifier
|
||||
identifier = function_call_request.public_send(oneof)
|
||||
function = case oneof
|
||||
when :name
|
||||
@functions_by_name[identifier]
|
||||
when :function_id
|
||||
@functions_by_id[identifier]
|
||||
else
|
||||
raise ArgumentError, "Unknown FunctionCallRequest.identifier #{identifier}"
|
||||
end
|
||||
|
||||
arguments = function_call_request.arguments.map do |argument|
|
||||
protofier.from_proto(argument)
|
||||
end
|
||||
|
||||
success = protofier.to_proto(function.call(arguments))
|
||||
accessed_argument_lists = arguments.filter_map do |argument|
|
||||
if argument.is_a?(Sass::Value::ArgumentList) && argument.instance_variable_get(:@keywords_accessed)
|
||||
argument.instance_variable_get(:@id)
|
||||
end
|
||||
end
|
||||
|
||||
EmbeddedProtocol::InboundMessage::FunctionCallResponse.new(
|
||||
id: function_call_request.id,
|
||||
success:,
|
||||
accessed_argument_lists:
|
||||
)
|
||||
rescue StandardError => e
|
||||
@session.backtrace = e.backtrace
|
||||
EmbeddedProtocol::InboundMessage::FunctionCallResponse.new(
|
||||
id: function_call_request.id,
|
||||
error: e.detailed_message(highlight: false)
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def protofier
|
||||
@protofier ||= Protofier.new(self)
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :FunctionRegistry
|
||||
end
|
||||
end
|
||||
end
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
class Compiler
|
||||
class Session
|
||||
# The {ImporterRegistry} class.
|
||||
#
|
||||
# It stores importers and handles import requests.
|
||||
class ImporterRegistry
|
||||
attr_reader :importers
|
||||
|
||||
def initialize(importers, load_paths, session:)
|
||||
@id = 0
|
||||
@importers_by_id = {}.compare_by_identity
|
||||
@importers = importers
|
||||
.map { |importer| register(importer) }
|
||||
.concat(
|
||||
load_paths.map do |load_path|
|
||||
EmbeddedProtocol::InboundMessage::CompileRequest::Importer.new(
|
||||
path: File.absolute_path(load_path)
|
||||
)
|
||||
end
|
||||
)
|
||||
|
||||
@session = session
|
||||
end
|
||||
|
||||
IMPORTER_ATTRS = %i[non_canonical_scheme].freeze
|
||||
|
||||
IMPORTER_METHODS = %i[canonicalize load find_file_url].freeze
|
||||
|
||||
private_constant :IMPORTER_ATTRS, :IMPORTER_METHODS
|
||||
|
||||
def register(importer)
|
||||
if importer.is_a?(Sass::NodePackageImporter)
|
||||
EmbeddedProtocol::InboundMessage::CompileRequest::Importer.new(
|
||||
node_package_importer: EmbeddedProtocol::NodePackageImporter.new(
|
||||
entry_point_directory: importer.instance_variable_get(:@entry_point_directory)
|
||||
)
|
||||
)
|
||||
else
|
||||
importer = Struct.new(importer, attrs: IMPORTER_ATTRS, methods: IMPORTER_METHODS) if importer.is_a?(::Hash)
|
||||
|
||||
is_importer = importer.respond_to?(:canonicalize) && importer.respond_to?(:load)
|
||||
is_file_importer = importer.respond_to?(:find_file_url)
|
||||
|
||||
raise ArgumentError, 'importer must be an Importer or a FileImporter' if is_importer == is_file_importer
|
||||
|
||||
id = @id
|
||||
@id = id.next
|
||||
|
||||
@importers_by_id[id] = importer
|
||||
if is_importer
|
||||
EmbeddedProtocol::InboundMessage::CompileRequest::Importer.new(
|
||||
importer_id: id,
|
||||
non_canonical_scheme: if importer.respond_to?(:non_canonical_scheme)
|
||||
Array(importer.non_canonical_scheme)
|
||||
else
|
||||
[]
|
||||
end
|
||||
)
|
||||
else
|
||||
EmbeddedProtocol::InboundMessage::CompileRequest::Importer.new(
|
||||
file_importer_id: id
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def canonicalize(canonicalize_request)
|
||||
importer = @importers_by_id[canonicalize_request.importer_id]
|
||||
canonicalize_context = CanonicalizeContext.new(canonicalize_request)
|
||||
url = importer.canonicalize(canonicalize_request.url,
|
||||
canonicalize_context)&.to_s
|
||||
|
||||
EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
|
||||
id: canonicalize_request.id,
|
||||
url:,
|
||||
containing_url_unused: canonicalize_context.instance_variable_get(:@containing_url_unused)
|
||||
)
|
||||
rescue StandardError => e
|
||||
@session.backtrace = e.backtrace
|
||||
EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
|
||||
id: canonicalize_request.id,
|
||||
error: e.detailed_message(highlight: false)
|
||||
)
|
||||
end
|
||||
|
||||
IMPORTER_RESULT_ATTRS = %i[contents syntax source_map_url].freeze
|
||||
|
||||
private_constant :IMPORTER_RESULT_ATTRS
|
||||
|
||||
def import(import_request)
|
||||
importer = @importers_by_id[import_request.importer_id]
|
||||
importer_result = importer.load(import_request.url)
|
||||
importer_result = Struct.new(importer_result, attrs: IMPORTER_RESULT_ATTRS) if importer_result.is_a?(::Hash)
|
||||
|
||||
EmbeddedProtocol::InboundMessage::ImportResponse.new(
|
||||
id: import_request.id,
|
||||
success: EmbeddedProtocol::InboundMessage::ImportResponse::ImportSuccess.new(
|
||||
contents: importer_result.contents.to_str,
|
||||
syntax: syntax_to_proto(importer_result.syntax),
|
||||
source_map_url: (importer_result.source_map_url&.to_s if importer_result.respond_to?(:source_map_url))
|
||||
)
|
||||
)
|
||||
rescue StandardError => e
|
||||
@session.backtrace = e.backtrace
|
||||
EmbeddedProtocol::InboundMessage::ImportResponse.new(
|
||||
id: import_request.id,
|
||||
error: e.detailed_message(highlight: false)
|
||||
)
|
||||
end
|
||||
|
||||
def file_import(file_import_request)
|
||||
importer = @importers_by_id[file_import_request.importer_id]
|
||||
canonicalize_context = CanonicalizeContext.new(file_import_request)
|
||||
file_url = importer.find_file_url(file_import_request.url,
|
||||
canonicalize_context)&.to_s
|
||||
|
||||
EmbeddedProtocol::InboundMessage::FileImportResponse.new(
|
||||
id: file_import_request.id,
|
||||
file_url:,
|
||||
containing_url_unused: canonicalize_context.instance_variable_get(:@containing_url_unused)
|
||||
)
|
||||
rescue StandardError => e
|
||||
@session.backtrace = e.backtrace
|
||||
EmbeddedProtocol::InboundMessage::FileImportResponse.new(
|
||||
id: file_import_request.id,
|
||||
error: e.detailed_message(highlight: false)
|
||||
)
|
||||
end
|
||||
|
||||
def syntax_to_proto(syntax)
|
||||
case syntax&.to_sym
|
||||
when :scss
|
||||
EmbeddedProtocol::Syntax::SCSS
|
||||
when :indented
|
||||
EmbeddedProtocol::Syntax::INDENTED
|
||||
when :css
|
||||
EmbeddedProtocol::Syntax::CSS
|
||||
else
|
||||
raise ArgumentError, 'syntax must be one of :scss, :indented, :css'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :ImporterRegistry
|
||||
end
|
||||
end
|
||||
end
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
class Compiler
|
||||
class Session
|
||||
# The {LoggerRegistry} class.
|
||||
#
|
||||
# It stores logger and handles log events.
|
||||
class LoggerRegistry
|
||||
LOGGER_METHODS = %i[debug warn].freeze
|
||||
|
||||
private_constant :LOGGER_METHODS
|
||||
|
||||
def initialize(logger)
|
||||
logger = Struct.new(logger, methods: LOGGER_METHODS) if logger.is_a?(::Hash)
|
||||
@logger = logger
|
||||
@logger_respond_to_debug = logger.respond_to?(:debug)
|
||||
@logger_respond_to_warn = logger.respond_to?(:warn)
|
||||
end
|
||||
|
||||
def log(event)
|
||||
case event.type
|
||||
when :DEBUG
|
||||
if @logger_respond_to_debug
|
||||
@logger.debug(event.message, DebugContext.new(event))
|
||||
else
|
||||
Kernel.warn(Path.pretty_formatted!(+event.formatted, event.span.url))
|
||||
end
|
||||
when :DEPRECATION_WARNING, :WARNING
|
||||
if @logger_respond_to_warn
|
||||
@logger.warn(event.message, WarnContext.new(event))
|
||||
else
|
||||
Kernel.warn(StackTrace.pretty_formatted!(+event.formatted, event.stack_trace))
|
||||
end
|
||||
else
|
||||
raise ArgumentError, "Unknown LogEvent.type #{event.type}"
|
||||
end
|
||||
end
|
||||
|
||||
# Contextual information passed to `debug`.
|
||||
class DebugContext
|
||||
# @return [Logger::SourceSpan, nil]
|
||||
attr_reader :span
|
||||
|
||||
def initialize(event)
|
||||
@span = event.span.nil? ? nil : Logger::SourceSpan.new(event.span)
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :DebugContext
|
||||
|
||||
# Contextual information passed to `warn`.
|
||||
class WarnContext < DebugContext
|
||||
# @return [Boolean]
|
||||
attr_reader :deprecation
|
||||
|
||||
# @return [String, nil]
|
||||
attr_reader :deprecation_type
|
||||
|
||||
# @return [String]
|
||||
attr_reader :stack
|
||||
|
||||
def initialize(event)
|
||||
super
|
||||
@deprecation = event.type == :DEPRECATION_WARNING
|
||||
@deprecation_type = (event.deprecation_type if @deprecation)
|
||||
@stack = event.stack_trace
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :WarnContext
|
||||
end
|
||||
|
||||
private_constant :LoggerRegistry
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,34 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
class Compiler
|
||||
class Session
|
||||
# @see https://pub.dev/documentation/path/latest/path/
|
||||
module Path
|
||||
module_function
|
||||
|
||||
# @see https://pub.dev/documentation/path/latest/path/Context/prettyUri.html
|
||||
def pretty_uri(uri)
|
||||
return uri unless uri&.start_with?('file:')
|
||||
|
||||
absolute_path = Uri.file_uri_to_path(uri)
|
||||
relative_path = Uri.decode_uri_component(Uri.relative(uri, Uri.pwd))
|
||||
relative_path.count('/') > absolute_path.count('/') ? absolute_path : relative_path
|
||||
end
|
||||
|
||||
def pretty_formatted!(formatted, uri)
|
||||
index = formatted.index(uri)
|
||||
return formatted unless index
|
||||
|
||||
replacement = pretty_uri(uri)
|
||||
return formatted if uri == replacement
|
||||
|
||||
formatted[index, uri.length] = replacement
|
||||
formatted
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :Path
|
||||
end
|
||||
end
|
||||
end
|
||||
+376
@@ -0,0 +1,376 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
class Compiler
|
||||
class Session
|
||||
# The {Protofier} class.
|
||||
#
|
||||
# It converts Pure Ruby types and Protobuf Ruby types.
|
||||
class Protofier
|
||||
def initialize(function_registry)
|
||||
@function_registry = function_registry
|
||||
end
|
||||
|
||||
def to_proto(obj)
|
||||
case obj
|
||||
when Sass::Value::String
|
||||
EmbeddedProtocol::Value.new(
|
||||
string: EmbeddedProtocol::Value::String.new(
|
||||
text: obj.text.to_str,
|
||||
quoted: obj.quoted?
|
||||
)
|
||||
)
|
||||
when Sass::Value::Number
|
||||
EmbeddedProtocol::Value.new(
|
||||
number: Number.to_proto(obj)
|
||||
)
|
||||
when Sass::Value::Color
|
||||
EmbeddedProtocol::Value.new(
|
||||
color: EmbeddedProtocol::Value::Color.new(
|
||||
channel1: obj.send(:channel0_or_nil),
|
||||
channel2: obj.send(:channel1_or_nil),
|
||||
channel3: obj.send(:channel2_or_nil),
|
||||
alpha: obj.send(:alpha_or_nil),
|
||||
space: obj.space
|
||||
)
|
||||
)
|
||||
when Sass::Value::ArgumentList
|
||||
if obj.instance_variable_get(:@compile_context) == @function_registry.compile_context
|
||||
EmbeddedProtocol::Value.new(
|
||||
argument_list: EmbeddedProtocol::Value::ArgumentList.new(
|
||||
id: obj.instance_variable_get(:@id)
|
||||
)
|
||||
)
|
||||
else
|
||||
EmbeddedProtocol::Value.new(
|
||||
argument_list: EmbeddedProtocol::Value::ArgumentList.new(
|
||||
contents: obj.to_a.map { |element| to_proto(element) },
|
||||
keywords: obj.keywords.each_with_object({}) { |(key, value), hash| hash[key.to_s] = to_proto(value) },
|
||||
separator: ListSeparator.to_proto(obj.separator)
|
||||
)
|
||||
)
|
||||
end
|
||||
when Sass::Value::List
|
||||
EmbeddedProtocol::Value.new(
|
||||
list: EmbeddedProtocol::Value::List.new(
|
||||
contents: obj.to_a.map { |element| to_proto(element) },
|
||||
separator: ListSeparator.to_proto(obj.separator),
|
||||
has_brackets: obj.bracketed?
|
||||
)
|
||||
)
|
||||
when Sass::Value::Map
|
||||
EmbeddedProtocol::Value.new(
|
||||
map: EmbeddedProtocol::Value::Map.new(
|
||||
entries: obj.contents.map do |key, value|
|
||||
EmbeddedProtocol::Value::Map::Entry.new(
|
||||
key: to_proto(key),
|
||||
value: to_proto(value)
|
||||
)
|
||||
end
|
||||
)
|
||||
)
|
||||
when Sass::Value::Function
|
||||
if obj.instance_variable_defined?(:@id)
|
||||
EmbeddedProtocol::Value.new(
|
||||
compiler_function: EmbeddedProtocol::Value::CompilerFunction.new(
|
||||
id: assert_compiler_value(obj).instance_variable_get(:@id)
|
||||
)
|
||||
)
|
||||
else
|
||||
EmbeddedProtocol::Value.new(
|
||||
host_function: EmbeddedProtocol::Value::HostFunction.new(
|
||||
id: @function_registry.register(obj.callback),
|
||||
signature: obj.signature
|
||||
)
|
||||
)
|
||||
end
|
||||
when Sass::Value::Mixin
|
||||
EmbeddedProtocol::Value.new(
|
||||
compiler_mixin: EmbeddedProtocol::Value::CompilerMixin.new(
|
||||
id: assert_compiler_value(obj).instance_variable_get(:@id)
|
||||
)
|
||||
)
|
||||
when Sass::Value::Calculation
|
||||
EmbeddedProtocol::Value.new(
|
||||
calculation: Calculation.to_proto(obj)
|
||||
)
|
||||
when Sass::Value::Boolean
|
||||
EmbeddedProtocol::Value.new(
|
||||
singleton: obj.value ? :TRUE : :FALSE
|
||||
)
|
||||
when Sass::Value::Null
|
||||
EmbeddedProtocol::Value.new(
|
||||
singleton: :NULL
|
||||
)
|
||||
else
|
||||
raise Sass::ScriptError, "Unknown Sass::Value #{obj}"
|
||||
end
|
||||
end
|
||||
|
||||
def from_proto(proto)
|
||||
oneof = proto.value
|
||||
obj = proto.public_send(oneof)
|
||||
case oneof
|
||||
when :string
|
||||
Sass::Value::String.new(
|
||||
obj.text,
|
||||
quoted: obj.quoted
|
||||
)
|
||||
when :number
|
||||
Number.from_proto(obj)
|
||||
when :color
|
||||
Sass::Value::Color.send(
|
||||
:for_space,
|
||||
obj.space,
|
||||
obj.has_channel1? ? obj.channel1 : nil,
|
||||
obj.has_channel2? ? obj.channel2 : nil,
|
||||
obj.has_channel3? ? obj.channel3 : nil,
|
||||
obj.has_alpha? ? obj.alpha : nil
|
||||
)
|
||||
when :argument_list
|
||||
compiler_value(
|
||||
Sass::Value::ArgumentList.new(
|
||||
obj.contents.map do |element|
|
||||
from_proto(element)
|
||||
end,
|
||||
obj.keywords.to_enum.with_object({}) do |(key, value), hash|
|
||||
hash[key.to_sym] = from_proto(value)
|
||||
end,
|
||||
ListSeparator.from_proto(obj.separator)
|
||||
),
|
||||
obj.id
|
||||
)
|
||||
when :list
|
||||
Sass::Value::List.new(
|
||||
obj.contents.map do |element|
|
||||
from_proto(element)
|
||||
end,
|
||||
separator: ListSeparator.from_proto(obj.separator),
|
||||
bracketed: obj.has_brackets
|
||||
)
|
||||
when :map
|
||||
Sass::Value::Map.new(
|
||||
obj.entries.to_enum.with_object({}) do |entry, hash|
|
||||
hash[from_proto(entry.key)] = from_proto(entry.value)
|
||||
end
|
||||
)
|
||||
when :compiler_function
|
||||
compiler_value(Sass::Value::Function.allocate, obj.id)
|
||||
when :host_function
|
||||
raise Sass::ScriptError, 'The compiler may not send Value.host_function to host'
|
||||
when :compiler_mixin
|
||||
compiler_value(Sass::Value::Mixin.allocate, obj.id)
|
||||
when :calculation
|
||||
Calculation.from_proto(obj)
|
||||
when :singleton
|
||||
case obj
|
||||
when :TRUE
|
||||
Sass::Value::Boolean::TRUE
|
||||
when :FALSE
|
||||
Sass::Value::Boolean::FALSE
|
||||
when :NULL
|
||||
Sass::Value::Null::NULL
|
||||
else
|
||||
raise Sass::ScriptError, "Unknown Value.singleton #{obj}"
|
||||
end
|
||||
else
|
||||
raise Sass::ScriptError, "Unknown Value.value #{obj}"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assert_compiler_value(value)
|
||||
unless value.instance_variable_get(:@compile_context) == @function_registry.compile_context
|
||||
raise Sass::ScriptError, "Value #{value} does not belong to this compilation"
|
||||
end
|
||||
|
||||
value
|
||||
end
|
||||
|
||||
def compiler_value(value, id)
|
||||
value.instance_variable_set(:@compile_context, @function_registry.compile_context)
|
||||
value.instance_variable_set(:@id, id)
|
||||
value
|
||||
end
|
||||
|
||||
# The {Number} Protofier.
|
||||
module Number
|
||||
module_function
|
||||
|
||||
def to_proto(obj)
|
||||
EmbeddedProtocol::Value::Number.new(
|
||||
value: obj.value.to_f,
|
||||
numerators: obj.numerator_units,
|
||||
denominators: obj.denominator_units
|
||||
)
|
||||
end
|
||||
|
||||
def from_proto(obj)
|
||||
Sass::Value::Number.new(
|
||||
obj.value, {
|
||||
numerator_units: obj.numerators.to_a,
|
||||
denominator_units: obj.denominators.to_a
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :Number
|
||||
|
||||
# The {Calculation} Protofier.
|
||||
module Calculation
|
||||
module_function
|
||||
|
||||
def to_proto(obj)
|
||||
EmbeddedProtocol::Value::Calculation.new(
|
||||
name: obj.name,
|
||||
arguments: obj.arguments.map { |argument| CalculationValue.to_proto(argument) }
|
||||
)
|
||||
end
|
||||
|
||||
def from_proto(obj)
|
||||
Sass::Value::Calculation.send(
|
||||
:new,
|
||||
obj.name,
|
||||
obj.arguments.map { |argument| CalculationValue.from_proto(argument) }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :Calculation
|
||||
|
||||
# The {CalculationValue} Protofier.
|
||||
module CalculationValue
|
||||
module_function
|
||||
|
||||
def to_proto(value)
|
||||
case value
|
||||
when Sass::Value::Number
|
||||
EmbeddedProtocol::Value::Calculation::CalculationValue.new(
|
||||
number: Number.to_proto(value)
|
||||
)
|
||||
when Sass::Value::Calculation
|
||||
EmbeddedProtocol::Value::Calculation::CalculationValue.new(
|
||||
calculation: Calculation.to_proto(value)
|
||||
)
|
||||
when Sass::Value::String
|
||||
EmbeddedProtocol::Value::Calculation::CalculationValue.new(
|
||||
string: value.text
|
||||
)
|
||||
when Sass::CalculationValue::CalculationOperation
|
||||
EmbeddedProtocol::Value::Calculation::CalculationValue.new(
|
||||
operation: EmbeddedProtocol::Value::Calculation::CalculationOperation.new(
|
||||
operator: CalculationOperator.to_proto(value.operator),
|
||||
left: to_proto(value.left),
|
||||
right: to_proto(value.right)
|
||||
)
|
||||
)
|
||||
else
|
||||
raise Sass::ScriptError, "Unknown CalculationValue #{value}"
|
||||
end
|
||||
end
|
||||
|
||||
def from_proto(value)
|
||||
oneof = value.value
|
||||
obj = value.public_send(oneof)
|
||||
case oneof
|
||||
when :number
|
||||
Number.from_proto(obj)
|
||||
when :calculation
|
||||
Calculation.from_proto(obj)
|
||||
when :string
|
||||
Sass::Value::String.new(obj, quoted: false)
|
||||
when :operation
|
||||
Sass::CalculationValue::CalculationOperation.new(
|
||||
CalculationOperator.from_proto(obj.operator),
|
||||
from_proto(obj.left),
|
||||
from_proto(obj.right)
|
||||
)
|
||||
else
|
||||
raise Sass::ScriptError, "Unknown CalculationValue #{value}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :CalculationValue
|
||||
|
||||
# The {CalculationOperator} Protofier.
|
||||
module CalculationOperator
|
||||
module_function
|
||||
|
||||
def to_proto(operator)
|
||||
case operator
|
||||
when '+'
|
||||
:PLUS
|
||||
when '-'
|
||||
:MINUS
|
||||
when '*'
|
||||
:TIMES
|
||||
when '/'
|
||||
:DIVIDE
|
||||
else
|
||||
raise Sass::ScriptError, "Unknown CalculationOperator #{separator}"
|
||||
end
|
||||
end
|
||||
|
||||
def from_proto(operator)
|
||||
case operator
|
||||
when :PLUS
|
||||
'+'
|
||||
when :MINUS
|
||||
'-'
|
||||
when :TIMES
|
||||
'*'
|
||||
when :DIVIDE
|
||||
'/'
|
||||
else
|
||||
raise Sass::ScriptError, "Unknown CalculationOperator #{separator}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :CalculationOperator
|
||||
|
||||
# The {ListSeparator} Protofier.
|
||||
module ListSeparator
|
||||
module_function
|
||||
|
||||
def to_proto(separator)
|
||||
case separator
|
||||
when ','
|
||||
:COMMA
|
||||
when ' '
|
||||
:SPACE
|
||||
when '/'
|
||||
:SLASH
|
||||
when nil
|
||||
:UNDECIDED
|
||||
else
|
||||
raise Sass::ScriptError, "Unknown ListSeparator #{separator}"
|
||||
end
|
||||
end
|
||||
|
||||
def from_proto(separator)
|
||||
case separator
|
||||
when :COMMA
|
||||
','
|
||||
when :SPACE
|
||||
' '
|
||||
when :SLASH
|
||||
'/'
|
||||
when :UNDECIDED
|
||||
nil
|
||||
else
|
||||
raise Sass::ScriptError, "Unknown ListSeparator #{separator}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :ListSeparator
|
||||
end
|
||||
|
||||
private_constant :Protofier
|
||||
end
|
||||
end
|
||||
end
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
class Compiler
|
||||
class Session
|
||||
# @see https://pub.dev/documentation/stack_trace/latest/stack_trace/
|
||||
module StackTrace
|
||||
module_function
|
||||
|
||||
# @see https://pub.dev/documentation/stack_trace/latest/stack_trace/Trace/toString.html
|
||||
def pretty_formatted!(formatted, stack_trace)
|
||||
longest = 0
|
||||
|
||||
frames = stack_trace.lines("\n", chomp: true).map do |frame|
|
||||
location, member = frame.split(/ +/, 2)
|
||||
uri, line_column = location.split(' ', 2)
|
||||
|
||||
uri = Path.pretty_uri(uri)
|
||||
location = line_column.nil? ? uri : "#{uri} #{line_column}"
|
||||
|
||||
longest = location.length if location.length > longest
|
||||
[frame, location, member]
|
||||
end
|
||||
|
||||
offset = formatted.length
|
||||
|
||||
frames.reverse_each do |frame, location, member|
|
||||
index = formatted.rindex(frame, offset)
|
||||
next unless index
|
||||
|
||||
offset = index
|
||||
|
||||
replacement = "#{location.ljust(longest)} #{member}"
|
||||
next if frame == replacement
|
||||
|
||||
formatted[index, frame.length] = replacement
|
||||
end
|
||||
|
||||
formatted
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :StackTrace
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
class Compiler
|
||||
class Session
|
||||
# The {Struct} class.
|
||||
#
|
||||
# It creates {::Struct}-like objects from {::Hash}.
|
||||
class Struct
|
||||
def initialize(hash, attrs: nil, methods: nil)
|
||||
@hash = hash
|
||||
@attrs = attrs
|
||||
@methods = methods
|
||||
end
|
||||
|
||||
def method_missing(symbol, ...)
|
||||
return super unless @hash.key?(symbol)
|
||||
|
||||
if @attrs&.include?(symbol)
|
||||
@hash.send(:[], symbol, ...)
|
||||
elsif @methods&.include?(symbol)
|
||||
@hash[symbol].call(...)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def respond_to_missing?(symbol, _include_all)
|
||||
@hash.key?(symbol) && (@attrs&.include?(symbol) || @methods&.include?(symbol))
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :Struct
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Sass
|
||||
class Compiler
|
||||
# The {Varint} module.
|
||||
#
|
||||
# It reads and writes varints.
|
||||
module Varint
|
||||
module_function
|
||||
|
||||
def length(value)
|
||||
return 1 if value < 128
|
||||
|
||||
(value.bit_length + 6) / 7
|
||||
end
|
||||
|
||||
def read(readable)
|
||||
value = bits = 0
|
||||
loop do
|
||||
byte = readable.readbyte
|
||||
value |= (byte & 0x7f) << bits
|
||||
bits += 7
|
||||
break if byte < 0x80
|
||||
end
|
||||
value
|
||||
end
|
||||
|
||||
def write(writeable, value)
|
||||
until value < 0x80
|
||||
writeable << ((value & 0x7f) | 0x80)
|
||||
value >>= 7
|
||||
end
|
||||
writeable << value
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :Varint
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user