Commit aadf8d47 by Michael Mifsud Committed by GitHub

Decouple the graph and render logic from the fs watcher (#2020)

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 (#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 #1896
Fixes #1891
parent 61d7e1c1
...@@ -3,13 +3,13 @@ ...@@ -3,13 +3,13 @@
var Emitter = require('events').EventEmitter, var Emitter = require('events').EventEmitter,
forEach = require('async-foreach').forEach, forEach = require('async-foreach').forEach,
Gaze = require('gaze'), Gaze = require('gaze'),
grapher = require('sass-graph'),
meow = require('meow'), meow = require('meow'),
util = require('util'), util = require('util'),
path = require('path'), path = require('path'),
glob = require('glob'), glob = require('glob'),
sass = require('../lib'), sass = require('../lib'),
render = require('../lib/render'), render = require('../lib/render'),
watcher = require('../lib/watcher'),
stdout = require('stdout-stream'), stdout = require('stdout-stream'),
stdin = require('get-stdin'), stdin = require('get-stdin'),
fs = require('fs'); fs = require('fs');
...@@ -229,63 +229,41 @@ function getOptions(args, options) { ...@@ -229,63 +229,41 @@ function getOptions(args, options) {
*/ */
function watch(options, emitter) { function watch(options, emitter) {
var buildGraph = function(options) { var handler = function(files) {
var graph; files.added.forEach(function(file) {
var graphOptions = { var watch = gaze.watched();
loadPaths: options.includePath, if (watch.indexOf(file) === -1) {
extensions: ['scss', 'sass', 'css'] gaze.add(file);
}; }
});
if (options.directory) { files.changed.forEach(function(file) {
graph = grapher.parseDir(options.directory, graphOptions); if (path.basename(file)[0] !== '_') {
} else { renderFile(file, options, emitter);
graph = grapher.parseFile(options.src, graphOptions);
} }
});
return graph; files.removed.forEach(function(file) {
gaze.remove(file);
});
}; };
var watch = []; watcher.reset(options);
var graph = buildGraph(options);
// Add all files to watch list
for (var i in graph.index) {
watch.push(i);
}
var gaze = new Gaze(); var gaze = new Gaze();
gaze.add(watch); gaze.add(watcher.reset(options));
gaze.on('error', emitter.emit.bind(emitter, 'error')); gaze.on('error', emitter.emit.bind(emitter, 'error'));
gaze.on('changed', function(file) { gaze.on('changed', function(file) {
var files = [file]; handler(watcher.changed(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);
}
});
}); });
gaze.on('added', function() { gaze.on('added', function(file) {
graph = buildGraph(options); handler(watcher.added(file));
}); });
gaze.on('deleted', function() { gaze.on('deleted', function(file) {
graph = buildGraph(options); handler(watcher.deleted(file));
}); });
} }
......
var grapher = require('sass-graph'),
clonedeep = require('lodash.clonedeep'),
config = {},
watcher = {},
graph = null;
watcher.reset = function(opts) {
config = clonedeep(opts || config || {});
var options = {
loadPaths: config.includePath,
extensions: ['scss', 'sass', 'css']
};
if (config.directory) {
graph = grapher.parseDir(config.directory, options);
} else {
graph = grapher.parseFile(config.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;
...@@ -75,12 +75,14 @@ ...@@ -75,12 +75,14 @@
"devDependencies": { "devDependencies": {
"coveralls": "^2.11.8", "coveralls": "^2.11.8",
"eslint": "^3.4.0", "eslint": "^3.4.0",
"fs-extra": "^0.30.0",
"istanbul": "^0.4.2", "istanbul": "^0.4.2",
"mocha": "^3.1.2", "mocha": "^3.1.2",
"mocha-lcov-reporter": "^1.2.0", "mocha-lcov-reporter": "^1.2.0",
"object-merge": "^2.5.1", "object-merge": "^2.5.1",
"read-yaml": "^1.0.0", "read-yaml": "^1.0.0",
"rimraf": "^2.5.2", "rimraf": "^2.5.2",
"sass-spec": "3.5.0-1" "sass-spec": "3.5.0-1",
"unique-temp-dir": "^1.0.0"
} }
} }
@import "partials/one";
.one {
color: red;
}
@import "partials/two";
.two {
color: blue;
}
@import "partials/three";
.three {
color: green;
}
var assert = require('assert'),
fs = require('fs-extra'),
path = require('path'),
temp = require('unique-temp-dir'),
watcher = require('../lib/watcher');
describe.only('watcher', function() {
var main, sibling;
var origin = path.join(__dirname, 'fixtures', 'watcher');
beforeEach(function() {
var fixture = temp();
fs.ensureDirSync(fixture);
fs.copySync(origin, fixture);
main = fs.realpathSync(path.join(fixture, 'main'));
sibling = fs.realpathSync(path.join(fixture, 'sibling'));
});
describe('with directory', function() {
beforeEach(function() {
watcher.reset({
directory: main,
loadPaths: [main]
});
});
describe('when a file is changed in the graph', function() {
it('should return the files to compile', function() {
var files = watcher.changed(path.join(main, 'partials', '_one.scss'));
assert.deepEqual(files, {
added: [],
changed: [path.join(main, 'one.scss')],
removed: [],
});
});
describe('and @imports previously not imported files', function() {
it('should also return the new imported files', function() {
var file = path.join(main, 'partials', '_one.scss');
fs.writeFileSync(file, '@import "partials/three.scss";', { flag: 'a' });
var files = watcher.changed(file);
assert.deepEqual(files, {
added: [path.join(main, 'partials', '_three.scss')],
changed: [path.join(main, 'one.scss')],
removed: [],
});
});
});
});
describe('when a file is changed outside the graph', function() {
it('should return an empty array', function() {
var files = watcher.changed(path.join(sibling, 'partials', '_three.scss'));
assert.deepEqual(files, {
added: [],
changed: [],
removed: [],
});
});
});
describe('when a file is added in the graph', function() {
it('should return only the added file', function() {
var file = path.join(main, 'three.scss');
fs.writeFileSync(file, '@import "paritals/two.scss";');
var files = watcher.added(file);
assert.deepEqual(files, {
added: [file],
changed: [],
removed: [],
});
});
describe('and @imports previously not imported files', function() {
it('should also return the new imported files', function() {
var file = path.join(main, 'three.scss');
fs.writeFileSync(file, '@import "partials/three.scss";');
var files = watcher.added(file);
assert.deepEqual(files, {
added: [
file,
path.join(main, 'partials', '_three.scss')
],
changed: [],
removed: [],
});
});
});
});
describe('when a file is added outside the graph', function() {
it('should return an empty array', function() {
var files = watcher.added(path.join(sibling, 'three.scss'));
assert.deepEqual(files, {
added: [],
changed: [],
removed: [],
});
});
});
describe('when a file is removed from the graph', function() {
it('should return the files to compile', function() {
var files = watcher.removed(path.join(main, 'partials', '_one.scss'));
assert.deepEqual(files, {
added: [],
changed: [path.join(main, 'one.scss')],
removed: [path.join(main, 'partials', '_one.scss')],
});
});
});
describe('when a file is removed from outside the graph', function() {
it('should return an empty array', function() {
var files = watcher.removed(path.join(sibling, 'partials', '_three.scss'));
assert.deepEqual(files, {
added: [],
changed: [],
removed: [],
});
});
});
});
describe('with file', function() {
beforeEach(function() {
watcher.reset({
src: path.join(main, 'one.scss'),
loadPaths: [main]
});
});
describe('when a file is changed in the graph', function() {
it('should return the files to compile', function() {
var files = watcher.changed(path.join(main, 'partials', '_one.scss'));
assert.deepEqual(files, {
added: [],
changed: [path.join(main, 'one.scss')],
removed: [],
});
});
});
describe('when a file is changed outside the graph', function() {
it('should return an empty array', function() {
var files = watcher.changed(path.join(sibling, 'partials', '_three.scss'));
assert.deepEqual(files, {
added: [],
changed: [],
removed: [],
});
});
});
describe('when a file is added', function() {
it('should return an empty array', function() {
var file = path.join(main, 'three.scss');
fs.writeFileSync(file, '@import "paritals/one.scss";');
var files = watcher.added(file);
assert.deepEqual(files, {
added: [],
changed: [],
removed: [],
});
});
});
describe('when a file is removed from the graph', function() {
it('should return the files to compile', function() {
var files = watcher.removed(path.join(main, 'partials', '_one.scss'));
assert.deepEqual(files, {
added: [],
changed: [path.join(main, 'one.scss')],
removed: [path.join(main, 'partials', '_one.scss')],
});
});
});
describe('when a file is removed from outside the graph', function() {
it('should return an empty array', function() {
var files = watcher.removed(path.join(sibling, 'partials', '_three.scss'));
assert.deepEqual(files, {
added: [],
changed: [],
removed: [],
});
});
});
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment