Read Sass from stdin (#137)

See #105
This commit is contained in:
Sam Rawlins 2017-10-06 18:59:49 -07:00 committed by Natalie Weizenbaum
parent 4697dcad47
commit 97f678a770
6 changed files with 111 additions and 3 deletions

View File

@ -16,6 +16,7 @@ import 'util/path.dart';
main(List<String> args) async {
var argParser = new ArgParser(allowTrailingOptions: true)
..addOption('precision', hide: true)
..addFlag('stdin', help: 'Read the stylesheet from stdin.')
..addOption('style',
abbr: 's',
help: 'Output style.',
@ -45,7 +46,8 @@ main(List<String> args) async {
return;
}
if (options['help'] as bool || options.rest.isEmpty) {
var stdinFlag = options['stdin'] as bool;
if (options['help'] as bool || (options.rest.isEmpty && !stdinFlag)) {
_printUsage(argParser, "Compile Sass to CSS.");
exitCode = 64;
return;
@ -54,7 +56,16 @@ main(List<String> args) async {
var color =
options.wasParsed('color') ? options['color'] as bool : hasTerminal;
try {
var css = compile(options.rest.first, color: color);
String css;
if (stdinFlag) {
css = compileString(await readStdin(), color: color);
} else {
var input = options.rest.first;
css = input == '-'
? compileString(await readStdin(), color: color)
: compile(input, color: color);
}
if (css.isNotEmpty) print(css);
} on SassException catch (error, stackTrace) {
stderr.writeln(error.toString(color: color));

View File

@ -2,6 +2,8 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'dart:async';
/// An output sink that writes to this process's standard error.
class Stderr {
/// Writes the string representation of [object] to standard error.
@ -41,6 +43,10 @@ String get currentPath => null;
/// the file isn't valid UTF-8.
String readFile(String path) => null;
/// Reads from the standard input for the current process until it closes,
/// returning the contents.
Future<String> readStdin() async => null;
/// Returns whether a file at [path] exists.
bool fileExists(String path) => null;

View File

@ -2,6 +2,9 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'dart:async';
import 'dart:convert';
import 'package:js/js.dart';
import 'package:source_span/source_span.dart';
@ -20,6 +23,13 @@ class _Stderr {
external void write(String text);
}
@JS()
class _Stdin {
external String read();
external void on(String event, void callback([object]));
}
@JS()
class _SystemError {
external String get message;
@ -92,6 +102,33 @@ _readFile(String path, [String encoding]) {
}
}
Future<String> readStdin() async {
var completer = new Completer<String>();
String contents;
var innerSink = new StringConversionSink.withCallback((String result) {
contents = result;
completer.complete(contents);
});
// Node defaults all buffers to 'utf8'.
var sink = UTF8.decoder.startChunkedConversion(innerSink);
_stdin.on('data', allowInterop(([chunk]) {
assert(chunk != null);
sink.add(chunk as List<int>);
}));
_stdin.on('end', allowInterop(([_]) {
// Callback for 'end' receives no args.
assert(_ == null);
sink.close();
}));
_stdin.on('error', allowInterop(([e]) {
assert(e != null);
stderr.writeln('Failed to read from stdin');
stderr.writeln(e);
completer.completeError(e);
}));
return completer.future;
}
/// Cleans up a Node system error's message.
String _cleanErrorMessage(_SystemError error) {
// The error message is of the form "$code: $text, $syscall '$path'". We just
@ -109,6 +146,9 @@ external _Stderr get _stderr;
final stderr = new Stderr(_stderr);
@JS("process.stdin")
external _Stdin get _stdin;
bool get hasTerminal => _hasTerminal ?? false;
bool get isWindows => _process.platform == 'win32';

View File

@ -2,13 +2,14 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'dart:async';
import 'dart:convert';
import 'dart:io' as io;
import 'package:path/path.dart' as p;
import 'package:source_span/source_span.dart';
import '../exception.dart';
import '../util/path.dart';
export 'dart:io' show exitCode, FileSystemException;
@ -43,6 +44,12 @@ String readFile(String path) {
}
}
Future<String> readStdin() async {
var completer = new Completer<String>();
completer.complete(await io.SYSTEM_ENCODING.decodeStream(io.stdin));
return completer.future;
}
bool fileExists(String path) => new io.File(path).existsSync();
bool dirExists(String path) => new io.Directory(path).existsSync();

View File

@ -14,6 +14,7 @@ dependencies:
args: "^0.13.0"
charcode: "^1.1.0"
collection: "^1.8.0"
convert: "^2.0.1"
path: "^1.0.0"
source_span: "^1.4.0"
string_scanner: ">=0.1.5 <2.0.0"

View File

@ -34,6 +34,49 @@ void sharedTests(Future<TestProcess> runSass(Iterable<String> arguments)) {
await sass.shouldExit(0);
});
test("compiles from stdin with the magic path -", () async {
var sass = await runSass(["-"]);
sass.stdin.writeln("a {b: 1 + 2}");
sass.stdin.close();
expect(
sass.stdout,
emitsInOrder([
"a {",
" b: 3;",
"}",
]));
await sass.shouldExit(0);
});
test("compiles from stdin with --stdin", () async {
var sass = await runSass(["--stdin"]);
sass.stdin.writeln("a {b: 1 + 2}");
sass.stdin.close();
expect(
sass.stdout,
emitsInOrder([
"a {",
" b: 3;",
"}",
]));
await sass.shouldExit(0);
});
test("gracefully reports errors from stdin", () async {
var sass = await runSass(["-"]);
sass.stdin.writeln("a {b: 1 + }");
sass.stdin.close();
expect(
sass.stderr,
emitsInOrder([
"Error: Expected expression.",
"a {b: 1 + }",
" ^",
" - 1:11 root stylesheet",
]));
await sass.shouldExit(65);
});
test("supports relative imports", () async {
await d.file("test.scss", "@import 'dir/test'").create();