Skip to content
This repository has been archived by the owner on May 15, 2023. It is now read-only.

Add support for importers #8

Merged
merged 1 commit into from Nov 8, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
48 changes: 31 additions & 17 deletions bin/dart_sass_embedded.dart
Expand Up @@ -11,6 +11,7 @@ import 'package:stream_channel/stream_channel.dart';

import 'package:sass_embedded/src/dispatcher.dart';
import 'package:sass_embedded/src/embedded_sass.pb.dart';
import 'package:sass_embedded/src/importer.dart';
import 'package:sass_embedded/src/logger.dart';
import 'package:sass_embedded/src/util/length_delimited_transformer.dart';
import 'package:sass_embedded/src/utils.dart';
Expand All @@ -29,7 +30,13 @@ void main(List<String> args) {
StreamChannel.withGuarantees(stdin, stdout, allowSinkErrors: false)
.transform(lengthDelimited));

dispatcher.listen((request) {
dispatcher.listen((request) async {
// Wait a single microtask tick so that we're running in a separate
// microtask from the initial request dispatch. Otherwise, [waitFor] will
// deadlock the event loop fiber that would otherwise be checking stdin for
// new input.
await Future.value();

var style =
request.style == InboundMessage_CompileRequest_OutputStyle.COMPRESSED
? sass.OutputStyle.compressed
Expand All @@ -42,12 +49,30 @@ void main(List<String> args) {
var sourceMapCallback = request.sourceMap
? (source_maps.SingleMapping map) => sourceMap = map
: null;

var importers = request.importers.map((importer) {
switch (importer.whichImporter()) {
case InboundMessage_CompileRequest_Importer_Importer.path:
return sass.FilesystemImporter(importer.path);

case InboundMessage_CompileRequest_Importer_Importer.importerId:
return Importer(dispatcher, request.id, importer.importerId);

case InboundMessage_CompileRequest_Importer_Importer.notSet:
throw mandatoryError("Importer.importer");

default:
throw "Unknown Importer.importer $importer.";
}
});

switch (request.whichInput()) {
case InboundMessage_CompileRequest_Input.string:
var input = request.string;
result = sass.compileString(input.source,
logger: logger,
syntax: _syntaxToSyntax(input.syntax),
importers: importers,
syntax: syntaxToSyntax(input.syntax),
style: style,
url: input.url.isEmpty ? null : input.url,
sourceMap: sourceMapCallback);
Expand All @@ -56,7 +81,10 @@ void main(List<String> args) {
case InboundMessage_CompileRequest_Input.path:
try {
result = sass.compile(request.path,
logger: logger, style: style, sourceMap: sourceMapCallback);
logger: logger,
importers: importers,
style: style,
sourceMap: sourceMapCallback);
} on FileSystemException catch (error) {
return OutboundMessage_CompileResponse()
..failure = (OutboundMessage_CompileResponse_CompileFailure()
Expand Down Expand Up @@ -85,17 +113,3 @@ void main(List<String> args) {
}
});
}

/// Converts a protocol buffer syntax enum into a Sass API syntax enum.
sass.Syntax _syntaxToSyntax(InboundMessage_Syntax syntax) {
switch (syntax) {
case InboundMessage_Syntax.SCSS:
return sass.Syntax.scss;
case InboundMessage_Syntax.INDENTED:
return sass.Syntax.sass;
case InboundMessage_Syntax.CSS:
return sass.Syntax.css;
default:
throw "Unknown syntax $syntax.";
}
}
130 changes: 111 additions & 19 deletions lib/src/dispatcher.dart
Expand Up @@ -11,12 +11,20 @@ import 'package:stack_trace/stack_trace.dart';
import 'package:stream_channel/stream_channel.dart';

import 'embedded_sass.pb.dart';
import 'utils.dart';

/// A class that dispatches messages to and from the host.
class Dispatcher {
/// The channel of encoded protocol buffers, connected to the host.
final StreamChannel<Uint8List> _channel;

/// Completers awaiting responses to outbound requests.
///
/// The completers are located at indexes in this list matching the request
/// IDs. `null` elements indicate IDs whose requests have been responded to,
/// and which are therefore free to re-use.
final _outstandingRequests = <Completer<GeneratedMessage>>[];

/// Creates a [Dispatcher] that sends and receives encoded protocol buffers
/// over [channel].
Dispatcher(this._channel);
Expand All @@ -43,7 +51,7 @@ class Dispatcher {

switch (message.whichMessage()) {
case InboundMessage_Message.error:
var error = message.ensureError();
var error = message.error;
stderr
.write("Host reported ${error.type.name.toLowerCase()} error");
if (error.id != -1) stderr.write(" with request ${error.id}");
Expand All @@ -54,12 +62,22 @@ class Dispatcher {
break;

case InboundMessage_Message.compileRequest:
var request = message.ensureCompileRequest();
var request = message.compileRequest;
var response = await callback(request);
response.id = request.id;
_send(OutboundMessage()..compileResponse = response);
break;

case InboundMessage_Message.canonicalizeResponse:
var response = message.canonicalizeResponse;
_dispatchResponse(response.id, response);
break;

case InboundMessage_Message.importResponse:
var response = message.importResponse;
_dispatchResponse(response.id, response);
break;

case InboundMessage_Message.notSet:
// PROTOCOL error from https://bit.ly/2poTt90
exitCode = 76;
Expand All @@ -72,19 +90,18 @@ class Dispatcher {
"Unknown message type: ${message.toDebugString()}");
}
} on ProtocolError catch (error) {
error.id = _messageId(message) ?? -1;
error.id = _inboundId(message) ?? -1;
stderr.write("Host caused ${error.type.name.toLowerCase()} error");
if (error.id != -1) stderr.write(" with request ${error.id}");
stderr.writeln(": ${error.message}");
_send(OutboundMessage()..error = error);
sendError(error);
} catch (error, stackTrace) {
var errorMessage = "$error\n${Chain.forTrace(stackTrace)}";
stderr.write("Internal compiler error: $errorMessage");
_send(OutboundMessage()
..error = (ProtocolError()
..type = ProtocolError_ErrorType.INTERNAL
..id = _messageId(message) ?? -1
..message = errorMessage));
sendError(ProtocolError()
..type = ProtocolError_ErrorType.INTERNAL
..id = _inboundId(message) ?? -1
..message = errorMessage);
_channel.sink.close();
}
});
Expand All @@ -94,6 +111,62 @@ class Dispatcher {
void sendLog(OutboundMessage_LogEvent event) =>
_send(OutboundMessage()..logEvent = event);

/// Sends [error] to the host.
void sendError(ProtocolError error) =>
_send(OutboundMessage()..error = error);

Future<InboundMessage_CanonicalizeResponse> sendCanonicalizeRequest(
OutboundMessage_CanonicalizeRequest request) =>
_sendRequest<InboundMessage_CanonicalizeResponse>(
OutboundMessage()..canonicalizeRequest = request);

Future<InboundMessage_ImportResponse> sendImportRequest(
OutboundMessage_ImportRequest request) =>
_sendRequest<InboundMessage_ImportResponse>(
OutboundMessage()..importRequest = request);

/// Sends [request] to the host and returns the message sent in response.
Future<T> _sendRequest<T extends GeneratedMessage>(
OutboundMessage request) async {
var id = _nextRequestId();
_setOutboundId(request, id);
_send(request);

var completer = Completer<T>();
_outstandingRequests[id] = completer;
return completer.future;
}

/// Returns an available request ID, and guarantees that its slot is available
/// in [_outstandingRequests].
int _nextRequestId() {
for (var i = 0; i < _outstandingRequests.length; i++) {
if (_outstandingRequests[i] == null) return i;
}

// If there are no empty slots, add another one.
_outstandingRequests.add(null);
return _outstandingRequests.length - 1;
}

/// Dispatches [response] to the appropriate outstanding request.
///
/// Throws an error if there's no outstanding request with the given [id] or
/// if that request is expecting a different type of response.
void _dispatchResponse<T extends GeneratedMessage>(int id, T response) {
var completer =
id < _outstandingRequests.length ? _outstandingRequests[id] : null;
if (completer == null) {
throw paramsError(
"Response ID $id doesn't match any outstanding requests.");
} else if (completer is! Completer<T>) {
throw paramsError("Request ID $id doesn't match response type "
"${response.runtimeType}.");
}

completer.complete(response);
}

/// Sends [message] to the host.
void _send(OutboundMessage message) =>
_channel.sink.add(message.writeToBuffer());
Expand All @@ -103,21 +176,40 @@ class Dispatcher {
..type = ProtocolError_ErrorType.PARSE
..message = message;

/// Returns the id for [message] if it it's a request or response, or `null`
/// Returns the id for [message] if it it's a request, or `null`
/// otherwise.
int _messageId(InboundMessage message) {
int _inboundId(InboundMessage message) {
if (message == null) return null;
switch (message.whichMessage()) {
case InboundMessage_Message.compileRequest:
return message.ensureCompileRequest().id;
case InboundMessage_Message.canonicalizeResponse:
return message.ensureCanonicalizeResponse().id;
case InboundMessage_Message.importResponse:
return message.ensureImportResponse().id;
return message.compileRequest.id;
case InboundMessage_Message.functionCallRequest:
return message.ensureFunctionCallRequest().id;
case InboundMessage_Message.functionCallResponse:
return message.ensureFunctionCallResponse().id;
return message.functionCallRequest.id;
default:
return null;
}
}

/// Sets the id for [message] to [id].
///
/// Throws an [ArgumentError] if [message] doesn't have an id field.
void _setOutboundId(OutboundMessage message, int id) {
switch (message.whichMessage()) {
case OutboundMessage_Message.compileResponse:
message.compileResponse.id = id;
break;
case OutboundMessage_Message.canonicalizeRequest:
message.canonicalizeRequest.id = id;
break;
case OutboundMessage_Message.importRequest:
message.importRequest.id = id;
break;
case OutboundMessage_Message.functionCallRequest:
message.functionCallRequest.id = id;
break;
case OutboundMessage_Message.functionCallResponse:
message.functionCallResponse.id = id;
break;
default:
return null;
}
Expand Down
109 changes: 109 additions & 0 deletions lib/src/importer.dart
@@ -0,0 +1,109 @@
// Copyright 2019 Google Inc. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

import 'dart:cli';

import 'package:meta/meta.dart';
import 'package:sass/sass.dart' as sass;

import 'dispatcher.dart';
import 'embedded_sass.pb.dart' hide SourceSpan;
import 'utils.dart';

/// An importer that asks the host to resolve imports.
class Importer extends sass.Importer {
/// The [Dispatcher] to which to send requests.
final Dispatcher _dispatcher;

/// The ID of the compilation in which this importer is used.
final int _compilationId;

/// The host-provided ID of the importer to invoke.
final int _importerId;

Importer(this._dispatcher, this._compilationId, this._importerId);

Uri canonicalize(Uri url) {
return waitFor(() async {
var response = await _dispatcher
.sendCanonicalizeRequest(OutboundMessage_CanonicalizeRequest()
..compilationId = _compilationId
..importerId = _importerId
..url = url.toString());

switch (response.whichResult()) {
case InboundMessage_CanonicalizeResponse_Result.url:
return _parseAbsoluteUrl("CanonicalizeResponse.url", response.url);

case InboundMessage_CanonicalizeResponse_Result.file:
throw "CanonicalizeResponse.file is not yet supported";

case InboundMessage_CanonicalizeResponse_Result.error:
throw response.error;

case InboundMessage_CanonicalizeResponse_Result.notSet:
return null;

default:
throw "Unknown CanonicalizeResponse.result $response.";
}
}());
}

sass.ImporterResult load(Uri url) {
return waitFor(() async {
var response =
await _dispatcher.sendImportRequest(OutboundMessage_ImportRequest()
..compilationId = _compilationId
..importerId = _importerId
..url = url.toString());

switch (response.whichResult()) {
case InboundMessage_ImportResponse_Result.success:
return sass.ImporterResult(response.success.contents,
sourceMapUrl: response.success.sourceMapUrl.isEmpty
? null
: _parseAbsoluteUrl("ImportResponse.success.source_map_url",
response.success.sourceMapUrl),
syntax: syntaxToSyntax(response.success.syntax));

case InboundMessage_ImportResponse_Result.error:
throw response.error;

case InboundMessage_ImportResponse_Result.notSet:
_sendAndThrow(mandatoryError("ImportResponse.result"));
break; // dart-lang/sdk#34048

default:
throw "Unknown ImporterResponse.result $response.";
}
}());
}

/// Parses [url] as a [Uri] and throws an error if it's invalid or relative
/// (including root-relative).
///
/// The [field] name is used in the error message if one is thrown.
Uri _parseAbsoluteUrl(String field, String url) {
Uri parsedUrl;
try {
parsedUrl = Uri.parse(url);
} on FormatException catch (error) {
_sendAndThrow(paramsError("$field is invalid: $error"));
}

if (parsedUrl.scheme.isNotEmpty) return parsedUrl;
_sendAndThrow(paramsError('$field must be absolute, was "$parsedUrl"'));
}

/// Sends [error] to the remote endpoint, and also throws it so that the Sass
/// compilation fails.
@alwaysThrows
void _sendAndThrow(ProtocolError error) {
_dispatcher.sendError(error);
throw "Protocol error: ${error.message}";
}

String toString() => "HostImporter";
}