Skip to content

Commit

Permalink
Decouple the graph and render logic from the fs watcher
Browse files Browse the repository at this point in the history
This logic is all tightly coupled in the bin. This logic has been
notoriously impossible to test due to weird fs timing issues. By
extracting the actual logic we're now able to test it in isolation.

With this in place replacing Gaze (sass#636) becomes a viable option.
This PR not only massively increases our test coverage for the
watcher but also address a bunch of known edge cases i.e. orphaned
imports when a files is deleted.

Closes sass#1896
Fixes sass#1891
  • Loading branch information
xzyfer committed Jun 15, 2017
1 parent ae4f935 commit 31d7d42
Show file tree
Hide file tree
Showing 11 changed files with 328 additions and 46 deletions.
69 changes: 24 additions & 45 deletions bin/node-sass
Expand Up @@ -10,6 +10,7 @@ var Emitter = require('events').EventEmitter,
glob = require('glob'),
sass = require('../lib'),
render = require('../lib/render'),
watcher = require('../lib/watcher'),
stdout = require('stdout-stream'),
stdin = require('get-stdin'),
fs = require('fs');
Expand Down Expand Up @@ -229,63 +230,41 @@ function getOptions(args, options) {
*/

function watch(options, emitter) {
var buildGraph = function(options) {
var graph;
var graphOptions = {
loadPaths: options.includePath,
extensions: ['scss', 'sass', 'css']
};

if (options.directory) {
graph = grapher.parseDir(options.directory, graphOptions);
} else {
graph = grapher.parseFile(options.src, graphOptions);
}
var handler = function(files) {
files.added.forEach(function(file) {
var watch = gaze.watched();
if (watch.indexOf(file) === -1) {
gaze.add(file);
}
});

return graph;
files.changed.forEach(function(file) {
if (path.basename(file)[0] !== '_') {
renderFile(file, options, emitter);
}
});

files.removed.forEach(function(file) {
gaze.remove(file);
});
};

var watch = [];
var graph = buildGraph(options);

// Add all files to watch list
for (var i in graph.index) {
watch.push(i);
}
watcher.reset(options);

var gaze = new Gaze();
gaze.add(watch);
gaze.add(watcher.reset(options));
gaze.on('error', emitter.emit.bind(emitter, 'error'));

gaze.on('changed', function(file) {
var files = [file];

// descendents may be added, so we need a new graph
graph = buildGraph(options);
graph.visitAncestors(file, function(parent) {
files.push(parent);
});

// Add children to watcher
graph.visitDescendents(file, function(child) {
if (watch.indexOf(child) === -1) {
watch.push(child);
gaze.add(child);
}
});
files.forEach(function(file) {
if (path.basename(file)[0] !== '_') {
renderFile(file, options, emitter);
}
});
handler(watcher.changed(file));
});

gaze.on('added', function() {
graph = buildGraph(options);
gaze.on('added', function(file) {
handler(watcher.added(file));
});

gaze.on('deleted', function() {
graph = buildGraph(options);
gaze.on('deleted', function(file) {
handler(watcher.deleted(file));
});
}

Expand Down
84 changes: 84 additions & 0 deletions lib/watcher.js
@@ -0,0 +1,84 @@
var grapher = require('sass-graph'),
clonedeep = require('lodash.clonedeep'),
watcher = {
opts: {}
},
graph = null;

watcher.reset = function(opts) {
this.opts = clonedeep(opts || this.opts || {});
var options = {
loadPaths: this.opts.includePath,
extensions: ['scss', 'sass', 'css']
};

if (this.opts.directory) {
graph = grapher.parseDir(this.opts.directory, options);
} else {
graph = grapher.parseFile(this.opts.src, options);
}

return Object.keys(graph.index);
};

watcher.changed = function(absolutePath) {
var files = {
added: [],
changed: [],
removed: [],
};

this.reset();

graph.visitAncestors(absolutePath, function(parent) {
files.changed.push(parent);
});

graph.visitDescendents(absolutePath, function(child) {
files.added.push(child);
});

return files;
};

watcher.added = function(absolutePath) {
var files = {
added: [],
changed: [],
removed: [],
};

this.reset();

if (Object.keys(graph.index).indexOf(absolutePath) !== -1) {
files.added.push(absolutePath);
}

graph.visitDescendents(absolutePath, function(child) {
files.added.push(child);
});

return files;
};

watcher.removed = function(absolutePath) {
var files = {
added: [],
changed: [],
removed: [],
};

graph.visitAncestors(absolutePath, function(parent) {
files.changed.push(parent);
});

if (Object.keys(graph.index).indexOf(absolutePath) !== -1) {
files.removed.push(absolutePath);
}

this.reset();

return files;
};

module.exports = watcher;
4 changes: 3 additions & 1 deletion package.json
Expand Up @@ -75,12 +75,14 @@
"devDependencies": {
"coveralls": "^2.11.8",
"eslint": "^3.4.0",
"fs-extra": "^0.30.0",
"istanbul": "^0.4.2",
"mocha": "^3.1.2",
"mocha-lcov-reporter": "^1.2.0",
"object-merge": "^2.5.1",
"read-yaml": "^1.0.0",
"rimraf": "^2.5.2",
"sass-spec": "3.5.0-1"
"sass-spec": "3.5.0-1",
"tempy": "^0.1.0"
}
}
5 changes: 5 additions & 0 deletions test/fixtures/watcher/main/one.scss
@@ -0,0 +1,5 @@
@import "partials/one";

.one {
color: red;
}
3 changes: 3 additions & 0 deletions test/fixtures/watcher/main/partials/_one.scss
@@ -0,0 +1,3 @@
.one {
color: darkred;
}
3 changes: 3 additions & 0 deletions test/fixtures/watcher/main/partials/_three.scss
@@ -0,0 +1,3 @@
.three {
color: darkgreen;
}
3 changes: 3 additions & 0 deletions test/fixtures/watcher/main/partials/_two.scss
@@ -0,0 +1,3 @@
.two {
color: darkblue;
}
5 changes: 5 additions & 0 deletions test/fixtures/watcher/main/two.scss
@@ -0,0 +1,5 @@
@import "partials/two";

.two {
color: blue;
}
3 changes: 3 additions & 0 deletions test/fixtures/watcher/sibling/partials/_three.scss
@@ -0,0 +1,3 @@
.three {
color: darkgreen;
}
5 changes: 5 additions & 0 deletions test/fixtures/watcher/sibling/three.scss
@@ -0,0 +1,5 @@
@import "partials/three";

.three {
color: green;
}

0 comments on commit 31d7d42

Please sign in to comment.