mirror of
https://github.com/danog/dart-sass.git
synced 2024-11-30 04:39:03 +01:00
parent
a2c0f15d67
commit
6bbb961675
@ -151,6 +151,13 @@ class AsyncImportCache {
|
||||
});
|
||||
}
|
||||
|
||||
/// Clears the cached canonical version of the given [url].
|
||||
///
|
||||
/// Has no effect if the canonical version of [url] has not been cached.
|
||||
void clearCanonicalize(Uri url) {
|
||||
_canonicalizeCache.remove(url);
|
||||
}
|
||||
|
||||
/// Clears the cached parse tree for the stylesheet with the given
|
||||
/// [canonicalUrl].
|
||||
///
|
||||
|
@ -11,6 +11,7 @@ import 'exception.dart';
|
||||
import 'executable/compile_stylesheet.dart';
|
||||
import 'executable/options.dart';
|
||||
import 'executable/repl.dart';
|
||||
import 'executable/watch.dart';
|
||||
import 'import_cache.dart';
|
||||
import 'io.dart';
|
||||
import 'stylesheet_graph.dart';
|
||||
@ -50,10 +51,16 @@ main(List<String> args) async {
|
||||
|
||||
var graph = new StylesheetGraph(new ImportCache([],
|
||||
loadPaths: options.loadPaths, logger: options.logger));
|
||||
if (options.watch) {
|
||||
await watch(options, graph);
|
||||
return;
|
||||
}
|
||||
|
||||
for (var source in options.sourcesToDestinations.keys) {
|
||||
var destination = options.sourcesToDestinations[source];
|
||||
try {
|
||||
await compileStylesheet(options, graph, source, destination);
|
||||
await compileStylesheet(options, graph, source, destination,
|
||||
ifModified: options.update);
|
||||
} on SassException catch (error, stackTrace) {
|
||||
// This is an immediately-invoked function expression to work around
|
||||
// dart-lang/sdk#33400.
|
||||
|
@ -26,10 +26,15 @@ import 'options.dart';
|
||||
/// If [source] is `null`, that indicates that the stylesheet should be read
|
||||
/// from stdin. If [destination] is `null`, that indicates that the stylesheet
|
||||
/// should be emitted to stdout.
|
||||
///
|
||||
/// If [ifModified] is `true`, only recompiles if [source]'s modification time
|
||||
/// or that of a file it imports is more recent than [destination]'s
|
||||
/// modification time. Note that these modification times are cached by [graph].
|
||||
Future compileStylesheet(ExecutableOptions options, StylesheetGraph graph,
|
||||
String source, String destination) async {
|
||||
String source, String destination,
|
||||
{bool ifModified: false}) async {
|
||||
var importer = new FilesystemImporter('.');
|
||||
if (options.update) {
|
||||
if (ifModified) {
|
||||
try {
|
||||
if (source != null &&
|
||||
destination != null &&
|
||||
@ -50,7 +55,7 @@ Future compileStylesheet(ExecutableOptions options, StylesheetGraph graph,
|
||||
importer: importer,
|
||||
logger: options.logger,
|
||||
sourceMap: options.emitSourceMap)
|
||||
: await evaluate(stylesheet,
|
||||
: evaluate(stylesheet,
|
||||
importCache: graph.importCache,
|
||||
importer: importer,
|
||||
logger: options.logger,
|
||||
@ -68,10 +73,12 @@ Future compileStylesheet(ExecutableOptions options, StylesheetGraph graph,
|
||||
writeFile(destination, css + "\n");
|
||||
}
|
||||
|
||||
if (!options.update || options.quiet) return;
|
||||
if (options.quiet || (!options.update && !options.watch)) return;
|
||||
var buffer = new StringBuffer();
|
||||
if (options.color) buffer.write('\u001b[32m');
|
||||
buffer.write('Compiled ${source ?? 'stdin'} to $destination.');
|
||||
|
||||
var sourceName = source == null ? 'stdin' : p.prettyUri(p.toUri(source));
|
||||
buffer.write('Compiled $sourceName to $destination.');
|
||||
if (options.color) buffer.write('\u001b[0m');
|
||||
print(buffer);
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import 'dart:collection';
|
||||
|
||||
import 'package:args/args.dart';
|
||||
import 'package:charcode/charcode.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import '../../sass.dart';
|
||||
@ -71,6 +72,9 @@ class ExecutableOptions {
|
||||
|
||||
parser
|
||||
..addSeparator(_separator('Other'))
|
||||
..addFlag('watch',
|
||||
help: 'Watch stylesheets and recompile when they change.',
|
||||
negatable: false)
|
||||
..addFlag('interactive',
|
||||
abbr: 'i',
|
||||
help: 'Run an interactive SassScript shell.',
|
||||
@ -163,6 +167,9 @@ class ExecutableOptions {
|
||||
/// Whether to update only files that have changed since the last compilation.
|
||||
bool get update => _options['update'] as bool;
|
||||
|
||||
/// Whether to continuously watch the filesystem for changes.
|
||||
bool get watch => _options['watch'] as bool;
|
||||
|
||||
/// A map from source paths to the destination paths where the compiled CSS
|
||||
/// should be written.
|
||||
///
|
||||
@ -172,7 +179,27 @@ class ExecutableOptions {
|
||||
/// input. A `null` destination indicates that a stylesheet should be written
|
||||
/// to standard output.
|
||||
Map<String, String> get sourcesToDestinations {
|
||||
if (_sourcesToDestinations != null) return _sourcesToDestinations;
|
||||
_ensureSources();
|
||||
return _sourcesToDestinations;
|
||||
}
|
||||
|
||||
Map<String, String> _sourcesToDestinations;
|
||||
|
||||
/// A map from source directories to the destination directories where the
|
||||
/// compiled CSS for stylesheets in the source directories should be written.
|
||||
///
|
||||
/// Considers keys to be the same if they represent the same path on disk.
|
||||
Map<String, String> get sourceDirectoriesToDestinations {
|
||||
_ensureSources();
|
||||
return _sourceDirectoriesToDestinations;
|
||||
}
|
||||
|
||||
Map<String, String> _sourceDirectoriesToDestinations;
|
||||
|
||||
/// Ensure that both [sourcesToDestinations] and [sourceDirectories] have been
|
||||
/// computed.
|
||||
void _ensureSources() {
|
||||
if (_sourcesToDestinations != null) return;
|
||||
|
||||
var stdin = _options['stdin'] as bool;
|
||||
if (_options.rest.isEmpty && !stdin) _fail("Compile Sass to CSS.");
|
||||
@ -203,6 +230,8 @@ class ExecutableOptions {
|
||||
_fail("Only one argument is allowed with --stdin.");
|
||||
} else if (update) {
|
||||
_fail("--update is not allowed with --stdin.");
|
||||
} else if (watch) {
|
||||
_fail("--watch is not allowed with --stdin.");
|
||||
}
|
||||
_sourcesToDestinations = new Map.unmodifiable(
|
||||
{null: _options.rest.isEmpty ? null : _options.rest.first});
|
||||
@ -211,13 +240,18 @@ class ExecutableOptions {
|
||||
} else {
|
||||
var source = _options.rest.first == '-' ? null : _options.rest.first;
|
||||
var destination = _options.rest.length == 1 ? null : _options.rest.last;
|
||||
if (update && destination == null) {
|
||||
_fail("--update is not allowed when printing to stdout.");
|
||||
if (destination == null) {
|
||||
if (update) {
|
||||
_fail("--update is not allowed when printing to stdout.");
|
||||
} else if (watch) {
|
||||
_fail("--watch is not allowed when printing to stdout.");
|
||||
}
|
||||
}
|
||||
_sourcesToDestinations =
|
||||
new UnmodifiableMapView(newPathMap({source: destination}));
|
||||
}
|
||||
return _sourcesToDestinations;
|
||||
_sourceDirectoriesToDestinations = const {};
|
||||
return;
|
||||
}
|
||||
|
||||
if (stdin) _fail('--stdin may not be used with ":" arguments.');
|
||||
@ -227,6 +261,7 @@ class ExecutableOptions {
|
||||
// directories have been resolved.
|
||||
var seen = new Set<String>();
|
||||
var sourcesToDestinations = newPathMap<String>();
|
||||
var sourceDirectoriesToDestinations = newPathMap<String>();
|
||||
for (var argument in _options.rest) {
|
||||
String source;
|
||||
String destination;
|
||||
@ -255,17 +290,17 @@ class ExecutableOptions {
|
||||
if (source == '-') {
|
||||
sourcesToDestinations[null] = destination;
|
||||
} else if (dirExists(source)) {
|
||||
sourceDirectoriesToDestinations[source] = destination;
|
||||
sourcesToDestinations.addAll(_listSourceDirectory(source, destination));
|
||||
} else {
|
||||
sourcesToDestinations[source] = destination;
|
||||
}
|
||||
}
|
||||
_sourcesToDestinations = new UnmodifiableMapView(sourcesToDestinations);
|
||||
return _sourcesToDestinations;
|
||||
_sourceDirectoriesToDestinations =
|
||||
new UnmodifiableMapView(sourceDirectoriesToDestinations);
|
||||
}
|
||||
|
||||
Map<String, String> _sourcesToDestinations;
|
||||
|
||||
/// Returns whether [string] contains an absolute Windows path at [index].
|
||||
bool _isWindowsPath(String string, int index) =>
|
||||
string.length > index + 2 &&
|
||||
|
230
lib/src/executable/watch.dart
Normal file
230
lib/src/executable/watch.dart
Normal file
@ -0,0 +1,230 @@
|
||||
// Copyright 2018 Google Inc. Use of this source code is governed by an
|
||||
// MIT-style license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:stack_trace/stack_trace.dart';
|
||||
import 'package:watcher/watcher.dart';
|
||||
|
||||
import '../exception.dart';
|
||||
import '../importer/filesystem.dart';
|
||||
import '../io.dart';
|
||||
import '../stylesheet_graph.dart';
|
||||
import '../util/multi_dir_watcher.dart';
|
||||
import '../util/path.dart';
|
||||
import 'compile_stylesheet.dart';
|
||||
import 'options.dart';
|
||||
|
||||
/// Watches all the files in [graph] for changes and updates them as necessary.
|
||||
Future watch(ExecutableOptions options, StylesheetGraph graph) async {
|
||||
var directoriesToWatch = <String>[]
|
||||
..addAll(options.sourceDirectoriesToDestinations.keys)
|
||||
..addAll(options.sourcesToDestinations.keys.map((path) => p.dirname(path)))
|
||||
..addAll(options.loadPaths);
|
||||
|
||||
var dirWatcher = new MultiDirWatcher();
|
||||
await Future.wait(directoriesToWatch.map((dir) {
|
||||
// If a directory doesn't exist, watch its parent directory so that we're
|
||||
// notified once it starts existing.
|
||||
while (!dirExists(dir)) dir = p.dirname(dir);
|
||||
return dirWatcher.watch(dir);
|
||||
}));
|
||||
|
||||
// Before we start paying attention to changes, compile all the stylesheets as
|
||||
// they currently exist. This ensures that changes that come in update a
|
||||
// known-good state.
|
||||
var watcher = new _Watcher(options, graph);
|
||||
for (var source in options.sourcesToDestinations.keys) {
|
||||
var destination = options.sourcesToDestinations[source];
|
||||
graph.addCanonical(new FilesystemImporter('.'),
|
||||
p.toUri(p.canonicalize(source)), p.toUri(source));
|
||||
await watcher.compile(source, destination, ifModified: true);
|
||||
}
|
||||
|
||||
print("Sass is watching for changes. Press Ctrl-C to stop.\n");
|
||||
await watcher.watch(dirWatcher);
|
||||
}
|
||||
|
||||
/// Holds state that's shared across functions that react to changes on the
|
||||
/// filesystem.
|
||||
class _Watcher {
|
||||
final ExecutableOptions _options;
|
||||
|
||||
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,
|
||||
{bool ifModified: false}) async {
|
||||
try {
|
||||
await compileStylesheet(_options, _graph, source, destination,
|
||||
ifModified: ifModified);
|
||||
} on SassException catch (error, stackTrace) {
|
||||
_printError(error.toString(color: _options.color), stackTrace);
|
||||
_delete(destination);
|
||||
} on FileSystemException catch (error, stackTrace) {
|
||||
_printError("Error reading ${p.relative(error.path)}: ${error.message}.",
|
||||
stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes the file at [path] and prints a message about it.
|
||||
void _delete(String path) {
|
||||
try {
|
||||
deleteFile(path);
|
||||
var buffer = new StringBuffer();
|
||||
if (_options.color) buffer.write("\u001b[33m");
|
||||
buffer.write("Deleted $path.");
|
||||
if (_options.color) buffer.write("\u001b[0m");
|
||||
print(buffer);
|
||||
} on FileSystemException {
|
||||
// If the file doesn't exist, that's fine.
|
||||
}
|
||||
}
|
||||
|
||||
/// Prints [message] to standard error, with [stackTrace] if [_options.trace]
|
||||
/// is set.
|
||||
void _printError(String message, StackTrace stackTrace) {
|
||||
stderr.writeln(message);
|
||||
|
||||
if (_options.trace) {
|
||||
stderr.writeln();
|
||||
stderr.writeln(new Trace.from(stackTrace).terse.toString().trimRight());
|
||||
}
|
||||
|
||||
stderr.writeln();
|
||||
}
|
||||
|
||||
/// Listens to `watcher.events` and updates the filesystem accordingly.
|
||||
///
|
||||
/// Returns a future that will only complete if an unexpected error occurs.
|
||||
Future watch(MultiDirWatcher watcher) async {
|
||||
loop:
|
||||
await for (var event in watcher.events) {
|
||||
var extension = p.extension(event.path);
|
||||
if (extension != '.sass' && extension != '.scss') continue;
|
||||
var url = p.toUri(p.canonicalize(event.path));
|
||||
|
||||
switch (event.type) {
|
||||
case ChangeType.MODIFY:
|
||||
if (!_graph.nodes.containsKey(url)) continue loop;
|
||||
|
||||
_graph.reload(url);
|
||||
await _recompileDownstream([_graph.nodes[url]]);
|
||||
break;
|
||||
|
||||
case ChangeType.ADD:
|
||||
await _retryPotentialImports(event.path);
|
||||
|
||||
var destination = _destinationFor(event.path);
|
||||
if (destination == null) continue loop;
|
||||
|
||||
_graph.addCanonical(
|
||||
new FilesystemImporter('.'), url, p.toUri(event.path));
|
||||
|
||||
await compile(event.path, destination);
|
||||
break;
|
||||
|
||||
case ChangeType.REMOVE:
|
||||
await _retryPotentialImports(event.path);
|
||||
if (!_graph.nodes.containsKey(url)) continue loop;
|
||||
|
||||
var destination = _destinationFor(event.path);
|
||||
if (destination != null) _delete(destination);
|
||||
|
||||
_graph.remove(url);
|
||||
await _recompileDownstream([_graph.nodes[url]]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Recompiles [nodes] and everything that transitively imports them, if
|
||||
/// necessary.
|
||||
Future _recompileDownstream(Iterable<StylesheetNode> nodes) async {
|
||||
var seen = new Set<StylesheetNode>();
|
||||
var toRecompile = new Queue<StylesheetNode>.from(nodes);
|
||||
|
||||
while (!toRecompile.isEmpty) {
|
||||
var node = toRecompile.removeFirst();
|
||||
if (!seen.add(node)) continue;
|
||||
|
||||
await _compileIfEntrypoint(node.canonicalUrl);
|
||||
toRecompile.addAll(node.downstream);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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;
|
||||
|
||||
var source = p.fromUri(url);
|
||||
var destination = _destinationFor(source);
|
||||
if (destination == null) return;
|
||||
|
||||
await compile(source, destination);
|
||||
}
|
||||
|
||||
/// If a Sass file at [source] should be compiled to CSS, returns the path to
|
||||
/// the CSS file it should be compiled to.
|
||||
///
|
||||
/// Otherwise, returns `null`.
|
||||
String _destinationFor(String source) {
|
||||
var destination = _options.sourcesToDestinations[source];
|
||||
if (destination != null) return destination;
|
||||
if (p.basename(source).startsWith('_')) return null;
|
||||
|
||||
for (var sourceDir in _options.sourceDirectoriesToDestinations.keys) {
|
||||
if (p.isWithin(sourceDir, source)) {
|
||||
return p.join(_options.sourceDirectoriesToDestinations[sourceDir],
|
||||
p.setExtension(p.relative(source, from: sourceDir), '.css'));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
var name = _name(p.basename(path));
|
||||
var changed = <StylesheetNode>[];
|
||||
for (var node in _graph.nodes.values) {
|
||||
var importChanged = false;
|
||||
for (var url in node.upstream.keys) {
|
||||
if (_name(pUrl.basename(url.path)) != name) continue;
|
||||
_graph.clearCanonicalize(url);
|
||||
|
||||
// If the import produces a different canonicalized URL than it did
|
||||
// before, it changed and the stylesheet needs to be recompiled.
|
||||
if (!importChanged) {
|
||||
Uri newCanonicalUrl;
|
||||
try {
|
||||
newCanonicalUrl = _graph.importCache
|
||||
.canonicalize(url, node.importer, node.canonicalUrl)
|
||||
?.item2;
|
||||
} catch (_) {
|
||||
// If the call to canonicalize failed, do nothing. We'll surface the
|
||||
// error more nicely when we try to recompile the file.
|
||||
}
|
||||
importChanged = newCanonicalUrl != node.upstream[url]?.canonicalUrl;
|
||||
}
|
||||
}
|
||||
if (importChanged) changed.add(node);
|
||||
}
|
||||
|
||||
await _recompileDownstream(changed);
|
||||
}
|
||||
|
||||
/// Removes an extension from [extension], and a leading underscore if it has one.
|
||||
String _name(String basename) {
|
||||
basename = p.withoutExtension(basename);
|
||||
return basename.startsWith("_") ? basename.substring(1) : basename;
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@
|
||||
// DO NOT EDIT. This file was generated from async_import_cache.dart.
|
||||
// See tool/synchronize.dart for details.
|
||||
//
|
||||
// Checksum: 90302aba01c574ffbc07e75a34606406e48df7fc
|
||||
// Checksum: 411ad1e666d5a138e69b51d9bdb58e9b22e88632
|
||||
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
@ -154,6 +154,13 @@ class ImportCache {
|
||||
});
|
||||
}
|
||||
|
||||
/// Clears the cached canonical version of the given [url].
|
||||
///
|
||||
/// Has no effect if the canonical version of [url] has not been cached.
|
||||
void clearCanonicalize(Uri url) {
|
||||
_canonicalizeCache.remove(url);
|
||||
}
|
||||
|
||||
/// Clears the cached parse tree for the stylesheet with the given
|
||||
/// [canonicalUrl].
|
||||
///
|
||||
|
@ -4,6 +4,8 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:watcher/watcher.dart';
|
||||
|
||||
/// An output sink that writes to this process's standard error.
|
||||
class Stderr {
|
||||
/// Writes the string representation of [object] to standard error.
|
||||
@ -79,3 +81,10 @@ DateTime modificationTime(String path) => null;
|
||||
|
||||
/// Gets and sets the exit code that the process will use when it exits.
|
||||
int exitCode;
|
||||
|
||||
/// Recursively watches the directory at [path] for modifications.
|
||||
///
|
||||
/// Returns a future that completes with a single-subscription stream once the
|
||||
/// directory has been scanned initially. The watch is canceled when the stream
|
||||
/// is closed.
|
||||
Future<Stream<WatchEvent>> watchDir(String path) => null;
|
||||
|
@ -8,8 +8,10 @@ import 'dart:convert';
|
||||
import 'package:dart2_constant/convert.dart' as convert;
|
||||
import 'package:js/js.dart';
|
||||
import 'package:source_span/source_span.dart';
|
||||
import 'package:watcher/watcher.dart';
|
||||
|
||||
import '../exception.dart';
|
||||
import '../node/chokidar.dart';
|
||||
import '../util/path.dart';
|
||||
|
||||
@JS()
|
||||
@ -237,3 +239,34 @@ external int get exitCode;
|
||||
|
||||
@JS("process.exitCode")
|
||||
external set exitCode(int code);
|
||||
|
||||
Future<Stream<WatchEvent>> watchDir(String path) {
|
||||
var watcher =
|
||||
chokidar.watch(path, new ChokidarOptions(disableGlobbing: true));
|
||||
|
||||
// Don't assign the controller until after the ready event fires. Otherwise,
|
||||
// Chokidar will give us a bunch of add events for files that already exist.
|
||||
StreamController<WatchEvent> controller;
|
||||
watcher
|
||||
..on(
|
||||
'add',
|
||||
allowInterop((String path, _) =>
|
||||
controller?.add(new WatchEvent(ChangeType.ADD, path))))
|
||||
..on(
|
||||
'change',
|
||||
allowInterop((String path, _) =>
|
||||
controller?.add(new WatchEvent(ChangeType.MODIFY, path))))
|
||||
..on(
|
||||
'unlink',
|
||||
allowInterop((String path) =>
|
||||
controller?.add(new WatchEvent(ChangeType.REMOVE, path))))
|
||||
..on('error', allowInterop((error) => controller?.addError(error)));
|
||||
|
||||
var completer = new Completer<Stream<WatchEvent>>();
|
||||
watcher.on('ready', allowInterop(() {
|
||||
controller = new StreamController<WatchEvent>();
|
||||
completer.complete(controller.stream);
|
||||
}));
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
@ -5,9 +5,11 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io' as io;
|
||||
|
||||
import 'package:async/async.dart';
|
||||
import 'package:dart2_constant/convert.dart' as convert;
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:source_span/source_span.dart';
|
||||
import 'package:watcher/watcher.dart';
|
||||
|
||||
import '../exception.dart';
|
||||
|
||||
@ -71,3 +73,16 @@ DateTime modificationTime(String path) {
|
||||
}
|
||||
return stat.modified;
|
||||
}
|
||||
|
||||
Future<Stream<WatchEvent>> watchDir(String path) async {
|
||||
var watcher = new DirectoryWatcher(path);
|
||||
|
||||
// Wrap [stream] in a [SubscriptionStream] so that its `onListen` event
|
||||
// triggers but the caller can still listen at their leisure.
|
||||
var stream = new SubscriptionStream<WatchEvent>(watcher.events
|
||||
.transform(const SingleSubscriptionTransformer<WatchEvent, WatchEvent>())
|
||||
.listen((e) => print(e)));
|
||||
await watcher.ready;
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
32
lib/src/node/chokidar.dart
Normal file
32
lib/src/node/chokidar.dart
Normal file
@ -0,0 +1,32 @@
|
||||
// Copyright 2018 Google Inc. Use of this source code is governed by an
|
||||
// MIT-style license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
import 'package:js/js.dart';
|
||||
|
||||
@JS("require")
|
||||
external Chokidar _require(String name);
|
||||
|
||||
@JS()
|
||||
class Chokidar {
|
||||
external ChokidarWatcher watch(String path, ChokidarOptions options);
|
||||
}
|
||||
|
||||
@JS()
|
||||
@anonymous
|
||||
class ChokidarOptions {
|
||||
external bool get disableGlobbing;
|
||||
|
||||
external factory ChokidarOptions({bool disableGlobbing});
|
||||
}
|
||||
|
||||
@JS()
|
||||
class ChokidarWatcher {
|
||||
external void on(String event, Function callback);
|
||||
external void close();
|
||||
}
|
||||
|
||||
/// The Chokidar module.
|
||||
///
|
||||
/// See [the docs on npm](https://www.npmjs.com/package/chokidar).
|
||||
final chokidar = _require("chokidar");
|
@ -130,6 +130,7 @@ class StylesheetGraph {
|
||||
remove(canonicalUrl);
|
||||
return null;
|
||||
}
|
||||
node._stylesheet = stylesheet;
|
||||
|
||||
node._stylesheet = stylesheet;
|
||||
node._replaceUpstream(
|
||||
@ -191,6 +192,17 @@ class StylesheetGraph {
|
||||
return node;
|
||||
}
|
||||
|
||||
/// Clears the cached canonical version of the given [url] in [importCache].
|
||||
///
|
||||
/// Also resets the cached modification times for stylesheets in the graph.
|
||||
void clearCanonicalize(Uri url) {
|
||||
// Rather than spending time computing exactly which modification times
|
||||
// should be updated, just clear the cache and let it be computed again
|
||||
// later.
|
||||
_transitiveModificationTimes.clear();
|
||||
importCache.clearCanonicalize(url);
|
||||
}
|
||||
|
||||
/// Runs [callback] and returns its result.
|
||||
///
|
||||
/// If [callback] throws any errors, ignores them and returns `null`. This is
|
||||
|
53
lib/src/util/multi_dir_watcher.dart
Normal file
53
lib/src/util/multi_dir_watcher.dart
Normal file
@ -0,0 +1,53 @@
|
||||
// Copyright 2018 Google Inc. Use of this source code is governed by an
|
||||
// MIT-style license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:async/async.dart';
|
||||
import 'package:watcher/watcher.dart';
|
||||
|
||||
import '../io.dart';
|
||||
import 'path.dart';
|
||||
|
||||
/// Watches multiple directories which may change over time recursively for changes.
|
||||
///
|
||||
/// This ensures that each directory is only watched once, even if one is a
|
||||
/// parent of another.
|
||||
class MultiDirWatcher {
|
||||
/// A map from paths to the event streams for those paths.
|
||||
///
|
||||
/// No key in this map is a parent directories of any other key in this map.
|
||||
final _watchers = <String, Stream<WatchEvent>>{};
|
||||
|
||||
/// The stream of events from all directories that are being watched.
|
||||
Stream<WatchEvent> get events => _group.stream;
|
||||
final _group = new StreamGroup<WatchEvent>();
|
||||
|
||||
/// Watches [directory] for changes.
|
||||
///
|
||||
/// Returns a [Future] that completes when [events] is ready to emit events
|
||||
/// from [directory].
|
||||
Future<void> watch(String directory) {
|
||||
var isParentOfExistingDir = false;
|
||||
for (var existingDir in _watchers.keys.toList()) {
|
||||
if (!isParentOfExistingDir &&
|
||||
(p.equals(existingDir, directory) ||
|
||||
p.isWithin(existingDir, directory))) {
|
||||
return new Future.value();
|
||||
}
|
||||
|
||||
if (p.isWithin(directory, existingDir)) {
|
||||
_group.remove(_watchers.remove(existingDir));
|
||||
isParentOfExistingDir = true;
|
||||
}
|
||||
}
|
||||
|
||||
var future = watchDir(directory);
|
||||
var stream = StreamCompleter.fromFuture(future);
|
||||
_watchers[directory] = stream;
|
||||
_group.add(stream);
|
||||
|
||||
return future;
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@
|
||||
"install dependencies used for testing the Node API."
|
||||
],
|
||||
"devDependencies": {
|
||||
"chokidar": "^2.0.0",
|
||||
"fibers": ">=1.0.0 <3.0.0",
|
||||
"intercept-stdout": "^0.1.2"
|
||||
}
|
||||
|
@ -16,6 +16,9 @@
|
||||
"engines": {
|
||||
"node": ">=0.11.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"chokidar": "^2.0.0"
|
||||
},
|
||||
"main": "sass.dart.js",
|
||||
"bin": "sass.js",
|
||||
"keywords": ["style", "scss", "sass", "preprocessor", "css"]
|
||||
|
@ -19,13 +19,14 @@ dependencies:
|
||||
convert: "^2.0.1"
|
||||
dart2_constant: "^1.0.0"
|
||||
meta: "^1.1.0"
|
||||
package_resolver: "^1.0.0"
|
||||
path: "^1.6.0"
|
||||
source_maps: "^0.10.5"
|
||||
source_span: "^1.4.0"
|
||||
string_scanner: ">=0.1.5 <2.0.0"
|
||||
stack_trace: ">=0.9.0 <2.0.0"
|
||||
string_scanner: ">=0.1.5 <2.0.0"
|
||||
tuple: "^1.0.0"
|
||||
package_resolver: ^1.0.0
|
||||
watcher: "^0.9.6"
|
||||
|
||||
dev_dependencies:
|
||||
archive: ">=1.0.0 <3.0.0"
|
||||
|
14
test/cli/dart/watch_test.dart
Normal file
14
test/cli/dart/watch_test.dart
Normal file
@ -0,0 +1,14 @@
|
||||
// Copyright 2018 Google Inc. Use of this source code is governed by an
|
||||
// MIT-style license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
@TestOn('vm')
|
||||
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../dart_test.dart';
|
||||
import '../shared/watch.dart';
|
||||
|
||||
void main() {
|
||||
sharedTests(runSass);
|
||||
}
|
17
test/cli/node/watch_test.dart
Normal file
17
test/cli/node/watch_test.dart
Normal file
@ -0,0 +1,17 @@
|
||||
// Copyright 2018 Google Inc. Use of this source code is governed by an
|
||||
// MIT-style license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
@TestOn('vm')
|
||||
@Tags(const ['node'])
|
||||
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../../ensure_npm_package.dart';
|
||||
import '../node_test.dart';
|
||||
import '../shared/watch.dart';
|
||||
|
||||
void main() {
|
||||
setUpAll(ensureNpmPackage);
|
||||
sharedTests(runSass);
|
||||
}
|
457
test/cli/shared/watch.dart
Normal file
457
test/cli/shared/watch.dart
Normal file
@ -0,0 +1,457 @@
|
||||
// Copyright 2018 Google Inc. Use of this source code is governed by an
|
||||
// MIT-style license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:test/test.dart';
|
||||
import 'package:test_descriptor/test_descriptor.dart' as d;
|
||||
import 'package:test_process/test_process.dart';
|
||||
|
||||
import 'package:sass/src/util/path.dart';
|
||||
|
||||
import '../../utils.dart';
|
||||
|
||||
/// Defines test that are shared between the Dart and Node.js CLI test suites.
|
||||
void sharedTests(Future<TestProcess> runSass(Iterable<String> arguments)) {
|
||||
Future<TestProcess> watch(Iterable<String> arguments) =>
|
||||
runSass(["--no-source-map", "--watch"]..addAll(arguments));
|
||||
|
||||
group("when started", () {
|
||||
test("updates a CSS file whose source was modified", () async {
|
||||
await d.file("out.css", "x {y: z}").create();
|
||||
await new Future.delayed(new Duration(milliseconds: 10));
|
||||
await d.file("test.scss", "a {b: c}").create();
|
||||
|
||||
var sass = await watch(["test.scss:out.css"]);
|
||||
await expectLater(sass.stdout, emits('Compiled test.scss to out.css.'));
|
||||
await expectLater(sass.stdout, _watchingForChanges);
|
||||
await sass.kill();
|
||||
|
||||
await d
|
||||
.file("out.css", equalsIgnoringWhitespace("a { b: c; }"))
|
||||
.validate();
|
||||
});
|
||||
|
||||
test("doesn't update a CSS file that wasn't modified", () async {
|
||||
await d.file("test.scss", "a {b: c}").create();
|
||||
await d.file("out.css", "x {y: z}").create();
|
||||
|
||||
var sass = await watch(["test.scss:out.css"]);
|
||||
await expectLater(sass.stdout, _watchingForChanges);
|
||||
await sass.kill();
|
||||
|
||||
await d.file("out.css", "x {y: z}").validate();
|
||||
});
|
||||
});
|
||||
|
||||
group("recompiles a watched file", () {
|
||||
test("when it's modified", () async {
|
||||
await d.file("test.scss", "a {b: c}").create();
|
||||
|
||||
var sass = await watch(["test.scss:out.css"]);
|
||||
await expectLater(sass.stdout, emits('Compiled test.scss to out.css.'));
|
||||
await expectLater(sass.stdout, _watchingForChanges);
|
||||
|
||||
await d.file("test.scss", "x {y: z}").create();
|
||||
await expectLater(sass.stdout, emits('Compiled test.scss to out.css.'));
|
||||
await sass.kill();
|
||||
|
||||
await d
|
||||
.file("out.css", equalsIgnoringWhitespace("x { y: z; }"))
|
||||
.validate();
|
||||
});
|
||||
|
||||
test("when it's modified when watched from a directory", () async {
|
||||
await d.dir("dir", [d.file("test.scss", "a {b: c}")]).create();
|
||||
|
||||
var sass = await watch(["dir:out"]);
|
||||
await expectLater(
|
||||
sass.stdout, emits('Compiled dir/test.scss to out/test.css.'));
|
||||
await expectLater(sass.stdout, _watchingForChanges);
|
||||
|
||||
await d.dir("dir", [d.file("test.scss", "x {y: z}")]).create();
|
||||
await expectLater(
|
||||
sass.stdout, emits('Compiled dir/test.scss to out/test.css.'));
|
||||
await sass.kill();
|
||||
|
||||
await d.dir("out", [
|
||||
d.file("test.css", equalsIgnoringWhitespace("x { y: z; }"))
|
||||
]).validate();
|
||||
});
|
||||
|
||||
test("when its dependency is modified", () async {
|
||||
await d.file("_other.scss", "a {b: c}").create();
|
||||
await d.file("test.scss", "@import 'other'").create();
|
||||
|
||||
var sass = await watch(["test.scss:out.css"]);
|
||||
await expectLater(sass.stdout, emits('Compiled test.scss to out.css.'));
|
||||
await expectLater(sass.stdout, _watchingForChanges);
|
||||
|
||||
await d.file("_other.scss", "x {y: z}").create();
|
||||
await expectLater(sass.stdout, emits('Compiled test.scss to out.css.'));
|
||||
await sass.kill();
|
||||
|
||||
await d
|
||||
.file("out.css", equalsIgnoringWhitespace("x { y: z; }"))
|
||||
.validate();
|
||||
});
|
||||
|
||||
test("when it's deleted and re-added", () async {
|
||||
await d.file("test.scss", "a {b: c}").create();
|
||||
|
||||
var sass = await watch(["test.scss:out.css"]);
|
||||
await expectLater(sass.stdout, emits('Compiled test.scss to out.css.'));
|
||||
await expectLater(sass.stdout, _watchingForChanges);
|
||||
|
||||
new File(p.join(d.sandbox, "test.scss")).deleteSync();
|
||||
await expectLater(sass.stdout, emits('Deleted out.css.'));
|
||||
|
||||
await d.file("test.scss", "x {y: z}").create();
|
||||
await expectLater(sass.stdout, emits('Compiled test.scss to out.css.'));
|
||||
await sass.kill();
|
||||
|
||||
await d
|
||||
.file("out.css", equalsIgnoringWhitespace("x { y: z; }"))
|
||||
.validate();
|
||||
});
|
||||
|
||||
group("when its dependency is deleted", () {
|
||||
test("and removes the output", () async {
|
||||
await d.file("_other.scss", "a {b: c}").create();
|
||||
await d.file("test.scss", "@import 'other'").create();
|
||||
|
||||
var sass = await watch(["test.scss:out.css"]);
|
||||
await expectLater(sass.stdout, emits('Compiled test.scss to out.css.'));
|
||||
await expectLater(sass.stdout, _watchingForChanges);
|
||||
|
||||
new File(p.join(d.sandbox, "_other.scss")).deleteSync();
|
||||
await expectLater(
|
||||
sass.stderr, emits("Error: Can't find stylesheet to import."));
|
||||
await expectLater(sass.stderr, emitsThrough(contains('test.scss 1:9')));
|
||||
await sass.kill();
|
||||
|
||||
await d.nothing("out.css").validate();
|
||||
});
|
||||
|
||||
test("but another is available", () async {
|
||||
await d.file("_other.scss", "a {b: c}").create();
|
||||
await d.file("test.scss", "@import 'other'").create();
|
||||
await d.dir("dir", [d.file("_other.scss", "x {y: z}")]).create();
|
||||
|
||||
var sass = await watch(["-I", "dir", "test.scss:out.css"]);
|
||||
await expectLater(sass.stdout, emits('Compiled test.scss to out.css.'));
|
||||
await expectLater(sass.stdout, _watchingForChanges);
|
||||
|
||||
new File(p.join(d.sandbox, "_other.scss")).deleteSync();
|
||||
await expectLater(sass.stdout, emits('Compiled test.scss to out.css.'));
|
||||
await sass.kill();
|
||||
|
||||
await d
|
||||
.file("out.css", equalsIgnoringWhitespace("x { y: z; }"))
|
||||
.validate();
|
||||
});
|
||||
|
||||
test("which resolves a conflict", () async {
|
||||
await d.file("_other.scss", "a {b: c}").create();
|
||||
await d.file("_other.sass", "x\n y: z").create();
|
||||
await d.file("test.scss", "@import 'other'").create();
|
||||
|
||||
var sass = await watch(["test.scss:out.css"]);
|
||||
await expectLater(sass.stderr,
|
||||
emits("Error: It's not clear which file to import. Found:"));
|
||||
await expectLater(sass.stdout, _watchingForChanges);
|
||||
|
||||
new File(p.join(d.sandbox, "_other.sass")).deleteSync();
|
||||
await expectLater(sass.stdout, emits('Compiled test.scss to out.css.'));
|
||||
await sass.kill();
|
||||
|
||||
await d
|
||||
.file("out.css", equalsIgnoringWhitespace("a { b: c; }"))
|
||||
.validate();
|
||||
});
|
||||
});
|
||||
|
||||
group("when a dependency is added", () {
|
||||
group("that was missing", () {
|
||||
test("relative to the file", () async {
|
||||
await d.file("test.scss", "@import 'other'").create();
|
||||
|
||||
var sass = await watch(["test.scss:out.css"]);
|
||||
await expectLater(
|
||||
sass.stderr, emits("Error: Can't find stylesheet to import."));
|
||||
await expectLater(
|
||||
sass.stderr, emitsThrough(contains("test.scss 1:9")));
|
||||
await expectLater(sass.stdout, _watchingForChanges);
|
||||
|
||||
await d.file("_other.scss", "a {b: c}").create();
|
||||
await expectLater(
|
||||
sass.stdout, emits('Compiled test.scss to out.css.'));
|
||||
await sass.kill();
|
||||
|
||||
await d
|
||||
.file("out.css", equalsIgnoringWhitespace("a { b: c; }"))
|
||||
.validate();
|
||||
});
|
||||
|
||||
test("on a load path", () async {
|
||||
await d.file("test.scss", "@import 'other'").create();
|
||||
await d.dir("dir").create();
|
||||
|
||||
var sass = await watch(["-I", "dir", "test.scss:out.css"]);
|
||||
await expectLater(
|
||||
sass.stderr, emits("Error: Can't find stylesheet to import."));
|
||||
await expectLater(
|
||||
sass.stderr, emitsThrough(contains("test.scss 1:9")));
|
||||
await expectLater(sass.stdout, _watchingForChanges);
|
||||
|
||||
await d.dir("dir", [d.file("_other.scss", "a {b: c}")]).create();
|
||||
await expectLater(
|
||||
sass.stdout, emits('Compiled test.scss to out.css.'));
|
||||
await sass.kill();
|
||||
|
||||
await d
|
||||
.file("out.css", equalsIgnoringWhitespace("a { b: c; }"))
|
||||
.validate();
|
||||
});
|
||||
|
||||
test("on a load path that was created", () async {
|
||||
await d
|
||||
.dir("dir1", [d.file("test.scss", "@import 'other'")]).create();
|
||||
|
||||
var sass = await watch(["-I", "dir2", "dir1:out"]);
|
||||
await expectLater(
|
||||
sass.stderr, emits("Error: Can't find stylesheet to import."));
|
||||
await expectLater(
|
||||
sass.stderr, emitsThrough(contains("dir1/test.scss 1:9")));
|
||||
await expectLater(sass.stdout, _watchingForChanges);
|
||||
|
||||
await d.dir("dir2", [d.file("_other.scss", "a {b: c}")]).create();
|
||||
await expectLater(
|
||||
sass.stdout, emits('Compiled dir1/test.scss to out/test.css.'));
|
||||
await sass.kill();
|
||||
|
||||
await d
|
||||
.file("out/test.css", equalsIgnoringWhitespace("a { b: c; }"))
|
||||
.validate();
|
||||
});
|
||||
});
|
||||
|
||||
test("that conflicts with the previous dependency", () async {
|
||||
await d.file("_other.scss", "a {b: c}").create();
|
||||
await d.file("test.scss", "@import 'other'").create();
|
||||
|
||||
var sass = await watch(["test.scss:out.css"]);
|
||||
await expectLater(sass.stdout, emits('Compiled test.scss to out.css.'));
|
||||
await expectLater(sass.stdout, _watchingForChanges);
|
||||
|
||||
await d.file("_other.sass", "x\n y: z").create();
|
||||
await expectLater(sass.stderr,
|
||||
emits("Error: It's not clear which file to import. Found:"));
|
||||
await expectLater(sass.stdout, emits("Deleted out.css."));
|
||||
await sass.kill();
|
||||
|
||||
await d.nothing("out.css").validate();
|
||||
});
|
||||
|
||||
group("that overrides the previous dependency", () {
|
||||
test("on an import path", () async {
|
||||
await d.file("test.scss", "@import 'other'").create();
|
||||
await d.dir("dir2", [d.file("_other.scss", "a {b: c}")]).create();
|
||||
await d.dir("dir1").create();
|
||||
|
||||
var sass =
|
||||
await watch(["-I", "dir1", "-I", "dir2", "test.scss:out.css"]);
|
||||
await expectLater(
|
||||
sass.stdout, emits('Compiled test.scss to out.css.'));
|
||||
await expectLater(sass.stdout, _watchingForChanges);
|
||||
|
||||
await d.dir("dir1", [d.file("_other.scss", "x {y: z}")]).create();
|
||||
await expectLater(
|
||||
sass.stdout, emits('Compiled test.scss to out.css.'));
|
||||
await sass.kill();
|
||||
|
||||
await d
|
||||
.file("out.css", equalsIgnoringWhitespace("x { y: z; }"))
|
||||
.validate();
|
||||
});
|
||||
|
||||
test("because it's relative", () async {
|
||||
await d.file("test.scss", "@import 'other'").create();
|
||||
await d.dir("dir", [d.file("_other.scss", "a {b: c}")]).create();
|
||||
|
||||
var sass = await watch(["-I", "dir", "test.scss:out.css"]);
|
||||
await expectLater(
|
||||
sass.stdout, emits('Compiled test.scss to out.css.'));
|
||||
await expectLater(sass.stdout, _watchingForChanges);
|
||||
|
||||
await d.file("_other.scss", "x {y: z}").create();
|
||||
await expectLater(
|
||||
sass.stdout, emits('Compiled test.scss to out.css.'));
|
||||
await sass.kill();
|
||||
|
||||
await d
|
||||
.file("out.css", equalsIgnoringWhitespace("x { y: z; }"))
|
||||
.validate();
|
||||
});
|
||||
|
||||
test("because it's not an index", () async {
|
||||
await d.file("test.scss", "@import 'other'").create();
|
||||
await d.dir("other", [d.file("_index.scss", "a {b: c}")]).create();
|
||||
|
||||
var sass = await watch(["test.scss:out.css"]);
|
||||
await expectLater(
|
||||
sass.stdout, emits('Compiled test.scss to out.css.'));
|
||||
await expectLater(sass.stdout, _watchingForChanges);
|
||||
|
||||
await d.file("_other.scss", "x {y: z}").create();
|
||||
await expectLater(
|
||||
sass.stdout, emits('Compiled test.scss to out.css.'));
|
||||
await sass.kill();
|
||||
|
||||
await d
|
||||
.file("out.css", equalsIgnoringWhitespace("x { y: z; }"))
|
||||
.validate();
|
||||
});
|
||||
});
|
||||
|
||||
test("gracefully handles a parse error", () async {
|
||||
await d.dir("dir").create();
|
||||
|
||||
var sass = await watch(["dir:out"]);
|
||||
await expectLater(sass.stdout, _watchingForChanges);
|
||||
|
||||
await d.dir("dir", [d.file("test.scss", "a {b: }")]).create();
|
||||
await expectLater(sass.stderr, emits('Error: Expected expression.'));
|
||||
await sass.kill();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group("doesn't recompile the watched file", () {
|
||||
test("when an unrelated file is modified", () async {
|
||||
await d.dir("dir", [
|
||||
d.file("test1.scss", "a {b: c}"),
|
||||
d.file("test2.scss", "a {b: c}")
|
||||
]).create();
|
||||
|
||||
var sass = await watch(["dir:out"]);
|
||||
await expectLater(
|
||||
sass.stdout,
|
||||
emitsInAnyOrder([
|
||||
'Compiled dir/test1.scss to out/test1.css.',
|
||||
'Compiled dir/test2.scss to out/test2.css.'
|
||||
]));
|
||||
await expectLater(sass.stdout, _watchingForChanges);
|
||||
|
||||
await d.dir("dir", [d.file("test2.scss", "x {y: z}")]).create();
|
||||
await expectLater(
|
||||
sass.stdout, emits('Compiled dir/test2.scss to out/test2.css.'));
|
||||
expect(
|
||||
sass.stdout, neverEmits('Compiled dir/test1.scss to out/test1.css.'));
|
||||
await tick;
|
||||
await sass.kill();
|
||||
});
|
||||
|
||||
test("when a potential dependency that's not actually imported is added",
|
||||
() async {
|
||||
await d.file("test.scss", "@import 'other'").create();
|
||||
await d.file("_other.scss", "a {b: c}").create();
|
||||
await d.dir("dir").create();
|
||||
|
||||
var sass = await watch(["-I", "dir", "test.scss:out.css"]);
|
||||
await expectLater(sass.stdout, emits('Compiled test.scss to out.css.'));
|
||||
await expectLater(sass.stdout, _watchingForChanges);
|
||||
|
||||
await d.dir("dir", [d.file("_other.scss", "a {b: c}")]).create();
|
||||
expect(sass.stdout, neverEmits('Compiled test.scss to out.css.'));
|
||||
await tick;
|
||||
await sass.kill();
|
||||
|
||||
await d
|
||||
.file("out.css", equalsIgnoringWhitespace("a { b: c; }"))
|
||||
.validate();
|
||||
});
|
||||
});
|
||||
|
||||
group("deletes the CSS", () {
|
||||
test("when a file is deleted", () async {
|
||||
await d.file("test.scss", "a {b: c}").create();
|
||||
|
||||
var sass = await watch(["test.scss:out.css"]);
|
||||
await expectLater(sass.stdout, emits('Compiled test.scss to out.css.'));
|
||||
await expectLater(sass.stdout, _watchingForChanges);
|
||||
|
||||
new File(p.join(d.sandbox, "test.scss")).deleteSync();
|
||||
await expectLater(sass.stdout, emits('Deleted out.css.'));
|
||||
await sass.kill();
|
||||
|
||||
await d.nothing("out.css").validate();
|
||||
});
|
||||
|
||||
test("when a file is deleted within a directory", () async {
|
||||
await d.dir("dir", [d.file("test.scss", "a {b: c}")]).create();
|
||||
|
||||
var sass = await watch(["dir:out"]);
|
||||
await expectLater(
|
||||
sass.stdout, emits('Compiled dir/test.scss to out/test.css.'));
|
||||
await expectLater(sass.stdout, _watchingForChanges);
|
||||
|
||||
new File(p.join(d.sandbox, "dir", "test.scss")).deleteSync();
|
||||
await expectLater(sass.stdout, emits('Deleted out/test.css.'));
|
||||
await sass.kill();
|
||||
|
||||
await d.dir("dir", [d.nothing("out.css")]).validate();
|
||||
});
|
||||
});
|
||||
|
||||
test("creates a new CSS file when a Sass file is added", () async {
|
||||
await d.dir("dir").create();
|
||||
|
||||
var sass = await watch(["dir:out"]);
|
||||
await expectLater(sass.stdout, _watchingForChanges);
|
||||
|
||||
await d.dir("dir", [d.file("test.scss", "a {b: c}")]).create();
|
||||
await expectLater(
|
||||
sass.stdout, emits('Compiled dir/test.scss to out/test.css.'));
|
||||
await sass.kill();
|
||||
|
||||
await d.dir("out", [
|
||||
d.file("test.css", equalsIgnoringWhitespace("a { b: c; }"))
|
||||
]).validate();
|
||||
});
|
||||
|
||||
test("doesn't create a new CSS file when a partial is added", () async {
|
||||
await d.dir("dir").create();
|
||||
|
||||
var sass = await watch(["dir:out"]);
|
||||
await expectLater(sass.stdout, _watchingForChanges);
|
||||
|
||||
await d.dir("dir", [d.file("_test.scss", "a {b: c}")]).create();
|
||||
expect(sass.stdout, neverEmits('Compiled dir/test.scss to out/test.css.'));
|
||||
await tick;
|
||||
await sass.kill();
|
||||
|
||||
await d.nothing("out/test.scss").validate();
|
||||
});
|
||||
|
||||
group("doesn't allow", () {
|
||||
test("--stdin", () async {
|
||||
var sass = await watch(["--stdin", "test.scss"]);
|
||||
expect(sass.stdout, emits('--watch is not allowed with --stdin.'));
|
||||
await sass.shouldExit(64);
|
||||
});
|
||||
|
||||
test("printing to stderr", () async {
|
||||
var sass = await watch(["test.scss"]);
|
||||
expect(sass.stdout,
|
||||
emits('--watch is not allowed when printing to stdout.'));
|
||||
await sass.shouldExit(64);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Matches the output that indicates that Sass is watching for changes.
|
||||
final _watchingForChanges =
|
||||
emitsInOrder(["Sass is watching for changes. Press Ctrl-C to stop.", ""]);
|
Loading…
Reference in New Issue
Block a user