Skip to content

Commit 96c46a2

Browse files
authored
Add a --stop-on-error flag (#391)
Closes #264
1 parent dab524d commit 96c46a2

File tree

8 files changed

+159
-18
lines changed

8 files changed

+159
-18
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
* Add a `--poll` flag to make `--watch` mode repeatedly check the filesystem for
66
updates rather than relying on native filesystem notifications.
77

8+
* Add a `--stop-on-error` flag to stop compiling additional files once an error
9+
is encountered.
10+
811
## 1.7.3
912

1013
* No user-visible changes.

lib/src/executable.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,14 @@ main(List<String> args) async {
8080
//
8181
// We let exitCode 66 take precedence for deterministic behavior.
8282
if (exitCode != 66) exitCode = 65;
83+
if (options.stopOnError) return;
8384
} on FileSystemException catch (error, stackTrace) {
8485
printError("Error reading ${p.relative(error.path)}: ${error.message}.",
8586
options.trace ? stackTrace : null);
8687

8788
// Error 66 indicates no input.
8889
exitCode = 66;
90+
if (options.stopOnError) return;
8991
}
9092
}
9193
} on UsageException catch (error) {

lib/src/executable/options.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ class ExecutableOptions {
7979
help: 'Manually check for changes rather than using a native '
8080
'watcher.\n'
8181
'Only valid with --watch.')
82+
..addFlag('stop-on-error',
83+
help: "Don't compile more files once an error is encountered.")
8284
..addFlag('interactive',
8385
abbr: 'i',
8486
help: 'Run an interactive SassScript shell.',
@@ -177,6 +179,10 @@ class ExecutableOptions {
177179
/// Whether to manually poll for changes when watching.
178180
bool get poll => _options['poll'] as bool;
179181

182+
/// Whether to stop compiling additional files once one file produces an
183+
/// error.
184+
bool get stopOnError => _options['stop-on-error'] as bool;
185+
180186
/// A map from source paths to the destination paths where the compiled CSS
181187
/// should be written.
182188
///

lib/src/executable/watch.dart

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,11 @@ Future watch(ExecutableOptions options, StylesheetGraph graph) async {
4141
var destination = options.sourcesToDestinations[source];
4242
graph.addCanonical(new FilesystemImporter('.'),
4343
p.toUri(p.canonicalize(source)), p.toUri(source));
44-
await watcher.compile(source, destination, ifModified: true);
44+
var success = await watcher.compile(source, destination, ifModified: true);
45+
if (!success && options.stopOnError) {
46+
dirWatcher.events.listen(null).cancel();
47+
return;
48+
}
4549
}
4650

4751
print("Sass is watching for changes. Press Ctrl-C to stop.\n");
@@ -51,25 +55,34 @@ Future watch(ExecutableOptions options, StylesheetGraph graph) async {
5155
/// Holds state that's shared across functions that react to changes on the
5256
/// filesystem.
5357
class _Watcher {
58+
/// The options for the Sass executable.
5459
final ExecutableOptions _options;
5560

61+
/// The graph of stylesheets being compiled.
5662
final StylesheetGraph _graph;
5763

5864
_Watcher(this._options, this._graph);
5965

6066
/// Compiles the stylesheet at [source] to [destination], and prints any
6167
/// errors that occur.
62-
Future compile(String source, String destination,
68+
///
69+
/// Returns whether or not compilation succeeded.
70+
Future<bool> compile(String source, String destination,
6371
{bool ifModified: false}) async {
6472
try {
6573
await compileStylesheet(_options, _graph, source, destination,
6674
ifModified: ifModified);
75+
return true;
6776
} on SassException catch (error, stackTrace) {
6877
_delete(destination);
6978
_printError(error.toString(color: _options.color), stackTrace);
79+
exitCode = 65;
80+
return false;
7081
} on FileSystemException catch (error, stackTrace) {
7182
_printError("Error reading ${p.relative(error.path)}: ${error.message}.",
7283
stackTrace);
84+
exitCode = 66;
85+
return false;
7386
}
7487
}
7588

@@ -97,7 +110,7 @@ class _Watcher {
97110
stderr.writeln(new Trace.from(stackTrace).terse.toString().trimRight());
98111
}
99112

100-
stderr.writeln();
113+
if (!_options.stopOnError) stderr.writeln();
101114
}
102115

103116
/// Listens to `watcher.events` and updates the filesystem accordingly.
@@ -119,31 +132,36 @@ class _Watcher {
119132
// from the graph.
120133
var node = _graph.nodes[url];
121134
_graph.reload(url);
122-
await _recompileDownstream([node]);
135+
var success = await _recompileDownstream([node]);
136+
if (!success && _options.stopOnError) return;
123137
break;
124138

125139
case ChangeType.ADD:
126-
await _retryPotentialImports(event.path);
140+
var success = await _retryPotentialImports(event.path);
141+
if (!success && _options.stopOnError) return;
127142

128143
var destination = _destinationFor(event.path);
129144
if (destination == null) continue loop;
130145

131146
_graph.addCanonical(
132147
new FilesystemImporter('.'), url, p.toUri(event.path));
133148

134-
await compile(event.path, destination);
149+
success = await compile(event.path, destination);
150+
if (!success && _options.stopOnError) return;
135151
break;
136152

137153
case ChangeType.REMOVE:
138-
await _retryPotentialImports(event.path);
154+
var success = await _retryPotentialImports(event.path);
155+
if (!success && _options.stopOnError) return;
139156
if (!_graph.nodes.containsKey(url)) continue loop;
140157

141158
var destination = _destinationFor(event.path);
142159
if (destination != null) _delete(destination);
143160

144161
var downstream = _graph.nodes[url].downstream;
145162
_graph.remove(url);
146-
await _recompileDownstream(downstream);
163+
success = await _recompileDownstream(downstream);
164+
if (!success && _options.stopOnError) return;
147165
break;
148166
}
149167
}
@@ -176,29 +194,38 @@ class _Watcher {
176194

177195
/// Recompiles [nodes] and everything that transitively imports them, if
178196
/// necessary.
179-
Future _recompileDownstream(Iterable<StylesheetNode> nodes) async {
197+
///
198+
/// Returns whether all recompilations succeeded.
199+
Future<bool> _recompileDownstream(Iterable<StylesheetNode> nodes) async {
180200
var seen = new Set<StylesheetNode>();
181201
var toRecompile = new Queue.of(nodes);
182202

203+
var allSucceeded = true;
183204
while (!toRecompile.isEmpty) {
184205
var node = toRecompile.removeFirst();
185206
if (!seen.add(node)) continue;
186207

187-
await _compileIfEntrypoint(node.canonicalUrl);
208+
var success = await _compileIfEntrypoint(node.canonicalUrl);
209+
allSucceeded = allSucceeded && success;
210+
if (!success && _options.stopOnError) return false;
211+
188212
toRecompile.addAll(node.downstream);
189213
}
214+
return allSucceeded;
190215
}
191216

192217
/// Compiles the stylesheet at [url] to CSS if it's an entrypoint that's being
193218
/// watched.
194-
Future _compileIfEntrypoint(Uri url) async {
195-
if (url.scheme != 'file') return;
219+
///
220+
/// Returns `false` if compilation failed, `true` otherwise.
221+
Future<bool> _compileIfEntrypoint(Uri url) async {
222+
if (url.scheme != 'file') return true;
196223

197224
var source = p.fromUri(url);
198225
var destination = _destinationFor(source);
199-
if (destination == null) return;
226+
if (destination == null) return true;
200227

201-
await compile(source, destination);
228+
return await compile(source, destination);
202229
}
203230

204231
/// If a Sass file at [source] should be compiled to CSS, returns the path to
@@ -223,7 +250,9 @@ class _Watcher {
223250
/// Re-runs all imports in [_graph] that might refer to [path], and recompiles
224251
/// the files that contain those imports if they end up importing new
225252
/// stylesheets.
226-
Future _retryPotentialImports(String path) async {
253+
///
254+
/// Returns whether all recompilations succeeded.
255+
Future<bool> _retryPotentialImports(String path) async {
227256
var name = _name(p.basename(path));
228257
var changed = <StylesheetNode>[];
229258
for (var node in _graph.nodes.values) {
@@ -250,7 +279,7 @@ class _Watcher {
250279
if (importChanged) changed.add(node);
251280
}
252281

253-
await _recompileDownstream(changed);
282+
return await _recompileDownstream(changed);
254283
}
255284

256285
/// Removes an extension from [extension], and a leading underscore if it has one.

lib/src/io/node.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,9 @@ Future<Stream<WatchEvent>> watchDir(String path, {bool poll: false}) {
263263

264264
var completer = new Completer<Stream<WatchEvent>>();
265265
watcher.on('ready', allowInterop(() {
266-
controller = new StreamController<WatchEvent>();
266+
controller = new StreamController<WatchEvent>(onCancel: () {
267+
watcher.close();
268+
});
267269
completer.complete(controller.stream);
268270
}));
269271

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: sass
2-
version: 1.8.0-dev
2+
version: 1.8.0
33
description: A Sass implementation in Dart.
44
author: Dart Team <[email protected]>
55
homepage: https://github.com/sass/dart-sass

test/cli/shared/colon_args.dart

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,41 @@ void sharedTests(Future<TestProcess> runSass(Iterable<String> arguments)) {
6868
await d.file("out2.css.map", contains("test2.scss")).validate();
6969
});
7070

71+
test("continues compiling after an error", () async {
72+
await d.file("test1.scss", "a {b: }").create();
73+
await d.file("test2.scss", "x {y: z}").create();
74+
75+
var sass = await runSass(
76+
["--no-source-map", "test1.scss:out1.css", "test2.scss:out2.css"]);
77+
await expectLater(sass.stderr, emits('Error: Expected expression.'));
78+
await expectLater(sass.stderr, emitsThrough(contains('test1.scss 1:7')));
79+
await sass.shouldExit(65);
80+
81+
await d.nothing("out1.css").validate();
82+
await d
83+
.file("out2.css", equalsIgnoringWhitespace("x { y: z; }"))
84+
.validate();
85+
});
86+
87+
test("stops compiling after an error with --stop-on-error", () async {
88+
await d.file("test1.scss", "a {b: }").create();
89+
await d.file("test2.scss", "x {y: z}").create();
90+
91+
var sass = await runSass(
92+
["--stop-on-error", "test1.scss:out1.css", "test2.scss:out2.css"]);
93+
await expectLater(
94+
sass.stderr,
95+
emitsInOrder([
96+
'Error: Expected expression.',
97+
emitsThrough(contains('test1.scss 1:7')),
98+
emitsDone
99+
]));
100+
await sass.shouldExit(65);
101+
102+
await d.nothing("out1.css").validate();
103+
await d.nothing("out2.css").validate();
104+
});
105+
71106
group("with a directory argument", () {
72107
test("compiles all the stylesheets in the directory", () async {
73108
await d.dir("in", [

test/cli/shared/watch.dart

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,48 @@ void sharedTests(Future<TestProcess> runSass(Iterable<String> arguments)) {
6464

6565
await d.file("out.css", "x {y: z}").validate();
6666
});
67+
68+
test("continues compiling after an error", () async {
69+
await d.file("test1.scss", "a {b: }").create();
70+
await d.file("test2.scss", "x {y: z}").create();
71+
72+
var sass =
73+
await watch(["test1.scss:out1.css", "test2.scss:out2.css"]);
74+
await expectLater(sass.stderr, emits('Error: Expected expression.'));
75+
await expectLater(
76+
sass.stderr, emitsThrough(contains('test1.scss 1:7')));
77+
await expectLater(
78+
sass.stdout, emitsThrough('Compiled test2.scss to out2.css.'));
79+
await expectLater(sass.stdout, _watchingForChanges);
80+
await sass.kill();
81+
82+
await d.nothing("out1.css").validate();
83+
await d
84+
.file("out2.css", equalsIgnoringWhitespace("x { y: z; }"))
85+
.validate();
86+
});
87+
88+
test("stops compiling after an error with --stop-on-error", () async {
89+
await d.file("test1.scss", "a {b: }").create();
90+
await d.file("test2.scss", "x {y: z}").create();
91+
92+
var sass = await watch([
93+
"--stop-on-error",
94+
"test1.scss:out1.css",
95+
"test2.scss:out2.css"
96+
]);
97+
await expectLater(
98+
sass.stderr,
99+
emitsInOrder([
100+
'Error: Expected expression.',
101+
emitsThrough(contains('test1.scss 1:7')),
102+
emitsDone
103+
]));
104+
await sass.shouldExit(65);
105+
106+
await d.nothing("out1.css").validate();
107+
await d.nothing("out2.css").validate();
108+
});
67109
});
68110

69111
group("recompiles a watched file", () {
@@ -168,6 +210,28 @@ void sharedTests(Future<TestProcess> runSass(Iterable<String> arguments)) {
168210
await d.nothing("out.css").validate();
169211
});
170212

213+
test("stops compiling after an error with --stop-on-error", () async {
214+
await d.file("test.scss", "a {b: c}").create();
215+
216+
var sass = await watch(["--stop-on-error", "test.scss:out.css"]);
217+
await expectLater(
218+
sass.stdout, emits('Compiled test.scss to out.css.'));
219+
await expectLater(sass.stdout, _watchingForChanges);
220+
await tickIfPoll();
221+
222+
await d.file("test.scss", "a {b: }").create();
223+
await expectLater(
224+
sass.stderr,
225+
emitsInOrder([
226+
'Error: Expected expression.',
227+
emitsThrough(contains('test.scss 1:7')),
228+
emitsDone
229+
]));
230+
await sass.shouldExit(65);
231+
232+
await d.nothing("out.css").validate();
233+
});
234+
171235
group("when its dependency is deleted", () {
172236
test("and removes the output", () async {
173237
await d.file("_other.scss", "a {b: c}").create();

0 commit comments

Comments
 (0)