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')); | |
5b4debd2 | 152 | const sass = require('node-sass'); |
a8109e75 AN |
153 | |
154 | // Verify the node version is new enough. | |
155 | var expected = semver.validRange(grunt.file.readJSON('package.json').engines.node); | |
156 | var actual = semver.valid(process.version); | |
157 | if (!semver.satisfies(actual, expected)) { | |
158 | grunt.fail.fatal('Node version not satisfied. Require ' + expected + ', version installed: ' + actual); | |
159 | } | |
160 | ||
161 | // Detect directories: | |
162 | // * gruntFilePath The real path on disk to this Gruntfile.js | |
163 | // * cwd The current working directory, which can be overridden by the `root` option | |
164 | // * relativeCwd The cwd, relative to the Gruntfile.js | |
165 | // * componentDirectory The root directory of the component if the cwd is in a valid component | |
166 | // * inComponent Whether the cwd is in a valid component | |
167 | // * runDir The componentDirectory or cwd if not in a component, relative to Gruntfile.js | |
168 | // * fullRunDir The full path to the runDir | |
169 | const gruntFilePath = fs.realpathSync(process.cwd()); | |
170 | const cwd = getCwd(grunt); | |
35e1470e | 171 | const relativeCwd = path.relative(gruntFilePath, cwd); |
a8109e75 AN |
172 | const componentDirectory = ComponentList.getOwningComponentDirectory(relativeCwd); |
173 | const inComponent = !!componentDirectory; | |
174 | const runDir = inComponent ? componentDirectory : relativeCwd; | |
175 | const fullRunDir = fs.realpathSync(gruntFilePath + path.sep + runDir); | |
843cf97b AN |
176 | grunt.log.debug('============================================================================'); |
177 | grunt.log.debug(`= Node version: ${process.versions.node}`); | |
178 | grunt.log.debug(`= grunt version: ${grunt.package.version}`); | |
179 | grunt.log.debug(`= process.cwd: '` + process.cwd() + `'`); | |
180 | grunt.log.debug(`= process.env.PWD: '${process.env.PWD}'`); | |
181 | grunt.log.debug(`= path.sep '${path.sep}'`); | |
182 | grunt.log.debug('============================================================================'); | |
183 | grunt.log.debug(`= gruntFilePath: '${gruntFilePath}'`); | |
184 | grunt.log.debug(`= relativeCwd: '${relativeCwd}'`); | |
185 | grunt.log.debug(`= componentDirectory: '${componentDirectory}'`); | |
186 | grunt.log.debug(`= inComponent: '${inComponent}'`); | |
187 | grunt.log.debug(`= runDir: '${runDir}'`); | |
188 | grunt.log.debug(`= fullRunDir: '${fullRunDir}'`); | |
189 | grunt.log.debug('============================================================================'); | |
a8109e75 AN |
190 | |
191 | if (inComponent) { | |
192 | grunt.log.ok(`Running tasks for component directory ${componentDirectory}`); | |
193 | } | |
194 | ||
843cf97b | 195 | let files = null; |
38d4f754 RW |
196 | if (grunt.option('files')) { |
197 | // Accept a comma separated list of files to process. | |
198 | files = grunt.option('files').split(','); | |
199 | } | |
200 | ||
d1a78060 AN |
201 | // If the cwd is the amd directory in the current component then it will be empty. |
202 | // If the cwd is a child of the component's AMD directory, the relative directory will not start with .. | |
203 | const inAMD = !path.relative(`${componentDirectory}/amd`, cwd).startsWith('..'); | |
adeb96d2 | 204 | |
5cc5f311 | 205 | // Globbing pattern for matching all AMD JS source files. |
a84d5236 AN |
206 | let amdSrc = []; |
207 | if (inComponent) { | |
208 | amdSrc.push(componentDirectory + "/amd/src/*.js"); | |
209 | amdSrc.push(componentDirectory + "/amd/src/**/*.js"); | |
9ea892d2 | 210 | } else { |
a84d5236 | 211 | amdSrc = ComponentList.getAmdSrcGlobList(); |
9ea892d2 | 212 | } |
5cc5f311 | 213 | |
b8693045 AN |
214 | let yuiSrc = []; |
215 | if (inComponent) { | |
216 | yuiSrc.push(componentDirectory + "/yui/src/**/*.js"); | |
217 | } else { | |
218 | yuiSrc = ComponentList.getYuiSrcGlobList(gruntFilePath + '/'); | |
219 | } | |
220 | ||
5cc5f311 DP |
221 | /** |
222 | * Function to generate the destination for the uglify task | |
223 | * (e.g. build/file.min.js). This function will be passed to | |
224 | * the rename property of files array when building dynamically: | |
225 | * http://gruntjs.com/configuring-tasks#building-the-files-object-dynamically | |
226 | * | |
227 | * @param {String} destPath the current destination | |
228 | * @param {String} srcPath the matched src path | |
229 | * @return {String} The rewritten destination path. | |
230 | */ | |
c53f86d4 | 231 | var babelRename = function(destPath, srcPath) { |
5cc5f311 DP |
232 | destPath = srcPath.replace('src', 'build'); |
233 | destPath = destPath.replace('.js', '.min.js'); | |
5cc5f311 DP |
234 | return destPath; |
235 | }; | |
236 | ||
30db70ab DP |
237 | /** |
238 | * Find thirdpartylibs.xml and generate an array of paths contained within | |
239 | * them (used to generate ignore files and so on). | |
240 | * | |
241 | * @return {array} The list of thirdparty paths. | |
242 | */ | |
243 | var getThirdPartyPathsFromXML = function() { | |
d7678ab3 AN |
244 | const thirdpartyfiles = ComponentList.getThirdPartyLibsList(gruntFilePath + '/'); |
245 | const libs = ['node_modules/', 'vendor/']; | |
30db70ab | 246 | |
b18478b9 | 247 | thirdpartyfiles.forEach(function(file) { |
d7678ab3 | 248 | const dirname = path.dirname(file); |
30db70ab | 249 | |
d7678ab3 AN |
250 | const doc = new DOMParser().parseFromString(grunt.file.read(file)); |
251 | const nodes = xpath.select("/libraries/library/location/text()", doc); | |
30db70ab | 252 | |
d7678ab3 | 253 | nodes.forEach(function(node) { |
d1a78060 | 254 | let lib = path.posix.join(dirname, node.toString()); |
d7678ab3 AN |
255 | if (grunt.file.isDir(lib)) { |
256 | // Ensure trailing slash on dirs. | |
257 | lib = lib.replace(/\/?$/, '/'); | |
258 | } | |
30db70ab | 259 | |
d7678ab3 AN |
260 | // Look for duplicate paths before adding to array. |
261 | if (libs.indexOf(lib) === -1) { | |
262 | libs.push(lib); | |
263 | } | |
264 | }); | |
30db70ab | 265 | }); |
d7678ab3 | 266 | |
30db70ab DP |
267 | return libs; |
268 | }; | |
269 | ||
48b5817e AN |
270 | /** |
271 | * Get the list of feature files to pass to the gherkin linter. | |
272 | * | |
273 | * @returns {Array} | |
274 | */ | |
275 | const getGherkinLintTargets = () => { | |
276 | if (files) { | |
277 | // Specific files were requested. Only check these. | |
278 | return files; | |
279 | } | |
280 | ||
281 | if (inComponent) { | |
282 | return [`${runDir}/tests/behat/*.feature`]; | |
283 | } | |
284 | ||
285 | return ['**/tests/behat/*.feature']; | |
286 | }; | |
287 | ||
adeb96d2 DW |
288 | // Project configuration. |
289 | grunt.initConfig({ | |
3adb62b7 | 290 | eslint: { |
30db70ab DP |
291 | // Even though warnings dont stop the build we don't display warnings by default because |
292 | // at this moment we've got too many core warnings. | |
749a38a7 MG |
293 | // To display warnings call: grunt eslint --show-lint-warnings |
294 | // To fail on warnings call: grunt eslint --max-lint-warnings=0 | |
295 | // Also --max-lint-warnings=-1 can be used to display warnings but not fail. | |
296 | options: { | |
297 | quiet: (!grunt.option('show-lint-warnings')) && (typeof grunt.option('max-lint-warnings') === 'undefined'), | |
298 | maxWarnings: ((typeof grunt.option('max-lint-warnings') !== 'undefined') ? grunt.option('max-lint-warnings') : -1) | |
299 | }, | |
38d4f754 | 300 | amd: {src: files ? files : amdSrc}, |
be4b3cc6 | 301 | // Check YUI module source files. |
b8693045 | 302 | yui: {src: files ? files : yuiSrc}, |
3adb62b7 | 303 | }, |
c53f86d4 RW |
304 | babel: { |
305 | options: { | |
306 | sourceMaps: true, | |
307 | comments: false, | |
308 | plugins: [ | |
309 | 'transform-es2015-modules-amd-lazy', | |
78b0a0c2 | 310 | 'system-import-transformer', |
c53f86d4 RW |
311 | // This plugin modifies the Babel transpiling for "export default" |
312 | // so that if it's used then only the exported value is returned | |
313 | // by the generated AMD module. | |
314 | // | |
315 | // It also adds the Moodle plugin name to the AMD module definition | |
316 | // so that it can be imported as expected in other modules. | |
317 | path.resolve('babel-plugin-add-module-to-define.js'), | |
318 | '@babel/plugin-syntax-dynamic-import', | |
319 | '@babel/plugin-syntax-import-meta', | |
320 | ['@babel/plugin-proposal-class-properties', {'loose': false}], | |
321 | '@babel/plugin-proposal-json-strings' | |
322 | ], | |
323 | presets: [ | |
324 | ['minify', { | |
325 | // This minification plugin needs to be disabled because it breaks the | |
326 | // source map generation and causes invalid source maps to be output. | |
327 | simplify: false, | |
328 | builtIns: false | |
329 | }], | |
330 | ['@babel/preset-env', { | |
331 | targets: { | |
332 | browsers: [ | |
333 | ">0.25%", | |
334 | "last 2 versions", | |
335 | "not ie <= 10", | |
336 | "not op_mini all", | |
337 | "not Opera > 0", | |
338 | "not dead" | |
339 | ] | |
340 | }, | |
341 | modules: false, | |
342 | useBuiltIns: false | |
343 | }] | |
344 | ] | |
345 | }, | |
346 | dist: { | |
5cc5f311 DP |
347 | files: [{ |
348 | expand: true, | |
38d4f754 | 349 | src: files ? files : amdSrc, |
c53f86d4 RW |
350 | rename: babelRename |
351 | }] | |
adeb96d2 | 352 | } |
a4a52e56 | 353 | }, |
af9edb2e BB |
354 | sass: { |
355 | dist: { | |
356 | files: { | |
de213bf0 BB |
357 | "theme/boost/style/moodle.css": "theme/boost/scss/preset/default.scss", |
358 | "theme/classic/style/moodle.css": "theme/classic/scss/classicgrunt.scss" | |
af9edb2e BB |
359 | } |
360 | }, | |
361 | options: { | |
5b4debd2 | 362 | implementation: sass, |
de213bf0 | 363 | includePaths: ["theme/boost/scss/", "theme/classic/scss/"] |
af9edb2e BB |
364 | } |
365 | }, | |
8efbb7b1 DP |
366 | watch: { |
367 | options: { | |
368 | nospawn: true // We need not to spawn so config can be changed dynamically. | |
369 | }, | |
370 | amd: { | |
a8109e75 AN |
371 | files: inComponent |
372 | ? ['amd/src/*.js', 'amd/src/**/*.js'] | |
373 | : ['**/amd/src/**/*.js'], | |
8efbb7b1 DP |
374 | tasks: ['amd'] |
375 | }, | |
38d4f754 | 376 | boost: { |
093be5c6 | 377 | files: [inComponent ? 'scss/**/*.scss' : 'theme/boost/scss/**/*.scss'], |
38d4f754 RW |
378 | tasks: ['scss'] |
379 | }, | |
380 | rawcss: { | |
093be5c6 AN |
381 | files: [ |
382 | '**/*.css', | |
383 | ], | |
384 | excludes: [ | |
385 | '**/moodle.css', | |
386 | '**/editor.css', | |
387 | ], | |
38d4f754 RW |
388 | tasks: ['rawcss'] |
389 | }, | |
8efbb7b1 | 390 | yui: { |
b8693045 AN |
391 | files: inComponent |
392 | ? ['yui/src/*.json', 'yui/src/**/*.js'] | |
393 | : ['**/yui/src/**/*.js'], | |
a1587268 | 394 | tasks: ['yui'] |
8efbb7b1 | 395 | }, |
d44f7e46 | 396 | gherkinlint: { |
ba8a53d0 | 397 | files: [inComponent ? 'tests/behat/*.feature' : '**/tests/behat/*.feature'], |
d44f7e46 RT |
398 | tasks: ['gherkinlint'] |
399 | } | |
1aa454ed DP |
400 | }, |
401 | shifter: { | |
402 | options: { | |
403 | recursive: true, | |
b8693045 AN |
404 | // Shifter takes a relative path. |
405 | paths: files ? files : [runDir] | |
1aa454ed | 406 | } |
855fc5d8 | 407 | }, |
8b02e2d9 DP |
408 | gherkinlint: { |
409 | options: { | |
48b5817e | 410 | files: getGherkinLintTargets(), |
8b02e2d9 DP |
411 | } |
412 | }, | |
adeb96d2 DW |
413 | }); |
414 | ||
30db70ab DP |
415 | /** |
416 | * Generate ignore files (utilising thirdpartylibs.xml data) | |
417 | */ | |
418 | tasks.ignorefiles = function() { | |
d7678ab3 AN |
419 | // An array of paths to third party directories. |
420 | const thirdPartyPaths = getThirdPartyPathsFromXML(); | |
421 | // Generate .eslintignore. | |
422 | const eslintIgnores = [ | |
423 | '# Generated by "grunt ignorefiles"', | |
424 | '*/**/yui/src/*/meta/', | |
425 | '*/**/build/', | |
426 | ].concat(thirdPartyPaths); | |
427 | grunt.file.write('.eslintignore', eslintIgnores.join('\n')); | |
428 | ||
429 | // Generate .stylelintignore. | |
430 | const stylelintIgnores = [ | |
431 | '# Generated by "grunt ignorefiles"', | |
432 | '**/yui/build/*', | |
433 | 'theme/boost/style/moodle.css', | |
434 | 'theme/classic/style/moodle.css', | |
435 | ].concat(thirdPartyPaths); | |
436 | grunt.file.write('.stylelintignore', stylelintIgnores.join('\n')); | |
30db70ab DP |
437 | }; |
438 | ||
1aa454ed DP |
439 | /** |
440 | * Shifter task. Is configured with a path to a specific file or a directory, | |
441 | * in the case of a specific file it will work out the right module to be built. | |
442 | * | |
443 | * Note that this task runs the invidiaul shifter jobs async (becase it spawns | |
444 | * so be careful to to call done(). | |
445 | */ | |
adeb96d2 | 446 | tasks.shifter = function() { |
30db70ab | 447 | var done = this.async(), |
1aa454ed | 448 | options = grunt.config('shifter.options'); |
adeb96d2 | 449 | |
1aa454ed | 450 | // Run the shifter processes one at a time to avoid confusing output. |
b18478b9 | 451 | async.eachSeries(options.paths, function(src, filedone) { |
1aa454ed | 452 | var args = []; |
b18478b9 | 453 | args.push(path.normalize(__dirname + '/node_modules/shifter/bin/shifter')); |
e67585f8 | 454 | |
1aa454ed DP |
455 | // Always ignore the node_modules directory. |
456 | args.push('--excludes', 'node_modules'); | |
457 | ||
adeb96d2 | 458 | // Determine the most appropriate options to run with based upon the current location. |
1aa454ed DP |
459 | if (grunt.file.isMatch('**/yui/**/*.js', src)) { |
460 | // When passed a JS file, build our containing module (this happen with | |
461 | // watch). | |
462 | grunt.log.debug('Shifter passed a specific JS file'); | |
463 | src = path.dirname(path.dirname(src)); | |
464 | options.recursive = false; | |
465 | } else if (grunt.file.isMatch('**/yui/src', src)) { | |
466 | // When in a src directory --walk all modules. | |
adeb96d2 DW |
467 | grunt.log.debug('In a src directory'); |
468 | args.push('--walk'); | |
1aa454ed DP |
469 | options.recursive = false; |
470 | } else if (grunt.file.isMatch('**/yui/src/*', src)) { | |
471 | // When in module, only build our module. | |
adeb96d2 | 472 | grunt.log.debug('In a module directory'); |
adeb96d2 | 473 | options.recursive = false; |
1aa454ed DP |
474 | } else if (grunt.file.isMatch('**/yui/src/*/js', src)) { |
475 | // When in module src, only build our module. | |
476 | grunt.log.debug('In a source directory'); | |
477 | src = path.dirname(src); | |
478 | options.recursive = false; | |
adeb96d2 DW |
479 | } |
480 | ||
1aa454ed DP |
481 | if (grunt.option('watch')) { |
482 | grunt.fail.fatal('The --watch option has been removed, please use `grunt watch` instead'); | |
adeb96d2 DW |
483 | } |
484 | ||
adeb96d2 DW |
485 | // Add the stderr option if appropriate |
486 | if (grunt.option('verbose')) { | |
487 | args.push('--lint-stderr'); | |
488 | } | |
489 | ||
a07afffc DP |
490 | if (grunt.option('no-color')) { |
491 | args.push('--color=false'); | |
492 | } | |
493 | ||
8f76bfb6 DM |
494 | var execShifter = function() { |
495 | ||
1aa454ed DP |
496 | grunt.log.ok("Running shifter on " + src); |
497 | grunt.util.spawn({ | |
498 | cmd: "node", | |
499 | args: args, | |
500 | opts: {cwd: src, stdio: 'inherit', env: process.env} | |
b18478b9 | 501 | }, function(error, result, code) { |
8f76bfb6 DM |
502 | if (code) { |
503 | grunt.fail.fatal('Shifter failed with code: ' + code); | |
504 | } else { | |
505 | grunt.log.ok('Shifter build complete.'); | |
1aa454ed | 506 | filedone(); |
8f76bfb6 DM |
507 | } |
508 | }); | |
509 | }; | |
510 | ||
adeb96d2 | 511 | // Actually run shifter. |
8f76bfb6 DM |
512 | if (!options.recursive) { |
513 | execShifter(); | |
514 | } else { | |
515 | // Check that there are yui modules otherwise shifter ends with exit code 1. | |
1aa454ed DP |
516 | if (grunt.file.expand({cwd: src}, '**/yui/src/**/*.js').length > 0) { |
517 | args.push('--recursive'); | |
518 | execShifter(); | |
519 | } else { | |
520 | grunt.log.ok('No YUI modules to build.'); | |
521 | filedone(); | |
522 | } | |
8f76bfb6 | 523 | } |
1aa454ed | 524 | }, done); |
adeb96d2 DW |
525 | }; |
526 | ||
8b02e2d9 | 527 | tasks.gherkinlint = function() { |
48b5817e AN |
528 | const done = this.async(); |
529 | const options = grunt.config('gherkinlint.options'); | |
530 | ||
531 | // Grab the gherkin-lint linter and required scaffolding. | |
5b4debd2 AN |
532 | const linter = require('gherkin-lint/dist/linter.js'); |
533 | const featureFinder = require('gherkin-lint/dist/feature-finder.js'); | |
534 | const configParser = require('gherkin-lint/dist/config-parser.js'); | |
535 | const formatter = require('gherkin-lint/dist/formatters/stylish.js'); | |
48b5817e AN |
536 | |
537 | // Run the linter. | |
5b4debd2 | 538 | return linter.lint( |
48b5817e AN |
539 | featureFinder.getFeatureFiles(grunt.file.expand(options.files)), |
540 | configParser.getConfiguration(configParser.defaultConfigFileName) | |
5b4debd2 AN |
541 | ) |
542 | .then(results => { | |
543 | // Print the results out uncondtionally. | |
544 | formatter.printResults(results); | |
545 | ||
546 | return results; | |
547 | }) | |
548 | .then(results => { | |
549 | // Report on the results. | |
550 | // The done function takes a bool whereby a falsey statement causes the task to fail. | |
551 | return results.every(result => result.errors.length === 0); | |
552 | }) | |
553 | .then(done); // eslint-disable-line promise/no-callback-in-promise | |
8b02e2d9 DP |
554 | }; |
555 | ||
adeb96d2 DW |
556 | tasks.startup = function() { |
557 | // Are we in a YUI directory? | |
e67585f8 | 558 | if (path.basename(path.resolve(cwd, '../../')) == 'yui') { |
a1587268 | 559 | grunt.task.run('yui'); |
adeb96d2 | 560 | // Are we in an AMD directory? |
c9b6feea | 561 | } else if (inAMD) { |
65d070ae | 562 | grunt.task.run('amd'); |
adeb96d2 DW |
563 | } else { |
564 | // Run them all!. | |
65d070ae DP |
565 | grunt.task.run('css'); |
566 | grunt.task.run('js'); | |
d44f7e46 | 567 | grunt.task.run('gherkinlint'); |
adeb96d2 DW |
568 | } |
569 | }; | |
570 | ||
38d4f754 RW |
571 | /** |
572 | * This is a wrapper task to handle the grunt watch command. It attempts to use | |
573 | * Watchman to monitor for file changes, if it's installed, because it's much faster. | |
574 | * | |
575 | * If Watchman isn't installed then it falls back to the grunt-contrib-watch file | |
576 | * watcher for backwards compatibility. | |
577 | */ | |
578 | tasks.watch = function() { | |
579 | var watchTaskDone = this.async(); | |
580 | var watchInitialised = false; | |
581 | var watchTaskQueue = {}; | |
582 | var processingQueue = false; | |
583 | ||
584 | // Grab the tasks and files that have been queued up and execute them. | |
585 | var processWatchTaskQueue = function() { | |
586 | if (!Object.keys(watchTaskQueue).length || processingQueue) { | |
587 | // If there is nothing in the queue or we're already processing then wait. | |
588 | return; | |
589 | } | |
590 | ||
591 | processingQueue = true; | |
592 | ||
593 | // Grab all tasks currently in the queue. | |
594 | var queueToProcess = watchTaskQueue; | |
595 | // Reset the queue. | |
596 | watchTaskQueue = {}; | |
597 | ||
598 | async.forEachSeries( | |
599 | Object.keys(queueToProcess), | |
600 | function(task, next) { | |
601 | var files = queueToProcess[task]; | |
602 | var filesOption = '--files=' + files.join(','); | |
603 | grunt.log.ok('Running task ' + task + ' for files ' + filesOption); | |
604 | ||
605 | // Spawn the task in a child process so that it doesn't kill this one | |
606 | // if it failed. | |
607 | grunt.util.spawn( | |
608 | { | |
609 | // Spawn with the grunt bin. | |
610 | grunt: true, | |
611 | // Run from current working dir and inherit stdio from process. | |
612 | opts: { | |
a84d5236 | 613 | cwd: fullRunDir, |
38d4f754 RW |
614 | stdio: 'inherit' |
615 | }, | |
616 | args: [task, filesOption] | |
617 | }, | |
618 | function(err, res, code) { | |
619 | if (code !== 0) { | |
620 | // The grunt task failed. | |
621 | grunt.log.error(err); | |
622 | } | |
623 | ||
624 | // Move on to the next task. | |
625 | next(); | |
626 | } | |
627 | ); | |
628 | }, | |
629 | function() { | |
630 | // No longer processing. | |
631 | processingQueue = false; | |
632 | // Once all of the tasks are done then recurse just in case more tasks | |
633 | // were queued while we were processing. | |
634 | processWatchTaskQueue(); | |
635 | } | |
636 | ); | |
637 | }; | |
638 | ||
a84d5236 AN |
639 | const originalWatchConfig = grunt.config.get(['watch']); |
640 | const watchConfig = Object.keys(originalWatchConfig).reduce(function(carry, key) { | |
38d4f754 RW |
641 | if (key == 'options') { |
642 | return carry; | |
643 | } | |
644 | ||
a84d5236 AN |
645 | const value = originalWatchConfig[key]; |
646 | ||
647 | const taskNames = value.tasks; | |
648 | const files = value.files; | |
649 | let excludes = []; | |
650 | if (value.excludes) { | |
651 | excludes = value.excludes; | |
652 | } | |
38d4f754 RW |
653 | |
654 | taskNames.forEach(function(taskName) { | |
a84d5236 AN |
655 | carry[taskName] = { |
656 | files, | |
657 | excludes, | |
658 | }; | |
38d4f754 RW |
659 | }); |
660 | ||
661 | return carry; | |
662 | }, {}); | |
663 | ||
664 | watchmanClient.on('error', function(error) { | |
665 | // We have to add an error handler here and parse the error string because the | |
666 | // example way from the docs to check if Watchman is installed doesn't actually work!! | |
667 | // See: https://github.com/facebook/watchman/issues/509 | |
668 | if (error.message.match('Watchman was not found')) { | |
669 | // If watchman isn't installed then we should fallback to the other watch task. | |
670 | grunt.log.ok('It is recommended that you install Watchman for better performance using the "watch" command.'); | |
671 | ||
672 | // Fallback to the old grunt-contrib-watch task. | |
673 | grunt.renameTask('watch-grunt', 'watch'); | |
674 | grunt.task.run(['watch']); | |
675 | // This task is finished. | |
676 | watchTaskDone(0); | |
677 | } else { | |
678 | grunt.log.error(error); | |
679 | // Fatal error. | |
680 | watchTaskDone(1); | |
681 | } | |
682 | }); | |
683 | ||
684 | watchmanClient.on('subscription', function(resp) { | |
685 | if (resp.subscription !== 'grunt-watch') { | |
686 | return; | |
687 | } | |
688 | ||
689 | resp.files.forEach(function(file) { | |
690 | grunt.log.ok('File changed: ' + file.name); | |
691 | ||
a84d5236 | 692 | var fullPath = fullRunDir + '/' + file.name; |
38d4f754 | 693 | Object.keys(watchConfig).forEach(function(task) { |
a84d5236 AN |
694 | |
695 | const fileGlobs = watchConfig[task].files; | |
696 | var match = fileGlobs.some(function(fileGlob) { | |
697 | return grunt.file.isMatch(`**/${fileGlob}`, fullPath); | |
38d4f754 | 698 | }); |
a84d5236 | 699 | |
38d4f754 RW |
700 | if (match) { |
701 | // If we are watching a subdirectory then the file.name will be relative | |
702 | // to that directory. However the grunt tasks expect the file paths to be | |
703 | // relative to the Gruntfile.js location so let's normalise them before | |
704 | // adding them to the queue. | |
705 | var relativePath = fullPath.replace(gruntFilePath + '/', ''); | |
706 | if (task in watchTaskQueue) { | |
707 | if (!watchTaskQueue[task].includes(relativePath)) { | |
708 | watchTaskQueue[task] = watchTaskQueue[task].concat(relativePath); | |
709 | } | |
710 | } else { | |
711 | watchTaskQueue[task] = [relativePath]; | |
712 | } | |
713 | } | |
714 | }); | |
715 | }); | |
716 | ||
717 | processWatchTaskQueue(); | |
718 | }); | |
719 | ||
720 | process.on('SIGINT', function() { | |
721 | // Let the user know that they may need to manually stop the Watchman daemon if they | |
722 | // no longer want it running. | |
723 | if (watchInitialised) { | |
724 | grunt.log.ok('The Watchman daemon may still be running and may need to be stopped manually.'); | |
725 | } | |
726 | ||
727 | process.exit(); | |
728 | }); | |
729 | ||
730 | // Initiate the watch on the current directory. | |
a84d5236 | 731 | watchmanClient.command(['watch-project', fullRunDir], function(watchError, watchResponse) { |
38d4f754 RW |
732 | if (watchError) { |
733 | grunt.log.error('Error initiating watch:', watchError); | |
734 | watchTaskDone(1); | |
735 | return; | |
736 | } | |
737 | ||
738 | if ('warning' in watchResponse) { | |
739 | grunt.log.error('warning: ', watchResponse.warning); | |
740 | } | |
741 | ||
742 | var watch = watchResponse.watch; | |
743 | var relativePath = watchResponse.relative_path; | |
744 | watchInitialised = true; | |
745 | ||
746 | watchmanClient.command(['clock', watch], function(clockError, clockResponse) { | |
747 | if (clockError) { | |
748 | grunt.log.error('Failed to query clock:', clockError); | |
749 | watchTaskDone(1); | |
750 | return; | |
751 | } | |
752 | ||
a84d5236 AN |
753 | // Generate the expression query used by watchman. |
754 | // Documentation is limited, but see https://facebook.github.io/watchman/docs/expr/allof.html for examples. | |
755 | // We generate an expression to match any value in the files list of all of our tasks, but excluding | |
756 | // all value in the excludes list of that task. | |
757 | // | |
758 | // [anyof, [ | |
759 | // [allof, [ | |
760 | // [anyof, [ | |
761 | // ['match', validPath, 'wholename'], | |
762 | // ['match', validPath, 'wholename'], | |
763 | // ], | |
764 | // [not, | |
765 | // [anyof, [ | |
766 | // ['match', invalidPath, 'wholename'], | |
767 | // ['match', invalidPath, 'wholename'], | |
768 | // ], | |
769 | // ], | |
770 | // ], | |
771 | var matchWholeName = fileGlob => ['match', fileGlob, 'wholename']; | |
38d4f754 | 772 | var matches = Object.keys(watchConfig).map(function(task) { |
a84d5236 AN |
773 | const matchAll = []; |
774 | matchAll.push(['anyof'].concat(watchConfig[task].files.map(matchWholeName))); | |
38d4f754 | 775 | |
a84d5236 AN |
776 | if (watchConfig[task].excludes.length) { |
777 | matchAll.push(['not', ['anyof'].concat(watchConfig[task].excludes.map(matchWholeName))]); | |
778 | } | |
779 | ||
780 | return ['allof'].concat(matchAll); | |
38d4f754 RW |
781 | }); |
782 | ||
a84d5236 AN |
783 | matches = ['anyof'].concat(matches); |
784 | ||
38d4f754 | 785 | var sub = { |
a84d5236 | 786 | expression: matches, |
38d4f754 RW |
787 | // Which fields we're interested in. |
788 | fields: ["name", "size", "type"], | |
789 | // Add our time constraint. | |
790 | since: clockResponse.clock | |
791 | }; | |
792 | ||
793 | if (relativePath) { | |
9ec0cbe9 | 794 | /* eslint-disable camelcase */ |
38d4f754 RW |
795 | sub.relative_root = relativePath; |
796 | } | |
797 | ||
798 | watchmanClient.command(['subscribe', watch, 'grunt-watch', sub], function(subscribeError) { | |
799 | if (subscribeError) { | |
800 | // Probably an error in the subscription criteria. | |
801 | grunt.log.error('failed to subscribe: ', subscribeError); | |
802 | watchTaskDone(1); | |
803 | return; | |
804 | } | |
805 | ||
a84d5236 | 806 | grunt.log.ok('Listening for changes to files in ' + fullRunDir); |
38d4f754 RW |
807 | }); |
808 | }); | |
809 | }); | |
810 | }; | |
811 | ||
1aa454ed DP |
812 | // On watch, we dynamically modify config to build only affected files. This |
813 | // method is slightly complicated to deal with multiple changed files at once (copied | |
814 | // from the grunt-contrib-watch readme). | |
815 | var changedFiles = Object.create(null); | |
816 | var onChange = grunt.util._.debounce(function() { | |
38d4f754 RW |
817 | var files = Object.keys(changedFiles); |
818 | grunt.config('eslint.amd.src', files); | |
819 | grunt.config('eslint.yui.src', files); | |
820 | grunt.config('shifter.options.paths', files); | |
821 | grunt.config('gherkinlint.options.files', files); | |
822 | grunt.config('babel.dist.files', [{expand: true, src: files, rename: babelRename}]); | |
823 | changedFiles = Object.create(null); | |
1aa454ed | 824 | }, 200); |
adeb96d2 | 825 | |
8efbb7b1 | 826 | grunt.event.on('watch', function(action, filepath) { |
38d4f754 RW |
827 | changedFiles[filepath] = action; |
828 | onChange(); | |
8efbb7b1 DP |
829 | }); |
830 | ||
adeb96d2 DW |
831 | // Register NPM tasks. |
832 | grunt.loadNpmTasks('grunt-contrib-uglify'); | |
8efbb7b1 | 833 | grunt.loadNpmTasks('grunt-contrib-watch'); |
af9edb2e | 834 | grunt.loadNpmTasks('grunt-sass'); |
3adb62b7 | 835 | grunt.loadNpmTasks('grunt-eslint'); |
855fc5d8 | 836 | grunt.loadNpmTasks('grunt-stylelint'); |
c53f86d4 | 837 | grunt.loadNpmTasks('grunt-babel'); |
adeb96d2 | 838 | |
38d4f754 RW |
839 | // Rename the grunt-contrib-watch "watch" task because we're going to wrap it. |
840 | grunt.renameTask('watch', 'watch-grunt'); | |
841 | ||
65d070ae | 842 | // Register JS tasks. |
adeb96d2 | 843 | grunt.registerTask('shifter', 'Run Shifter against the current directory', tasks.shifter); |
8b02e2d9 | 844 | grunt.registerTask('gherkinlint', 'Run gherkinlint against the current directory', tasks.gherkinlint); |
30db70ab | 845 | grunt.registerTask('ignorefiles', 'Generate ignore files for linters', tasks.ignorefiles); |
38d4f754 | 846 | grunt.registerTask('watch', 'Run tasks on file changes', tasks.watch); |
a1587268 | 847 | grunt.registerTask('yui', ['eslint:yui', 'shifter']); |
c53f86d4 | 848 | grunt.registerTask('amd', ['eslint:amd', 'babel']); |
a1587268 | 849 | grunt.registerTask('js', ['amd', 'yui']); |
65d070ae | 850 | |
093be5c6 AN |
851 | // Register CSS tasks. |
852 | registerStyleLintTasks(grunt, files, fullRunDir); | |
adeb96d2 DW |
853 | |
854 | // Register the startup task. | |
855 | grunt.registerTask('startup', 'Run the correct tasks for the current directory', tasks.startup); | |
856 | ||
857 | // Register the default task. | |
858 | grunt.registerTask('default', ['startup']); | |
859 | }; |