UNPKG

6.13 kBJavaScriptView Raw
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
10module.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};