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*
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

View File

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

View File

@ -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<String>();
var sourcesToDestinations = <String, String>{};
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<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.
bool get emitSourceMap {
if (!(_options['source-map'] as bool)) {

View File

@ -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<String> listDir(String path) => null;
/// Gets and sets the exit code that the process will use when it exits.
int exitCode;

View File

@ -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<String> 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<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
/// [FileSystemException]s.
T _systemErrorToFileSystemException<T>(T callback()) {

View File

@ -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<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();
});
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 =