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

Commit

Permalink
Add support for importers
Browse files Browse the repository at this point in the history
  • Loading branch information
nex3 committed Nov 5, 2019
1 parent bcac97b commit 6145662
Show file tree
Hide file tree
Showing 8 changed files with 826 additions and 44 deletions.
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";
}

0 comments on commit 6145662

Please sign in to comment.