Skip to content

Commit c7ab426

Browse files
mcrummnex3
andauthored
Abort sass if stdin is closed when watching (#1411)
Co-authored-by: Natalie Weizenbaum <[email protected]>
1 parent db85276 commit c7ab426

File tree

8 files changed

+125
-60
lines changed

8 files changed

+125
-60
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
## 1.49.12
1+
## 1.50.0
2+
3+
### Command Line Interface
4+
5+
* Closing the standard input stream will now cause the `--watch` command to stop
6+
running.
27

38
### Embedded Sass
49

bin/sass.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import 'dart:isolate';
66

7+
import 'package:async/async.dart';
78
import 'package:path/path.dart' as p;
89
import 'package:stack_trace/stack_trace.dart';
910
import 'package:term_glyph/term_glyph.dart' as term_glyph;
@@ -55,7 +56,8 @@ Future<void> main(List<String> args) async {
5556
var graph = StylesheetGraph(
5657
ImportCache(loadPaths: options.loadPaths, logger: options.logger));
5758
if (options.watch) {
58-
await watch(options, graph);
59+
await CancelableOperation.race([onStdinClose(), watch(options, graph)])
60+
.valueOrCancellation();
5961
return;
6062
}
6163

lib/src/executable/watch.dart

Lines changed: 71 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
// MIT-style license that can be found in the LICENSE file or at
33
// https://opensource.org/licenses/MIT.
44

5+
import 'dart:async';
56
import 'dart:collection';
67

8+
import 'package:async/async.dart';
79
import 'package:path/path.dart' as p;
810
import 'package:stack_trace/stack_trace.dart';
911
import 'package:stream_transform/stream_transform.dart';
@@ -19,41 +21,46 @@ import 'compile_stylesheet.dart';
1921
import 'options.dart';
2022

2123
/// Watches all the files in [graph] for changes and updates them as necessary.
22-
Future<void> watch(ExecutableOptions options, StylesheetGraph graph) async {
23-
var directoriesToWatch = [
24-
..._sourceDirectoriesToDestinations(options).keys,
25-
for (var dir in _sourcesToDestinations(options).keys) p.dirname(dir),
26-
...options.loadPaths
27-
];
28-
29-
var dirWatcher = MultiDirWatcher(poll: options.poll);
30-
await Future.wait(directoriesToWatch.map((dir) {
31-
// If a directory doesn't exist, watch its parent directory so that we're
32-
// notified once it starts existing.
33-
while (!dirExists(dir)) {
34-
dir = p.dirname(dir);
35-
}
36-
return dirWatcher.watch(dir);
37-
}));
38-
39-
// Before we start paying attention to changes, compile all the stylesheets as
40-
// they currently exist. This ensures that changes that come in update a
41-
// known-good state.
42-
var watcher = _Watcher(options, graph);
43-
for (var entry in _sourcesToDestinations(options).entries) {
44-
graph.addCanonical(FilesystemImporter('.'),
45-
p.toUri(canonicalize(entry.key)), p.toUri(entry.key),
46-
recanonicalize: false);
47-
var success =
48-
await watcher.compile(entry.key, entry.value, ifModified: true);
49-
if (!success && options.stopOnError) {
50-
dirWatcher.events.listen(null).cancel();
51-
return;
24+
///z
25+
/// Canceling the operation closes the watcher.
26+
CancelableOperation<void> watch(
27+
ExecutableOptions options, StylesheetGraph graph) {
28+
return unwrapCancelableOperation(() async {
29+
var directoriesToWatch = [
30+
..._sourceDirectoriesToDestinations(options).keys,
31+
for (var dir in _sourcesToDestinations(options).keys) p.dirname(dir),
32+
...options.loadPaths
33+
];
34+
35+
var dirWatcher = MultiDirWatcher(poll: options.poll);
36+
await Future.wait(directoriesToWatch.map((dir) {
37+
// If a directory doesn't exist, watch its parent directory so that we're
38+
// notified once it starts existing.
39+
while (!dirExists(dir)) {
40+
dir = p.dirname(dir);
41+
}
42+
return dirWatcher.watch(dir);
43+
}));
44+
45+
// Before we start paying attention to changes, compile all the stylesheets as
46+
// they currently exist. This ensures that changes that come in update a
47+
// known-good state.
48+
var watcher = _Watcher(options, graph);
49+
for (var entry in _sourcesToDestinations(options).entries) {
50+
graph.addCanonical(FilesystemImporter('.'),
51+
p.toUri(canonicalize(entry.key)), p.toUri(entry.key),
52+
recanonicalize: false);
53+
var success =
54+
await watcher.compile(entry.key, entry.value, ifModified: true);
55+
if (!success && options.stopOnError) {
56+
dirWatcher.events.listen(null).cancel();
57+
return CancelableOperation.fromFuture(Future<void>.value());
58+
}
5259
}
53-
}
5460

55-
print("Sass is watching for changes. Press Ctrl-C to stop.\n");
56-
await watcher.watch(dirWatcher);
61+
print("Sass is watching for changes. Press Ctrl-C to stop.\n");
62+
return watcher.watch(dirWatcher);
63+
}());
5764
}
5865

5966
/// Holds state that's shared across functions that react to changes on the
@@ -124,31 +131,39 @@ class _Watcher {
124131

125132
/// Listens to `watcher.events` and updates the filesystem accordingly.
126133
///
127-
/// Returns a future that will only complete if an unexpected error occurs.
128-
Future<void> watch(MultiDirWatcher watcher) async {
129-
await for (var event in _debounceEvents(watcher.events)) {
130-
var extension = p.extension(event.path);
131-
if (extension != '.sass' && extension != '.scss' && extension != '.css') {
132-
continue;
133-
}
134+
/// Returns an operation that will only complete if an unexpected error occurs
135+
/// (or if a complation error occurs and `--stop-on-error` is passed). This
136+
/// operation can be cancelled to close the watcher.
137+
CancelableOperation<void> watch(MultiDirWatcher watcher) {
138+
StreamSubscription<WatchEvent>? subscription;
139+
return CancelableOperation<void>.fromFuture(() async {
140+
subscription = _debounceEvents(watcher.events).listen(null);
141+
await for (var event in SubscriptionStream(subscription!)) {
142+
var extension = p.extension(event.path);
143+
if (extension != '.sass' &&
144+
extension != '.scss' &&
145+
extension != '.css') {
146+
continue;
147+
}
134148

135-
switch (event.type) {
136-
case ChangeType.MODIFY:
137-
var success = await _handleModify(event.path);
138-
if (!success && _options.stopOnError) return;
139-
break;
140-
141-
case ChangeType.ADD:
142-
var success = await _handleAdd(event.path);
143-
if (!success && _options.stopOnError) return;
144-
break;
145-
146-
case ChangeType.REMOVE:
147-
var success = await _handleRemove(event.path);
148-
if (!success && _options.stopOnError) return;
149-
break;
149+
switch (event.type) {
150+
case ChangeType.MODIFY:
151+
var success = await _handleModify(event.path);
152+
if (!success && _options.stopOnError) return;
153+
break;
154+
155+
case ChangeType.ADD:
156+
var success = await _handleAdd(event.path);
157+
if (!success && _options.stopOnError) return;
158+
break;
159+
160+
case ChangeType.REMOVE:
161+
var success = await _handleRemove(event.path);
162+
if (!success && _options.stopOnError) return;
163+
break;
164+
}
150165
}
151-
}
166+
}(), onCancel: () => subscription?.cancel());
152167
}
153168

154169
/// Handles a modify event for the stylesheet at [path].

lib/src/io/interface.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// MIT-style license that can be found in the LICENSE file or at
33
// https://opensource.org/licenses/MIT.
44

5+
import 'package:async/async.dart';
56
import 'package:watcher/watcher.dart';
67

78
/// An output sink that writes to this process's standard error.
@@ -94,6 +95,15 @@ String? getEnvironmentVariable(String name) => throw '';
9495
int get exitCode => throw '';
9596
set exitCode(int value) => throw '';
9697

98+
/// If stdin is a TTY, returns a [CancelableOperation] that completes once it
99+
/// closes.
100+
///
101+
/// Otherwise, returns a [CancelableOperation] that never completes.
102+
///
103+
/// As long as this is uncanceled, it will monopolize stdin so that nothing else
104+
/// can read from it.
105+
CancelableOperation<void> onStdinClose() => throw '';
106+
97107
/// Recursively watches the directory at [path] for modifications.
98108
///
99109
/// Returns a future that completes with a single-subscription stream once the

lib/src/io/node.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'dart:async';
66
import 'dart:convert';
77
import 'dart:js_util';
88

9+
import 'package:async/async.dart';
910
import 'package:js/js.dart';
1011
import 'package:node_interop/fs.dart';
1112
import 'package:node_interop/node_interop.dart';
@@ -195,6 +196,9 @@ final stderr = Stderr(process.stderr);
195196
@JS('process.stdout.isTTY')
196197
external bool? get isTTY;
197198

199+
@JS('process.stdin.isTTY')
200+
external bool? get isStdinTTY;
201+
198202
bool get hasTerminal => isTTY == true;
199203

200204
bool get isWindows => process.platform == 'win32';
@@ -212,6 +216,14 @@ int get exitCode => process.exitCode;
212216

213217
set exitCode(int code) => process.exitCode = code;
214218

219+
CancelableOperation<void> onStdinClose() {
220+
var completer = CancelableCompleter<void>();
221+
if (isStdinTTY == true) {
222+
process.stdin.on('end', allowInterop(() => completer.complete()));
223+
}
224+
return completer.operation;
225+
}
226+
215227
Future<Stream<WatchEvent>> watchDir(String path, {bool poll = false}) {
216228
var watcher = chokidar.watch(
217229
path, ChokidarOptions(disableGlobbing: true, usePolling: poll));

lib/src/io/vm.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ DateTime modificationTime(String path) {
9090

9191
String? getEnvironmentVariable(String name) => io.Platform.environment[name];
9292

93+
CancelableOperation<void> onStdinClose() => io.stdin.hasTerminal
94+
? CancelableOperation.fromSubscription(io.stdin.listen(null))
95+
: CancelableCompleter<void>().operation;
96+
9397
Future<Stream<WatchEvent>> watchDir(String path, {bool poll = false}) async {
9498
var watcher = poll ? PollingDirectoryWatcher(path) : DirectoryWatcher(path);
9599

lib/src/utils.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import 'dart:math' as math;
66

7+
import 'package:async/async.dart';
78
import 'package:charcode/charcode.dart';
89
import 'package:collection/collection.dart';
910
import 'package:source_span/source_span.dart';
@@ -379,6 +380,22 @@ Future<V> putIfAbsentAsync<K, V>(
379380
return value;
380381
}
381382

383+
/// Unwraps a [Future] that wraps a [CancelableOperation].
384+
///
385+
/// If the returned operation is cancelled, it will cancel the inner operation
386+
/// as soon as the future completes.
387+
CancelableOperation<T> unwrapCancelableOperation<T>(
388+
Future<CancelableOperation<T>> future) {
389+
var completer = CancelableCompleter<T>(
390+
onCancel: () => future.then((operation) => operation.cancel()));
391+
392+
future.then((operation) {
393+
operation.then(completer.complete, onError: completer.completeError);
394+
}, onError: completer.completeError);
395+
396+
return completer.operation;
397+
}
398+
382399
/// Returns a deep copy of a map that contains maps.
383400
Map<K1, Map<K2, V>> copyMapOfMap<K1, K2, V>(Map<K1, Map<K2, V>> map) =>
384401
{for (var entry in map.entries) entry.key: Map.of(entry.value)};

pubspec.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: sass
2-
version: 1.49.12-dev
2+
version: 1.50.0-dev
33
description: A Sass implementation in Dart.
44
homepage: https://github.com/sass/dart-sass
55

@@ -12,7 +12,7 @@ environment:
1212

1313
dependencies:
1414
args: ^2.0.0
15-
async: ^2.5.0
15+
async: ^2.9.0
1616
charcode: ^1.2.0
1717
cli_repl: ^0.2.1
1818
collection: ^1.15.0

0 commit comments

Comments
 (0)