mikser
Version:
Real-time static site generator
407 lines (375 loc) • 15.9 kB
JavaScript
var Promise = require('bluebird');
var path = require('path');
var cluster = require('cluster');
var chokidar = require('chokidar');
var extend = require('node.extend');
var fs = require("fs-extra-promise");
var _ = require('lodash');
var S = require('string');
var minimatch = require("minimatch");
module.exports = function(mikser) {
var watcher = {
watchers: {},
outputStack: {}
};
_.defaultsDeep(mikser.config, {
renderDelay: 1000,
copyDelay: 3000,
watcher: {
output: false,
reload: false,
ignored: [],
files: ['**/*'],
build: ['**/*.js', '**/*.css', '**/*.less', '**/*.coffee', '**/*.scss', '**/*.sass', '**/*.ts']
}
});
if (mikser.config.watcher.files) mikser.config.watcher.files = mikser.config.watcher.files.concat(mikser.config.watcher.build.map((glob) => '!' + glob));
var debug = mikser.debug('watcher');
let plugged = true;
if(cluster.isMaster) {
mikser.cli
.option('-W, --no-watch', 'don\'t watch your website for changes')
.init();
if (mikser.options.watch !== false) {
mikser.options = _.defaults({
watch: mikser.cli.watch,
}, mikser.options);
}
let renderTimeout;
let lastRenderId;
function render(action, actionId) {
if (renderTimeout && actionId && lastRenderId == actionId) clearTimeout(renderTimeout);
lastRenderId = actionId;
renderTimeout = setTimeout(() => {
action().then(() => {
return mikser.scheduler.process();
});
}, mikser.config.renderDelay);
};
let copying = false;
let copyingTimout;
function copy() {
if (!copying) {
if (copyingTimout) clearTimeout(copyingTimout);
copyingTimout = setTimeout(() => {
watcher.unplug().then(() => {
return mikser.manager.sync().then(() => {
return watcher.plug().then(() => {
copying = false;
});
});
});
}, mikser.config.copyDelay);
}
};
watcher.plug = function(force) {
if (!plugged || force) {
plugged = true;
let outputStack = watcher.outputStack;
watcher.outputStack = {};
let pending = _.keys(outputStack);
debug('Pending:', pending.length);
return Promise.map(pending, (file) => {
debug('Plugging:', file);
return outputStack[file]();
}).then(() => {
if (_.keys(watcher.outputStack).length) {
return watcher.plug(true);
} else {
if (!force && pending.length && !_.keys(watcher.outputStack).length) {
console.log('Pending file actions finished:', pending.length);
}
}
if (!force) {
debug('Plugged');
}
});
}
return Promise.resolve();
}
watcher.unplug = function() {
if (plugged) {
plugged = false;
debug('Unplugged');
}
return Promise.resolve();
}
watcher.stop = function(name) {
if (name) {
if (watcher.watchers[name]) {
console.log('Stop watching:', name);
watcher.watchers[name].close();
delete watcher.watchers[name];
}
} else {
for(let key in watcher.watchers) {
console.log('Stop watching:', key);
watcher.watchers[key].close();
delete watcher.watchers[key];
}
}
return Promise.resolve();
}
let outputAction = function(event, file) {
for (let ignore of mikser.config.watcher.ignored) {
if (minimatch(file, ignore)) {
debug('File ignored', file, ignore);
return;
}
}
let action = () => {
return mikser.emit('mikser.watcher.outputAction', event, file).catch((err) => mikser.diagnostics.log('error', err));;
};
if (plugged) {
return action();
} else {
debug('Scheduling:', file);
watcher.outputStack[file] = action;
}
};
let documentAction = function(event, file, folder) {
debug('Document', event + ':', path.sep + file);
return mikser.emit('mikser.watcher.documentAction', event, file).then(() => {
if (event == 'change') {
console.log('Document', event + ':', path.sep + file);
return render(() => mikser.manager.importDocument(file), file);
}
else if (event == 'add') {
let action = function() {
return mikser.manager.importDocument(file).then(() => {
return mikser.scheduler.scheduleAllDocuments();
});
}
return Promise.delay(2000).then(() => {
return fs.existsAsync(path.join(folder, file)).then((exists) => {
if (exists) {
console.log('Document', event + ':', path.sep + file);
let documentId = S(file).replaceAll('\\','/').ensureLeft('/').s;
if (mikser.state.entities['documents'][documentId]) {
return render(() => mikser.manager.importDocument(file), file);
}
return render(action, file);
}
});
});
}
else if (event == 'unlink') {
return Promise.delay(2000).then(() => {
return fs.existsAsync(path.join(folder, file)).then((exists) => {
if (!exists) {
console.log('Document', event + ':', path.sep + file);
return render(() => mikser.manager.deleteDocument(file), file);
}
});
});
}
}).catch((err) => mikser.diagnostics.log('error', err));
};
let layoutAction = function(event, file, folder) {
debug('Layout', event + ':', path.sep + file);
return mikser.emit('mikser.watcher.layoutAction', event, file).then(() => {
if (event == 'change') {
console.log('Layout', event + ':', path.sep + file);
return render(() => mikser.manager.importLayout(file), file);
} else if (event == 'add') {
return Promise.delay(2000).then(() => {
return fs.existsAsync(path.join(folder, file)).then((exists) => {
if (exists) {
console.log('Layout', event + ':', path.sep + file);
return render(() => mikser.manager.importLayout(file), file);
}
});
});
} else if (event == 'unlink') {
return Promise.delay(2000).then(() => {
return fs.existsAsync(path.join(folder, file)).then((exists) => {
if (!exists) return render(() => mikser.manager.deleteLayout(file), file);
});
});
}
}).catch((err) => mikser.diagnostics.log('error', err));
};
let viewAction = function(event, file, folder) {
debug('View', event + ':', path.sep + file);
return mikser.emit('mikser.watcher.viewAction', event, file).then(() => {
if (event == 'change') {
console.log('View', event + ':', path.sep + file);
return render(() => mikser.manager.importView(file), file);
} else if (event == 'add') {
return Promise.delay(2000).then(() => {
return fs.existsAsync(path.join(folder, file)).then((exists) => {
if (exists) {
console.log('View', event + ':', path.sep + file);
return render(() => mikser.manager.importView(file), file);
}
});
});
} else if (event == 'unlink') {
return Promise.delay(2000).then(() => {
return fs.existsAsync(path.join(folder, file)).then((exists) => {
if (!exists) {
console.log('View', event + ':', path.sep + file);
return render(() => mikser.manager.deleteView(file), file);
}
});
});
}
}).catch((err) => mikser.diagnostics.log('error', err));
};
let pluginsAction = function(event, file, folder) {
debug('Plugin', event + ':', path.sep + file);
return mikser.emit('mikser.watcher.pluginsAction', event, file).then(() => {
if (event == 'change') {
console.log('Plugin', event + ':', path.sep + file);
return render(() => mikser.manager.importPlugin(file), file);
} else if (event == 'add') {
return Promise.delay(2000).then(() => {
return fs.existsAsync(path.join(folder, file)).then((exists) => {
if (exists) {
console.log('Plugin', event + ':', path.sep + file);
return render(() => mikser.manager.importPlugin(file), file);
}
}).catch();
});
} else if (event == 'unlink') {
return Promise.delay(2000).then(() => {
return fs.existsAsync(path.join(folder, file)).then((exists) => {
if (!exists) {
console.log('Plugin', event + ':', path.sep + file);
return render(() => mikser.manager.deletePlugin(file), file);
}
}).catch(() => render(() => mikser.manager.deletePlugin(file), file));
});
}
}).catch((err) => mikser.diagnostics.log('error', err));
};
let fileAction = function(event, file, folder) {
debug('File', event + ':', path.sep + file);
file = path.join(folder, file);
for (let ignore of mikser.config.watcher.ignored) {
if (minimatch(file, ignore)) return;
}
return mikser.emit('mikser.watcher.fileAction', event, file).then(() => {
let action = () => {
return watcher.unplug().then(() => {
return mikser.tools.compile(file).then((processed) => {
return watcher.plug().then(() => {
if (!processed) {
copy(() => mikser);
}
});
});
});
}
if (event == 'add') {
return Promise.delay(200).then(() => {
return fs.existsAsync(file).then((exists) => {
if (exists) {
console.log('File', event + ':', file);
return action();
}
}).catch();
});
} else if (event == 'unlink'){
return Promise.delay(2000).then(() => {
return fs.existsAsync(file).then((exists) => {
if (!exists) {
console.log('File', event + ':', file);
return action();
}
}).catch(() => action);
});
} else if (event == 'change') {
console.log('File', event + ':', file);
return action();
}
}).catch((err) => mikser.diagnostics.log('error', err));
};
watcher.start = function() {
if (mikser.options.watch && mikser.config.watcher) {
if (mikser.config.watcher.reload && !watcher.watchers.reload) {
let watcherReload = chokidar.watch(mikser.config.watcher.reload, { cwd: mikser.config.outputFolder, ignoreInitial: true })
watcherReload.on('all', (event, file) => outputAction(event, file, mikser.config.outputFolder));
watcher.watchers.reload = watcherReload;
console.log('Watching reload:', mikser.config.outputFolder);
}
if (mikser.config.watcher.output && !watcher.watchers.output) {
let watcherOutput = chokidar.watch(mikser.config.watcher.output, { cwd: mikser.config.outputFolder, ignoreInitial: true, interval: 5007 })
watcherOutput.on('all', (event, file) => outputAction(event, file, mikser.config.outputFolder));
watcher.watchers.output = watcherOutput;
console.log('Watching output:', mikser.config.outputFolder);
}
if (mikser.config.documentsPattern && !watcher.watchers.documents) {
let watcherDocuments = chokidar.watch(mikser.config.documentsPattern, { cwd: mikser.config.documentsFolder, ignoreInitial: true, ignored: /[\/\\]\./})
watcherDocuments.on('all', (event, file) => documentAction(event, file, mikser.config.documentsFolder));
watcher.watchers.documents = watcherDocuments;
console.log('Watching documents:', mikser.config.documentsFolder);
}
if (mikser.config.layoutsPattern && !watcher.watchers.layouts) {
let watcherLayouts = chokidar.watch(mikser.config.layoutsPattern, { cwd: mikser.config.layoutsFolder, ignoreInitial: true, ignored: /[\/\\]\./})
watcherLayouts.on('all', (event, file) => layoutAction(event, file, mikser.config.layoutsFolder));
watcher.watchers.layouts = watcherLayouts;
console.log('Watching layouts:', mikser.config.layoutsFolder);
}
if (fs.existsSync(mikser.config.viewsFolder)) {
if (mikser.config.viewsPattern && !watcher.watchers.views) {
let watcherViews = chokidar.watch(mikser.config.viewsPattern, { cwd: mikser.config.viewsFolder, ignoreInitial: true, ignored: /[\/\\]\./})
watcherViews.on('all', (event, file) => viewAction(event, file, mikser.config.viewsFolder));
watcher.watchers.views = watcherViews;
console.log('Watching views:', mikser.config.viewsFolder);
}
}
if (mikser.config.pluginsPattern && fs.existsSync(mikser.config.pluginsFolder)) {
if (!watcher.watchers.plugins) {
let watcherPlugins = chokidar.watch(mikser.config.pluginsPattern, { cwd: mikser.config.pluginsFolder, ignoreInitial: true, ignored: /[\/\\]\./})
watcherPlugins.on('all', (event, file) => pluginsAction(event, file, mikser.config.pluginsFolder));
watcher.watchers.plugins = watcherPlugins;
console.log('Watching plugins:', mikser.config.pluginsFolder);
}
if (mikser.config.watcher.build && !watcher.watchers.buildViews) {
let watcherBuildViews = chokidar.watch(mikser.config.watcher.build, { cwd: mikser.config.viewsFolder, ignoreInitial: true, ignored: /[\/\\]\./})
watcherBuildViews.on('all', (event, file) => fileAction(event, path.join(mikser.config.viewsFolder, file), mikser.config.viewsFolder));
watcher.watchers.buildViews = watcherBuildViews;
console.log('Watching build layouts:', mikser.config.viewsFolder);
}
}
if (mikser.config.watcher.build && !watcher.watchers.buldFiles) {
let watcherBuildFiles = chokidar.watch(mikser.config.watcher.build, { cwd: mikser.config.filesFolder, ignoreInitial: true, ignored: /[\/\\]\./})
watcherBuildFiles.on('all', (event, file) => fileAction(event, file, mikser.config.filesFolder));
watcher.watchers.buldFiles = watcherBuildFiles;
console.log('Watching build files:', mikser.config.filesFolder);
}
if (mikser.config.watcher.build && !watcher.watchers.buildLayouts) {
let watcherBuildLayouts = chokidar.watch(mikser.config.watcher.build, { cwd: mikser.config.layoutsFolder, ignoreInitial: true, ignored: /[\/\\]\./})
watcherBuildLayouts.on('all', (event, file) => fileAction(event, file, mikser.config.layoutsFolder));
watcher.watchers.buildLayouts = watcherBuildLayouts;
console.log('Watching build layouts:', mikser.config.layoutsFolder);
}
if (mikser.config.watcher.files && !watcher.watchers.files) {
let watcherFiles = chokidar.watch(mikser.config.watcher.files, { cwd: mikser.config.filesFolder, interval: 5007, ignoreInitial: true, ignored: /[\/\\]\./})
watcherFiles.on('all', (event, file) => fileAction(event, file, mikser.config.filesFolder));
watcher.watchers.files = watcherFiles;
console.log('Watching files:', mikser.config.filesFolder);
}
if (fs.existsSync(mikser.config.sharedFolder)) {
if (mikser.config.watcher.build && !watcher.watchers.buildShared) {
let watcherBuildShared = chokidar.watch(mikser.config.watcher.build, { cwd: mikser.config.sharedFolder, ignoreInitial: true, ignored: /[\/\\]\./})
watcherBuildShared.on('all', (event, file) => fileAction(event, file, mikser.config.sharedFolder));
watcher.watchers.buildShared = watcherBuildShared;
console.log('Watching build shared:', mikser.config.sharedFolder);
}
if (mikser.config.watcher.files && !watcher.watchers.sharedFolder) {
let watcherShared = chokidar.watch(mikser.config.watcher.files, { cwd: mikser.config.sharedFolder, ignoreInitial: true, interval: 5007, ignored: /[\/\\]\./})
watcherShared.on('all', (event, file) => fileAction(event, file, mikser.config.sharedFolder));
watcher.watchers.shared = watcherShared;
console.log('Watching shared:', mikser.config.sharedFolder);
}
}
}
return Promise.resolve();
};
}
mikser.watcher = watcher;
return Promise.resolve(mikser);
}