Skip to content

Commit a333059

Browse files
committed
Keep recompiling downstream dependencies after an error in --watch
Prior to this, the watcher handled all the logic around recompiling stylesheets if an upstream file was deleted or added in a way that could affect their import resolution. This left a gap where the stylesheet graph wouldn't be aware that a newly-added file had become upstream dependency of an existing downstream file, leading to recompilation failures. This commit fixes that by moving all that logic into the stylesheet graph. The graph now has full and sole responsibility for providing a consistent view of which stylesheets depend on one another even as the shape of the graph changes, and the watcher is just a client of that logic. Closes #550
1 parent f8c74d1 commit a333059

File tree

11 files changed

+177
-124
lines changed

11 files changed

+177
-124
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
## 1.26.1
2+
3+
### Command Line Interface
4+
5+
* Fix a longstanding bug where `--watch` mode could enter into a state where
6+
recompilation would not occur after a syntax error was introduced into a
7+
dependency and then fixed.
8+
19
## 1.26.0
210

311
* **Potentially breaking bug fix:** `@use` rules whose URLs' basenames begin

lib/src/async_import_cache.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ class AsyncImportCache {
112112
/// canonicalize [url] (resolved relative to [baseUrl] if it's passed).
113113
///
114114
/// If any importers understand [url], returns that importer as well as the
115-
/// canonicalized URL. Otherwise, returns `null`.
115+
/// canonicalized URL and the original URL resolved relative to [baseUrl] if
116+
/// applicable. Otherwise, returns `null`.
116117
Future<Tuple3<AsyncImporter, Uri, Uri>> canonicalize(Uri url,
117118
{AsyncImporter baseImporter, Uri baseUrl, bool forImport = false}) async {
118119
if (baseImporter != null) {

lib/src/executable/watch.dart

Lines changed: 8 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import 'dart:async';
66
import 'dart:collection';
77

8-
import 'package:meta/meta.dart';
98
import 'package:path/path.dart' as p;
109
import 'package:stack_trace/stack_trace.dart';
1110
import 'package:stream_transform/stream_transform.dart';
@@ -165,32 +164,26 @@ class _Watcher {
165164
///
166165
/// Returns whether all necessary recompilations succeeded.
167166
Future<bool> _handleAdd(String path) async {
168-
var success = await _retryPotentialImports(path);
169-
if (!success && _options.stopOnError) return false;
170-
171167
var destination = _destinationFor(path);
172-
if (destination == null) return true;
173168

174-
_graph.addCanonical(
169+
var success = destination == null || await compile(path, destination);
170+
var downstream = _graph.addCanonical(
175171
FilesystemImporter('.'), _canonicalize(path), p.toUri(path));
176-
177-
return await compile(path, destination);
172+
return await _recompileDownstream(downstream) && success;
178173
}
179174

180175
/// Handles a remove event for the stylesheet at [url].
181176
///
182177
/// Returns whether all necessary recompilations succeeded.
183178
Future<bool> _handleRemove(String path) async {
184179
var url = _canonicalize(path);
185-
var success = await _retryPotentialImports(path);
186-
if (!success && _options.stopOnError) return false;
187-
if (!_graph.nodes.containsKey(url)) return true;
188180

189-
var destination = _destinationFor(path);
190-
if (destination != null) _delete(destination);
181+
if (_graph.nodes.containsKey(url)) {
182+
var destination = _destinationFor(path);
183+
if (destination != null) _delete(destination);
184+
}
191185

192-
var downstream = _graph.nodes[url].downstream;
193-
_graph.remove(url);
186+
var downstream = _graph.remove(FilesystemImporter('.'), url);
194187
return await _recompileDownstream(downstream);
195188
}
196189

@@ -278,56 +271,4 @@ class _Watcher {
278271

279272
return null;
280273
}
281-
282-
/// Re-runs all imports in [_graph] that might refer to [path], and recompiles
283-
/// the files that contain those imports if they end up importing new
284-
/// stylesheets.
285-
///
286-
/// Returns whether all recompilations succeeded.
287-
Future<bool> _retryPotentialImports(String path) async {
288-
var name = _name(p.basename(path));
289-
var changed = <StylesheetNode>[];
290-
for (var node in _graph.nodes.values) {
291-
var importChanged = false;
292-
void recanonicalize(Uri url, StylesheetNode upstream,
293-
{@required bool forImport}) {
294-
if (_name(p.url.basename(url.path)) != name) return;
295-
_graph.clearCanonicalize(url);
296-
297-
// If the import produces a different canonicalized URL than it did
298-
// before, it changed and the stylesheet needs to be recompiled.
299-
if (!importChanged) {
300-
Uri newCanonicalUrl;
301-
try {
302-
newCanonicalUrl = _graph.importCache
303-
.canonicalize(url,
304-
baseImporter: node.importer,
305-
baseUrl: node.canonicalUrl,
306-
forImport: forImport)
307-
?.item2;
308-
} catch (_) {
309-
// If the call to canonicalize failed, do nothing. We'll surface
310-
// the error more nicely when we try to recompile the file.
311-
}
312-
importChanged = newCanonicalUrl != upstream?.canonicalUrl;
313-
}
314-
}
315-
316-
for (var entry in node.upstream.entries) {
317-
recanonicalize(entry.key, entry.value, forImport: false);
318-
}
319-
for (var entry in node.upstreamImports.entries) {
320-
recanonicalize(entry.key, entry.value, forImport: true);
321-
}
322-
if (importChanged) changed.add(node);
323-
}
324-
325-
return await _recompileDownstream(changed);
326-
}
327-
328-
/// Removes an extension from [extension], and a leading underscore if it has one.
329-
String _name(String basename) {
330-
basename = p.withoutExtension(basename);
331-
return basename.startsWith("_") ? basename.substring(1) : basename;
332-
}
333274
}

lib/src/import_cache.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
// DO NOT EDIT. This file was generated from async_import_cache.dart.
66
// See tool/grind/synchronize.dart for details.
77
//
8-
// Checksum: 3ca2f221c3c1503c688be49d4f7501bd9be8031a
8+
// Checksum: c1dd0f2a41b03bd1227a2358688bf44499b9d90c
99
//
1010
// ignore_for_file: unused_import
1111

@@ -116,7 +116,8 @@ class ImportCache {
116116
/// canonicalize [url] (resolved relative to [baseUrl] if it's passed).
117117
///
118118
/// If any importers understand [url], returns that importer as well as the
119-
/// canonicalized URL. Otherwise, returns `null`.
119+
/// canonicalized URL and the original URL resolved relative to [baseUrl] if
120+
/// applicable. Otherwise, returns `null`.
120121
Tuple3<Importer, Uri, Uri> canonicalize(Uri url,
121122
{Importer baseImporter, Uri baseUrl, bool forImport = false}) {
122123
if (baseImporter != null) {

lib/src/importer.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,6 @@ abstract class Importer extends AsyncImporter {
3636
ImporterResult load(Uri url);
3737

3838
DateTime modificationTime(Uri url) => DateTime.now();
39+
40+
bool couldCanonicalize(Uri url, Uri canonicalUrl) => true;
3941
}

lib/src/importer/async.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,13 @@ abstract class AsyncImporter {
107107
/// If this throws an exception, the exception is ignored and the current time
108108
/// is used as the modification time.
109109
FutureOr<DateTime> modificationTime(Uri url) => DateTime.now();
110+
111+
/// Without accessing the filesystem, returns whether or not passing [url] to
112+
/// [canonicalize] could possibly return [canonicalUrl].
113+
///
114+
/// This is expected to be very efficient, and subclasses are allowed to
115+
/// return false positives if it would be inefficient to determine whether
116+
/// [url] would actually resolve to [canonicalUrl]. Subclasses are not allowed
117+
/// to return false negatives.
118+
FutureOr<bool> couldCanonicalize(Uri url, Uri canonicalUrl) => true;
110119
}

lib/src/importer/filesystem.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,19 @@ class FilesystemImporter extends Importer {
3939

4040
DateTime modificationTime(Uri url) => io.modificationTime(p.fromUri(url));
4141

42+
bool couldCanonicalize(Uri url, Uri canonicalUrl) {
43+
if (url.scheme != 'file' && url.scheme != '') return false;
44+
if (canonicalUrl.scheme != 'file') return false;
45+
46+
var basename = p.url.basename(url.path);
47+
var canonicalBasename = p.url.basename(canonicalUrl.path);
48+
if (!basename.startsWith("_") && canonicalBasename.startsWith("_")) {
49+
canonicalBasename = canonicalBasename.substring(1);
50+
}
51+
52+
return basename == canonicalBasename ||
53+
basename == p.url.withoutExtension(canonicalBasename);
54+
}
55+
4256
String toString() => _loadPath;
4357
}

lib/src/importer/no_op.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'result.dart';
1212
class NoOpImporter extends Importer {
1313
Uri canonicalize(Uri url) => null;
1414
ImporterResult load(Uri url) => null;
15+
bool couldCanonicalize(Uri url, Uri canonicalUrl) => false;
1516

1617
String toString() => "(unknown)";
1718
}

lib/src/importer/package.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,9 @@ class PackageImporter extends Importer {
4747
DateTime modificationTime(Uri url) =>
4848
_filesystemImporter.modificationTime(url);
4949

50+
bool couldCanonicalize(Uri url, Uri canonicalUrl) =>
51+
(url.scheme == 'file' || url.scheme == 'package' || url.scheme == '') &&
52+
_filesystemImporter.couldCanonicalize(Uri(path: url.path), canonicalUrl);
53+
5054
String toString() => "package:...";
5155
}

0 commit comments

Comments
 (0)