Merge branch 'MDL-77275-master' of https://github.com/laurentdavid/moodle
[moodle.git] / .grunt / tasks / javascript.js
blobb22abaa73b1dc7e4dcb8f09da7c242b52d24db80
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.
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 */
18 /**
19  * @copyright  2021 Andrew Nicols
20  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
21  */
23 /**
24  * Function to generate the destination for the uglify task
25  * (e.g. build/file.min.js). This function will be passed to
26  * the rename property of files array when building dynamically:
27  * http://gruntjs.com/configuring-tasks#building-the-files-object-dynamically
28  *
29  * @param {String} destPath the current destination
30  * @param {String} srcPath the  matched src path
31  * @return {String} The rewritten destination path.
32  */
34 const babelRename = function(destPath, srcPath) {
35     destPath = srcPath.replace('src', 'build');
36     destPath = destPath.replace('.js', '.min.js');
37     return destPath;
40 module.exports = grunt => {
41     // Load the Ignorefiles tasks.
42     require('./ignorefiles')(grunt);
44     // Load the Shifter tasks.
45     require('./shifter')(grunt);
47     // Load ESLint.
48     require('./eslint')(grunt);
50     // Load jsconfig.
51     require('./jsconfig')(grunt);
53     // Load JSDoc.
54     require('./jsdoc')(grunt);
56     const path = require('path');
58     // Register JS tasks.
59     grunt.registerTask('yui', ['eslint:yui', 'shifter']);
60     grunt.registerTask('amd', ['ignorefiles', 'eslint:amd', 'rollup']);
61     grunt.registerTask('js', ['amd', 'yui']);
63     // Register NPM tasks.
64     grunt.loadNpmTasks('grunt-contrib-uglify');
65     grunt.loadNpmTasks('grunt-contrib-watch');
66     grunt.loadNpmTasks('grunt-rollup');
68     const babelTransform = require('@babel/core').transform;
69     const babel = (options = {}) => {
70         return {
71             name: 'babel',
73             transform: (code, id) => {
74                 grunt.log.debug(`Transforming ${id}`);
75                 options.filename = id;
76                 const transformed = babelTransform(code, options);
78                 return {
79                     code: transformed.code,
80                     map: transformed.map
81                 };
82             }
83         };
84     };
86     // Note: We have to use a rate limit plugin here because rollup runs all tasks asynchronously and in parallel.
87     // When we kick off a full run, if we kick off a rollup of every file this will fork-bomb the machine.
88     // To work around this we use a concurrent Promise queue based on the number of available processors.
89     const rateLimit = () => {
90         const queue = [];
91         let queueRunner;
93         const startQueue = () => {
94             if (queueRunner) {
95                 return;
96             }
98             queueRunner = setTimeout(() => {
99                 const limit = Math.max(1, require('os').cpus().length / 2);
100                 grunt.log.debug(`Starting rollup with queue size of ${limit}`);
101                 runQueue(limit);
102             }, 100);
103         };
105         // The queue runner will run the next `size` items in the queue.
106         const runQueue = (size = 1) => {
107             queue.splice(0, size).forEach(resolve => {
108                 resolve();
109             });
110         };
112         return {
113             name: 'ratelimit',
115             // The options hook is run in parallel.
116             // We can return an unresolved Promise which is queued for later resolution.
117             options: async() => {
118                 return new Promise(resolve => {
119                     queue.push(resolve);
120                     startQueue();
121                 });
122             },
124             // When an item in the queue completes, start the next item in the queue.
125             buildEnd: () => {
126                 runQueue();
127             },
128         };
129     };
131     const terser = require('rollup-plugin-terser').terser;
132     grunt.config.merge({
133         rollup: {
134             options: {
135                 format: 'esm',
136                 dir: 'output',
137                 sourcemap: true,
138                 treeshake: false,
139                 context: 'window',
140                 plugins: [
141                     rateLimit(),
142                     babel({
143                         sourceMaps: true,
144                         comments: false,
145                         compact: false,
146                         plugins: [
147                             'transform-es2015-modules-amd-lazy',
148                             'system-import-transformer',
149                             // This plugin modifies the Babel transpiling for "export default"
150                             // so that if it's used then only the exported value is returned
151                             // by the generated AMD module.
152                             //
153                             // It also adds the Moodle plugin name to the AMD module definition
154                             // so that it can be imported as expected in other modules.
155                             path.resolve('.grunt/babel-plugin-add-module-to-define.js'),
156                             '@babel/plugin-syntax-dynamic-import',
157                             '@babel/plugin-syntax-import-meta',
158                             ['@babel/plugin-proposal-class-properties', {'loose': false}],
159                             '@babel/plugin-proposal-json-strings'
160                         ],
161                         presets: [
162                             ['@babel/preset-env', {
163                                 targets: {
164                                     browsers: [
165                                         ">0.3%",
166                                         "last 2 versions",
167                                         "not ie >= 0",
168                                         "not op_mini all",
169                                         "not Opera > 0",
170                                         "not dead"
171                                     ]
172                                 },
173                                 modules: false,
174                                 useBuiltIns: false
175                             }]
176                         ]
177                     }),
179                     terser({
180                         // Do not mangle variables.
181                         // Makes debugging easier.
182                         mangle: false,
183                     }),
184                 ],
185             },
186             dist: {
187                 files: [{
188                     expand: true,
189                     src: grunt.moodleEnv.files ? grunt.moodleEnv.files : grunt.moodleEnv.amdSrc,
190                     rename: babelRename
191                 }],
192             },
193         },
194     });
196     grunt.config.merge({
197         watch: {
198             amd: {
199                 files: grunt.moodleEnv.inComponent
200                     ? ['amd/src/*.js', 'amd/src/**/*.js']
201                     : ['**/amd/src/**/*.js'],
202                 tasks: ['amd']
203             },
204         },
205     });
207     // Add the 'js' task as a startup task.
208     grunt.moodleEnv.startupTasks.push('js');
210     // On watch, we dynamically modify config to build only affected files. This
211     // method is slightly complicated to deal with multiple changed files at once (copied
212     // from the grunt-contrib-watch readme).
213     let changedFiles = Object.create(null);
214     const onChange = grunt.util._.debounce(function() {
215         const files = Object.keys(changedFiles);
216         grunt.config('rollup.dist.files', [{expand: true, src: files, rename: babelRename}]);
217         changedFiles = Object.create(null);
218     }, 200);
220     grunt.event.on('watch', function(action, filepath) {
221         changedFiles[filepath] = action;
222         onChange();
223     });
225     return {
226         babelRename,
227     };