1 | /*
|
2 | * grunt
|
3 | * http://gruntjs.com/
|
4 | *
|
5 | * Copyright (c) 2012 "Cowboy" Ben Alman
|
6 | * Licensed under the MIT license.
|
7 | * https://github.com/gruntjs/grunt/blob/master/LICENSE-MIT
|
8 | */
|
9 |
|
10 | module.exports = function(grunt) {
|
11 |
|
12 | // Nodejs libs.
|
13 | var fs = require('fs');
|
14 | var path = require('path');
|
15 | // In Nodejs 0.8.0, existsSync moved from path -> fs.
|
16 | var existsSync = fs.existsSync || path.existsSync;
|
17 |
|
18 | // ==========================================================================
|
19 | // TASKS
|
20 | // ==========================================================================
|
21 |
|
22 | // Keep track of last modified times of files, in case files are reported to
|
23 | // have changed incorrectly.
|
24 | var mtimes = {};
|
25 |
|
26 | grunt.registerTask('watch', 'Run predefined tasks whenever watched files change.', function(target) {
|
27 | this.requiresConfig('watch');
|
28 | // Build an array of files/tasks objects.
|
29 | var watch = grunt.config('watch');
|
30 | var targets = target ? [target] : Object.keys(watch).filter(function(key) {
|
31 | return typeof watch[key] !== 'string' && !Array.isArray(watch[key]);
|
32 | });
|
33 | targets = targets.map(function(target) {
|
34 | // Fail if any required config properties have been omitted.
|
35 | target = ['watch', target];
|
36 | this.requiresConfig(target.concat('files'), target.concat('tasks'));
|
37 | return grunt.config(target);
|
38 | }, this);
|
39 |
|
40 | // Allow "basic" non-target format.
|
41 | if (typeof watch.files === 'string' || Array.isArray(watch.files)) {
|
42 | targets.push({files: watch.files, tasks: watch.tasks});
|
43 | }
|
44 |
|
45 | grunt.log.write('Waiting...');
|
46 |
|
47 | // This task is asynchronous.
|
48 | var taskDone = this.async();
|
49 | // Get a list of files to be watched.
|
50 | var patterns = grunt.utils._.chain(targets).pluck('files').flatten().uniq().value();
|
51 | var getFiles = grunt.file.expandFiles.bind(grunt.file, patterns);
|
52 | // The tasks to be run.
|
53 | var tasks = []; //grunt.config(tasksProp);
|
54 | // This task's name + optional args, in string format.
|
55 | var nameArgs = this.nameArgs;
|
56 | // An ID by which the setInterval can be canceled.
|
57 | var intervalId;
|
58 | // Files that are being watched.
|
59 | var watchedFiles = {};
|
60 | // File changes to be logged.
|
61 | var changedFiles = {};
|
62 |
|
63 | // Define an alternate fail "warn" behavior.
|
64 | grunt.fail.warnAlternate = function() {
|
65 | grunt.task.clearQueue({untilMarker: true}).run(nameArgs);
|
66 | };
|
67 |
|
68 | // Cleanup when files have changed. This is debounced to handle situations
|
69 | // where editors save multiple files "simultaneously" and should wait until
|
70 | // all the files are saved.
|
71 | var done = grunt.utils._.debounce(function() {
|
72 | // Clear the files-added setInterval.
|
73 | clearInterval(intervalId);
|
74 | // Ok!
|
75 | grunt.log.ok();
|
76 | var fileArray = Object.keys(changedFiles);
|
77 | fileArray.forEach(function(filepath) {
|
78 | // Log which file has changed, and how.
|
79 | grunt.log.ok('File "' + filepath + '" ' + changedFiles[filepath] + '.');
|
80 | // Clear the modified file's cached require data.
|
81 | grunt.file.clearRequireCache(filepath);
|
82 | });
|
83 | // Unwatch all watched files.
|
84 | Object.keys(watchedFiles).forEach(unWatchFile);
|
85 | // For each specified target, test to see if any files matching that
|
86 | // target's file patterns were modified.
|
87 | targets.forEach(function(target) {
|
88 | var files = grunt.file.expandFiles(target.files);
|
89 | var intersection = grunt.utils._.intersection(fileArray, files);
|
90 | // Enqueue specified tasks if a matching file was found.
|
91 | if (intersection.length > 0 && target.tasks) {
|
92 | grunt.task.run(target.tasks).mark();
|
93 | }
|
94 | });
|
95 | // Enqueue the watch task, so that it loops.
|
96 | grunt.task.run(nameArgs);
|
97 | // Continue task queue.
|
98 | taskDone();
|
99 | }, 250);
|
100 |
|
101 | // Handle file changes.
|
102 | function fileChanged(status, filepath) {
|
103 | // If file was deleted and then re-added, consider it changed.
|
104 | if (changedFiles[filepath] === 'deleted' && status === 'added') {
|
105 | status = 'changed';
|
106 | }
|
107 | // Keep track of changed status for later.
|
108 | changedFiles[filepath] = status;
|
109 | // Execute debounced done function.
|
110 | done();
|
111 | }
|
112 |
|
113 | // Watch a file.
|
114 | function watchFile(filepath) {
|
115 | if (!watchedFiles[filepath]) {
|
116 | // Watch this file for changes. This probably won't scale to hundreds of
|
117 | // files.. but I bet someone will try it!
|
118 | watchedFiles[filepath] = fs.watch(filepath, function(event) {
|
119 | var mtime;
|
120 | // Has the file been deleted?
|
121 | var deleted = !existsSync(filepath);
|
122 | if (deleted) {
|
123 | // If file was deleted, stop watching file.
|
124 | unWatchFile(filepath);
|
125 | // Remove from mtimes.
|
126 | delete mtimes[filepath];
|
127 | } else {
|
128 | // Get last modified time of file.
|
129 | mtime = +fs.statSync(filepath).mtime;
|
130 | // If same as stored mtime, the file hasn't changed.
|
131 | if (mtime === mtimes[filepath]) { return; }
|
132 | // Otherwise it has, store mtime for later use.
|
133 | mtimes[filepath] = mtime;
|
134 | }
|
135 | // Call "change" for this file, setting status appropriately (rename ->
|
136 | // renamed, change -> changed).
|
137 | fileChanged(deleted ? 'deleted' : event + 'd', filepath);
|
138 | });
|
139 | }
|
140 | }
|
141 |
|
142 | // Unwatch a file.
|
143 | function unWatchFile(filepath) {
|
144 | if (watchedFiles[filepath]) {
|
145 | // Close watcher.
|
146 | watchedFiles[filepath].close();
|
147 | // Remove from watched files.
|
148 | delete watchedFiles[filepath];
|
149 | }
|
150 | }
|
151 |
|
152 | // Watch all currently existing files for changes.
|
153 | getFiles().forEach(watchFile);
|
154 |
|
155 | // Watch for files to be added.
|
156 | intervalId = setInterval(function() {
|
157 | // Files that have been added since last interval execution.
|
158 | var added = grunt.utils._.difference(getFiles(), Object.keys(watchedFiles));
|
159 | added.forEach(function(filepath) {
|
160 | // This file has been added.
|
161 | fileChanged('added', filepath);
|
162 | // Watch this file.
|
163 | watchFile(filepath);
|
164 | });
|
165 | }, 200);
|
166 | });
|
167 |
|
168 | };
|