Support compiling entire directories at once (#324)

Partially addresses #264
This commit is contained in:
Natalie Weizenbaum 2018-05-22 23:06:33 +01:00 committed by GitHub
parent 0c9e3683c6
commit d68acf9ac2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 149 additions and 8 deletions

View File

@ -6,6 +6,10 @@
`sass input.scss:output.css`. Note that unlike Ruby Sass, this *always* `sass input.scss:output.css`. Note that unlike Ruby Sass, this *always*
compiles files by default regardless of when they were modified. 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 ## 1.3.2
* Add support for `@elseif` as an alias of `@else if`. This is not an * Add support for `@elseif` as an alias of `@else if`. This is not an

View File

@ -65,8 +65,8 @@ main(List<String> args) async {
} }
} on UsageException catch (error) { } on UsageException catch (error) {
print("${error.message}\n"); print("${error.message}\n");
print("Usage: sass <input> [output]\n" print("Usage: sass <input.scss> [output.css]\n"
" sass <input>:<output> <input>:<output>\n"); " sass <input.scss>:<output.css> <input/>:<output/>\n");
print(ExecutableOptions.usage); print(ExecutableOptions.usage);
exitCode = 64; exitCode = 64;
} catch (error, stackTrace) { } catch (error, stackTrace) {

View File

@ -178,6 +178,10 @@ class ExecutableOptions {
if (stdin) _fail('--stdin may not be used with ":" arguments.'); 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<String>();
var sourcesToDestinations = <String, String>{}; var sourcesToDestinations = <String, String>{};
for (var argument in _options.rest) { for (var argument in _options.rest) {
var components = argument.split(":"); var components = argument.split(":");
@ -186,12 +190,19 @@ class ExecutableOptions {
} }
assert(components.length == 2); assert(components.length == 2);
var source = components.first == '-' ? null : components.first; var source = components.first;
if (sourcesToDestinations.containsKey(source)) { var destination = components.last;
_fail('Duplicate source "${components.first}".'); 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); _sourcesToDestinations = new Map.unmodifiable(sourcesToDestinations);
return _sourcesToDestinations; return _sourcesToDestinations;
@ -199,6 +210,23 @@ class ExecutableOptions {
Map<String, String> _sourcesToDestinations; Map<String, String> _sourcesToDestinations;
/// Returns the sub-map of [sourcesToDestinations] for the given [source] and
/// [destination] directories.
Map<String, String> _listSourceDirectory(String source, String destination) {
var map = <String, String>{};
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. /// Whether to emit a source map file.
bool get emitSourceMap { bool get emitSourceMap {
if (!(_options['source-map'] as bool)) { if (!(_options['source-map'] as bool)) {

View File

@ -65,5 +65,9 @@ bool dirExists(String path) => null;
/// necessary. /// necessary.
void ensureDir(String path) => null; void ensureDir(String path) => null;
/// Recursively lists the files (not sub-directories) of the directory at
/// [path].
Iterable<String> listDir(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;

View File

@ -18,6 +18,14 @@ class _FS {
external void writeFileSync(String path, String data); external void writeFileSync(String path, String data);
external bool existsSync(String path); external bool existsSync(String path);
external void mkdirSync(String path); external void mkdirSync(String path);
external _Stat statSync(String path);
external List<String> readdirSync(String path);
}
@JS()
class _Stat {
external bool isFile();
external bool isDirectory();
} }
@JS() @JS()
@ -135,9 +143,25 @@ String _cleanErrorMessage(_SystemError error) {
error.message.length - ", ${error.syscall} '${error.path}'".length); 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) { void ensureDir(String path) {
return _systemErrorToFileSystemException(() { return _systemErrorToFileSystemException(() {
@ -153,6 +177,16 @@ void ensureDir(String path) {
}); });
} }
Iterable<String> listDir(String path) {
Iterable<String> 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 /// Runs callback and converts any [_SystemError]s it throws into
/// [FileSystemException]s. /// [FileSystemException]s.
T _systemErrorToFileSystemException<T>(T callback()) { T _systemErrorToFileSystemException<T>(T callback()) {

View File

@ -56,3 +56,8 @@ bool dirExists(String path) => new io.Directory(path).existsSync();
void ensureDir(String path) => void ensureDir(String path) =>
new io.Directory(path).createSync(recursive: true); new io.Directory(path).createSync(recursive: true);
Iterable<String> listDir(String path) => new io.Directory(path)
.listSync(recursive: true)
.where((entity) => entity is io.File)
.map((entity) => entity.path);

View File

@ -213,6 +213,72 @@ void sharedTests(Future<TestProcess> runSass(Iterable<String> arguments)) {
await d.file("out2.css.map", contains("test2.scss")).validate(); 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", () { group("reports all", () {
test("file-not-found errors", () async { test("file-not-found errors", () async {
var sass = var sass =