Add a --stop-on-error flag (#391)

Closes #264
This commit is contained in:
Natalie Weizenbaum 2018-06-29 18:12:36 -07:00 committed by GitHub
parent dab524d277
commit 96c46a242e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 159 additions and 18 deletions

View File

@ -5,6 +5,9 @@
* Add a `--poll` flag to make `--watch` mode repeatedly check the filesystem for
updates rather than relying on native filesystem notifications.
* Add a `--stop-on-error` flag to stop compiling additional files once an error
is encountered.
## 1.7.3
* No user-visible changes.

View File

@ -80,12 +80,14 @@ main(List<String> args) async {
//
// We let exitCode 66 take precedence for deterministic behavior.
if (exitCode != 66) exitCode = 65;
if (options.stopOnError) return;
} on FileSystemException catch (error, stackTrace) {
printError("Error reading ${p.relative(error.path)}: ${error.message}.",
options.trace ? stackTrace : null);
// Error 66 indicates no input.
exitCode = 66;
if (options.stopOnError) return;
}
}
} on UsageException catch (error) {

View File

@ -79,6 +79,8 @@ class ExecutableOptions {
help: 'Manually check for changes rather than using a native '
'watcher.\n'
'Only valid with --watch.')
..addFlag('stop-on-error',
help: "Don't compile more files once an error is encountered.")
..addFlag('interactive',
abbr: 'i',
help: 'Run an interactive SassScript shell.',
@ -177,6 +179,10 @@ class ExecutableOptions {
/// Whether to manually poll for changes when watching.
bool get poll => _options['poll'] as bool;
/// Whether to stop compiling additional files once one file produces an
/// error.
bool get stopOnError => _options['stop-on-error'] as bool;
/// A map from source paths to the destination paths where the compiled CSS
/// should be written.
///

View File

@ -41,7 +41,11 @@ Future watch(ExecutableOptions options, StylesheetGraph graph) async {
var destination = options.sourcesToDestinations[source];
graph.addCanonical(new FilesystemImporter('.'),
p.toUri(p.canonicalize(source)), p.toUri(source));
await watcher.compile(source, destination, ifModified: true);
var success = await watcher.compile(source, destination, ifModified: true);
if (!success && options.stopOnError) {
dirWatcher.events.listen(null).cancel();
return;
}
}
print("Sass is watching for changes. Press Ctrl-C to stop.\n");
@ -51,25 +55,34 @@ Future watch(ExecutableOptions options, StylesheetGraph graph) async {
/// Holds state that's shared across functions that react to changes on the
/// filesystem.
class _Watcher {
/// The options for the Sass executable.
final ExecutableOptions _options;
/// The graph of stylesheets being compiled.
final StylesheetGraph _graph;
_Watcher(this._options, this._graph);
/// Compiles the stylesheet at [source] to [destination], and prints any
/// errors that occur.
Future compile(String source, String destination,
///
/// Returns whether or not compilation succeeded.
Future<bool> compile(String source, String destination,
{bool ifModified: false}) async {
try {
await compileStylesheet(_options, _graph, source, destination,
ifModified: ifModified);
return true;
} on SassException catch (error, stackTrace) {
_delete(destination);
_printError(error.toString(color: _options.color), stackTrace);
exitCode = 65;
return false;
} on FileSystemException catch (error, stackTrace) {
_printError("Error reading ${p.relative(error.path)}: ${error.message}.",
stackTrace);
exitCode = 66;
return false;
}
}
@ -97,7 +110,7 @@ class _Watcher {
stderr.writeln(new Trace.from(stackTrace).terse.toString().trimRight());
}
stderr.writeln();
if (!_options.stopOnError) stderr.writeln();
}
/// Listens to `watcher.events` and updates the filesystem accordingly.
@ -119,11 +132,13 @@ class _Watcher {
// from the graph.
var node = _graph.nodes[url];
_graph.reload(url);
await _recompileDownstream([node]);
var success = await _recompileDownstream([node]);
if (!success && _options.stopOnError) return;
break;
case ChangeType.ADD:
await _retryPotentialImports(event.path);
var success = await _retryPotentialImports(event.path);
if (!success && _options.stopOnError) return;
var destination = _destinationFor(event.path);
if (destination == null) continue loop;
@ -131,11 +146,13 @@ class _Watcher {
_graph.addCanonical(
new FilesystemImporter('.'), url, p.toUri(event.path));
await compile(event.path, destination);
success = await compile(event.path, destination);
if (!success && _options.stopOnError) return;
break;
case ChangeType.REMOVE:
await _retryPotentialImports(event.path);
var success = await _retryPotentialImports(event.path);
if (!success && _options.stopOnError) return;
if (!_graph.nodes.containsKey(url)) continue loop;
var destination = _destinationFor(event.path);
@ -143,7 +160,8 @@ class _Watcher {
var downstream = _graph.nodes[url].downstream;
_graph.remove(url);
await _recompileDownstream(downstream);
success = await _recompileDownstream(downstream);
if (!success && _options.stopOnError) return;
break;
}
}
@ -176,29 +194,38 @@ class _Watcher {
/// Recompiles [nodes] and everything that transitively imports them, if
/// necessary.
Future _recompileDownstream(Iterable<StylesheetNode> nodes) async {
///
/// Returns whether all recompilations succeeded.
Future<bool> _recompileDownstream(Iterable<StylesheetNode> nodes) async {
var seen = new Set<StylesheetNode>();
var toRecompile = new Queue.of(nodes);
var allSucceeded = true;
while (!toRecompile.isEmpty) {
var node = toRecompile.removeFirst();
if (!seen.add(node)) continue;
await _compileIfEntrypoint(node.canonicalUrl);
var success = await _compileIfEntrypoint(node.canonicalUrl);
allSucceeded = allSucceeded && success;
if (!success && _options.stopOnError) return false;
toRecompile.addAll(node.downstream);
}
return allSucceeded;
}
/// Compiles the stylesheet at [url] to CSS if it's an entrypoint that's being
/// watched.
Future _compileIfEntrypoint(Uri url) async {
if (url.scheme != 'file') return;
///
/// Returns `false` if compilation failed, `true` otherwise.
Future<bool> _compileIfEntrypoint(Uri url) async {
if (url.scheme != 'file') return true;
var source = p.fromUri(url);
var destination = _destinationFor(source);
if (destination == null) return;
if (destination == null) return true;
await compile(source, destination);
return await compile(source, destination);
}
/// If a Sass file at [source] should be compiled to CSS, returns the path to
@ -223,7 +250,9 @@ class _Watcher {
/// Re-runs all imports in [_graph] that might refer to [path], and recompiles
/// the files that contain those imports if they end up importing new
/// stylesheets.
Future _retryPotentialImports(String path) async {
///
/// Returns whether all recompilations succeeded.
Future<bool> _retryPotentialImports(String path) async {
var name = _name(p.basename(path));
var changed = <StylesheetNode>[];
for (var node in _graph.nodes.values) {
@ -250,7 +279,7 @@ class _Watcher {
if (importChanged) changed.add(node);
}
await _recompileDownstream(changed);
return await _recompileDownstream(changed);
}
/// Removes an extension from [extension], and a leading underscore if it has one.

View File

@ -263,7 +263,9 @@ Future<Stream<WatchEvent>> watchDir(String path, {bool poll: false}) {
var completer = new Completer<Stream<WatchEvent>>();
watcher.on('ready', allowInterop(() {
controller = new StreamController<WatchEvent>();
controller = new StreamController<WatchEvent>(onCancel: () {
watcher.close();
});
completer.complete(controller.stream);
}));

View File

@ -1,5 +1,5 @@
name: sass
version: 1.8.0-dev
version: 1.8.0
description: A Sass implementation in Dart.
author: Dart Team <misc@dartlang.org>
homepage: https://github.com/sass/dart-sass

View File

@ -68,6 +68,41 @@ void sharedTests(Future<TestProcess> runSass(Iterable<String> arguments)) {
await d.file("out2.css.map", contains("test2.scss")).validate();
});
test("continues compiling after an error", () async {
await d.file("test1.scss", "a {b: }").create();
await d.file("test2.scss", "x {y: z}").create();
var sass = await runSass(
["--no-source-map", "test1.scss:out1.css", "test2.scss:out2.css"]);
await expectLater(sass.stderr, emits('Error: Expected expression.'));
await expectLater(sass.stderr, emitsThrough(contains('test1.scss 1:7')));
await sass.shouldExit(65);
await d.nothing("out1.css").validate();
await d
.file("out2.css", equalsIgnoringWhitespace("x { y: z; }"))
.validate();
});
test("stops compiling after an error with --stop-on-error", () async {
await d.file("test1.scss", "a {b: }").create();
await d.file("test2.scss", "x {y: z}").create();
var sass = await runSass(
["--stop-on-error", "test1.scss:out1.css", "test2.scss:out2.css"]);
await expectLater(
sass.stderr,
emitsInOrder([
'Error: Expected expression.',
emitsThrough(contains('test1.scss 1:7')),
emitsDone
]));
await sass.shouldExit(65);
await d.nothing("out1.css").validate();
await d.nothing("out2.css").validate();
});
group("with a directory argument", () {
test("compiles all the stylesheets in the directory", () async {
await d.dir("in", [

View File

@ -64,6 +64,48 @@ void sharedTests(Future<TestProcess> runSass(Iterable<String> arguments)) {
await d.file("out.css", "x {y: z}").validate();
});
test("continues compiling after an error", () async {
await d.file("test1.scss", "a {b: }").create();
await d.file("test2.scss", "x {y: z}").create();
var sass =
await watch(["test1.scss:out1.css", "test2.scss:out2.css"]);
await expectLater(sass.stderr, emits('Error: Expected expression.'));
await expectLater(
sass.stderr, emitsThrough(contains('test1.scss 1:7')));
await expectLater(
sass.stdout, emitsThrough('Compiled test2.scss to out2.css.'));
await expectLater(sass.stdout, _watchingForChanges);
await sass.kill();
await d.nothing("out1.css").validate();
await d
.file("out2.css", equalsIgnoringWhitespace("x { y: z; }"))
.validate();
});
test("stops compiling after an error with --stop-on-error", () async {
await d.file("test1.scss", "a {b: }").create();
await d.file("test2.scss", "x {y: z}").create();
var sass = await watch([
"--stop-on-error",
"test1.scss:out1.css",
"test2.scss:out2.css"
]);
await expectLater(
sass.stderr,
emitsInOrder([
'Error: Expected expression.',
emitsThrough(contains('test1.scss 1:7')),
emitsDone
]));
await sass.shouldExit(65);
await d.nothing("out1.css").validate();
await d.nothing("out2.css").validate();
});
});
group("recompiles a watched file", () {
@ -168,6 +210,28 @@ void sharedTests(Future<TestProcess> runSass(Iterable<String> arguments)) {
await d.nothing("out.css").validate();
});
test("stops compiling after an error with --stop-on-error", () async {
await d.file("test.scss", "a {b: c}").create();
var sass = await watch(["--stop-on-error", "test.scss:out.css"]);
await expectLater(
sass.stdout, emits('Compiled test.scss to out.css.'));
await expectLater(sass.stdout, _watchingForChanges);
await tickIfPoll();
await d.file("test.scss", "a {b: }").create();
await expectLater(
sass.stderr,
emitsInOrder([
'Error: Expected expression.',
emitsThrough(contains('test.scss 1:7')),
emitsDone
]));
await sass.shouldExit(65);
await d.nothing("out.css").validate();
});
group("when its dependency is deleted", () {
test("and removes the output", () async {
await d.file("_other.scss", "a {b: c}").create();