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
|
/// Clears the cached parse tree for the stylesheet with the given
|
||||||
/// [canonicalUrl].
|
/// [canonicalUrl].
|
||||||
///
|
///
|
||||||
|
@ -11,6 +11,7 @@ import 'exception.dart';
|
|||||||
import 'executable/compile_stylesheet.dart';
|
import 'executable/compile_stylesheet.dart';
|
||||||
import 'executable/options.dart';
|
import 'executable/options.dart';
|
||||||
import 'executable/repl.dart';
|
import 'executable/repl.dart';
|
||||||
|
import 'executable/watch.dart';
|
||||||
import 'import_cache.dart';
|
import 'import_cache.dart';
|
||||||
import 'io.dart';
|
import 'io.dart';
|
||||||
import 'stylesheet_graph.dart';
|
import 'stylesheet_graph.dart';
|
||||||
@ -50,10 +51,16 @@ main(List<String> args) async {
|
|||||||
|
|
||||||
var graph = new StylesheetGraph(new ImportCache([],
|
var graph = new StylesheetGraph(new ImportCache([],
|
||||||
loadPaths: options.loadPaths, logger: options.logger));
|
loadPaths: options.loadPaths, logger: options.logger));
|
||||||
|
if (options.watch) {
|
||||||
|
await watch(options, graph);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (var source in options.sourcesToDestinations.keys) {
|
for (var source in options.sourcesToDestinations.keys) {
|
||||||
var destination = options.sourcesToDestinations[source];
|
var destination = options.sourcesToDestinations[source];
|
||||||
try {
|
try {
|
||||||
await compileStylesheet(options, graph, source, destination);
|
await compileStylesheet(options, graph, source, destination,
|
||||||
|
ifModified: options.update);
|
||||||
} on SassException catch (error, stackTrace) {
|
} on SassException catch (error, stackTrace) {
|
||||||
// This is an immediately-invoked function expression to work around
|
// This is an immediately-invoked function expression to work around
|
||||||
// dart-lang/sdk#33400.
|
// dart-lang/sdk#33400.
|
||||||
|
@ -26,10 +26,15 @@ import 'options.dart';
|
|||||||
/// If [source] is `null`, that indicates that the stylesheet should be read
|
/// If [source] is `null`, that indicates that the stylesheet should be read
|
||||||
/// from stdin. If [destination] is `null`, that indicates that the stylesheet
|
/// from stdin. If [destination] is `null`, that indicates that the stylesheet
|
||||||
/// should be emitted to stdout.
|
/// 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,
|
Future compileStylesheet(ExecutableOptions options, StylesheetGraph graph,
|
||||||
String source, String destination) async {
|
String source, String destination,
|
||||||
|
{bool ifModified: false}) async {
|
||||||
var importer = new FilesystemImporter('.');
|
var importer = new FilesystemImporter('.');
|
||||||
if (options.update) {
|
if (ifModified) {
|
||||||
try {
|
try {
|
||||||
if (source != null &&
|
if (source != null &&
|
||||||
destination != null &&
|
destination != null &&
|
||||||
@ -50,7 +55,7 @@ Future compileStylesheet(ExecutableOptions options, StylesheetGraph graph,
|
|||||||
importer: importer,
|
importer: importer,
|
||||||
logger: options.logger,
|
logger: options.logger,
|
||||||
sourceMap: options.emitSourceMap)
|
sourceMap: options.emitSourceMap)
|
||||||
: await evaluate(stylesheet,
|
: evaluate(stylesheet,
|
||||||
importCache: graph.importCache,
|
importCache: graph.importCache,
|
||||||
importer: importer,
|
importer: importer,
|
||||||
logger: options.logger,
|
logger: options.logger,
|
||||||
@ -68,10 +73,12 @@ Future compileStylesheet(ExecutableOptions options, StylesheetGraph graph,
|
|||||||
writeFile(destination, css + "\n");
|
writeFile(destination, css + "\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options.update || options.quiet) return;
|
if (options.quiet || (!options.update && !options.watch)) return;
|
||||||
var buffer = new StringBuffer();
|
var buffer = new StringBuffer();
|
||||||
if (options.color) buffer.write('\u001b[32m');
|
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');
|
if (options.color) buffer.write('\u001b[0m');
|
||||||
print(buffer);
|
print(buffer);
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import 'dart:collection';
|
|||||||
|
|
||||||
import 'package:args/args.dart';
|
import 'package:args/args.dart';
|
||||||
import 'package:charcode/charcode.dart';
|
import 'package:charcode/charcode.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
import '../../sass.dart';
|
import '../../sass.dart';
|
||||||
@ -71,6 +72,9 @@ class ExecutableOptions {
|
|||||||
|
|
||||||
parser
|
parser
|
||||||
..addSeparator(_separator('Other'))
|
..addSeparator(_separator('Other'))
|
||||||
|
..addFlag('watch',
|
||||||
|
help: 'Watch stylesheets and recompile when they change.',
|
||||||
|
negatable: false)
|
||||||
..addFlag('interactive',
|
..addFlag('interactive',
|
||||||
abbr: 'i',
|
abbr: 'i',
|
||||||
help: 'Run an interactive SassScript shell.',
|
help: 'Run an interactive SassScript shell.',
|
||||||
@ -163,6 +167,9 @@ class ExecutableOptions {
|
|||||||
/// Whether to update only files that have changed since the last compilation.
|
/// Whether to update only files that have changed since the last compilation.
|
||||||
bool get update => _options['update'] as bool;
|
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
|
/// A map from source paths to the destination paths where the compiled CSS
|
||||||
/// should be written.
|
/// should be written.
|
||||||
///
|
///
|
||||||
@ -172,7 +179,27 @@ class ExecutableOptions {
|
|||||||
/// input. A `null` destination indicates that a stylesheet should be written
|
/// input. A `null` destination indicates that a stylesheet should be written
|
||||||
/// to standard output.
|
/// to standard output.
|
||||||
Map<String, String> get sourcesToDestinations {
|
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;
|
var stdin = _options['stdin'] as bool;
|
||||||
if (_options.rest.isEmpty && !stdin) _fail("Compile Sass to CSS.");
|
if (_options.rest.isEmpty && !stdin) _fail("Compile Sass to CSS.");
|
||||||
@ -203,6 +230,8 @@ class ExecutableOptions {
|
|||||||
_fail("Only one argument is allowed with --stdin.");
|
_fail("Only one argument is allowed with --stdin.");
|
||||||
} else if (update) {
|
} else if (update) {
|
||||||
_fail("--update is not allowed with --stdin.");
|
_fail("--update is not allowed with --stdin.");
|
||||||
|
} else if (watch) {
|
||||||
|
_fail("--watch is not allowed with --stdin.");
|
||||||
}
|
}
|
||||||
_sourcesToDestinations = new Map.unmodifiable(
|
_sourcesToDestinations = new Map.unmodifiable(
|
||||||
{null: _options.rest.isEmpty ? null : _options.rest.first});
|
{null: _options.rest.isEmpty ? null : _options.rest.first});
|
||||||
@ -211,13 +240,18 @@ class ExecutableOptions {
|
|||||||
} else {
|
} else {
|
||||||
var source = _options.rest.first == '-' ? null : _options.rest.first;
|
var source = _options.rest.first == '-' ? null : _options.rest.first;
|
||||||
var destination = _options.rest.length == 1 ? null : _options.rest.last;
|
var destination = _options.rest.length == 1 ? null : _options.rest.last;
|
||||||
if (update && destination == null) {
|
if (destination == null) {
|
||||||
_fail("--update is not allowed when printing to stdout.");
|
if (update) {
|
||||||
|
_fail("--update is not allowed when printing to stdout.");
|
||||||
|
} else if (watch) {
|
||||||
|
_fail("--watch is not allowed when printing to stdout.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_sourcesToDestinations =
|
_sourcesToDestinations =
|
||||||
new UnmodifiableMapView(newPathMap({source: destination}));
|
new UnmodifiableMapView(newPathMap({source: destination}));
|
||||||
}
|
}
|
||||||
return _sourcesToDestinations;
|
_sourceDirectoriesToDestinations = const {};
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stdin) _fail('--stdin may not be used with ":" arguments.');
|
if (stdin) _fail('--stdin may not be used with ":" arguments.');
|
||||||
@ -227,6 +261,7 @@ class ExecutableOptions {
|
|||||||
// directories have been resolved.
|
// directories have been resolved.
|
||||||
var seen = new Set<String>();
|
var seen = new Set<String>();
|
||||||
var sourcesToDestinations = newPathMap<String>();
|
var sourcesToDestinations = newPathMap<String>();
|
||||||
|
var sourceDirectoriesToDestinations = newPathMap<String>();
|
||||||
for (var argument in _options.rest) {
|
for (var argument in _options.rest) {
|
||||||
String source;
|
String source;
|
||||||
String destination;
|
String destination;
|
||||||
@ -255,17 +290,17 @@ class ExecutableOptions {
|
|||||||
if (source == '-') {
|
if (source == '-') {
|
||||||
sourcesToDestinations[null] = destination;
|
sourcesToDestinations[null] = destination;
|
||||||
} else if (dirExists(source)) {
|
} else if (dirExists(source)) {
|
||||||
|
sourceDirectoriesToDestinations[source] = destination;
|
||||||
sourcesToDestinations.addAll(_listSourceDirectory(source, destination));
|
sourcesToDestinations.addAll(_listSourceDirectory(source, destination));
|
||||||
} else {
|
} else {
|
||||||
sourcesToDestinations[source] = destination;
|
sourcesToDestinations[source] = destination;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_sourcesToDestinations = new UnmodifiableMapView(sourcesToDestinations);
|
_sourcesToDestinations = new UnmodifiableMapView(sourcesToDestinations);
|
||||||
return _sourcesToDestinations;
|
_sourceDirectoriesToDestinations =
|
||||||
|
new UnmodifiableMapView(sourceDirectoriesToDestinations);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, String> _sourcesToDestinations;
|
|
||||||
|
|
||||||
/// Returns whether [string] contains an absolute Windows path at [index].
|
/// Returns whether [string] contains an absolute Windows path at [index].
|
||||||
bool _isWindowsPath(String string, int index) =>
|
bool _isWindowsPath(String string, int index) =>
|
||||||
string.length > index + 2 &&
|
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.
|
// DO NOT EDIT. This file was generated from async_import_cache.dart.
|
||||||
// See tool/synchronize.dart for details.
|
// See tool/synchronize.dart for details.
|
||||||
//
|
//
|
||||||
// Checksum: 90302aba01c574ffbc07e75a34606406e48df7fc
|
// Checksum: 411ad1e666d5a138e69b51d9bdb58e9b22e88632
|
||||||
|
|
||||||
import 'package:tuple/tuple.dart';
|
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
|
/// Clears the cached parse tree for the stylesheet with the given
|
||||||
/// [canonicalUrl].
|
/// [canonicalUrl].
|
||||||
///
|
///
|
||||||
|
@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:watcher/watcher.dart';
|
||||||
|
|
||||||
/// An output sink that writes to this process's standard error.
|
/// An output sink that writes to this process's standard error.
|
||||||
class Stderr {
|
class Stderr {
|
||||||
/// Writes the string representation of [object] to standard error.
|
/// 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.
|
/// Gets and sets the exit code that the process will use when it exits.
|
||||||
int exitCode;
|
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:dart2_constant/convert.dart' as convert;
|
||||||
import 'package:js/js.dart';
|
import 'package:js/js.dart';
|
||||||
import 'package:source_span/source_span.dart';
|
import 'package:source_span/source_span.dart';
|
||||||
|
import 'package:watcher/watcher.dart';
|
||||||
|
|
||||||
import '../exception.dart';
|
import '../exception.dart';
|
||||||
|
import '../node/chokidar.dart';
|
||||||
import '../util/path.dart';
|
import '../util/path.dart';
|
||||||
|
|
||||||
@JS()
|
@JS()
|
||||||
@ -237,3 +239,34 @@ external int get exitCode;
|
|||||||
|
|
||||||
@JS("process.exitCode")
|
@JS("process.exitCode")
|
||||||
external set exitCode(int code);
|
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:async';
|
||||||
import 'dart:io' as io;
|
import 'dart:io' as io;
|
||||||
|
|
||||||
|
import 'package:async/async.dart';
|
||||||
import 'package:dart2_constant/convert.dart' as convert;
|
import 'package:dart2_constant/convert.dart' as convert;
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:source_span/source_span.dart';
|
import 'package:source_span/source_span.dart';
|
||||||
|
import 'package:watcher/watcher.dart';
|
||||||
|
|
||||||
import '../exception.dart';
|
import '../exception.dart';
|
||||||
|
|
||||||
@ -71,3 +73,16 @@ DateTime modificationTime(String path) {
|
|||||||
}
|
}
|
||||||
return stat.modified;
|
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);
|
remove(canonicalUrl);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
node._stylesheet = stylesheet;
|
||||||
|
|
||||||
node._stylesheet = stylesheet;
|
node._stylesheet = stylesheet;
|
||||||
node._replaceUpstream(
|
node._replaceUpstream(
|
||||||
@ -191,6 +192,17 @@ class StylesheetGraph {
|
|||||||
return node;
|
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.
|
/// Runs [callback] and returns its result.
|
||||||
///
|
///
|
||||||
/// If [callback] throws any errors, ignores them and returns `null`. This is
|
/// 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."
|
"install dependencies used for testing the Node API."
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"chokidar": "^2.0.0",
|
||||||
"fibers": ">=1.0.0 <3.0.0",
|
"fibers": ">=1.0.0 <3.0.0",
|
||||||
"intercept-stdout": "^0.1.2"
|
"intercept-stdout": "^0.1.2"
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,9 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.11.8"
|
"node": ">=0.11.8"
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"chokidar": "^2.0.0"
|
||||||
|
},
|
||||||
"main": "sass.dart.js",
|
"main": "sass.dart.js",
|
||||||
"bin": "sass.js",
|
"bin": "sass.js",
|
||||||
"keywords": ["style", "scss", "sass", "preprocessor", "css"]
|
"keywords": ["style", "scss", "sass", "preprocessor", "css"]
|
||||||
|
@ -19,13 +19,14 @@ dependencies:
|
|||||||
convert: "^2.0.1"
|
convert: "^2.0.1"
|
||||||
dart2_constant: "^1.0.0"
|
dart2_constant: "^1.0.0"
|
||||||
meta: "^1.1.0"
|
meta: "^1.1.0"
|
||||||
|
package_resolver: "^1.0.0"
|
||||||
path: "^1.6.0"
|
path: "^1.6.0"
|
||||||
source_maps: "^0.10.5"
|
source_maps: "^0.10.5"
|
||||||
source_span: "^1.4.0"
|
source_span: "^1.4.0"
|
||||||
string_scanner: ">=0.1.5 <2.0.0"
|
|
||||||
stack_trace: ">=0.9.0 <2.0.0"
|
stack_trace: ">=0.9.0 <2.0.0"
|
||||||
|
string_scanner: ">=0.1.5 <2.0.0"
|
||||||
tuple: "^1.0.0"
|
tuple: "^1.0.0"
|
||||||
package_resolver: ^1.0.0
|
watcher: "^0.9.6"
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
archive: ">=1.0.0 <3.0.0"
|
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