Commit | Line | Data |
---|---|---|
adeb96d2 DW |
1 | // This file is part of Moodle - http://moodle.org/ |
2 | // | |
3 | // Moodle is free software: you can redistribute it and/or modify | |
4 | // it under the terms of the GNU General Public License as published by | |
5 | // the Free Software Foundation, either version 3 of the License, or | |
6 | // (at your option) any later version. | |
7 | // | |
8 | // Moodle is distributed in the hope that it will be useful, | |
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of | |
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
11 | // GNU General Public License for more details. | |
12 | // | |
13 | // You should have received a copy of the GNU General Public License | |
14 | // along with Moodle. If not, see <http://www.gnu.org/licenses/>. | |
0b777a06 | 15 | /* jshint node: true, browser: false */ |
3adb62b7 | 16 | /* eslint-env node */ |
adeb96d2 DW |
17 | |
18 | /** | |
19 | * @copyright 2014 Andrew Nicols | |
20 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
21 | */ | |
22 | ||
a8109e75 AN |
23 | /* eslint-env node */ |
24 | ||
adeb96d2 | 25 | /** |
a8109e75 AN |
26 | * Calculate the cwd, taking into consideration the `root` option (for Windows). |
27 | * | |
28 | * @param {Object} grunt | |
29 | * @returns {String} The current directory as best we can determine | |
adeb96d2 | 30 | */ |
a8109e75 AN |
31 | const getCwd = grunt => { |
32 | const fs = require('fs'); | |
33 | const path = require('path'); | |
adeb96d2 | 34 | |
a8109e75 | 35 | let cwd = fs.realpathSync(process.env.PWD || process.cwd()); |
00cceb7f DP |
36 | |
37 | // Windows users can't run grunt in a subdirectory, so allow them to set | |
38 | // the root by passing --root=path/to/dir. | |
39 | if (grunt.option('root')) { | |
a8109e75 | 40 | const root = grunt.option('root'); |
00cceb7f | 41 | if (grunt.file.exists(__dirname, root)) { |
a8109e75 | 42 | cwd = fs.realpathSync(path.join(__dirname, root)); |
b18478b9 | 43 | grunt.log.ok('Setting root to ' + cwd); |
00cceb7f | 44 | } else { |
b18478b9 | 45 | grunt.fail.fatal('Setting root to ' + root + ' failed - path does not exist'); |
00cceb7f DP |
46 | } |
47 | } | |
48 | ||
a8109e75 AN |
49 | return cwd; |
50 | }; | |
51 | ||
093be5c6 AN |
52 | /** |
53 | * Register any stylelint tasks. | |
54 | * | |
55 | * @param {Object} grunt | |
56 | * @param {Array} files | |
57 | * @param {String} fullRunDir | |
58 | */ | |
59 | const registerStyleLintTasks = (grunt, files, fullRunDir) => { | |
60 | const getCssConfigForFiles = files => { | |
61 | return { | |
62 | stylelint: { | |
63 | css: { | |
64 | // Use a fully-qualified path. | |
65 | src: files, | |
66 | options: { | |
67 | configOverrides: { | |
68 | rules: { | |
69 | // These rules have to be disabled in .stylelintrc for scss compat. | |
70 | "at-rule-no-unknown": true, | |
71 | } | |
72 | } | |
73 | } | |
74 | }, | |
75 | }, | |
76 | }; | |
77 | }; | |
78 | ||
79 | const getScssConfigForFiles = files => { | |
80 | return { | |
81 | stylelint: { | |
82 | scss: { | |
83 | options: {syntax: 'scss'}, | |
84 | src: files, | |
85 | }, | |
86 | }, | |
87 | }; | |
88 | }; | |
89 | ||
90 | let hasCss = true; | |
91 | let hasScss = true; | |
92 | ||
93 | if (files) { | |
94 | // Specific files were passed. Just set them up. | |
95 | grunt.config.merge(getCssConfigForFiles(files)); | |
96 | grunt.config.merge(getScssConfigForFiles(files)); | |
97 | } else { | |
98 | // The stylelint system does not handle the case where there was no file to lint. | |
99 | // Check whether there are any files to lint in the current directory. | |
100 | const glob = require('glob'); | |
101 | ||
102 | const scssSrc = []; | |
103 | glob.sync(`${fullRunDir}/**/*.scss`).forEach(path => scssSrc.push(path)); | |
104 | ||
105 | if (scssSrc.length) { | |
106 | grunt.config.merge(getScssConfigForFiles(scssSrc)); | |
107 | } else { | |
108 | hasScss = false; | |
109 | } | |
110 | ||
111 | const cssSrc = []; | |
112 | glob.sync(`${fullRunDir}/**/*.css`).forEach(path => cssSrc.push(path)); | |
113 | ||
114 | if (cssSrc.length) { | |
115 | grunt.config.merge(getCssConfigForFiles(cssSrc)); | |
116 | } else { | |
117 | hasCss = false; | |
118 | } | |
119 | } | |
120 | ||
121 | const scssTasks = ['sass']; | |
122 | if (hasScss) { | |
123 | scssTasks.unshift('stylelint:scss'); | |
124 | } | |
125 | grunt.registerTask('scss', scssTasks); | |
126 | ||
127 | const cssTasks = []; | |
128 | if (hasCss) { | |
129 | cssTasks.push('stylelint:css'); | |
130 | } | |
131 | grunt.registerTask('rawcss', cssTasks); | |
132 | ||
133 | grunt.registerTask('css', ['scss', 'rawcss']); | |
134 | }; | |
135 | ||
a8109e75 AN |
136 | /** |
137 | * Grunt configuration. | |
138 | * | |
139 | * @param {Object} grunt | |
140 | */ | |
141 | module.exports = function(grunt) { | |
142 | const path = require('path'); | |
143 | const tasks = {}; | |
144 | const async = require('async'); | |
145 | const DOMParser = require('xmldom').DOMParser; | |
146 | const xpath = require('xpath'); | |
147 | const semver = require('semver'); | |
148 | const watchman = require('fb-watchman'); | |
149 | const watchmanClient = new watchman.Client(); | |
150 | const fs = require('fs'); | |
151 | const ComponentList = require(path.resolve('GruntfileComponents.js')); | |
152 | ||
153 | // Verify the node version is new enough. | |
154 | var expected = semver.validRange(grunt.file.readJSON('package.json').engines.node); | |
155 | var actual = semver.valid(process.version); | |
156 | if (!semver.satisfies(actual, expected)) { | |
157 | grunt.fail.fatal('Node version not satisfied. Require ' + expected + ', version installed: ' + actual); | |
158 | } | |
159 | ||
160 | // Detect directories: | |
161 | // * gruntFilePath The real path on disk to this Gruntfile.js | |
162 | // * cwd The current working directory, which can be overridden by the `root` option | |
163 | // * relativeCwd The cwd, relative to the Gruntfile.js | |
164 | // * componentDirectory The root directory of the component if the cwd is in a valid component | |
165 | // * inComponent Whether the cwd is in a valid component | |
166 | // * runDir The componentDirectory or cwd if not in a component, relative to Gruntfile.js | |
167 | // * fullRunDir The full path to the runDir | |
168 | const gruntFilePath = fs.realpathSync(process.cwd()); | |
169 | const cwd = getCwd(grunt); | |
170 | const relativeCwd = cwd.replace(new RegExp(`${gruntFilePath}/?`), ''); | |
171 | const componentDirectory = ComponentList.getOwningComponentDirectory(relativeCwd); | |
172 | const inComponent = !!componentDirectory; | |
173 | const runDir = inComponent ? componentDirectory : relativeCwd; | |
174 | const fullRunDir = fs.realpathSync(gruntFilePath + path.sep + runDir); | |
175 | grunt.log.debug(`The cwd was detected as ${cwd} with a fullRunDir of ${fullRunDir}`); | |
176 | ||
177 | if (inComponent) { | |
178 | grunt.log.ok(`Running tasks for component directory ${componentDirectory}`); | |
179 | } | |
180 | ||
38d4f754 RW |
181 | var files = null; |
182 | if (grunt.option('files')) { | |
183 | // Accept a comma separated list of files to process. | |
184 | files = grunt.option('files').split(','); | |
185 | } | |
186 | ||
a84d5236 | 187 | const inAMD = path.basename(cwd) == 'amd'; |
adeb96d2 | 188 | |
5cc5f311 | 189 | // Globbing pattern for matching all AMD JS source files. |
a84d5236 AN |
190 | let amdSrc = []; |
191 | if (inComponent) { | |
192 | amdSrc.push(componentDirectory + "/amd/src/*.js"); | |
193 | amdSrc.push(componentDirectory + "/amd/src/**/*.js"); | |
9ea892d2 | 194 | } else { |
a84d5236 | 195 | amdSrc = ComponentList.getAmdSrcGlobList(); |
9ea892d2 | 196 | } |
5cc5f311 | 197 | |
b8693045 AN |
198 | let yuiSrc = []; |
199 | if (inComponent) { | |
200 | yuiSrc.push(componentDirectory + "/yui/src/**/*.js"); | |
201 | } else { | |
202 | yuiSrc = ComponentList.getYuiSrcGlobList(gruntFilePath + '/'); | |
203 | } | |
204 | ||
5cc5f311 DP |
205 | /** |
206 | * Function to generate the destination for the uglify task | |
207 | * (e.g. build/file.min.js). This function will be passed to | |
208 | * the rename property of files array when building dynamically: | |
209 | * http://gruntjs.com/configuring-tasks#building-the-files-object-dynamically | |
210 | * | |
211 | * @param {String} destPath the current destination | |
212 | * @param {String} srcPath the matched src path | |
213 | * @return {String} The rewritten destination path. | |
214 | */ | |
c53f86d4 | 215 | var babelRename = function(destPath, srcPath) { |
5cc5f311 DP |
216 | destPath = srcPath.replace('src', 'build'); |
217 | destPath = destPath.replace('.js', '.min.js'); | |
5cc5f311 DP |
218 | return destPath; |
219 | }; | |
220 | ||
30db70ab DP |
221 | /** |
222 | * Find thirdpartylibs.xml and generate an array of paths contained within | |
223 | * them (used to generate ignore files and so on). | |
224 | * | |
225 | * @return {array} The list of thirdparty paths. | |
226 | */ | |
227 | var getThirdPartyPathsFromXML = function() { | |
d7678ab3 AN |
228 | const thirdpartyfiles = ComponentList.getThirdPartyLibsList(gruntFilePath + '/'); |
229 | const libs = ['node_modules/', 'vendor/']; | |
30db70ab | 230 | |
b18478b9 | 231 | thirdpartyfiles.forEach(function(file) { |
d7678ab3 | 232 | const dirname = path.dirname(file); |
30db70ab | 233 | |
d7678ab3 AN |
234 | const doc = new DOMParser().parseFromString(grunt.file.read(file)); |
235 | const nodes = xpath.select("/libraries/library/location/text()", doc); | |
30db70ab | 236 | |
d7678ab3 AN |
237 | nodes.forEach(function(node) { |
238 | let lib = path.join(dirname, node.toString()); | |
239 | if (grunt.file.isDir(lib)) { | |
240 | // Ensure trailing slash on dirs. | |
241 | lib = lib.replace(/\/?$/, '/'); | |
242 | } | |
30db70ab | 243 | |
d7678ab3 AN |
244 | // Look for duplicate paths before adding to array. |
245 | if (libs.indexOf(lib) === -1) { | |
246 | libs.push(lib); | |
247 | } | |
248 | }); | |
30db70ab | 249 | }); |
d7678ab3 | 250 | |
30db70ab DP |
251 | return libs; |
252 | }; | |
253 | ||
adeb96d2 DW |
254 | // Project configuration. |
255 | grunt.initConfig({ | |
3adb62b7 | 256 | eslint: { |
30db70ab DP |
257 | // Even though warnings dont stop the build we don't display warnings by default because |
258 | // at this moment we've got too many core warnings. | |
037de719 | 259 | options: {quiet: !grunt.option('show-lint-warnings')}, |
38d4f754 | 260 | amd: {src: files ? files : amdSrc}, |
be4b3cc6 | 261 | // Check YUI module source files. |
b8693045 | 262 | yui: {src: files ? files : yuiSrc}, |
3adb62b7 | 263 | }, |
c53f86d4 RW |
264 | babel: { |
265 | options: { | |
266 | sourceMaps: true, | |
267 | comments: false, | |
268 | plugins: [ | |
269 | 'transform-es2015-modules-amd-lazy', | |
78b0a0c2 | 270 | 'system-import-transformer', |
c53f86d4 RW |
271 | // This plugin modifies the Babel transpiling for "export default" |
272 | // so that if it's used then only the exported value is returned | |
273 | // by the generated AMD module. | |
274 | // | |
275 | // It also adds the Moodle plugin name to the AMD module definition | |
276 | // so that it can be imported as expected in other modules. | |
277 | path.resolve('babel-plugin-add-module-to-define.js'), | |
278 | '@babel/plugin-syntax-dynamic-import', | |
279 | '@babel/plugin-syntax-import-meta', | |
280 | ['@babel/plugin-proposal-class-properties', {'loose': false}], | |
281 | '@babel/plugin-proposal-json-strings' | |
282 | ], | |
283 | presets: [ | |
284 | ['minify', { | |
285 | // This minification plugin needs to be disabled because it breaks the | |
286 | // source map generation and causes invalid source maps to be output. | |
287 | simplify: false, | |
288 | builtIns: false | |
289 | }], | |
290 | ['@babel/preset-env', { | |
291 | targets: { | |
292 | browsers: [ | |
293 | ">0.25%", | |
294 | "last 2 versions", | |
295 | "not ie <= 10", | |
296 | "not op_mini all", | |
297 | "not Opera > 0", | |
298 | "not dead" | |
299 | ] | |
300 | }, | |
301 | modules: false, | |
302 | useBuiltIns: false | |
303 | }] | |
304 | ] | |
305 | }, | |
306 | dist: { | |
5cc5f311 DP |
307 | files: [{ |
308 | expand: true, | |
38d4f754 | 309 | src: files ? files : amdSrc, |
c53f86d4 RW |
310 | rename: babelRename |
311 | }] | |
adeb96d2 | 312 | } |
a4a52e56 | 313 | }, |
af9edb2e BB |
314 | sass: { |
315 | dist: { | |
316 | files: { | |
de213bf0 BB |
317 | "theme/boost/style/moodle.css": "theme/boost/scss/preset/default.scss", |
318 | "theme/classic/style/moodle.css": "theme/classic/scss/classicgrunt.scss" | |
af9edb2e BB |
319 | } |
320 | }, | |
321 | options: { | |
de213bf0 | 322 | includePaths: ["theme/boost/scss/", "theme/classic/scss/"] |
af9edb2e BB |
323 | } |
324 | }, | |
8efbb7b1 DP |
325 | watch: { |
326 | options: { | |
327 | nospawn: true // We need not to spawn so config can be changed dynamically. | |
328 | }, | |
329 | amd: { | |
a8109e75 AN |
330 | files: inComponent |
331 | ? ['amd/src/*.js', 'amd/src/**/*.js'] | |
332 | : ['**/amd/src/**/*.js'], | |
8efbb7b1 DP |
333 | tasks: ['amd'] |
334 | }, | |
38d4f754 | 335 | boost: { |
093be5c6 | 336 | files: [inComponent ? 'scss/**/*.scss' : 'theme/boost/scss/**/*.scss'], |
38d4f754 RW |
337 | tasks: ['scss'] |
338 | }, | |
339 | rawcss: { | |
093be5c6 AN |
340 | files: [ |
341 | '**/*.css', | |
342 | ], | |
343 | excludes: [ | |
344 | '**/moodle.css', | |
345 | '**/editor.css', | |
346 | ], | |
38d4f754 RW |
347 | tasks: ['rawcss'] |
348 | }, | |
8efbb7b1 | 349 | yui: { |
b8693045 AN |
350 | files: inComponent |
351 | ? ['yui/src/*.json', 'yui/src/**/*.js'] | |
352 | : ['**/yui/src/**/*.js'], | |
a1587268 | 353 | tasks: ['yui'] |
8efbb7b1 | 354 | }, |
d44f7e46 | 355 | gherkinlint: { |
ba8a53d0 | 356 | files: [inComponent ? 'tests/behat/*.feature' : '**/tests/behat/*.feature'], |
d44f7e46 RT |
357 | tasks: ['gherkinlint'] |
358 | } | |
1aa454ed DP |
359 | }, |
360 | shifter: { | |
361 | options: { | |
362 | recursive: true, | |
b8693045 AN |
363 | // Shifter takes a relative path. |
364 | paths: files ? files : [runDir] | |
1aa454ed | 365 | } |
855fc5d8 | 366 | }, |
8b02e2d9 DP |
367 | gherkinlint: { |
368 | options: { | |
38d4f754 | 369 | files: files ? files : ['**/tests/behat/*.feature'], |
8b02e2d9 DP |
370 | } |
371 | }, | |
adeb96d2 DW |
372 | }); |
373 | ||
30db70ab DP |
374 | /** |
375 | * Generate ignore files (utilising thirdpartylibs.xml data) | |
376 | */ | |
377 | tasks.ignorefiles = function() { | |
d7678ab3 AN |
378 | // An array of paths to third party directories. |
379 | const thirdPartyPaths = getThirdPartyPathsFromXML(); | |
380 | // Generate .eslintignore. | |
381 | const eslintIgnores = [ | |
382 | '# Generated by "grunt ignorefiles"', | |
383 | '*/**/yui/src/*/meta/', | |
384 | '*/**/build/', | |
385 | ].concat(thirdPartyPaths); | |
386 | grunt.file.write('.eslintignore', eslintIgnores.join('\n')); | |
387 | ||
388 | // Generate .stylelintignore. | |
389 | const stylelintIgnores = [ | |
390 | '# Generated by "grunt ignorefiles"', | |
391 | '**/yui/build/*', | |
392 | 'theme/boost/style/moodle.css', | |
393 | 'theme/classic/style/moodle.css', | |
394 | ].concat(thirdPartyPaths); | |
395 | grunt.file.write('.stylelintignore', stylelintIgnores.join('\n')); | |
30db70ab DP |
396 | }; |
397 | ||
1aa454ed DP |
398 | /** |
399 | * Shifter task. Is configured with a path to a specific file or a directory, | |
400 | * in the case of a specific file it will work out the right module to be built. | |
401 | * | |
402 | * Note that this task runs the invidiaul shifter jobs async (becase it spawns | |
403 | * so be careful to to call done(). | |
404 | */ | |
adeb96d2 | 405 | tasks.shifter = function() { |
30db70ab | 406 | var done = this.async(), |
1aa454ed | 407 | options = grunt.config('shifter.options'); |
adeb96d2 | 408 | |
1aa454ed | 409 | // Run the shifter processes one at a time to avoid confusing output. |
b18478b9 | 410 | async.eachSeries(options.paths, function(src, filedone) { |
1aa454ed | 411 | var args = []; |
b18478b9 | 412 | args.push(path.normalize(__dirname + '/node_modules/shifter/bin/shifter')); |
e67585f8 | 413 | |
1aa454ed DP |
414 | // Always ignore the node_modules directory. |
415 | args.push('--excludes', 'node_modules'); | |
416 | ||
adeb96d2 | 417 | // Determine the most appropriate options to run with based upon the current location. |
1aa454ed DP |
418 | if (grunt.file.isMatch('**/yui/**/*.js', src)) { |
419 | // When passed a JS file, build our containing module (this happen with | |
420 | // watch). | |
421 | grunt.log.debug('Shifter passed a specific JS file'); | |
422 | src = path.dirname(path.dirname(src)); | |
423 | options.recursive = false; | |
424 | } else if (grunt.file.isMatch('**/yui/src', src)) { | |
425 | // When in a src directory --walk all modules. | |
adeb96d2 DW |
426 | grunt.log.debug('In a src directory'); |
427 | args.push('--walk'); | |
1aa454ed DP |
428 | options.recursive = false; |
429 | } else if (grunt.file.isMatch('**/yui/src/*', src)) { | |
430 | // When in module, only build our module. | |
adeb96d2 | 431 | grunt.log.debug('In a module directory'); |
adeb96d2 | 432 | options.recursive = false; |
1aa454ed DP |
433 | } else if (grunt.file.isMatch('**/yui/src/*/js', src)) { |
434 | // When in module src, only build our module. | |
435 | grunt.log.debug('In a source directory'); | |
436 | src = path.dirname(src); | |
437 | options.recursive = false; | |
adeb96d2 DW |
438 | } |
439 | ||
1aa454ed DP |
440 | if (grunt.option('watch')) { |
441 | grunt.fail.fatal('The --watch option has been removed, please use `grunt watch` instead'); | |
adeb96d2 DW |
442 | } |
443 | ||
adeb96d2 DW |
444 | // Add the stderr option if appropriate |
445 | if (grunt.option('verbose')) { | |
446 | args.push('--lint-stderr'); | |
447 | } | |
448 | ||
a07afffc DP |
449 | if (grunt.option('no-color')) { |
450 | args.push('--color=false'); | |
451 | } | |
452 | ||
8f76bfb6 DM |
453 | var execShifter = function() { |
454 | ||
1aa454ed DP |
455 | grunt.log.ok("Running shifter on " + src); |
456 | grunt.util.spawn({ | |
457 | cmd: "node", | |
458 | args: args, | |
459 | opts: {cwd: src, stdio: 'inherit', env: process.env} | |
b18478b9 | 460 | }, function(error, result, code) { |
8f76bfb6 DM |
461 | if (code) { |
462 | grunt.fail.fatal('Shifter failed with code: ' + code); | |
463 | } else { | |
464 | grunt.log.ok('Shifter build complete.'); | |
1aa454ed | 465 | filedone(); |
8f76bfb6 DM |
466 | } |
467 | }); | |
468 | }; | |
469 | ||
adeb96d2 | 470 | // Actually run shifter. |
8f76bfb6 DM |
471 | if (!options.recursive) { |
472 | execShifter(); | |
473 | } else { | |
474 | // Check that there are yui modules otherwise shifter ends with exit code 1. | |
1aa454ed DP |
475 | if (grunt.file.expand({cwd: src}, '**/yui/src/**/*.js').length > 0) { |
476 | args.push('--recursive'); | |
477 | execShifter(); | |
478 | } else { | |
479 | grunt.log.ok('No YUI modules to build.'); | |
480 | filedone(); | |
481 | } | |
8f76bfb6 | 482 | } |
1aa454ed | 483 | }, done); |
adeb96d2 DW |
484 | }; |
485 | ||
8b02e2d9 DP |
486 | tasks.gherkinlint = function() { |
487 | var done = this.async(), | |
488 | options = grunt.config('gherkinlint.options'); | |
489 | ||
490 | var args = grunt.file.expand(options.files); | |
491 | args.unshift(path.normalize(__dirname + '/node_modules/.bin/gherkin-lint')); | |
492 | grunt.util.spawn({ | |
493 | cmd: 'node', | |
494 | args: args, | |
495 | opts: {stdio: 'inherit', env: process.env} | |
496 | }, function(error, result, code) { | |
497 | // Propagate the exit code. | |
58923606 | 498 | done(code === 0); |
8b02e2d9 DP |
499 | }); |
500 | }; | |
501 | ||
adeb96d2 DW |
502 | tasks.startup = function() { |
503 | // Are we in a YUI directory? | |
e67585f8 | 504 | if (path.basename(path.resolve(cwd, '../../')) == 'yui') { |
a1587268 | 505 | grunt.task.run('yui'); |
adeb96d2 | 506 | // Are we in an AMD directory? |
c9b6feea | 507 | } else if (inAMD) { |
65d070ae | 508 | grunt.task.run('amd'); |
adeb96d2 DW |
509 | } else { |
510 | // Run them all!. | |
65d070ae DP |
511 | grunt.task.run('css'); |
512 | grunt.task.run('js'); | |
d44f7e46 | 513 | grunt.task.run('gherkinlint'); |
adeb96d2 DW |
514 | } |
515 | }; | |
516 | ||
38d4f754 RW |
517 | /** |
518 | * This is a wrapper task to handle the grunt watch command. It attempts to use | |
519 | * Watchman to monitor for file changes, if it's installed, because it's much faster. | |
520 | * | |
521 | * If Watchman isn't installed then it falls back to the grunt-contrib-watch file | |
522 | * watcher for backwards compatibility. | |
523 | */ | |
524 | tasks.watch = function() { | |
525 | var watchTaskDone = this.async(); | |
526 | var watchInitialised = false; | |
527 | var watchTaskQueue = {}; | |
528 | var processingQueue = false; | |
529 | ||
530 | // Grab the tasks and files that have been queued up and execute them. | |
531 | var processWatchTaskQueue = function() { | |
532 | if (!Object.keys(watchTaskQueue).length || processingQueue) { | |
533 | // If there is nothing in the queue or we're already processing then wait. | |
534 | return; | |
535 | } | |
536 | ||
537 | processingQueue = true; | |
538 | ||
539 | // Grab all tasks currently in the queue. | |
540 | var queueToProcess = watchTaskQueue; | |
541 | // Reset the queue. | |
542 | watchTaskQueue = {}; | |
543 | ||
544 | async.forEachSeries( | |
545 | Object.keys(queueToProcess), | |
546 | function(task, next) { | |
547 | var files = queueToProcess[task]; | |
548 | var filesOption = '--files=' + files.join(','); | |
549 | grunt.log.ok('Running task ' + task + ' for files ' + filesOption); | |
550 | ||
551 | // Spawn the task in a child process so that it doesn't kill this one | |
552 | // if it failed. | |
553 | grunt.util.spawn( | |
554 | { | |
555 | // Spawn with the grunt bin. | |
556 | grunt: true, | |
557 | // Run from current working dir and inherit stdio from process. | |
558 | opts: { | |
a84d5236 | 559 | cwd: fullRunDir, |
38d4f754 RW |
560 | stdio: 'inherit' |
561 | }, | |
562 | args: [task, filesOption] | |
563 | }, | |
564 | function(err, res, code) { | |
565 | if (code !== 0) { | |
566 | // The grunt task failed. | |
567 | grunt.log.error(err); | |
568 | } | |
569 | ||
570 | // Move on to the next task. | |
571 | next(); | |
572 | } | |
573 | ); | |
574 | }, | |
575 | function() { | |
576 | // No longer processing. | |
577 | processingQueue = false; | |
578 | // Once all of the tasks are done then recurse just in case more tasks | |
579 | // were queued while we were processing. | |
580 | processWatchTaskQueue(); | |
581 | } | |
582 | ); | |
583 | }; | |
584 | ||
a84d5236 AN |
585 | const originalWatchConfig = grunt.config.get(['watch']); |
586 | const watchConfig = Object.keys(originalWatchConfig).reduce(function(carry, key) { | |
38d4f754 RW |
587 | if (key == 'options') { |
588 | return carry; | |
589 | } | |
590 | ||
a84d5236 AN |
591 | const value = originalWatchConfig[key]; |
592 | ||
593 | const taskNames = value.tasks; | |
594 | const files = value.files; | |
595 | let excludes = []; | |
596 | if (value.excludes) { | |
597 | excludes = value.excludes; | |
598 | } | |
38d4f754 RW |
599 | |
600 | taskNames.forEach(function(taskName) { | |
a84d5236 AN |
601 | carry[taskName] = { |
602 | files, | |
603 | excludes, | |
604 | }; | |
38d4f754 RW |
605 | }); |
606 | ||
607 | return carry; | |
608 | }, {}); | |
609 | ||
610 | watchmanClient.on('error', function(error) { | |
611 | // We have to add an error handler here and parse the error string because the | |
612 | // example way from the docs to check if Watchman is installed doesn't actually work!! | |
613 | // See: https://github.com/facebook/watchman/issues/509 | |
614 | if (error.message.match('Watchman was not found')) { | |
615 | // If watchman isn't installed then we should fallback to the other watch task. | |
616 | grunt.log.ok('It is recommended that you install Watchman for better performance using the "watch" command.'); | |
617 | ||
618 | // Fallback to the old grunt-contrib-watch task. | |
619 | grunt.renameTask('watch-grunt', 'watch'); | |
620 | grunt.task.run(['watch']); | |
621 | // This task is finished. | |
622 | watchTaskDone(0); | |
623 | } else { | |
624 | grunt.log.error(error); | |
625 | // Fatal error. | |
626 | watchTaskDone(1); | |
627 | } | |
628 | }); | |
629 | ||
630 | watchmanClient.on('subscription', function(resp) { | |
631 | if (resp.subscription !== 'grunt-watch') { | |
632 | return; | |
633 | } | |
634 | ||
635 | resp.files.forEach(function(file) { | |
636 | grunt.log.ok('File changed: ' + file.name); | |
637 | ||
a84d5236 | 638 | var fullPath = fullRunDir + '/' + file.name; |
38d4f754 | 639 | Object.keys(watchConfig).forEach(function(task) { |
a84d5236 AN |
640 | |
641 | const fileGlobs = watchConfig[task].files; | |
642 | var match = fileGlobs.some(function(fileGlob) { | |
643 | return grunt.file.isMatch(`**/${fileGlob}`, fullPath); | |
38d4f754 | 644 | }); |
a84d5236 | 645 | |
38d4f754 RW |
646 | if (match) { |
647 | // If we are watching a subdirectory then the file.name will be relative | |
648 | // to that directory. However the grunt tasks expect the file paths to be | |
649 | // relative to the Gruntfile.js location so let's normalise them before | |
650 | // adding them to the queue. | |
651 | var relativePath = fullPath.replace(gruntFilePath + '/', ''); | |
652 | if (task in watchTaskQueue) { | |
653 | if (!watchTaskQueue[task].includes(relativePath)) { | |
654 | watchTaskQueue[task] = watchTaskQueue[task].concat(relativePath); | |
655 | } | |
656 | } else { | |
657 | watchTaskQueue[task] = [relativePath]; | |
658 | } | |
659 | } | |
660 | }); | |
661 | }); | |
662 | ||
663 | processWatchTaskQueue(); | |
664 | }); | |
665 | ||
666 | process.on('SIGINT', function() { | |
667 | // Let the user know that they may need to manually stop the Watchman daemon if they | |
668 | // no longer want it running. | |
669 | if (watchInitialised) { | |
670 | grunt.log.ok('The Watchman daemon may still be running and may need to be stopped manually.'); | |
671 | } | |
672 | ||
673 | process.exit(); | |
674 | }); | |
675 | ||
676 | // Initiate the watch on the current directory. | |
a84d5236 | 677 | watchmanClient.command(['watch-project', fullRunDir], function(watchError, watchResponse) { |
38d4f754 RW |
678 | if (watchError) { |
679 | grunt.log.error('Error initiating watch:', watchError); | |
680 | watchTaskDone(1); | |
681 | return; | |
682 | } | |
683 | ||
684 | if ('warning' in watchResponse) { | |
685 | grunt.log.error('warning: ', watchResponse.warning); | |
686 | } | |
687 | ||
688 | var watch = watchResponse.watch; | |
689 | var relativePath = watchResponse.relative_path; | |
690 | watchInitialised = true; | |
691 | ||
692 | watchmanClient.command(['clock', watch], function(clockError, clockResponse) { | |
693 | if (clockError) { | |
694 | grunt.log.error('Failed to query clock:', clockError); | |
695 | watchTaskDone(1); | |
696 | return; | |
697 | } | |
698 | ||
a84d5236 AN |
699 | // Generate the expression query used by watchman. |
700 | // Documentation is limited, but see https://facebook.github.io/watchman/docs/expr/allof.html for examples. | |
701 | // We generate an expression to match any value in the files list of all of our tasks, but excluding | |
702 | // all value in the excludes list of that task. | |
703 | // | |
704 | // [anyof, [ | |
705 | // [allof, [ | |
706 | // [anyof, [ | |
707 | // ['match', validPath, 'wholename'], | |
708 | // ['match', validPath, 'wholename'], | |
709 | // ], | |
710 | // [not, | |
711 | // [anyof, [ | |
712 | // ['match', invalidPath, 'wholename'], | |
713 | // ['match', invalidPath, 'wholename'], | |
714 | // ], | |
715 | // ], | |
716 | // ], | |
717 | var matchWholeName = fileGlob => ['match', fileGlob, 'wholename']; | |
38d4f754 | 718 | var matches = Object.keys(watchConfig).map(function(task) { |
a84d5236 AN |
719 | const matchAll = []; |
720 | matchAll.push(['anyof'].concat(watchConfig[task].files.map(matchWholeName))); | |
38d4f754 | 721 | |
a84d5236 AN |
722 | if (watchConfig[task].excludes.length) { |
723 | matchAll.push(['not', ['anyof'].concat(watchConfig[task].excludes.map(matchWholeName))]); | |
724 | } | |
725 | ||
726 | return ['allof'].concat(matchAll); | |
38d4f754 RW |
727 | }); |
728 | ||
a84d5236 AN |
729 | matches = ['anyof'].concat(matches); |
730 | ||
38d4f754 | 731 | var sub = { |
a84d5236 | 732 | expression: matches, |
38d4f754 RW |
733 | // Which fields we're interested in. |
734 | fields: ["name", "size", "type"], | |
735 | // Add our time constraint. | |
736 | since: clockResponse.clock | |
737 | }; | |
738 | ||
739 | if (relativePath) { | |
9ec0cbe9 | 740 | /* eslint-disable camelcase */ |
38d4f754 RW |
741 | sub.relative_root = relativePath; |
742 | } | |
743 | ||
744 | watchmanClient.command(['subscribe', watch, 'grunt-watch', sub], function(subscribeError) { | |
745 | if (subscribeError) { | |
746 | // Probably an error in the subscription criteria. | |
747 | grunt.log.error('failed to subscribe: ', subscribeError); | |
748 | watchTaskDone(1); | |
749 | return; | |
750 | } | |
751 | ||
a84d5236 | 752 | grunt.log.ok('Listening for changes to files in ' + fullRunDir); |
38d4f754 RW |
753 | }); |
754 | }); | |
755 | }); | |
756 | }; | |
757 | ||
1aa454ed DP |
758 | // On watch, we dynamically modify config to build only affected files. This |
759 | // method is slightly complicated to deal with multiple changed files at once (copied | |
760 | // from the grunt-contrib-watch readme). | |
761 | var changedFiles = Object.create(null); | |
762 | var onChange = grunt.util._.debounce(function() { | |
38d4f754 RW |
763 | var files = Object.keys(changedFiles); |
764 | grunt.config('eslint.amd.src', files); | |
765 | grunt.config('eslint.yui.src', files); | |
766 | grunt.config('shifter.options.paths', files); | |
767 | grunt.config('gherkinlint.options.files', files); | |
768 | grunt.config('babel.dist.files', [{expand: true, src: files, rename: babelRename}]); | |
769 | changedFiles = Object.create(null); | |
1aa454ed | 770 | }, 200); |
adeb96d2 | 771 | |
8efbb7b1 | 772 | grunt.event.on('watch', function(action, filepath) { |
38d4f754 RW |
773 | changedFiles[filepath] = action; |
774 | onChange(); | |
8efbb7b1 DP |
775 | }); |
776 | ||
adeb96d2 DW |
777 | // Register NPM tasks. |
778 | grunt.loadNpmTasks('grunt-contrib-uglify'); | |
8efbb7b1 | 779 | grunt.loadNpmTasks('grunt-contrib-watch'); |
af9edb2e | 780 | grunt.loadNpmTasks('grunt-sass'); |
3adb62b7 | 781 | grunt.loadNpmTasks('grunt-eslint'); |
855fc5d8 | 782 | grunt.loadNpmTasks('grunt-stylelint'); |
c53f86d4 | 783 | grunt.loadNpmTasks('grunt-babel'); |
adeb96d2 | 784 | |
38d4f754 RW |
785 | // Rename the grunt-contrib-watch "watch" task because we're going to wrap it. |
786 | grunt.renameTask('watch', 'watch-grunt'); | |
787 | ||
65d070ae | 788 | // Register JS tasks. |
adeb96d2 | 789 | grunt.registerTask('shifter', 'Run Shifter against the current directory', tasks.shifter); |
8b02e2d9 | 790 | grunt.registerTask('gherkinlint', 'Run gherkinlint against the current directory', tasks.gherkinlint); |
30db70ab | 791 | grunt.registerTask('ignorefiles', 'Generate ignore files for linters', tasks.ignorefiles); |
38d4f754 | 792 | grunt.registerTask('watch', 'Run tasks on file changes', tasks.watch); |
a1587268 | 793 | grunt.registerTask('yui', ['eslint:yui', 'shifter']); |
c53f86d4 | 794 | grunt.registerTask('amd', ['eslint:amd', 'babel']); |
a1587268 | 795 | grunt.registerTask('js', ['amd', 'yui']); |
65d070ae | 796 | |
093be5c6 AN |
797 | // Register CSS tasks. |
798 | registerStyleLintTasks(grunt, files, fullRunDir); | |
adeb96d2 DW |
799 | |
800 | // Register the startup task. | |
801 | grunt.registerTask('startup', 'Run the correct tasks for the current directory', tasks.startup); | |
802 | ||
803 | // Register the default task. | |
804 | grunt.registerTask('default', ['startup']); | |
805 | }; |