diff --git a/CHANGELOG.md b/CHANGELOG.md index 80ace5c5..8f52e6ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ `sass input.scss:output.css`. Note that unlike Ruby Sass, this *always* compiles files by default regardless of when they were modified. + This syntax also supports compiling entire directories at once. For example, + `sass templates/stylesheets:public/css` compiles all non-partial Sass files + in `templates/stylesheets` to CSS files in `public/css`. + ## 1.3.2 * Add support for `@elseif` as an alias of `@else if`. This is not an diff --git a/lib/src/executable.dart b/lib/src/executable.dart index 1254821c..7c7c6ed0 100644 --- a/lib/src/executable.dart +++ b/lib/src/executable.dart @@ -65,8 +65,8 @@ main(List args) async { } } on UsageException catch (error) { print("${error.message}\n"); - print("Usage: sass [output]\n" - " sass : :\n"); + print("Usage: sass [output.css]\n" + " sass : :\n"); print(ExecutableOptions.usage); exitCode = 64; } catch (error, stackTrace) { diff --git a/lib/src/executable_options.dart b/lib/src/executable_options.dart index b55cd5ea..7f5bd59a 100644 --- a/lib/src/executable_options.dart +++ b/lib/src/executable_options.dart @@ -178,6 +178,10 @@ class ExecutableOptions { if (stdin) _fail('--stdin may not be used with ":" arguments.'); + // Track [seen] separately from `sourcesToDestinations.keys` because we want + // to report errors for sources as users entered them, rather than after + // directories have been resolved. + var seen = new Set(); var sourcesToDestinations = {}; for (var argument in _options.rest) { var components = argument.split(":"); @@ -186,12 +190,19 @@ class ExecutableOptions { } assert(components.length == 2); - var source = components.first == '-' ? null : components.first; - if (sourcesToDestinations.containsKey(source)) { - _fail('Duplicate source "${components.first}".'); + var source = components.first; + var destination = components.last; + if (!seen.add(source)) { + _fail('Duplicate source "${source}".'); } - sourcesToDestinations[source] = components.last; + if (source == '-') { + sourcesToDestinations[null] = destination; + } else if (dirExists(source)) { + sourcesToDestinations.addAll(_listSourceDirectory(source, destination)); + } else { + sourcesToDestinations[source] = destination; + } } _sourcesToDestinations = new Map.unmodifiable(sourcesToDestinations); return _sourcesToDestinations; @@ -199,6 +210,23 @@ class ExecutableOptions { Map _sourcesToDestinations; + /// Returns the sub-map of [sourcesToDestinations] for the given [source] and + /// [destination] directories. + Map _listSourceDirectory(String source, String destination) { + var map = {}; + for (var path in listDir(source)) { + var basename = p.basename(path); + if (basename.startsWith("_")) continue; + + var extension = p.extension(path); + if (extension != ".scss" && extension != ".sass") continue; + + map[path] = p.join( + destination, p.setExtension(p.relative(path, from: source), '.css')); + } + return map; + } + /// Whether to emit a source map file. bool get emitSourceMap { if (!(_options['source-map'] as bool)) { diff --git a/lib/src/io/interface.dart b/lib/src/io/interface.dart index 260e0282..de2e4c32 100644 --- a/lib/src/io/interface.dart +++ b/lib/src/io/interface.dart @@ -65,5 +65,9 @@ bool dirExists(String path) => null; /// necessary. void ensureDir(String path) => null; +/// Recursively lists the files (not sub-directories) of the directory at +/// [path]. +Iterable listDir(String path) => null; + /// Gets and sets the exit code that the process will use when it exits. int exitCode; diff --git a/lib/src/io/node.dart b/lib/src/io/node.dart index 85636727..24ca4753 100644 --- a/lib/src/io/node.dart +++ b/lib/src/io/node.dart @@ -18,6 +18,14 @@ class _FS { external void writeFileSync(String path, String data); external bool existsSync(String path); external void mkdirSync(String path); + external _Stat statSync(String path); + external List readdirSync(String path); +} + +@JS() +class _Stat { + external bool isFile(); + external bool isDirectory(); } @JS() @@ -135,9 +143,25 @@ String _cleanErrorMessage(_SystemError error) { error.message.length - ", ${error.syscall} '${error.path}'".length); } -bool fileExists(String path) => _fs.existsSync(path); +bool fileExists(String path) { + try { + return _fs.statSync(path).isFile(); + } catch (error) { + var systemError = error as _SystemError; + if (systemError.code == 'ENOENT') return false; + rethrow; + } +} -bool dirExists(String path) => _fs.existsSync(path); +bool dirExists(String path) { + try { + return _fs.statSync(path).isDirectory(); + } catch (error) { + var systemError = error as _SystemError; + if (systemError.code == 'ENOENT') return false; + rethrow; + } +} void ensureDir(String path) { return _systemErrorToFileSystemException(() { @@ -153,6 +177,16 @@ void ensureDir(String path) { }); } +Iterable listDir(String path) { + Iterable list(String parent) => + _fs.readdirSync(parent).expand((child) { + var path = p.join(parent, child); + return dirExists(path) ? listDir(path) : [path]; + }); + + return _systemErrorToFileSystemException(() => list(path)); +} + /// Runs callback and converts any [_SystemError]s it throws into /// [FileSystemException]s. T _systemErrorToFileSystemException(T callback()) { diff --git a/lib/src/io/vm.dart b/lib/src/io/vm.dart index 59e1c3f2..f049336f 100644 --- a/lib/src/io/vm.dart +++ b/lib/src/io/vm.dart @@ -56,3 +56,8 @@ bool dirExists(String path) => new io.Directory(path).existsSync(); void ensureDir(String path) => new io.Directory(path).createSync(recursive: true); + +Iterable listDir(String path) => new io.Directory(path) + .listSync(recursive: true) + .where((entity) => entity is io.File) + .map((entity) => entity.path); diff --git a/test/cli_shared.dart b/test/cli_shared.dart index b6380a7d..bcc9ea7c 100644 --- a/test/cli_shared.dart +++ b/test/cli_shared.dart @@ -213,6 +213,72 @@ void sharedTests(Future runSass(Iterable arguments)) { await d.file("out2.css.map", contains("test2.scss")).validate(); }); + group("with a directory argument", () { + test("compiles all the stylesheets in the directory", () async { + await d.dir("in", [ + d.file("test1.scss", "a {b: c}"), + d.file("test2.sass", "x\n y: z") + ]).create(); + + var sass = await runSass(["--no-source-map", "in:out"]); + expect(sass.stdout, emitsDone); + await sass.shouldExit(0); + + await d.dir("out", [ + d.file("test1.css", equalsIgnoringWhitespace("a { b: c; }")), + d.file("test2.css", equalsIgnoringWhitespace("x { y: z; }")) + ]).validate(); + }); + + test("creates subdirectories in the destination", () async { + await d.dir("in", [ + d.dir("sub", [d.file("test.scss", "a {b: c}")]) + ]).create(); + + var sass = await runSass(["--no-source-map", "in:out"]); + expect(sass.stdout, emitsDone); + await sass.shouldExit(0); + + await d.dir("out", [ + d.dir("sub", + [d.file("test.css", equalsIgnoringWhitespace("a { b: c; }"))]) + ]).validate(); + }); + + test("ignores partials", () async { + await d.dir("in", [ + d.file("_fake.scss", "a {b:"), + d.file("real.scss", "x {y: z}") + ]).create(); + + var sass = await runSass(["--no-source-map", "in:out"]); + expect(sass.stdout, emitsDone); + await sass.shouldExit(0); + + await d.dir("out", [ + d.file("real.css", equalsIgnoringWhitespace("x { y: z; }")), + d.nothing("fake.css"), + d.nothing("_fake.css") + ]).validate(); + }); + + test("ignores files without a Sass extension", () async { + await d.dir("in", [ + d.file("fake.szss", "a {b:"), + d.file("real.scss", "x {y: z}") + ]).create(); + + var sass = await runSass(["--no-source-map", "in:out"]); + expect(sass.stdout, emitsDone); + await sass.shouldExit(0); + + await d.dir("out", [ + d.file("real.css", equalsIgnoringWhitespace("x { y: z; }")), + d.nothing("fake.css") + ]).validate(); + }); + }); + group("reports all", () { test("file-not-found errors", () async { var sass =