-
Notifications
You must be signed in to change notification settings - Fork 356
/
taskrunner.js
387 lines (342 loc) · 11.6 KB
/
taskrunner.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
/*
* grunt-contrib-watch
* http://gruntjs.com/
*
* Copyright (c) 2018 "Cowboy" Ben Alman, contributors
* Licensed under the MIT license.
*/
'use strict';
var path = require('path');
var EE = require('events').EventEmitter;
var util = require('util');
var _ = require('lodash');
var async = require('async');
// Track which targets to run after reload
var reloadTargets = [];
// A default target name for config where targets are not used (keep this unique)
var defaultTargetName = '_$_default_$_';
module.exports = function(grunt) {
var TaskRun = require('./taskrun')(grunt);
var livereload = require('./livereload')(grunt);
function Runner() {
EE.call(this);
// Name of the task
this.name = 'watch';
// Options for the runner
this.options = {};
// Function to close the task
this.done = function() {};
// Targets available to task run
this.targets = Object.create(null);
// The queue of task runs
this.queue = [];
// Whether we're actively running tasks
this.running = false;
// If a nospawn task has ran (and needs the watch to restart)
this.nospawn = false;
// Set to true before run() to reload task
this.reload = false;
// For re-queuing arguments with the task that originally ran this
this.nameArgs = [];
// A list of changed files to feed to task runs for livereload
this.changedFiles = Object.create(null);
}
util.inherits(Runner, EE);
// Init a task for taskrun
Runner.prototype.init = function init(name, defaults, done) {
var self = this;
self.name = name || grunt.task.current.name || 'watch';
self.options = self._options(grunt.config([self.name, 'options']) || {}, defaults || {});
self.reload = false;
self.nameArgs = grunt.task.current.nameArgs ? grunt.task.current.nameArgs : self.name;
// Normalize cwd option
if (typeof self.options.cwd === 'string') {
self.options.cwd = {files: self.options.cwd, spawn: self.options.cwd};
}
// Function to call when closing the task
self.done = done || grunt.task.current.async();
// If a default livereload server for all targets
// Use task level unless target level overrides
var taskLRConfig = grunt.config([self.name, 'options', 'livereload']);
if (self.options.target && taskLRConfig) {
var targetLRConfig = grunt.config([self.name, self.options.target, 'options', 'livereload']);
if (targetLRConfig) {
// Dont use task level as target level will be used instead
taskLRConfig = false;
}
}
if (taskLRConfig) {
self.livereload = livereload(taskLRConfig);
}
// Return the targets normalized
var targets = self._getTargets(self.name);
if (self.running) {
// If previously running, complete the last run
self.complete();
} else if (reloadTargets.length > 0) {
// If not previously running but has items in the queue, needs run
self.queue = reloadTargets;
reloadTargets = [];
self.run();
} else {
if (!self.hadError) {
// Check whether target's tasks should run at start w/ atBegin option
self.queue = targets.filter(function(tr) {
return tr.options.atBegin === true && tr.tasks.length > 0;
}).map(function(tr) {
return tr.name;
});
} else {
// There was an error in atBegin task, we can't re-run it, as this would
// create an infinite loop of failing tasks
// See https://github.com/gruntjs/grunt-contrib-watch/issues/169
self.queue = [];
self.hadError = false;
}
if (self.queue.length > 0) {
self.run();
}
}
return targets;
};
// Normalize targets from config
Runner.prototype._getTargets = function _getTargets(name) {
var self = this;
grunt.task.current.requiresConfig(name);
var config = grunt.config(name);
var onlyTarget = self.options.target ? self.options.target : false;
var targets = (onlyTarget ? [onlyTarget] : Object.keys(config)).filter(function(key) {
if (key === 'options') {
return false;
}
return typeof config[key] !== 'string' && !Array.isArray(config[key]);
}).map(function(target) {
// Fail if any required config properties have been omitted
grunt.task.current.requiresConfig([name, target, 'files']);
var cfg = grunt.config([name, target]);
cfg.name = target;
cfg.options = self._options(cfg.options || {}, self.options);
self.add(cfg);
return cfg;
}, self);
// Allow "basic" non-target format
if (typeof config.files === 'string' || Array.isArray(config.files)) {
var cfg = {
files: config.files,
tasks: config.tasks,
name: defaultTargetName,
options: self._options(config.options || {}, self.options)
};
targets.push(cfg);
self.add(cfg);
}
return targets;
};
// Default options
Runner.prototype._options = function _options() {
var args = Array.prototype.slice.call(arguments).concat({
// The cwd to spawn within
cwd: process.cwd(),
// Additional cli args to append when spawning
cliArgs: _.without.apply(null, [[].slice.call(process.argv, 2)].concat(grunt.cli.tasks)),
interrupt: false,
nospawn: false,
spawn: true,
atBegin: false,
event: ['all'],
target: null
});
return _.defaults.apply(_, args);
};
// Run the current queue of task runs
Runner.prototype.run = _.debounce(function run() {
var self = this;
if (self.queue.length < 1) {
self.running = false;
return;
}
// Re-grab task options in case they changed between runs
self.options = self._options(grunt.config([self.name, 'options']) || {}, self.options);
// If we should interrupt
if (self.running === true) {
var shouldInterrupt = true;
self.queue.forEach(function(name) {
var tr = self.targets[name];
if (tr && tr.options.interrupt !== true) {
shouldInterrupt = false;
return false;
}
});
if (shouldInterrupt === true) {
self.interrupt();
} else {
// Dont interrupt the tasks running
return;
}
}
// If we should reload
if (self.reload) {
return self.reloadTask();
}
// Trigger that tasks runs have started
self.emit('start');
self.running = true;
// Run each target
var shouldComplete = true;
async.forEachSeries(self.queue, function(name, next) {
var tr = self.targets[name];
if (!tr) {
return next();
}
// Re-grab options in case they changed between runs
tr.options = self._options(grunt.config([self.name, name, 'options']) || {}, tr.options, self.options);
if (tr.options.spawn === false || tr.options.nospawn === true) {
shouldComplete = false;
}
tr.run(next);
}, function() {
if (shouldComplete) {
self.complete();
} else {
grunt.task.mark().run(self.nameArgs);
self.done();
}
});
}, 250);
// Push targets onto the queue
Runner.prototype.add = function add(target) {
var self = this;
if (!this.targets[target.name || 0]) {
// Private method for getting latest config for a watch target
target._getConfig = function(name) {
var cfgPath = [self.name];
if (target.name !== defaultTargetName) {
cfgPath.push(target.name);
}
if (name) {
cfgPath.push(name);
}
return grunt.config(cfgPath);
};
// Create a new TaskRun instance
var tr = new TaskRun(target);
// Add livereload to task runs
// Get directly from config as task level options are merged.
// We only want a single default LR server and then
// allow each target to override their own.
var lrconfig = grunt.config([this.name, target.name || 0, 'options', 'livereload']);
if (lrconfig) {
tr.livereload = livereload(lrconfig);
} else if (this.livereload && lrconfig !== false) {
tr.livereload = this.livereload;
}
return this.targets[tr.name] = tr;
}
return false;
};
// Do this when queued task runs have completed/scheduled
Runner.prototype.complete = function complete() {
var self = this;
if (self.running === false) {
return;
}
self.running = false;
var time = 0;
for (var i = 0, len = self.queue.length; i < len; ++i) {
var name = self.queue[i];
var target = self.targets[name];
if (!target) {
return;
}
if (target.startedAt !== false) {
time += target.complete();
self.queue.splice(i--, 1);
len--;
// if we're just livereloading and no tasks
// it can happen too fast and we dont report it
if (target.options.livereload && target.tasks.length < 1) {
time += 0.0001;
}
}
}
var elapsed = (time > 0) ? Number(time / 1000) : 0;
self.changedFiles = Object.create(null);
self.emit('end', elapsed);
};
// Run through completing every target in the queue
Runner.prototype._completeQueue = function _completeQueue() {
var self = this;
self.queue.forEach(function(name) {
var target = self.targets[name];
if (!target) {
return;
}
target.complete();
});
};
// Interrupt the running tasks
Runner.prototype.interrupt = function interrupt() {
var self = this;
self._completeQueue();
grunt.task.clearQueue();
self.emit('interrupt');
};
// Attempt to make this task run forever
Runner.prototype.forever = function forever() {
var self = this;
function rerun() {
// Clear queue and rerun to prevent failing
self._completeQueue();
grunt.task.clearQueue();
grunt.task.run(self.nameArgs);
self.running = false;
// Mark that there was an error and we needed to rerun
self.hadError = true;
}
grunt.fail.forever_warncount = 0;
grunt.fail.forever_errorcount = 0;
grunt.warn = grunt.fail.warn = function(e) {
grunt.fail.forever_warncount ++;
var message = typeof e === 'string' ? e : e.message;
grunt.log.writeln(('Warning: ' + message).yellow);
if (!grunt.option('force')) {
rerun();
}
};
grunt.fatal = grunt.fail.fatal = function(e) {
grunt.fail.forever_errorcount ++;
var message = typeof e === 'string' ? e : e.message;
grunt.log.writeln(('Fatal error: ' + message).red);
rerun();
};
};
// Clear the require cache for all passed filepaths.
Runner.prototype.clearRequireCache = function() {
// If a non-string argument is passed, it's an array of filepaths, otherwise
// each filepath is passed individually.
var filepaths = typeof arguments[0] !== 'string' ? arguments[0] : Array.prototype.slice(arguments);
// For each filepath, clear the require cache, if necessary.
filepaths.forEach(function(filepath) {
var abspath = path.resolve(filepath);
if (require.cache[abspath]) {
grunt.verbose.write('Clearing require cache for "' + filepath + '" file...').ok();
delete require.cache[abspath];
}
});
};
// Reload this watch task, like when a Gruntfile is edited
Runner.prototype.reloadTask = function() {
var self = this;
// Which targets to run after reload
reloadTargets = self.queue;
self.emit('reload', reloadTargets);
// Re-init the watch task config
grunt.task.init([self.name]);
// Complete all running tasks
self._completeQueue();
// Run the watch task again
grunt.task.run(self.nameArgs);
self.done();
};
return new Runner();
};