mirror of
https://github.com/moodle/moodle.git
synced 2025-08-10 03:16:42 +02:00
Merge branch 'MDL-68496-311' of git://github.com/andrewnicols/moodle into MOODLE_311_STABLE
This commit is contained in:
commit
859a5589b2
15 changed files with 1298 additions and 761 deletions
|
@ -1,4 +1,5 @@
|
||||||
# Generated by "grunt ignorefiles"
|
# Generated by "grunt ignorefiles"
|
||||||
|
!/.grunt
|
||||||
*/**/yui/src/*/meta/
|
*/**/yui/src/*/meta/
|
||||||
*/**/build/
|
*/**/build/
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
|
@ -204,7 +204,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ["**/amd/src/*.js", "**/amd/src/**/*.js", "Gruntfile*.js", "babel-plugin-add-module-to-define.js"],
|
files: ["**/amd/src/*.js", "**/amd/src/**/*.js", "Gruntfile.js", ".grunt/*.js", ".grunt/tasks/*.js"],
|
||||||
// We support es6 now. Woot!
|
// We support es6 now. Woot!
|
||||||
env: {
|
env: {
|
||||||
es6: true
|
es6: true
|
||||||
|
|
|
@ -39,7 +39,7 @@ module.exports = ({template, types}) => {
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
const ComponentList = require(path.resolve('GruntfileComponents.js'));
|
const ComponentList = require(path.join(process.cwd(), '.grunt', 'components.js'));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search the list of components that match the given file name
|
* Search the list of components that match the given file name
|
|
@ -92,6 +92,16 @@ const fetchComponentData = () => {
|
||||||
return componentData;
|
return componentData;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of component paths.
|
||||||
|
*
|
||||||
|
* @param {string} relativeTo
|
||||||
|
* @returns {array}
|
||||||
|
*/
|
||||||
|
const getComponentPaths = (relativeTo = '') => fetchComponentData().pathList.map(componentPath => {
|
||||||
|
return componentPath.replace(relativeTo, '');
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the list of paths to build AMD sources.
|
* Get the list of paths to build AMD sources.
|
||||||
*
|
*
|
||||||
|
@ -140,6 +150,49 @@ const getThirdPartyLibsList = relativeTo => {
|
||||||
.sort();
|
.sort();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of thirdparty library paths.
|
||||||
|
*
|
||||||
|
* @returns {array}
|
||||||
|
*/
|
||||||
|
const getThirdPartyPaths = () => {
|
||||||
|
const DOMParser = require('xmldom').DOMParser;
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const xpath = require('xpath');
|
||||||
|
|
||||||
|
const thirdpartyfiles = getThirdPartyLibsList(fs.realpathSync('./'));
|
||||||
|
const libs = ['node_modules/', 'vendor/'];
|
||||||
|
|
||||||
|
const addLibToList = lib => {
|
||||||
|
if (!lib.match('\\*') && fs.statSync(lib).isDirectory()) {
|
||||||
|
// Ensure trailing slash on dirs.
|
||||||
|
lib = lib.replace(/\/?$/, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for duplicate paths before adding to array.
|
||||||
|
if (libs.indexOf(lib) === -1) {
|
||||||
|
libs.push(lib);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
thirdpartyfiles.forEach(function(file) {
|
||||||
|
const dirname = path.dirname(file);
|
||||||
|
|
||||||
|
const xmlContent = fs.readFileSync(file, 'utf8');
|
||||||
|
const doc = new DOMParser().parseFromString(xmlContent);
|
||||||
|
const nodes = xpath.select("/libraries/library/location/text()", doc);
|
||||||
|
|
||||||
|
nodes.forEach(function(node) {
|
||||||
|
let lib = path.posix.join(dirname, node.toString());
|
||||||
|
addLibToList(lib);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return libs;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the name of the component matching the specified path.
|
* Find the name of the component matching the specified path.
|
||||||
*
|
*
|
||||||
|
@ -182,7 +235,9 @@ const getOwningComponentDirectory = checkPath => {
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getAmdSrcGlobList,
|
getAmdSrcGlobList,
|
||||||
getComponentFromPath,
|
getComponentFromPath,
|
||||||
|
getComponentPaths,
|
||||||
getOwningComponentDirectory,
|
getOwningComponentDirectory,
|
||||||
getYuiSrcGlobList,
|
getYuiSrcGlobList,
|
||||||
getThirdPartyLibsList,
|
getThirdPartyLibsList,
|
||||||
|
getThirdPartyPaths,
|
||||||
};
|
};
|
64
.grunt/tasks/eslint.js
Normal file
64
.grunt/tasks/eslint.js
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
// This file is part of Moodle - http://moodle.org/
|
||||||
|
//
|
||||||
|
// Moodle is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Moodle is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
/* jshint node: true, browser: false */
|
||||||
|
/* eslint-env node */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright 2021 Andrew Nicols
|
||||||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = grunt => {
|
||||||
|
const files = grunt.moodleEnv.files;
|
||||||
|
|
||||||
|
// Project configuration.
|
||||||
|
grunt.config.merge({
|
||||||
|
eslint: {
|
||||||
|
// Even though warnings dont stop the build we don't display warnings by default because
|
||||||
|
// at this moment we've got too many core warnings.
|
||||||
|
// To display warnings call: grunt eslint --show-lint-warnings
|
||||||
|
// To fail on warnings call: grunt eslint --max-lint-warnings=0
|
||||||
|
// Also --max-lint-warnings=-1 can be used to display warnings but not fail.
|
||||||
|
options: {
|
||||||
|
quiet: (!grunt.option('show-lint-warnings')) && (typeof grunt.option('max-lint-warnings') === 'undefined'),
|
||||||
|
maxWarnings: ((typeof grunt.option('max-lint-warnings') !== 'undefined') ? grunt.option('max-lint-warnings') : -1)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Check AMD src files.
|
||||||
|
amd: {src: files ? files : grunt.moodleEnv.amdSrc},
|
||||||
|
|
||||||
|
// Check YUI module source files.
|
||||||
|
yui: {src: files ? files : grunt.moodleEnv.yuiSrc},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
grunt.loadNpmTasks('grunt-eslint');
|
||||||
|
|
||||||
|
// On watch, we dynamically modify config to build only affected files. This
|
||||||
|
// method is slightly complicated to deal with multiple changed files at once (copied
|
||||||
|
// from the grunt-contrib-watch readme).
|
||||||
|
let changedFiles = Object.create(null);
|
||||||
|
const onChange = grunt.util._.debounce(function() {
|
||||||
|
const files = Object.keys(changedFiles);
|
||||||
|
grunt.config('eslint.amd.src', files);
|
||||||
|
grunt.config('eslint.yui.src', files);
|
||||||
|
changedFiles = Object.create(null);
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
grunt.event.on('watch', (action, filepath) => {
|
||||||
|
changedFiles[filepath] = action;
|
||||||
|
onChange();
|
||||||
|
});
|
||||||
|
};
|
89
.grunt/tasks/gherkinlint.js
Normal file
89
.grunt/tasks/gherkinlint.js
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
// This file is part of Moodle - http://moodle.org/
|
||||||
|
//
|
||||||
|
// Moodle is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Moodle is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
/* jshint node: true, browser: false */
|
||||||
|
/* eslint-env node */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright 2021 Andrew Nicols
|
||||||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = grunt => {
|
||||||
|
/**
|
||||||
|
* Get the list of feature files to pass to the gherkin linter.
|
||||||
|
*
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
const getGherkinLintTargets = () => {
|
||||||
|
if (grunt.moodleEnv.files) {
|
||||||
|
// Specific files were requested. Only check these.
|
||||||
|
return grunt.moodleEnv.files;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (grunt.moodleEnv.inComponent) {
|
||||||
|
return [`${grunt.moodleEnv.runDir}/tests/behat/*.feature`];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['**/tests/behat/*.feature'];
|
||||||
|
};
|
||||||
|
|
||||||
|
const handler = function() {
|
||||||
|
const done = this.async();
|
||||||
|
const options = grunt.config('gherkinlint.options');
|
||||||
|
|
||||||
|
// Grab the gherkin-lint linter and required scaffolding.
|
||||||
|
const linter = require('gherkin-lint/dist/linter.js');
|
||||||
|
const featureFinder = require('gherkin-lint/dist/feature-finder.js');
|
||||||
|
const configParser = require('gherkin-lint/dist/config-parser.js');
|
||||||
|
const formatter = require('gherkin-lint/dist/formatters/stylish.js');
|
||||||
|
|
||||||
|
// Run the linter.
|
||||||
|
return linter.lint(
|
||||||
|
featureFinder.getFeatureFiles(grunt.file.expand(options.files)),
|
||||||
|
configParser.getConfiguration(configParser.defaultConfigFileName)
|
||||||
|
)
|
||||||
|
.then(results => {
|
||||||
|
// Print the results out uncondtionally.
|
||||||
|
formatter.printResults(results);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
})
|
||||||
|
.then(results => {
|
||||||
|
// Report on the results.
|
||||||
|
// The done function takes a bool whereby a falsey statement causes the task to fail.
|
||||||
|
return results.every(result => result.errors.length === 0);
|
||||||
|
})
|
||||||
|
.then(done); // eslint-disable-line promise/no-callback-in-promise
|
||||||
|
};
|
||||||
|
|
||||||
|
grunt.registerTask('gherkinlint', 'Run gherkinlint against the current directory', handler);
|
||||||
|
|
||||||
|
grunt.config.set('gherkinlint', {
|
||||||
|
options: {
|
||||||
|
files: getGherkinLintTargets(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
grunt.config.merge({
|
||||||
|
watch: {
|
||||||
|
gherkinlint: {
|
||||||
|
files: [grunt.moodleEnv.inComponent ? 'tests/behat/*.feature' : '**/tests/behat/*.feature'],
|
||||||
|
tasks: ['gherkinlint'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return handler;
|
||||||
|
};
|
59
.grunt/tasks/ignorefiles.js
Normal file
59
.grunt/tasks/ignorefiles.js
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
// This file is part of Moodle - http://moodle.org/
|
||||||
|
//
|
||||||
|
// Moodle is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Moodle is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
/* jshint node: true, browser: false */
|
||||||
|
/* eslint-env node */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright 2021 Andrew Nicols
|
||||||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = grunt => {
|
||||||
|
/**
|
||||||
|
* Generate ignore files (utilising thirdpartylibs.xml data)
|
||||||
|
*/
|
||||||
|
const handler = function() {
|
||||||
|
const path = require('path');
|
||||||
|
const ComponentList = require(path.join(process.cwd(), '.grunt', 'components.js'));
|
||||||
|
|
||||||
|
// An array of paths to third party directories.
|
||||||
|
const thirdPartyPaths = ComponentList.getThirdPartyPaths();
|
||||||
|
|
||||||
|
// Generate .eslintignore.
|
||||||
|
const eslintIgnores = [
|
||||||
|
'# Generated by "grunt ignorefiles"',
|
||||||
|
// Do not ignore the .grunt directory.
|
||||||
|
'!/.grunt',
|
||||||
|
|
||||||
|
// Ignore all yui/src meta directories and build directories.
|
||||||
|
'*/**/yui/src/*/meta/',
|
||||||
|
'*/**/build/',
|
||||||
|
].concat(thirdPartyPaths);
|
||||||
|
grunt.file.write('.eslintignore', eslintIgnores.join('\n'));
|
||||||
|
|
||||||
|
// Generate .stylelintignore.
|
||||||
|
const stylelintIgnores = [
|
||||||
|
'# Generated by "grunt ignorefiles"',
|
||||||
|
'**/yui/build/*',
|
||||||
|
'theme/boost/style/moodle.css',
|
||||||
|
'theme/classic/style/moodle.css',
|
||||||
|
].concat(thirdPartyPaths);
|
||||||
|
grunt.file.write('.stylelintignore', stylelintIgnores.join('\n'));
|
||||||
|
};
|
||||||
|
|
||||||
|
grunt.registerTask('ignorefiles', 'Generate ignore files for linters', handler);
|
||||||
|
|
||||||
|
return handler;
|
||||||
|
};
|
141
.grunt/tasks/javascript.js
Normal file
141
.grunt/tasks/javascript.js
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
// This file is part of Moodle - http://moodle.org/
|
||||||
|
//
|
||||||
|
// Moodle is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Moodle is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
/* jshint node: true, browser: false */
|
||||||
|
/* eslint-env node */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright 2021 Andrew Nicols
|
||||||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to generate the destination for the uglify task
|
||||||
|
* (e.g. build/file.min.js). This function will be passed to
|
||||||
|
* the rename property of files array when building dynamically:
|
||||||
|
* http://gruntjs.com/configuring-tasks#building-the-files-object-dynamically
|
||||||
|
*
|
||||||
|
* @param {String} destPath the current destination
|
||||||
|
* @param {String} srcPath the matched src path
|
||||||
|
* @return {String} The rewritten destination path.
|
||||||
|
*/
|
||||||
|
const babelRename = function(destPath, srcPath) {
|
||||||
|
destPath = srcPath.replace('src', 'build');
|
||||||
|
destPath = destPath.replace('.js', '.min.js');
|
||||||
|
return destPath;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = grunt => {
|
||||||
|
// Load the Shifter tasks.
|
||||||
|
require('./shifter')(grunt);
|
||||||
|
|
||||||
|
// Load ESLint.
|
||||||
|
require('./eslint')(grunt);
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Register JS tasks.
|
||||||
|
grunt.registerTask('yui', ['eslint:yui', 'shifter']);
|
||||||
|
grunt.registerTask('amd', ['eslint:amd', 'babel']);
|
||||||
|
grunt.registerTask('js', ['amd', 'yui']);
|
||||||
|
|
||||||
|
// Register NPM tasks.
|
||||||
|
grunt.loadNpmTasks('grunt-contrib-uglify');
|
||||||
|
grunt.loadNpmTasks('grunt-contrib-watch');
|
||||||
|
|
||||||
|
// Load the Babel tasks and config.
|
||||||
|
grunt.loadNpmTasks('grunt-babel');
|
||||||
|
grunt.config.merge({
|
||||||
|
babel: {
|
||||||
|
options: {
|
||||||
|
sourceMaps: true,
|
||||||
|
comments: false,
|
||||||
|
plugins: [
|
||||||
|
'transform-es2015-modules-amd-lazy',
|
||||||
|
'system-import-transformer',
|
||||||
|
// This plugin modifies the Babel transpiling for "export default"
|
||||||
|
// so that if it's used then only the exported value is returned
|
||||||
|
// by the generated AMD module.
|
||||||
|
//
|
||||||
|
// It also adds the Moodle plugin name to the AMD module definition
|
||||||
|
// so that it can be imported as expected in other modules.
|
||||||
|
path.resolve('.grunt/babel-plugin-add-module-to-define.js'),
|
||||||
|
'@babel/plugin-syntax-dynamic-import',
|
||||||
|
'@babel/plugin-syntax-import-meta',
|
||||||
|
['@babel/plugin-proposal-class-properties', {'loose': false}],
|
||||||
|
'@babel/plugin-proposal-json-strings'
|
||||||
|
],
|
||||||
|
presets: [
|
||||||
|
['minify', {
|
||||||
|
// This minification plugin needs to be disabled because it breaks the
|
||||||
|
// source map generation and causes invalid source maps to be output.
|
||||||
|
simplify: false,
|
||||||
|
builtIns: false
|
||||||
|
}],
|
||||||
|
['@babel/preset-env', {
|
||||||
|
targets: {
|
||||||
|
browsers: [
|
||||||
|
">0.25%",
|
||||||
|
"last 2 versions",
|
||||||
|
"not ie <= 10",
|
||||||
|
"not op_mini all",
|
||||||
|
"not Opera > 0",
|
||||||
|
"not dead"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
modules: false,
|
||||||
|
useBuiltIns: false
|
||||||
|
}]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
dist: {
|
||||||
|
files: [{
|
||||||
|
expand: true,
|
||||||
|
src: grunt.moodleEnv.files ? grunt.moodleEnv.files : grunt.moodleEnv.amdSrc,
|
||||||
|
rename: babelRename
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
grunt.config.merge({
|
||||||
|
watch: {
|
||||||
|
amd: {
|
||||||
|
files: grunt.moodleEnv.inComponent
|
||||||
|
? ['amd/src/*.js', 'amd/src/**/*.js']
|
||||||
|
: ['**/amd/src/**/*.js'],
|
||||||
|
tasks: ['amd']
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// On watch, we dynamically modify config to build only affected files. This
|
||||||
|
// method is slightly complicated to deal with multiple changed files at once (copied
|
||||||
|
// from the grunt-contrib-watch readme).
|
||||||
|
let changedFiles = Object.create(null);
|
||||||
|
const onChange = grunt.util._.debounce(function() {
|
||||||
|
const files = Object.keys(changedFiles);
|
||||||
|
grunt.config('babel.dist.files', [{expand: true, src: files, rename: babelRename}]);
|
||||||
|
changedFiles = Object.create(null);
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
grunt.event.on('watch', function(action, filepath) {
|
||||||
|
changedFiles[filepath] = action;
|
||||||
|
onChange();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
babelRename,
|
||||||
|
};
|
||||||
|
};
|
40
.grunt/tasks/sass.js
Normal file
40
.grunt/tasks/sass.js
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
// This file is part of Moodle - http://moodle.org/
|
||||||
|
//
|
||||||
|
// Moodle is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Moodle is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
/* jshint node: true, browser: false */
|
||||||
|
/* eslint-env node */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright 2021 Andrew Nicols
|
||||||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = grunt => {
|
||||||
|
grunt.loadNpmTasks('grunt-sass');
|
||||||
|
|
||||||
|
grunt.config.merge({
|
||||||
|
sass: {
|
||||||
|
dist: {
|
||||||
|
files: {
|
||||||
|
"theme/boost/style/moodle.css": "theme/boost/scss/preset/default.scss",
|
||||||
|
"theme/classic/style/moodle.css": "theme/classic/scss/classicgrunt.scss"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
implementation: require('node-sass'),
|
||||||
|
includePaths: ["theme/boost/scss/", "theme/classic/scss/"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
155
.grunt/tasks/shifter.js
Normal file
155
.grunt/tasks/shifter.js
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
// This file is part of Moodle - http://moodle.org/
|
||||||
|
//
|
||||||
|
// Moodle is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Moodle is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
/* jshint node: true, browser: false */
|
||||||
|
/* eslint-env node */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright 2021 Andrew Nicols
|
||||||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-env node */
|
||||||
|
|
||||||
|
module.exports = grunt => {
|
||||||
|
/**
|
||||||
|
* Shifter task. Is configured with a path to a specific file or a directory,
|
||||||
|
* in the case of a specific file it will work out the right module to be built.
|
||||||
|
*
|
||||||
|
* Note that this task runs the invidiaul shifter jobs async (becase it spawns
|
||||||
|
* so be careful to to call done().
|
||||||
|
*/
|
||||||
|
const handler = function() {
|
||||||
|
const done = this.async();
|
||||||
|
const options = grunt.config('shifter.options');
|
||||||
|
const async = require('async');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Run the shifter processes one at a time to avoid confusing output.
|
||||||
|
async.eachSeries(options.paths, function(src, filedone) {
|
||||||
|
var args = [];
|
||||||
|
args.push(path.normalize(process.cwd() + '/node_modules/shifter/bin/shifter'));
|
||||||
|
|
||||||
|
// Always ignore the node_modules directory.
|
||||||
|
args.push('--excludes', 'node_modules');
|
||||||
|
|
||||||
|
// Determine the most appropriate options to run with based upon the current location.
|
||||||
|
if (grunt.file.isMatch('**/yui/**/*.js', src)) {
|
||||||
|
// When passed a JS file, build our containing module (this happen with
|
||||||
|
// watch).
|
||||||
|
grunt.log.debug('Shifter passed a specific JS file');
|
||||||
|
src = path.dirname(path.dirname(src));
|
||||||
|
options.recursive = false;
|
||||||
|
} else if (grunt.file.isMatch('**/yui/src', src)) {
|
||||||
|
// When in a src directory --walk all modules.
|
||||||
|
grunt.log.debug('In a src directory');
|
||||||
|
args.push('--walk');
|
||||||
|
options.recursive = false;
|
||||||
|
} else if (grunt.file.isMatch('**/yui/src/*', src)) {
|
||||||
|
// When in module, only build our module.
|
||||||
|
grunt.log.debug('In a module directory');
|
||||||
|
options.recursive = false;
|
||||||
|
} else if (grunt.file.isMatch('**/yui/src/*/js', src)) {
|
||||||
|
// When in module src, only build our module.
|
||||||
|
grunt.log.debug('In a source directory');
|
||||||
|
src = path.dirname(src);
|
||||||
|
options.recursive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (grunt.option('watch')) {
|
||||||
|
grunt.fail.fatal('The --watch option has been removed, please use `grunt watch` instead');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the stderr option if appropriate
|
||||||
|
if (grunt.option('verbose')) {
|
||||||
|
args.push('--lint-stderr');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (grunt.option('no-color')) {
|
||||||
|
args.push('--color=false');
|
||||||
|
}
|
||||||
|
|
||||||
|
var execShifter = function() {
|
||||||
|
|
||||||
|
grunt.log.ok("Running shifter on " + src);
|
||||||
|
grunt.util.spawn({
|
||||||
|
cmd: "node",
|
||||||
|
args: args,
|
||||||
|
opts: {cwd: src, stdio: 'inherit', env: process.env}
|
||||||
|
}, function(error, result, code) {
|
||||||
|
if (code) {
|
||||||
|
grunt.fail.fatal('Shifter failed with code: ' + code);
|
||||||
|
} else {
|
||||||
|
grunt.log.ok('Shifter build complete.');
|
||||||
|
filedone();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Actually run shifter.
|
||||||
|
if (!options.recursive) {
|
||||||
|
execShifter();
|
||||||
|
} else {
|
||||||
|
// Check that there are yui modules otherwise shifter ends with exit code 1.
|
||||||
|
if (grunt.file.expand({cwd: src}, '**/yui/src/**/*.js').length > 0) {
|
||||||
|
args.push('--recursive');
|
||||||
|
execShifter();
|
||||||
|
} else {
|
||||||
|
grunt.log.ok('No YUI modules to build.');
|
||||||
|
filedone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, done);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Register the shifter task.
|
||||||
|
grunt.registerTask('shifter', 'Run Shifter against the current directory', handler);
|
||||||
|
|
||||||
|
// Configure it.
|
||||||
|
grunt.config.set('shifter', {
|
||||||
|
options: {
|
||||||
|
recursive: true,
|
||||||
|
// Shifter takes a relative path.
|
||||||
|
paths: grunt.moodleEnv.files ? grunt.moodleEnv.files : [grunt.moodleEnv.runDir]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
grunt.config.merge({
|
||||||
|
watch: {
|
||||||
|
yui: {
|
||||||
|
files: grunt.moodleEnv.inComponent
|
||||||
|
? ['yui/src/*.json', 'yui/src/**/*.js']
|
||||||
|
: ['**/yui/src/**/*.js'],
|
||||||
|
tasks: ['yui']
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// On watch, we dynamically modify config to build only affected files. This
|
||||||
|
// method is slightly complicated to deal with multiple changed files at once (copied
|
||||||
|
// from the grunt-contrib-watch readme).
|
||||||
|
let changedFiles = Object.create(null);
|
||||||
|
const onChange = grunt.util._.debounce(function() {
|
||||||
|
const files = Object.keys(changedFiles);
|
||||||
|
grunt.config('shifter.options.paths', files);
|
||||||
|
changedFiles = Object.create(null);
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
grunt.event.on('watch', (action, filepath) => {
|
||||||
|
changedFiles[filepath] = action;
|
||||||
|
onChange();
|
||||||
|
});
|
||||||
|
|
||||||
|
return handler;
|
||||||
|
};
|
48
.grunt/tasks/startup.js
Normal file
48
.grunt/tasks/startup.js
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
// This file is part of Moodle - http://moodle.org/
|
||||||
|
//
|
||||||
|
// Moodle is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Moodle is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
/* jshint node: true, browser: false */
|
||||||
|
/* eslint-env node */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright 2021 Andrew Nicols
|
||||||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = grunt => {
|
||||||
|
/**
|
||||||
|
* Generate ignore files (utilising thirdpartylibs.xml data)
|
||||||
|
*/
|
||||||
|
const handler = function() {
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Are we in a YUI directory?
|
||||||
|
if (path.basename(path.resolve(grunt.moodleEnv.cwd, '../../')) == 'yui') {
|
||||||
|
grunt.task.run('yui');
|
||||||
|
// Are we in an AMD directory?
|
||||||
|
} else if (grunt.moodleEnv.inAMD) {
|
||||||
|
grunt.task.run('amd');
|
||||||
|
} else {
|
||||||
|
// Run them all!.
|
||||||
|
grunt.task.run('css');
|
||||||
|
grunt.task.run('js');
|
||||||
|
grunt.task.run('gherkinlint');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Register the startup task.
|
||||||
|
grunt.registerTask('startup', 'Run the correct tasks for the current directory', handler);
|
||||||
|
|
||||||
|
return handler;
|
||||||
|
};
|
29
.grunt/tasks/style.js
Normal file
29
.grunt/tasks/style.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
// This file is part of Moodle - http://moodle.org/
|
||||||
|
//
|
||||||
|
// Moodle is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Moodle is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
/* jshint node: true, browser: false */
|
||||||
|
/* eslint-env node */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright 2021 Andrew Nicols
|
||||||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = grunt => {
|
||||||
|
// Load the Style Lint tasks.
|
||||||
|
require('./stylelint')(grunt);
|
||||||
|
|
||||||
|
// Load the SASS tasks.
|
||||||
|
require('./sass')(grunt);
|
||||||
|
};
|
181
.grunt/tasks/stylelint.js
Normal file
181
.grunt/tasks/stylelint.js
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
// This file is part of Moodle - http://moodle.org/
|
||||||
|
//
|
||||||
|
// Moodle is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Moodle is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
/* jshint node: true, browser: false */
|
||||||
|
/* eslint-env node */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright 2021 Andrew Nicols
|
||||||
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = grunt => {
|
||||||
|
|
||||||
|
const getCssConfigForFiles = files => {
|
||||||
|
return {
|
||||||
|
stylelint: {
|
||||||
|
css: {
|
||||||
|
// Use a fully-qualified path.
|
||||||
|
src: files,
|
||||||
|
options: {
|
||||||
|
configOverrides: {
|
||||||
|
rules: {
|
||||||
|
// These rules have to be disabled in .stylelintrc for scss compat.
|
||||||
|
"at-rule-no-unknown": true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getScssConfigForFiles = files => {
|
||||||
|
return {
|
||||||
|
stylelint: {
|
||||||
|
scss: {
|
||||||
|
options: {syntax: 'scss'},
|
||||||
|
src: files,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register any stylelint tasks.
|
||||||
|
*
|
||||||
|
* @param {Object} grunt
|
||||||
|
* @param {Array} files
|
||||||
|
* @param {String} fullRunDir
|
||||||
|
*/
|
||||||
|
const registerStyleLintTasks = () => {
|
||||||
|
const glob = require('glob');
|
||||||
|
|
||||||
|
// The stylelinters do not handle the case where a configuration was provided but no files were included.
|
||||||
|
// Keep track of whether any files were found.
|
||||||
|
let hasCss = false;
|
||||||
|
let hasScss = false;
|
||||||
|
|
||||||
|
// The stylelint processors do not take a path argument. They always check all provided values.
|
||||||
|
// As a result we must check through each glob and determine if any files match the current directory.
|
||||||
|
const scssFiles = [];
|
||||||
|
const cssFiles = [];
|
||||||
|
|
||||||
|
const requestedFiles = grunt.moodleEnv.files;
|
||||||
|
if (requestedFiles) {
|
||||||
|
// Grunt was called with a files argument.
|
||||||
|
// Check whether each of the requested files matches either the CSS or SCSS source file list.
|
||||||
|
|
||||||
|
requestedFiles.forEach(changedFilePath => {
|
||||||
|
let matchesGlob;
|
||||||
|
|
||||||
|
// Check whether this watched path matches any watched SCSS file.
|
||||||
|
matchesGlob = grunt.moodleEnv.scssSrc.some(watchedPathGlob => {
|
||||||
|
return glob.sync(watchedPathGlob).indexOf(changedFilePath) !== -1;
|
||||||
|
});
|
||||||
|
if (matchesGlob) {
|
||||||
|
scssFiles.push(changedFilePath);
|
||||||
|
hasScss = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whether this watched path matches any watched CSS file.
|
||||||
|
matchesGlob = grunt.moodleEnv.cssSrc.some(watchedPathGlob => {
|
||||||
|
return glob.sync(watchedPathGlob).indexOf(changedFilePath) !== -1;
|
||||||
|
});
|
||||||
|
if (matchesGlob) {
|
||||||
|
cssFiles.push(changedFilePath);
|
||||||
|
hasCss = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Grunt was called without a list of files.
|
||||||
|
// The start directory (runDir) may be a child dir of the project.
|
||||||
|
// Check each scssSrc file to see if it's in the start directory.
|
||||||
|
// This means that we can lint just mod/*/styles.css if started in the mod directory.
|
||||||
|
|
||||||
|
grunt.moodleEnv.scssSrc.forEach(path => {
|
||||||
|
if (path.startsWith(grunt.moodleEnv.runDir)) {
|
||||||
|
scssFiles.push(path);
|
||||||
|
hasScss = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
grunt.moodleEnv.cssSrc.forEach(path => {
|
||||||
|
if (path.startsWith(grunt.moodleEnv.runDir)) {
|
||||||
|
cssFiles.push(path);
|
||||||
|
hasCss = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the tasks.
|
||||||
|
const scssTasks = ['sass'];
|
||||||
|
if (hasScss) {
|
||||||
|
grunt.config.merge(getScssConfigForFiles(scssFiles));
|
||||||
|
scssTasks.unshift('stylelint:scss');
|
||||||
|
}
|
||||||
|
|
||||||
|
const cssTasks = [];
|
||||||
|
if (hasCss) {
|
||||||
|
grunt.config.merge(getCssConfigForFiles(cssFiles));
|
||||||
|
cssTasks.push('stylelint:css');
|
||||||
|
}
|
||||||
|
|
||||||
|
// The tasks must be registered, even if empty to ensure a consistent command list.
|
||||||
|
// They jsut won't run anything.
|
||||||
|
grunt.registerTask('scss', scssTasks);
|
||||||
|
grunt.registerTask('rawcss', cssTasks);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Register CSS tasks.
|
||||||
|
grunt.loadNpmTasks('grunt-stylelint');
|
||||||
|
|
||||||
|
// Register the style lint tasks.
|
||||||
|
registerStyleLintTasks();
|
||||||
|
grunt.registerTask('css', ['scss', 'rawcss']);
|
||||||
|
|
||||||
|
const getCoreThemeMatches = () => {
|
||||||
|
const scssMatch = 'scss/**/*.scss';
|
||||||
|
|
||||||
|
if (grunt.moodleEnv.inTheme) {
|
||||||
|
return [scssMatch];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (grunt.moodleEnv.runDir.startsWith('theme')) {
|
||||||
|
return [`*/${scssMatch}`];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [`theme/*/${scssMatch}`];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add the watch configuration for rawcss, and scss.
|
||||||
|
grunt.config.merge({
|
||||||
|
watch: {
|
||||||
|
rawcss: {
|
||||||
|
files: [
|
||||||
|
'**/*.css',
|
||||||
|
],
|
||||||
|
excludes: [
|
||||||
|
'**/moodle.css',
|
||||||
|
'**/editor.css',
|
||||||
|
],
|
||||||
|
tasks: ['rawcss']
|
||||||
|
},
|
||||||
|
scss: {
|
||||||
|
files: getCoreThemeMatches(),
|
||||||
|
tasks: ['scss']
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
272
.grunt/tasks/watch.js
Normal file
272
.grunt/tasks/watch.js
Normal file
|
@ -0,0 +1,272 @@
|
||||||
|
/**
|
||||||
|
* This is a wrapper task to handle the grunt watch command. It attempts to use
|
||||||
|
* Watchman to monitor for file changes, if it's installed, because it's much faster.
|
||||||
|
*
|
||||||
|
* If Watchman isn't installed then it falls back to the grunt-contrib-watch file
|
||||||
|
* watcher for backwards compatibility.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-env node */
|
||||||
|
|
||||||
|
module.exports = grunt => {
|
||||||
|
/**
|
||||||
|
* This is a wrapper task to handle the grunt watch command. It attempts to use
|
||||||
|
* Watchman to monitor for file changes, if it's installed, because it's much faster.
|
||||||
|
*
|
||||||
|
* If Watchman isn't installed then it falls back to the grunt-contrib-watch file
|
||||||
|
* watcher for backwards compatibility.
|
||||||
|
*/
|
||||||
|
const watchHandler = function() {
|
||||||
|
const async = require('async');
|
||||||
|
const watchTaskDone = this.async();
|
||||||
|
let watchInitialised = false;
|
||||||
|
let watchTaskQueue = {};
|
||||||
|
let processingQueue = false;
|
||||||
|
|
||||||
|
const watchman = require('fb-watchman');
|
||||||
|
const watchmanClient = new watchman.Client();
|
||||||
|
|
||||||
|
// Grab the tasks and files that have been queued up and execute them.
|
||||||
|
var processWatchTaskQueue = function() {
|
||||||
|
if (!Object.keys(watchTaskQueue).length || processingQueue) {
|
||||||
|
// If there is nothing in the queue or we're already processing then wait.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
processingQueue = true;
|
||||||
|
|
||||||
|
// Grab all tasks currently in the queue.
|
||||||
|
var queueToProcess = watchTaskQueue;
|
||||||
|
// Reset the queue.
|
||||||
|
watchTaskQueue = {};
|
||||||
|
|
||||||
|
async.forEachSeries(
|
||||||
|
Object.keys(queueToProcess),
|
||||||
|
function(task, next) {
|
||||||
|
var files = queueToProcess[task];
|
||||||
|
var filesOption = '--files=' + files.join(',');
|
||||||
|
grunt.log.ok('Running task ' + task + ' for files ' + filesOption);
|
||||||
|
|
||||||
|
// Spawn the task in a child process so that it doesn't kill this one
|
||||||
|
// if it failed.
|
||||||
|
grunt.util.spawn(
|
||||||
|
{
|
||||||
|
// Spawn with the grunt bin.
|
||||||
|
grunt: true,
|
||||||
|
// Run from current working dir and inherit stdio from process.
|
||||||
|
opts: {
|
||||||
|
cwd: grunt.moodleEnv.fullRunDir,
|
||||||
|
stdio: 'inherit'
|
||||||
|
},
|
||||||
|
args: [task, filesOption]
|
||||||
|
},
|
||||||
|
function(err, res, code) {
|
||||||
|
if (code !== 0) {
|
||||||
|
// The grunt task failed.
|
||||||
|
grunt.log.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move on to the next task.
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
function() {
|
||||||
|
// No longer processing.
|
||||||
|
processingQueue = false;
|
||||||
|
// Once all of the tasks are done then recurse just in case more tasks
|
||||||
|
// were queued while we were processing.
|
||||||
|
processWatchTaskQueue();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const originalWatchConfig = grunt.config.get(['watch']);
|
||||||
|
const watchConfig = Object.keys(originalWatchConfig).reduce(function(carry, key) {
|
||||||
|
if (key == 'options') {
|
||||||
|
return carry;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = originalWatchConfig[key];
|
||||||
|
|
||||||
|
const taskNames = value.tasks;
|
||||||
|
const files = value.files;
|
||||||
|
let excludes = [];
|
||||||
|
if (value.excludes) {
|
||||||
|
excludes = value.excludes;
|
||||||
|
}
|
||||||
|
|
||||||
|
taskNames.forEach(function(taskName) {
|
||||||
|
carry[taskName] = {
|
||||||
|
files,
|
||||||
|
excludes,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return carry;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
watchmanClient.on('error', function(error) {
|
||||||
|
// We have to add an error handler here and parse the error string because the
|
||||||
|
// example way from the docs to check if Watchman is installed doesn't actually work!!
|
||||||
|
// See: https://github.com/facebook/watchman/issues/509
|
||||||
|
if (error.message.match('Watchman was not found')) {
|
||||||
|
// If watchman isn't installed then we should fallback to the other watch task.
|
||||||
|
grunt.log.ok('It is recommended that you install Watchman for better performance using the "watch" command.');
|
||||||
|
|
||||||
|
// Fallback to the old grunt-contrib-watch task.
|
||||||
|
grunt.renameTask('watch-grunt', 'watch');
|
||||||
|
grunt.task.run(['watch']);
|
||||||
|
// This task is finished.
|
||||||
|
watchTaskDone(0);
|
||||||
|
} else {
|
||||||
|
grunt.log.error(error);
|
||||||
|
// Fatal error.
|
||||||
|
watchTaskDone(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watchmanClient.on('subscription', function(resp) {
|
||||||
|
if (resp.subscription !== 'grunt-watch') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.files.forEach(function(file) {
|
||||||
|
grunt.log.ok('File changed: ' + file.name);
|
||||||
|
|
||||||
|
var fullPath = grunt.moodleEnv.fullRunDir + '/' + file.name;
|
||||||
|
Object.keys(watchConfig).forEach(function(task) {
|
||||||
|
|
||||||
|
const fileGlobs = watchConfig[task].files;
|
||||||
|
var match = fileGlobs.some(function(fileGlob) {
|
||||||
|
return grunt.file.isMatch(`**/${fileGlob}`, fullPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
// If we are watching a subdirectory then the file.name will be relative
|
||||||
|
// to that directory. However the grunt tasks expect the file paths to be
|
||||||
|
// relative to the Gruntfile.js location so let's normalise them before
|
||||||
|
// adding them to the queue.
|
||||||
|
var relativePath = fullPath.replace(grunt.moodleEnv.gruntFilePath + '/', '');
|
||||||
|
if (task in watchTaskQueue) {
|
||||||
|
if (!watchTaskQueue[task].includes(relativePath)) {
|
||||||
|
watchTaskQueue[task] = watchTaskQueue[task].concat(relativePath);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
watchTaskQueue[task] = [relativePath];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
processWatchTaskQueue();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('SIGINT', function() {
|
||||||
|
// Let the user know that they may need to manually stop the Watchman daemon if they
|
||||||
|
// no longer want it running.
|
||||||
|
if (watchInitialised) {
|
||||||
|
grunt.log.ok('The Watchman daemon may still be running and may need to be stopped manually.');
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initiate the watch on the current directory.
|
||||||
|
watchmanClient.command(['watch-project', grunt.moodleEnv.fullRunDir], function(watchError, watchResponse) {
|
||||||
|
if (watchError) {
|
||||||
|
grunt.log.error('Error initiating watch:', watchError);
|
||||||
|
watchTaskDone(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('warning' in watchResponse) {
|
||||||
|
grunt.log.error('warning: ', watchResponse.warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
var watch = watchResponse.watch;
|
||||||
|
var relativePath = watchResponse.relative_path;
|
||||||
|
watchInitialised = true;
|
||||||
|
|
||||||
|
watchmanClient.command(['clock', watch], function(clockError, clockResponse) {
|
||||||
|
if (clockError) {
|
||||||
|
grunt.log.error('Failed to query clock:', clockError);
|
||||||
|
watchTaskDone(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the expression query used by watchman.
|
||||||
|
// Documentation is limited, but see https://facebook.github.io/watchman/docs/expr/allof.html for examples.
|
||||||
|
// We generate an expression to match any value in the files list of all of our tasks, but excluding
|
||||||
|
// all value in the excludes list of that task.
|
||||||
|
//
|
||||||
|
// [anyof, [
|
||||||
|
// [allof, [
|
||||||
|
// [anyof, [
|
||||||
|
// ['match', validPath, 'wholename'],
|
||||||
|
// ['match', validPath, 'wholename'],
|
||||||
|
// ],
|
||||||
|
// [not,
|
||||||
|
// [anyof, [
|
||||||
|
// ['match', invalidPath, 'wholename'],
|
||||||
|
// ['match', invalidPath, 'wholename'],
|
||||||
|
// ],
|
||||||
|
// ],
|
||||||
|
// ],
|
||||||
|
var matchWholeName = fileGlob => ['match', fileGlob, 'wholename'];
|
||||||
|
var matches = Object.keys(watchConfig).map(function(task) {
|
||||||
|
const matchAll = [];
|
||||||
|
matchAll.push(['anyof'].concat(watchConfig[task].files.map(matchWholeName)));
|
||||||
|
|
||||||
|
if (watchConfig[task].excludes.length) {
|
||||||
|
matchAll.push(['not', ['anyof'].concat(watchConfig[task].excludes.map(matchWholeName))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['allof'].concat(matchAll);
|
||||||
|
});
|
||||||
|
|
||||||
|
matches = ['anyof'].concat(matches);
|
||||||
|
|
||||||
|
var sub = {
|
||||||
|
expression: matches,
|
||||||
|
// Which fields we're interested in.
|
||||||
|
fields: ["name", "size", "type"],
|
||||||
|
// Add our time constraint.
|
||||||
|
since: clockResponse.clock
|
||||||
|
};
|
||||||
|
|
||||||
|
if (relativePath) {
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
sub.relative_root = relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
watchmanClient.command(['subscribe', watch, 'grunt-watch', sub], function(subscribeError) {
|
||||||
|
if (subscribeError) {
|
||||||
|
// Probably an error in the subscription criteria.
|
||||||
|
grunt.log.error('failed to subscribe: ', subscribeError);
|
||||||
|
watchTaskDone(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
grunt.log.ok('Listening for changes to files in ' + grunt.moodleEnv.fullRunDir);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Rename the grunt-contrib-watch "watch" task because we're going to wrap it.
|
||||||
|
grunt.renameTask('watch', 'watch-grunt');
|
||||||
|
|
||||||
|
// Register the new watch handler.
|
||||||
|
grunt.registerTask('watch', 'Run tasks on file changes', watchHandler);
|
||||||
|
|
||||||
|
grunt.config.merge({
|
||||||
|
watch: {
|
||||||
|
options: {
|
||||||
|
nospawn: true // We need not to spawn so config can be changed dynamically.
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return watchHandler;
|
||||||
|
};
|
921
Gruntfile.js
921
Gruntfile.js
|
@ -16,147 +16,129 @@
|
||||||
/* eslint-env node */
|
/* eslint-env node */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Grunt configuration for Moodle.
|
||||||
|
*
|
||||||
* @copyright 2014 Andrew Nicols
|
* @copyright 2014 Andrew Nicols
|
||||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* eslint-env node */
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate the cwd, taking into consideration the `root` option (for Windows).
|
* Setup the Grunt Moodle environment.
|
||||||
*
|
*
|
||||||
* @param {Object} grunt
|
* @param {Grunt} grunt
|
||||||
* @returns {String} The current directory as best we can determine
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
const getCwd = grunt => {
|
const setupMoodleEnvironment = grunt => {
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const ComponentList = require(path.join(process.cwd(), '.grunt', 'components.js'));
|
||||||
|
|
||||||
let cwd = fs.realpathSync(process.env.PWD || process.cwd());
|
const getAmdConfiguration = () => {
|
||||||
|
// If the cwd is the amd directory in the current component then it will be empty.
|
||||||
|
// If the cwd is a child of the component's AMD directory, the relative directory will not start with ..
|
||||||
|
let inAMD = !path.relative(`${componentDirectory}/amd`, cwd).startsWith('..');
|
||||||
|
|
||||||
// Windows users can't run grunt in a subdirectory, so allow them to set
|
// Globbing pattern for matching all AMD JS source files.
|
||||||
// the root by passing --root=path/to/dir.
|
let amdSrc = [];
|
||||||
if (grunt.option('root')) {
|
if (inComponent) {
|
||||||
const root = grunt.option('root');
|
amdSrc.push(
|
||||||
if (grunt.file.exists(__dirname, root)) {
|
componentDirectory + "/amd/src/*.js",
|
||||||
cwd = fs.realpathSync(path.join(__dirname, root));
|
componentDirectory + "/amd/src/**/*.js"
|
||||||
grunt.log.ok('Setting root to ' + cwd);
|
);
|
||||||
} else {
|
} else {
|
||||||
grunt.fail.fatal('Setting root to ' + root + ' failed - path does not exist');
|
amdSrc = ComponentList.getAmdSrcGlobList();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return cwd;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register any stylelint tasks.
|
|
||||||
*
|
|
||||||
* @param {Object} grunt
|
|
||||||
* @param {Array} files
|
|
||||||
* @param {String} fullRunDir
|
|
||||||
*/
|
|
||||||
const registerStyleLintTasks = (grunt, files, fullRunDir) => {
|
|
||||||
const getCssConfigForFiles = files => {
|
|
||||||
return {
|
return {
|
||||||
stylelint: {
|
inAMD,
|
||||||
css: {
|
amdSrc,
|
||||||
// Use a fully-qualified path.
|
|
||||||
src: files,
|
|
||||||
options: {
|
|
||||||
configOverrides: {
|
|
||||||
rules: {
|
|
||||||
// These rules have to be disabled in .stylelintrc for scss compat.
|
|
||||||
"at-rule-no-unknown": true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getScssConfigForFiles = files => {
|
const getYuiConfiguration = () => {
|
||||||
|
let yuiSrc = [];
|
||||||
|
if (inComponent) {
|
||||||
|
yuiSrc.push(componentDirectory + "/yui/src/**/*.js");
|
||||||
|
} else {
|
||||||
|
yuiSrc = ComponentList.getYuiSrcGlobList(gruntFilePath + '/');
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
stylelint: {
|
yuiSrc,
|
||||||
scss: {
|
|
||||||
options: {syntax: 'scss'},
|
|
||||||
src: files,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
let hasCss = true;
|
const getStyleConfiguration = () => {
|
||||||
let hasScss = true;
|
const ComponentList = require(path.join(process.cwd(), '.grunt', 'components.js'));
|
||||||
|
// Build the cssSrc and scssSrc.
|
||||||
|
// Valid paths are:
|
||||||
|
// [component]/styles.css; and either
|
||||||
|
// [theme/[themename]]/scss/**/*.scss; or
|
||||||
|
// [theme/[themename]]/style/*.css.
|
||||||
|
//
|
||||||
|
// If a theme has scss, then it is assumed that the style directory contains generated content.
|
||||||
|
let cssSrc = [];
|
||||||
|
let scssSrc = [];
|
||||||
|
|
||||||
if (files) {
|
const checkComponentDirectory = componentDirectory => {
|
||||||
// Specific files were passed. Just set them up.
|
const isTheme = componentDirectory.startsWith('theme/');
|
||||||
grunt.config.merge(getCssConfigForFiles(files));
|
if (isTheme) {
|
||||||
grunt.config.merge(getScssConfigForFiles(files));
|
const scssDirectory = `${componentDirectory}/scss`;
|
||||||
} else {
|
|
||||||
// The stylelint system does not handle the case where there was no file to lint.
|
|
||||||
// Check whether there are any files to lint in the current directory.
|
|
||||||
const glob = require('glob');
|
|
||||||
|
|
||||||
const scssSrc = [];
|
if (fs.existsSync(scssDirectory)) {
|
||||||
glob.sync(`${fullRunDir}/**/*.scss`).forEach(path => scssSrc.push(path));
|
// This theme has an SCSS directory.
|
||||||
|
// Include all scss files within it recursively, but do not check for css files.
|
||||||
|
scssSrc.push(`${scssDirectory}/*.scss`);
|
||||||
|
scssSrc.push(`${scssDirectory}/**/*.scss`);
|
||||||
|
} else {
|
||||||
|
// This theme has no SCSS directory.
|
||||||
|
// Only hte CSS files in the top-level directory are checked.
|
||||||
|
cssSrc.push(`${componentDirectory}/style/*.css`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// This is not a theme.
|
||||||
|
// All other plugin types are restricted to a single styles.css in their top level.
|
||||||
|
cssSrc.push(`${componentDirectory}/styles.css`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (scssSrc.length) {
|
if (inComponent) {
|
||||||
grunt.config.merge(getScssConfigForFiles(scssSrc));
|
checkComponentDirectory(componentDirectory);
|
||||||
} else {
|
} else {
|
||||||
hasScss = false;
|
ComponentList.getComponentPaths(`${gruntFilePath}/`).forEach(componentPath => {
|
||||||
|
checkComponentDirectory(componentPath);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const cssSrc = [];
|
return {
|
||||||
glob.sync(`${fullRunDir}/**/*.css`).forEach(path => cssSrc.push(path));
|
cssSrc,
|
||||||
|
scssSrc,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
if (cssSrc.length) {
|
/**
|
||||||
grunt.config.merge(getCssConfigForFiles(cssSrc));
|
* Calculate the cwd, taking into consideration the `root` option (for Windows).
|
||||||
} else {
|
*
|
||||||
hasCss = false;
|
* @param {Object} grunt
|
||||||
|
* @returns {String} The current directory as best we can determine
|
||||||
|
*/
|
||||||
|
const getCwd = grunt => {
|
||||||
|
let cwd = fs.realpathSync(process.env.PWD || process.cwd());
|
||||||
|
|
||||||
|
// Windows users can't run grunt in a subdirectory, so allow them to set
|
||||||
|
// the root by passing --root=path/to/dir.
|
||||||
|
if (grunt.option('root')) {
|
||||||
|
const root = grunt.option('root');
|
||||||
|
if (grunt.file.exists(__dirname, root)) {
|
||||||
|
cwd = fs.realpathSync(path.join(__dirname, root));
|
||||||
|
grunt.log.ok('Setting root to ' + cwd);
|
||||||
|
} else {
|
||||||
|
grunt.fail.fatal('Setting root to ' + root + ' failed - path does not exist');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const scssTasks = ['sass'];
|
return cwd;
|
||||||
if (hasScss) {
|
};
|
||||||
scssTasks.unshift('stylelint:scss');
|
|
||||||
}
|
|
||||||
grunt.registerTask('scss', scssTasks);
|
|
||||||
|
|
||||||
const cssTasks = [];
|
|
||||||
if (hasCss) {
|
|
||||||
cssTasks.push('stylelint:css');
|
|
||||||
}
|
|
||||||
grunt.registerTask('rawcss', cssTasks);
|
|
||||||
|
|
||||||
grunt.registerTask('css', ['scss', 'rawcss']);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Grunt configuration.
|
|
||||||
*
|
|
||||||
* @param {Object} grunt
|
|
||||||
*/
|
|
||||||
module.exports = function(grunt) {
|
|
||||||
const path = require('path');
|
|
||||||
const tasks = {};
|
|
||||||
const async = require('async');
|
|
||||||
const DOMParser = require('xmldom').DOMParser;
|
|
||||||
const xpath = require('xpath');
|
|
||||||
const semver = require('semver');
|
|
||||||
const watchman = require('fb-watchman');
|
|
||||||
const watchmanClient = new watchman.Client();
|
|
||||||
const fs = require('fs');
|
|
||||||
const ComponentList = require(path.resolve('GruntfileComponents.js'));
|
|
||||||
const sass = require('node-sass');
|
|
||||||
|
|
||||||
// Verify the node version is new enough.
|
|
||||||
var expected = semver.validRange(grunt.file.readJSON('package.json').engines.node);
|
|
||||||
var actual = semver.valid(process.version);
|
|
||||||
if (!semver.satisfies(actual, expected)) {
|
|
||||||
grunt.fail.fatal('Node version not satisfied. Require ' + expected + ', version installed: ' + actual);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detect directories:
|
// Detect directories:
|
||||||
// * gruntFilePath The real path on disk to this Gruntfile.js
|
// * gruntFilePath The real path on disk to this Gruntfile.js
|
||||||
|
@ -171,8 +153,19 @@ module.exports = function(grunt) {
|
||||||
const relativeCwd = path.relative(gruntFilePath, cwd);
|
const relativeCwd = path.relative(gruntFilePath, cwd);
|
||||||
const componentDirectory = ComponentList.getOwningComponentDirectory(relativeCwd);
|
const componentDirectory = ComponentList.getOwningComponentDirectory(relativeCwd);
|
||||||
const inComponent = !!componentDirectory;
|
const inComponent = !!componentDirectory;
|
||||||
|
const inTheme = !!componentDirectory && componentDirectory.startsWith('theme/');
|
||||||
const runDir = inComponent ? componentDirectory : relativeCwd;
|
const runDir = inComponent ? componentDirectory : relativeCwd;
|
||||||
const fullRunDir = fs.realpathSync(gruntFilePath + path.sep + runDir);
|
const fullRunDir = fs.realpathSync(gruntFilePath + path.sep + runDir);
|
||||||
|
const {inAMD, amdSrc} = getAmdConfiguration();
|
||||||
|
const {yuiSrc} = getYuiConfiguration();
|
||||||
|
const {cssSrc, scssSrc} = getStyleConfiguration();
|
||||||
|
|
||||||
|
let files = null;
|
||||||
|
if (grunt.option('files')) {
|
||||||
|
// Accept a comma separated list of files to process.
|
||||||
|
files = grunt.option('files').split(',');
|
||||||
|
}
|
||||||
|
|
||||||
grunt.log.debug('============================================================================');
|
grunt.log.debug('============================================================================');
|
||||||
grunt.log.debug(`= Node version: ${process.versions.node}`);
|
grunt.log.debug(`= Node version: ${process.versions.node}`);
|
||||||
grunt.log.debug(`= grunt version: ${grunt.package.version}`);
|
grunt.log.debug(`= grunt version: ${grunt.package.version}`);
|
||||||
|
@ -192,667 +185,77 @@ module.exports = function(grunt) {
|
||||||
grunt.log.ok(`Running tasks for component directory ${componentDirectory}`);
|
grunt.log.ok(`Running tasks for component directory ${componentDirectory}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let files = null;
|
return {
|
||||||
if (grunt.option('files')) {
|
amdSrc,
|
||||||
// Accept a comma separated list of files to process.
|
componentDirectory,
|
||||||
files = grunt.option('files').split(',');
|
cwd,
|
||||||
|
cssSrc,
|
||||||
|
files,
|
||||||
|
fullRunDir,
|
||||||
|
gruntFilePath,
|
||||||
|
inAMD,
|
||||||
|
inComponent,
|
||||||
|
inTheme,
|
||||||
|
relativeCwd,
|
||||||
|
runDir,
|
||||||
|
scssSrc,
|
||||||
|
yuiSrc,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify tha tthe current NodeJS version matches the required version in package.json.
|
||||||
|
*
|
||||||
|
* @param {Grunt} grunt
|
||||||
|
*/
|
||||||
|
const verifyNodeVersion = grunt => {
|
||||||
|
const semver = require('semver');
|
||||||
|
|
||||||
|
// Verify the node version is new enough.
|
||||||
|
var expected = semver.validRange(grunt.file.readJSON('package.json').engines.node);
|
||||||
|
var actual = semver.valid(process.version);
|
||||||
|
if (!semver.satisfies(actual, expected)) {
|
||||||
|
grunt.fail.fatal('Node version not satisfied. Require ' + expected + ', version installed: ' + actual);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// If the cwd is the amd directory in the current component then it will be empty.
|
/**
|
||||||
// If the cwd is a child of the component's AMD directory, the relative directory will not start with ..
|
* Grunt configuration.
|
||||||
const inAMD = !path.relative(`${componentDirectory}/amd`, cwd).startsWith('..');
|
*
|
||||||
|
* @param {Grunt} grunt
|
||||||
|
*/
|
||||||
|
module.exports = function(grunt) {
|
||||||
|
// Verify that the Node version meets our requirements.
|
||||||
|
verifyNodeVersion(grunt);
|
||||||
|
|
||||||
// Globbing pattern for matching all AMD JS source files.
|
// Setup the Moodle environemnt within the Grunt object.
|
||||||
let amdSrc = [];
|
grunt.moodleEnv = setupMoodleEnvironment(grunt);
|
||||||
if (inComponent) {
|
|
||||||
amdSrc.push(componentDirectory + "/amd/src/*.js");
|
|
||||||
amdSrc.push(componentDirectory + "/amd/src/**/*.js");
|
|
||||||
} else {
|
|
||||||
amdSrc = ComponentList.getAmdSrcGlobList();
|
|
||||||
}
|
|
||||||
|
|
||||||
let yuiSrc = [];
|
|
||||||
if (inComponent) {
|
|
||||||
yuiSrc.push(componentDirectory + "/yui/src/**/*.js");
|
|
||||||
} else {
|
|
||||||
yuiSrc = ComponentList.getYuiSrcGlobList(gruntFilePath + '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function to generate the destination for the uglify task
|
* Add the named task.
|
||||||
* (e.g. build/file.min.js). This function will be passed to
|
|
||||||
* the rename property of files array when building dynamically:
|
|
||||||
* http://gruntjs.com/configuring-tasks#building-the-files-object-dynamically
|
|
||||||
*
|
*
|
||||||
* @param {String} destPath the current destination
|
* @param {string} name
|
||||||
* @param {String} srcPath the matched src path
|
* @param {Grunt} grunt
|
||||||
* @return {String} The rewritten destination path.
|
|
||||||
*/
|
*/
|
||||||
var babelRename = function(destPath, srcPath) {
|
const addTask = (name, grunt) => {
|
||||||
destPath = srcPath.replace('src', 'build');
|
const path = require('path');
|
||||||
destPath = destPath.replace('.js', '.min.js');
|
const taskPath = path.resolve(`./.grunt/tasks/${name}.js`);
|
||||||
return destPath;
|
|
||||||
|
grunt.log.debug(`Including tasks for ${name} from ${taskPath}`);
|
||||||
|
|
||||||
|
require(path.resolve(`./.grunt/tasks/${name}.js`))(grunt);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Find thirdpartylibs.xml and generate an array of paths contained within
|
|
||||||
* them (used to generate ignore files and so on).
|
|
||||||
*
|
|
||||||
* @return {array} The list of thirdparty paths.
|
|
||||||
*/
|
|
||||||
var getThirdPartyPathsFromXML = function() {
|
|
||||||
const thirdpartyfiles = ComponentList.getThirdPartyLibsList(gruntFilePath + '/');
|
|
||||||
const libs = ['node_modules/', 'vendor/'];
|
|
||||||
|
|
||||||
thirdpartyfiles.forEach(function(file) {
|
// Add Moodle task configuration.
|
||||||
const dirname = path.dirname(file);
|
addTask('gherkinlint', grunt);
|
||||||
|
addTask('ignorefiles', grunt);
|
||||||
|
|
||||||
const doc = new DOMParser().parseFromString(grunt.file.read(file));
|
addTask('javascript', grunt);
|
||||||
const nodes = xpath.select("/libraries/library/location/text()", doc);
|
addTask('style', grunt);
|
||||||
|
|
||||||
nodes.forEach(function(node) {
|
addTask('watch', grunt);
|
||||||
let lib = path.posix.join(dirname, node.toString());
|
addTask('startup', grunt);
|
||||||
if (grunt.file.isDir(lib)) {
|
|
||||||
// Ensure trailing slash on dirs.
|
|
||||||
lib = lib.replace(/\/?$/, '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look for duplicate paths before adding to array.
|
|
||||||
if (libs.indexOf(lib) === -1) {
|
|
||||||
libs.push(lib);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return libs;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the list of feature files to pass to the gherkin linter.
|
|
||||||
*
|
|
||||||
* @returns {Array}
|
|
||||||
*/
|
|
||||||
const getGherkinLintTargets = () => {
|
|
||||||
if (files) {
|
|
||||||
// Specific files were requested. Only check these.
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inComponent) {
|
|
||||||
return [`${runDir}/tests/behat/*.feature`];
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['**/tests/behat/*.feature'];
|
|
||||||
};
|
|
||||||
|
|
||||||
// Project configuration.
|
|
||||||
grunt.initConfig({
|
|
||||||
eslint: {
|
|
||||||
// Even though warnings dont stop the build we don't display warnings by default because
|
|
||||||
// at this moment we've got too many core warnings.
|
|
||||||
// To display warnings call: grunt eslint --show-lint-warnings
|
|
||||||
// To fail on warnings call: grunt eslint --max-lint-warnings=0
|
|
||||||
// Also --max-lint-warnings=-1 can be used to display warnings but not fail.
|
|
||||||
options: {
|
|
||||||
quiet: (!grunt.option('show-lint-warnings')) && (typeof grunt.option('max-lint-warnings') === 'undefined'),
|
|
||||||
maxWarnings: ((typeof grunt.option('max-lint-warnings') !== 'undefined') ? grunt.option('max-lint-warnings') : -1)
|
|
||||||
},
|
|
||||||
amd: {src: files ? files : amdSrc},
|
|
||||||
// Check YUI module source files.
|
|
||||||
yui: {src: files ? files : yuiSrc},
|
|
||||||
},
|
|
||||||
babel: {
|
|
||||||
options: {
|
|
||||||
sourceMaps: true,
|
|
||||||
comments: false,
|
|
||||||
plugins: [
|
|
||||||
'transform-es2015-modules-amd-lazy',
|
|
||||||
'system-import-transformer',
|
|
||||||
// This plugin modifies the Babel transpiling for "export default"
|
|
||||||
// so that if it's used then only the exported value is returned
|
|
||||||
// by the generated AMD module.
|
|
||||||
//
|
|
||||||
// It also adds the Moodle plugin name to the AMD module definition
|
|
||||||
// so that it can be imported as expected in other modules.
|
|
||||||
path.resolve('babel-plugin-add-module-to-define.js'),
|
|
||||||
'@babel/plugin-syntax-dynamic-import',
|
|
||||||
'@babel/plugin-syntax-import-meta',
|
|
||||||
['@babel/plugin-proposal-class-properties', {'loose': false}],
|
|
||||||
'@babel/plugin-proposal-json-strings'
|
|
||||||
],
|
|
||||||
presets: [
|
|
||||||
['minify', {
|
|
||||||
// This minification plugin needs to be disabled because it breaks the
|
|
||||||
// source map generation and causes invalid source maps to be output.
|
|
||||||
simplify: false,
|
|
||||||
builtIns: false
|
|
||||||
}],
|
|
||||||
['@babel/preset-env', {
|
|
||||||
targets: {
|
|
||||||
browsers: [
|
|
||||||
">0.25%",
|
|
||||||
"last 2 versions",
|
|
||||||
"not ie <= 10",
|
|
||||||
"not op_mini all",
|
|
||||||
"not Opera > 0",
|
|
||||||
"not dead"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
modules: false,
|
|
||||||
useBuiltIns: false
|
|
||||||
}]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
dist: {
|
|
||||||
files: [{
|
|
||||||
expand: true,
|
|
||||||
src: files ? files : amdSrc,
|
|
||||||
rename: babelRename
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sass: {
|
|
||||||
dist: {
|
|
||||||
files: {
|
|
||||||
"theme/boost/style/moodle.css": "theme/boost/scss/preset/default.scss",
|
|
||||||
"theme/classic/style/moodle.css": "theme/classic/scss/classicgrunt.scss"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
implementation: sass,
|
|
||||||
includePaths: ["theme/boost/scss/", "theme/classic/scss/"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
options: {
|
|
||||||
nospawn: true // We need not to spawn so config can be changed dynamically.
|
|
||||||
},
|
|
||||||
amd: {
|
|
||||||
files: inComponent
|
|
||||||
? ['amd/src/*.js', 'amd/src/**/*.js']
|
|
||||||
: ['**/amd/src/**/*.js'],
|
|
||||||
tasks: ['amd']
|
|
||||||
},
|
|
||||||
boost: {
|
|
||||||
files: [inComponent ? 'scss/**/*.scss' : 'theme/boost/scss/**/*.scss'],
|
|
||||||
tasks: ['scss']
|
|
||||||
},
|
|
||||||
rawcss: {
|
|
||||||
files: [
|
|
||||||
'**/*.css',
|
|
||||||
],
|
|
||||||
excludes: [
|
|
||||||
'**/moodle.css',
|
|
||||||
'**/editor.css',
|
|
||||||
],
|
|
||||||
tasks: ['rawcss']
|
|
||||||
},
|
|
||||||
yui: {
|
|
||||||
files: inComponent
|
|
||||||
? ['yui/src/*.json', 'yui/src/**/*.js']
|
|
||||||
: ['**/yui/src/**/*.js'],
|
|
||||||
tasks: ['yui']
|
|
||||||
},
|
|
||||||
gherkinlint: {
|
|
||||||
files: [inComponent ? 'tests/behat/*.feature' : '**/tests/behat/*.feature'],
|
|
||||||
tasks: ['gherkinlint']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
shifter: {
|
|
||||||
options: {
|
|
||||||
recursive: true,
|
|
||||||
// Shifter takes a relative path.
|
|
||||||
paths: files ? files : [runDir]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
gherkinlint: {
|
|
||||||
options: {
|
|
||||||
files: getGherkinLintTargets(),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate ignore files (utilising thirdpartylibs.xml data)
|
|
||||||
*/
|
|
||||||
tasks.ignorefiles = function() {
|
|
||||||
// An array of paths to third party directories.
|
|
||||||
const thirdPartyPaths = getThirdPartyPathsFromXML();
|
|
||||||
// Generate .eslintignore.
|
|
||||||
const eslintIgnores = [
|
|
||||||
'# Generated by "grunt ignorefiles"',
|
|
||||||
'*/**/yui/src/*/meta/',
|
|
||||||
'*/**/build/',
|
|
||||||
].concat(thirdPartyPaths);
|
|
||||||
grunt.file.write('.eslintignore', eslintIgnores.join('\n'));
|
|
||||||
|
|
||||||
// Generate .stylelintignore.
|
|
||||||
const stylelintIgnores = [
|
|
||||||
'# Generated by "grunt ignorefiles"',
|
|
||||||
'**/yui/build/*',
|
|
||||||
'theme/boost/style/moodle.css',
|
|
||||||
'theme/classic/style/moodle.css',
|
|
||||||
].concat(thirdPartyPaths);
|
|
||||||
grunt.file.write('.stylelintignore', stylelintIgnores.join('\n'));
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shifter task. Is configured with a path to a specific file or a directory,
|
|
||||||
* in the case of a specific file it will work out the right module to be built.
|
|
||||||
*
|
|
||||||
* Note that this task runs the invidiaul shifter jobs async (becase it spawns
|
|
||||||
* so be careful to to call done().
|
|
||||||
*/
|
|
||||||
tasks.shifter = function() {
|
|
||||||
var done = this.async(),
|
|
||||||
options = grunt.config('shifter.options');
|
|
||||||
|
|
||||||
// Run the shifter processes one at a time to avoid confusing output.
|
|
||||||
async.eachSeries(options.paths, function(src, filedone) {
|
|
||||||
var args = [];
|
|
||||||
args.push(path.normalize(__dirname + '/node_modules/shifter/bin/shifter'));
|
|
||||||
|
|
||||||
// Always ignore the node_modules directory.
|
|
||||||
args.push('--excludes', 'node_modules');
|
|
||||||
|
|
||||||
// Determine the most appropriate options to run with based upon the current location.
|
|
||||||
if (grunt.file.isMatch('**/yui/**/*.js', src)) {
|
|
||||||
// When passed a JS file, build our containing module (this happen with
|
|
||||||
// watch).
|
|
||||||
grunt.log.debug('Shifter passed a specific JS file');
|
|
||||||
src = path.dirname(path.dirname(src));
|
|
||||||
options.recursive = false;
|
|
||||||
} else if (grunt.file.isMatch('**/yui/src', src)) {
|
|
||||||
// When in a src directory --walk all modules.
|
|
||||||
grunt.log.debug('In a src directory');
|
|
||||||
args.push('--walk');
|
|
||||||
options.recursive = false;
|
|
||||||
} else if (grunt.file.isMatch('**/yui/src/*', src)) {
|
|
||||||
// When in module, only build our module.
|
|
||||||
grunt.log.debug('In a module directory');
|
|
||||||
options.recursive = false;
|
|
||||||
} else if (grunt.file.isMatch('**/yui/src/*/js', src)) {
|
|
||||||
// When in module src, only build our module.
|
|
||||||
grunt.log.debug('In a source directory');
|
|
||||||
src = path.dirname(src);
|
|
||||||
options.recursive = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (grunt.option('watch')) {
|
|
||||||
grunt.fail.fatal('The --watch option has been removed, please use `grunt watch` instead');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the stderr option if appropriate
|
|
||||||
if (grunt.option('verbose')) {
|
|
||||||
args.push('--lint-stderr');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (grunt.option('no-color')) {
|
|
||||||
args.push('--color=false');
|
|
||||||
}
|
|
||||||
|
|
||||||
var execShifter = function() {
|
|
||||||
|
|
||||||
grunt.log.ok("Running shifter on " + src);
|
|
||||||
grunt.util.spawn({
|
|
||||||
cmd: "node",
|
|
||||||
args: args,
|
|
||||||
opts: {cwd: src, stdio: 'inherit', env: process.env}
|
|
||||||
}, function(error, result, code) {
|
|
||||||
if (code) {
|
|
||||||
grunt.fail.fatal('Shifter failed with code: ' + code);
|
|
||||||
} else {
|
|
||||||
grunt.log.ok('Shifter build complete.');
|
|
||||||
filedone();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Actually run shifter.
|
|
||||||
if (!options.recursive) {
|
|
||||||
execShifter();
|
|
||||||
} else {
|
|
||||||
// Check that there are yui modules otherwise shifter ends with exit code 1.
|
|
||||||
if (grunt.file.expand({cwd: src}, '**/yui/src/**/*.js').length > 0) {
|
|
||||||
args.push('--recursive');
|
|
||||||
execShifter();
|
|
||||||
} else {
|
|
||||||
grunt.log.ok('No YUI modules to build.');
|
|
||||||
filedone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, done);
|
|
||||||
};
|
|
||||||
|
|
||||||
tasks.gherkinlint = function() {
|
|
||||||
const done = this.async();
|
|
||||||
const options = grunt.config('gherkinlint.options');
|
|
||||||
|
|
||||||
// Grab the gherkin-lint linter and required scaffolding.
|
|
||||||
const linter = require('gherkin-lint/dist/linter.js');
|
|
||||||
const featureFinder = require('gherkin-lint/dist/feature-finder.js');
|
|
||||||
const configParser = require('gherkin-lint/dist/config-parser.js');
|
|
||||||
const formatter = require('gherkin-lint/dist/formatters/stylish.js');
|
|
||||||
|
|
||||||
// Run the linter.
|
|
||||||
return linter.lint(
|
|
||||||
featureFinder.getFeatureFiles(grunt.file.expand(options.files)),
|
|
||||||
configParser.getConfiguration(configParser.defaultConfigFileName)
|
|
||||||
)
|
|
||||||
.then(results => {
|
|
||||||
// Print the results out uncondtionally.
|
|
||||||
formatter.printResults(results);
|
|
||||||
|
|
||||||
return results;
|
|
||||||
})
|
|
||||||
.then(results => {
|
|
||||||
// Report on the results.
|
|
||||||
// The done function takes a bool whereby a falsey statement causes the task to fail.
|
|
||||||
return results.every(result => result.errors.length === 0);
|
|
||||||
})
|
|
||||||
.then(done); // eslint-disable-line promise/no-callback-in-promise
|
|
||||||
};
|
|
||||||
|
|
||||||
tasks.startup = function() {
|
|
||||||
// Are we in a YUI directory?
|
|
||||||
if (path.basename(path.resolve(cwd, '../../')) == 'yui') {
|
|
||||||
grunt.task.run('yui');
|
|
||||||
// Are we in an AMD directory?
|
|
||||||
} else if (inAMD) {
|
|
||||||
grunt.task.run('amd');
|
|
||||||
} else {
|
|
||||||
// Run them all!.
|
|
||||||
grunt.task.run('css');
|
|
||||||
grunt.task.run('js');
|
|
||||||
grunt.task.run('gherkinlint');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a wrapper task to handle the grunt watch command. It attempts to use
|
|
||||||
* Watchman to monitor for file changes, if it's installed, because it's much faster.
|
|
||||||
*
|
|
||||||
* If Watchman isn't installed then it falls back to the grunt-contrib-watch file
|
|
||||||
* watcher for backwards compatibility.
|
|
||||||
*/
|
|
||||||
tasks.watch = function() {
|
|
||||||
var watchTaskDone = this.async();
|
|
||||||
var watchInitialised = false;
|
|
||||||
var watchTaskQueue = {};
|
|
||||||
var processingQueue = false;
|
|
||||||
|
|
||||||
// Grab the tasks and files that have been queued up and execute them.
|
|
||||||
var processWatchTaskQueue = function() {
|
|
||||||
if (!Object.keys(watchTaskQueue).length || processingQueue) {
|
|
||||||
// If there is nothing in the queue or we're already processing then wait.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
processingQueue = true;
|
|
||||||
|
|
||||||
// Grab all tasks currently in the queue.
|
|
||||||
var queueToProcess = watchTaskQueue;
|
|
||||||
// Reset the queue.
|
|
||||||
watchTaskQueue = {};
|
|
||||||
|
|
||||||
async.forEachSeries(
|
|
||||||
Object.keys(queueToProcess),
|
|
||||||
function(task, next) {
|
|
||||||
var files = queueToProcess[task];
|
|
||||||
var filesOption = '--files=' + files.join(',');
|
|
||||||
grunt.log.ok('Running task ' + task + ' for files ' + filesOption);
|
|
||||||
|
|
||||||
// Spawn the task in a child process so that it doesn't kill this one
|
|
||||||
// if it failed.
|
|
||||||
grunt.util.spawn(
|
|
||||||
{
|
|
||||||
// Spawn with the grunt bin.
|
|
||||||
grunt: true,
|
|
||||||
// Run from current working dir and inherit stdio from process.
|
|
||||||
opts: {
|
|
||||||
cwd: fullRunDir,
|
|
||||||
stdio: 'inherit'
|
|
||||||
},
|
|
||||||
args: [task, filesOption]
|
|
||||||
},
|
|
||||||
function(err, res, code) {
|
|
||||||
if (code !== 0) {
|
|
||||||
// The grunt task failed.
|
|
||||||
grunt.log.error(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move on to the next task.
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
function() {
|
|
||||||
// No longer processing.
|
|
||||||
processingQueue = false;
|
|
||||||
// Once all of the tasks are done then recurse just in case more tasks
|
|
||||||
// were queued while we were processing.
|
|
||||||
processWatchTaskQueue();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const originalWatchConfig = grunt.config.get(['watch']);
|
|
||||||
const watchConfig = Object.keys(originalWatchConfig).reduce(function(carry, key) {
|
|
||||||
if (key == 'options') {
|
|
||||||
return carry;
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = originalWatchConfig[key];
|
|
||||||
|
|
||||||
const taskNames = value.tasks;
|
|
||||||
const files = value.files;
|
|
||||||
let excludes = [];
|
|
||||||
if (value.excludes) {
|
|
||||||
excludes = value.excludes;
|
|
||||||
}
|
|
||||||
|
|
||||||
taskNames.forEach(function(taskName) {
|
|
||||||
carry[taskName] = {
|
|
||||||
files,
|
|
||||||
excludes,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return carry;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
watchmanClient.on('error', function(error) {
|
|
||||||
// We have to add an error handler here and parse the error string because the
|
|
||||||
// example way from the docs to check if Watchman is installed doesn't actually work!!
|
|
||||||
// See: https://github.com/facebook/watchman/issues/509
|
|
||||||
if (error.message.match('Watchman was not found')) {
|
|
||||||
// If watchman isn't installed then we should fallback to the other watch task.
|
|
||||||
grunt.log.ok('It is recommended that you install Watchman for better performance using the "watch" command.');
|
|
||||||
|
|
||||||
// Fallback to the old grunt-contrib-watch task.
|
|
||||||
grunt.renameTask('watch-grunt', 'watch');
|
|
||||||
grunt.task.run(['watch']);
|
|
||||||
// This task is finished.
|
|
||||||
watchTaskDone(0);
|
|
||||||
} else {
|
|
||||||
grunt.log.error(error);
|
|
||||||
// Fatal error.
|
|
||||||
watchTaskDone(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
watchmanClient.on('subscription', function(resp) {
|
|
||||||
if (resp.subscription !== 'grunt-watch') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.files.forEach(function(file) {
|
|
||||||
grunt.log.ok('File changed: ' + file.name);
|
|
||||||
|
|
||||||
var fullPath = fullRunDir + '/' + file.name;
|
|
||||||
Object.keys(watchConfig).forEach(function(task) {
|
|
||||||
|
|
||||||
const fileGlobs = watchConfig[task].files;
|
|
||||||
var match = fileGlobs.some(function(fileGlob) {
|
|
||||||
return grunt.file.isMatch(`**/${fileGlob}`, fullPath);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
// If we are watching a subdirectory then the file.name will be relative
|
|
||||||
// to that directory. However the grunt tasks expect the file paths to be
|
|
||||||
// relative to the Gruntfile.js location so let's normalise them before
|
|
||||||
// adding them to the queue.
|
|
||||||
var relativePath = fullPath.replace(gruntFilePath + '/', '');
|
|
||||||
if (task in watchTaskQueue) {
|
|
||||||
if (!watchTaskQueue[task].includes(relativePath)) {
|
|
||||||
watchTaskQueue[task] = watchTaskQueue[task].concat(relativePath);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
watchTaskQueue[task] = [relativePath];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
processWatchTaskQueue();
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on('SIGINT', function() {
|
|
||||||
// Let the user know that they may need to manually stop the Watchman daemon if they
|
|
||||||
// no longer want it running.
|
|
||||||
if (watchInitialised) {
|
|
||||||
grunt.log.ok('The Watchman daemon may still be running and may need to be stopped manually.');
|
|
||||||
}
|
|
||||||
|
|
||||||
process.exit();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initiate the watch on the current directory.
|
|
||||||
watchmanClient.command(['watch-project', fullRunDir], function(watchError, watchResponse) {
|
|
||||||
if (watchError) {
|
|
||||||
grunt.log.error('Error initiating watch:', watchError);
|
|
||||||
watchTaskDone(1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('warning' in watchResponse) {
|
|
||||||
grunt.log.error('warning: ', watchResponse.warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
var watch = watchResponse.watch;
|
|
||||||
var relativePath = watchResponse.relative_path;
|
|
||||||
watchInitialised = true;
|
|
||||||
|
|
||||||
watchmanClient.command(['clock', watch], function(clockError, clockResponse) {
|
|
||||||
if (clockError) {
|
|
||||||
grunt.log.error('Failed to query clock:', clockError);
|
|
||||||
watchTaskDone(1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate the expression query used by watchman.
|
|
||||||
// Documentation is limited, but see https://facebook.github.io/watchman/docs/expr/allof.html for examples.
|
|
||||||
// We generate an expression to match any value in the files list of all of our tasks, but excluding
|
|
||||||
// all value in the excludes list of that task.
|
|
||||||
//
|
|
||||||
// [anyof, [
|
|
||||||
// [allof, [
|
|
||||||
// [anyof, [
|
|
||||||
// ['match', validPath, 'wholename'],
|
|
||||||
// ['match', validPath, 'wholename'],
|
|
||||||
// ],
|
|
||||||
// [not,
|
|
||||||
// [anyof, [
|
|
||||||
// ['match', invalidPath, 'wholename'],
|
|
||||||
// ['match', invalidPath, 'wholename'],
|
|
||||||
// ],
|
|
||||||
// ],
|
|
||||||
// ],
|
|
||||||
var matchWholeName = fileGlob => ['match', fileGlob, 'wholename'];
|
|
||||||
var matches = Object.keys(watchConfig).map(function(task) {
|
|
||||||
const matchAll = [];
|
|
||||||
matchAll.push(['anyof'].concat(watchConfig[task].files.map(matchWholeName)));
|
|
||||||
|
|
||||||
if (watchConfig[task].excludes.length) {
|
|
||||||
matchAll.push(['not', ['anyof'].concat(watchConfig[task].excludes.map(matchWholeName))]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['allof'].concat(matchAll);
|
|
||||||
});
|
|
||||||
|
|
||||||
matches = ['anyof'].concat(matches);
|
|
||||||
|
|
||||||
var sub = {
|
|
||||||
expression: matches,
|
|
||||||
// Which fields we're interested in.
|
|
||||||
fields: ["name", "size", "type"],
|
|
||||||
// Add our time constraint.
|
|
||||||
since: clockResponse.clock
|
|
||||||
};
|
|
||||||
|
|
||||||
if (relativePath) {
|
|
||||||
/* eslint-disable camelcase */
|
|
||||||
sub.relative_root = relativePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
watchmanClient.command(['subscribe', watch, 'grunt-watch', sub], function(subscribeError) {
|
|
||||||
if (subscribeError) {
|
|
||||||
// Probably an error in the subscription criteria.
|
|
||||||
grunt.log.error('failed to subscribe: ', subscribeError);
|
|
||||||
watchTaskDone(1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
grunt.log.ok('Listening for changes to files in ' + fullRunDir);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// On watch, we dynamically modify config to build only affected files. This
|
|
||||||
// method is slightly complicated to deal with multiple changed files at once (copied
|
|
||||||
// from the grunt-contrib-watch readme).
|
|
||||||
var changedFiles = Object.create(null);
|
|
||||||
var onChange = grunt.util._.debounce(function() {
|
|
||||||
var files = Object.keys(changedFiles);
|
|
||||||
grunt.config('eslint.amd.src', files);
|
|
||||||
grunt.config('eslint.yui.src', files);
|
|
||||||
grunt.config('shifter.options.paths', files);
|
|
||||||
grunt.config('gherkinlint.options.files', files);
|
|
||||||
grunt.config('babel.dist.files', [{expand: true, src: files, rename: babelRename}]);
|
|
||||||
changedFiles = Object.create(null);
|
|
||||||
}, 200);
|
|
||||||
|
|
||||||
grunt.event.on('watch', function(action, filepath) {
|
|
||||||
changedFiles[filepath] = action;
|
|
||||||
onChange();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Register NPM tasks.
|
|
||||||
grunt.loadNpmTasks('grunt-contrib-uglify');
|
|
||||||
grunt.loadNpmTasks('grunt-contrib-watch');
|
|
||||||
grunt.loadNpmTasks('grunt-sass');
|
|
||||||
grunt.loadNpmTasks('grunt-eslint');
|
|
||||||
grunt.loadNpmTasks('grunt-stylelint');
|
|
||||||
grunt.loadNpmTasks('grunt-babel');
|
|
||||||
|
|
||||||
// Rename the grunt-contrib-watch "watch" task because we're going to wrap it.
|
|
||||||
grunt.renameTask('watch', 'watch-grunt');
|
|
||||||
|
|
||||||
// Register JS tasks.
|
|
||||||
grunt.registerTask('shifter', 'Run Shifter against the current directory', tasks.shifter);
|
|
||||||
grunt.registerTask('gherkinlint', 'Run gherkinlint against the current directory', tasks.gherkinlint);
|
|
||||||
grunt.registerTask('ignorefiles', 'Generate ignore files for linters', tasks.ignorefiles);
|
|
||||||
grunt.registerTask('watch', 'Run tasks on file changes', tasks.watch);
|
|
||||||
grunt.registerTask('yui', ['eslint:yui', 'shifter']);
|
|
||||||
grunt.registerTask('amd', ['eslint:amd', 'babel']);
|
|
||||||
grunt.registerTask('js', ['amd', 'yui']);
|
|
||||||
|
|
||||||
// Register CSS tasks.
|
|
||||||
registerStyleLintTasks(grunt, files, fullRunDir);
|
|
||||||
|
|
||||||
// Register the startup task.
|
|
||||||
grunt.registerTask('startup', 'Run the correct tasks for the current directory', tasks.startup);
|
|
||||||
|
|
||||||
// Register the default task.
|
// Register the default task.
|
||||||
grunt.registerTask('default', ['startup']);
|
grunt.registerTask('default', ['startup']);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue