UNPKG

erkalicious-grunt-este-watch

Version:

Run predefined tasks whenever watched file changes. LiveReload included. Erkalicious fork.

445 lines (343 loc) 10.1 kB
/*globals module*/ /** * @fileoverview File watcher for Grunt.js * Copyright (c) 2013 Daniel Steigerwald * * @param {Grunt} grunt - the grunt instance */ module.exports = function exports(grunt) { 'use strict'; /*globals process*/ /*globals require*/ var fs = require('fs'), path = require('path'), tinylr = require('tiny-lr-fork'), semver = require('semver'), DEBOUNCE_DELAY = 200, WAIT_FOR_UNLOCK_INTERVAL = 10, WAIT_FOR_UNLOCK_TRY_LIMIT = 50, files = [], changedFilesForLiveReload = [], isRunning = false, deferredFiles = [], dispatchers = [], done, firstRun = true, lrServer, options, watchers = {}; if (semver.lt(process.versions.node, '0.9.2')) { grunt.fail.warn('Use node 0.9.2+, due to buggy fs.watch'); } grunt.registerTask('esteWatch', 'Este files watcher.', function esteWatch() { options = this.options({ dirs: [ 'bower_components/closure-library/**/', 'bower_components/este-library/**/', '!bower_components/este-library/node_modules/**/', 'client/**/{js,css}/**/' ], ignoredFiles: [], livereload: { enabled: true, port: 35729, extensions: ['js', 'css'] }, // friendly beep on error beep: true, errorStack: true }); done = this.async(); if (firstRun) { firstRun = false; if (options.livereload.enabled) { grunt.log.writeln('Starting live reload server on port: ' + options.livereload.port); runLiveReloadServer(); } keepThisTaskRunForeverViaHideousHack(); } refreshWatchers(); grunt.log.ok('Waiting...'); isRunning = false; dispatchWaitingChanges(); }); function dispatchWaitingChanges() { if (isRunning) { return; } if (deferredFiles.length) { var theFiles = grunt.util._.uniq(deferredFiles), theDispatchers = grunt.util._.uniq(dispatchers); deferredFiles.length = 0; dispatchers.length = 0; grunt.verbose.ok('Files changed within watch task:\n' + theFiles.join('\n')); theFiles.forEach(function dispatch(filepath) { if (theDispatchers.indexOf(filepath) === -1) { grunt.verbose.ok('dispatched deferred: ' + filepath); onFileChange(filepath); } else { grunt.verbose.ok('Didn\'t dispatch: ' + filepath); } }); } } // TODO: handle hypothetic situation, when task create dir function refreshWatchers() { var start = Date.now(), allDirs = grunt.file.expand(options.dirs), dirsAlreadyWatched = Object.keys(watchers), newDirs = [], oldDirs = []; allDirs.forEach(function watchDir(dir) { if (!watchers[dir]) { newDirs.push(dir); watchers[dir] = fs.watch(dir, function fireCallback(event, filename) { onDirChange(event, filename, dir); }); } }); for (var i = 0; i < dirsAlreadyWatched.length; i++) { var dir = dirsAlreadyWatched[i]; if (allDirs.indexOf(dir) === -1) { oldDirs.push(dir); watchers[dir].close(); delete watchers[dir]; } } function dirString(num) { if (num === 1) { return 'dir is'; } return 'dirs are'; } if (oldDirs.length) { grunt.verbose.writeln('Dirs removed from watch list: ' + oldDirs.join(', ')); grunt.log.writeln((oldDirs.length + ' ' + dirString(oldDirs.length) + ' no longer watched').cyan); } if (newDirs.length) { grunt.verbose.writeln('Dirs added to watch list: ' + newDirs.join(', ')); grunt.log.writeln((newDirs.length + ' more ' + dirString(newDirs.length) + ' now being watched, within ' + (Date.now() - start) + ' ms.').cyan); } } function runLiveReloadServer() { /*globals Buffer*/ // If key and cert file paths are provided, read them. ['key', 'cert'].forEach(function read(attr) { if (options.livereload[attr] && !Buffer.isBuffer(options.livereload[attr])) { options.livereload[attr] = grunt.file.read(options.livereload[attr]); } }); lrServer = tinylr(options.livereload); lrServer.server.removeAllListeners('error'); lrServer.server.on('error', function error(err) { if (err.code === 'EADDRINUSE') { grunt.fatal('Port ' + options.livereload.port + ' is already in use by another process.'); grunt.fatal('Open OS process manager and kill all node\'s processes.'); } else { grunt.fatal(err); } process.exit(1); }); lrServer.listen(options.livereload.port, function livereloadError(err) { if (err) { grunt.fatal(err); return; } grunt.verbose.ok('LiveReload server successfully started on port: ' + options.livereload.port); }); } function stopAndRun(tasks, reset) { tasks.push('esteWatch'); if (reset) { files.length = 0; changedFilesForLiveReload.length = 0; deferredFiles.length = 0; dispatchers.length = 0; } if (done) { done(); done = null; grunt.verbose.ok('Called grunt este watch done'); } else if (grunt.task.current.async) { grunt.verbose.ok('Called done of the current task'); grunt.task.current.async()(); } grunt.task.clearQueue(); grunt.task.run(tasks); } // TODO: fork&fix Grunt function keepThisTaskRunForeverViaHideousHack() { function createLog(isWarning) { return function log(e) { var message; if (typeof e === 'string') { message = e; } else { message = (options.errorStack && e.stack) ? e.stack : e.message; } var line = options.beep ? '\x07' : ''; if (isWarning) { line += ('Warning: ' + message).yellow; } else { line += ('Fatal error: ' + message).red; } grunt.log.writeln(line); if (!grunt.option('force')) { stopAndRun([], true); } }; } grunt.warn = grunt.fail.warn = createLog(true); grunt.fatal = grunt.fail.fatal = createLog(false); } function onDirChange(event, filename, dir) { var filepath = path.join(dir || '', filename || ''); // Normalize \\ paths to / paths. Yet another Windows fix. filepath = filepath.replace(/\\/g, '/'); var minimatchOptions = { dot: true, matchBase: true, nocomment: true, nonegate: true }; if (grunt.file.isMatch(minimatchOptions, options.ignoredFiles, filepath)) { return; } // fs.statSync fails on deleted symlink dir with 'Abort trap: 6' exception // https://github.com/bevry/watchr/issues/42 // https://github.com/joyent/node/issues/4261 var fileExists = fs.existsSync(filepath); if (!fileExists) { return; } grunt.verbose.ok('changed: ' + filepath); if (fs.statSync(filepath).isDirectory()) { grunt.verbose.ok('Dir changed: ' + filepath); } else if (isRunning) { grunt.verbose.ok('Still running, so defer: ' + filepath); deferredFiles.push(filepath); } else { onFileChange(filepath); } } function onFileChange(filepath) { var minimatchOptions = { dot: true, matchBase: true, nocomment: true, nonegate: true }; if (grunt.file.isMatch(minimatchOptions, options.ignoredFiles, filepath)) { return; } if (options.livereload.enabled) { changedFilesForLiveReload.push(filepath); } files.push(filepath); runQueue(); } function getFilepathTasks(filepath) { var ext = path.extname(filepath).slice(1), config = grunt.config.get(['esteWatch', ext]); if (!config) { config = grunt.config.get(['esteWatch', '*']); } if (!config) { return []; } var tasks = config(filepath) || []; if (!Array.isArray(tasks)) { tasks = [tasks]; } return tasks; } var runRequest; function runQueue() { // No-op if no files have been changed if (!files.length) { return; } clearTimeout(runRequest); // Create a timeout that simulates an asynchronous request. Add a short // time to catch the instances where child tasks edit files more than // once. runRequest = setTimeout(function runTasks() { isRunning = true; var theFiles = grunt.util._.uniq(files); files.length = 0; var tasks = [], taskFiles = []; for (var i = 0; i < theFiles.length; i++) { var filepath = theFiles[i], fileTasks = getFilepathTasks(filepath); if (fileTasks.length) { var waitTryCount = 0; waitForFileUnlock(filepath, fileTasks); } } function waitForFileUnlock(filepath, fileTasks) { var isLocked = false, unlockTimer; waitTryCount++; try { fs.readFileSync(filepath); } catch (e) { // File is locked isLocked = true; } if (!isLocked || waitTryCount > WAIT_FOR_UNLOCK_TRY_LIMIT) { taskFiles.push(filepath); dispatchers.push(filepath); var wasEqual = false; listEquality: for (var i = 0; i < tasks.length; i++) { var addedTasks = tasks[i]; if (addedTasks.length !== fileTasks.length) { continue; } for (var j = 0; j < addedTasks.length; j++) { if (addedTasks[j] !== fileTasks[j]) { continue listEquality; } } wasEqual = true; break; } if (!wasEqual) { tasks.push(fileTasks); } } else { grunt.verbose.writeln('Waiting for file to unlock (' + waitTryCount + '): ' + path); clearTimeout(unlockTimer); unlockTimer = setTimeout(waitForFileUnlock, WAIT_FOR_UNLOCK_INTERVAL); } } var livereloadFiles = grunt.util._.uniq(changedFilesForLiveReload).filter(function filter(filepath) { return options.livereload.extensions.indexOf(path.extname(filepath).slice(1)) !== -1; }); changedFilesForLiveReload.length = 0; var changedFiles = grunt.util._.uniq(livereloadFiles.concat(taskFiles)); grunt.log.writeln(''); grunt.log.writeln('File(s) changed:'); grunt.log.ok(changedFiles.join('\n')); if (options.livereload.enabled && livereloadFiles.length) { grunt.verbose.ok('Notifying live reload about: ' + livereloadFiles.join(', ')); lrServer.changed({ body: { files: livereloadFiles } }); } if (tasks.length) { var flattenedTasks = tasks.reduce(function flatten(trans, arr) { return trans.concat(arr); }); stopAndRun(flattenedTasks); } else { grunt.log.writeln(''); grunt.log.writeln('Continuing watch'); grunt.log.ok('Waiting...'); isRunning = false; } }, DEBOUNCE_DELAY); } };