2 * This is a wrapper task to handle the grunt watch command. It attempts to use
3 * Watchman to monitor for file changes, if it's installed, because it's much faster.
5 * If Watchman isn't installed then it falls back to the grunt-contrib-watch file
6 * watcher for backwards compatibility.
11 module.exports = grunt => {
13 * This is a wrapper task to handle the grunt watch command. It attempts to use
14 * Watchman to monitor for file changes, if it's installed, because it's much faster.
16 * If Watchman isn't installed then it falls back to the grunt-contrib-watch file
17 * watcher for backwards compatibility.
19 const watchHandler = function() {
20 const async = require('async');
21 const watchTaskDone = this.async();
22 let watchInitialised = false;
23 let watchTaskQueue = {};
24 let processingQueue = false;
26 const watchman = require('fb-watchman');
27 const watchmanClient = new watchman.Client();
29 // Grab the tasks and files that have been queued up and execute them.
30 var processWatchTaskQueue = function() {
31 if (!Object.keys(watchTaskQueue).length || processingQueue) {
32 // If there is nothing in the queue or we're already processing then wait.
36 processingQueue = true;
38 // Grab all tasks currently in the queue.
39 var queueToProcess = watchTaskQueue;
44 Object.keys(queueToProcess),
45 function(task, next) {
46 var files = queueToProcess[task];
47 var filesOption = '--files=' + files.join(',');
48 grunt.log.ok('Running task ' + task + ' for files ' + filesOption);
50 // Spawn the task in a child process so that it doesn't kill this one
54 // Spawn with the grunt bin.
56 // Run from current working dir and inherit stdio from process.
58 cwd: grunt.moodleEnv.fullRunDir,
61 args: [task, filesOption]
63 function(err, res, code) {
65 // The grunt task failed.
69 // Move on to the next task.
75 // No longer processing.
76 processingQueue = false;
77 // Once all of the tasks are done then recurse just in case more tasks
78 // were queued while we were processing.
79 processWatchTaskQueue();
84 const originalWatchConfig = grunt.config.get(['watch']);
85 const watchConfig = Object.keys(originalWatchConfig).reduce(function(carry, key) {
86 if (key == 'options') {
90 const value = originalWatchConfig[key];
92 const taskNames = value.tasks;
93 const files = value.files;
96 excludes = value.excludes;
99 taskNames.forEach(function(taskName) {
109 watchmanClient.on('error', function(error) {
110 // We have to add an error handler here and parse the error string because the
111 // example way from the docs to check if Watchman is installed doesn't actually work!!
112 // See: https://github.com/facebook/watchman/issues/509
113 if (error.message.match('Watchman was not found')) {
114 // If watchman isn't installed then we should fallback to the other watch task.
115 grunt.log.ok('It is recommended that you install Watchman for better performance using the "watch" command.');
117 // Fallback to the old grunt-contrib-watch task.
118 grunt.renameTask('watch-grunt', 'watch');
119 grunt.task.run(['watch']);
120 // This task is finished.
123 grunt.log.error(error);
129 watchmanClient.on('subscription', function(resp) {
130 if (resp.subscription !== 'grunt-watch') {
134 resp.files.forEach(function(file) {
135 grunt.log.ok('File changed: ' + file.name);
137 var fullPath = grunt.moodleEnv.fullRunDir + '/' + file.name;
138 Object.keys(watchConfig).forEach(function(task) {
140 const fileGlobs = watchConfig[task].files;
141 var match = fileGlobs.some(function(fileGlob) {
142 return grunt.file.isMatch(`**/${fileGlob}`, fullPath);
146 // If we are watching a subdirectory then the file.name will be relative
147 // to that directory. However the grunt tasks expect the file paths to be
148 // relative to the Gruntfile.js location so let's normalise them before
149 // adding them to the queue.
150 var relativePath = fullPath.replace(grunt.moodleEnv.gruntFilePath + '/', '');
151 if (task in watchTaskQueue) {
152 if (!watchTaskQueue[task].includes(relativePath)) {
153 watchTaskQueue[task] = watchTaskQueue[task].concat(relativePath);
156 watchTaskQueue[task] = [relativePath];
162 processWatchTaskQueue();
165 process.on('SIGINT', function() {
166 // Let the user know that they may need to manually stop the Watchman daemon if they
167 // no longer want it running.
168 if (watchInitialised) {
169 grunt.log.ok('The Watchman daemon may still be running and may need to be stopped manually.');
175 // Initiate the watch on the current directory.
176 watchmanClient.command(['watch-project', grunt.moodleEnv.fullRunDir], function(watchError, watchResponse) {
178 grunt.log.error('Error initiating watch:', watchError);
183 if ('warning' in watchResponse) {
184 grunt.log.error('warning: ', watchResponse.warning);
187 var watch = watchResponse.watch;
188 var relativePath = watchResponse.relative_path;
189 watchInitialised = true;
191 watchmanClient.command(['clock', watch], function(clockError, clockResponse) {
193 grunt.log.error('Failed to query clock:', clockError);
198 // Generate the expression query used by watchman.
199 // Documentation is limited, but see https://facebook.github.io/watchman/docs/expr/allof.html for examples.
200 // We generate an expression to match any value in the files list of all of our tasks, but excluding
201 // all value in the excludes list of that task.
206 // ['match', validPath, 'wholename'],
207 // ['match', validPath, 'wholename'],
211 // ['match', invalidPath, 'wholename'],
212 // ['match', invalidPath, 'wholename'],
216 var matchWholeName = fileGlob => ['match', fileGlob, 'wholename'];
217 var matches = Object.keys(watchConfig).map(function(task) {
219 matchAll.push(['anyof'].concat(watchConfig[task].files.map(matchWholeName)));
221 if (watchConfig[task].excludes.length) {
222 matchAll.push(['not', ['anyof'].concat(watchConfig[task].excludes.map(matchWholeName))]);
225 return ['allof'].concat(matchAll);
228 matches = ['anyof'].concat(matches);
232 // Which fields we're interested in.
233 fields: ["name", "size", "type"],
234 // Add our time constraint.
235 since: clockResponse.clock
239 /* eslint-disable camelcase */
240 sub.relative_root = relativePath;
243 watchmanClient.command(['subscribe', watch, 'grunt-watch', sub], function(subscribeError) {
244 if (subscribeError) {
245 // Probably an error in the subscription criteria.
246 grunt.log.error('failed to subscribe: ', subscribeError);
251 grunt.log.ok('Listening for changes to files in ' + grunt.moodleEnv.fullRunDir);
257 // Rename the grunt-contrib-watch "watch" task because we're going to wrap it.
258 grunt.renameTask('watch', 'watch-grunt');
260 // Register the new watch handler.
261 grunt.registerTask('watch', 'Run tasks on file changes', watchHandler);
266 nospawn: true // We need not to spawn so config can be changed dynamically.