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