ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç components.js000064400000034743152265256020007307 0ustar00// 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 . /** * Helper functions for working with Moodle component names, directories, and sources. * * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ "use strict"; /* eslint-env node */ /** @var {Object} A list of subsystems in Moodle */ const componentData = {}; /** * Load details of all moodle modules. * * @returns {object} */ const fetchComponentData = () => { const fs = require('fs'); const path = require('path'); const glob = require('glob'); const gruntFilePath = process.cwd(); if (!Object.entries(componentData).length) { componentData.subsystems = {}; componentData.pathList = []; componentData.components = {}; componentData.standardComponents = {}; // Fetch the component definiitions from the distributed JSON file. const components = JSON.parse(fs.readFileSync(`${gruntFilePath}/lib/components.json`)); const pluginData = JSON.parse(fs.readFileSync(`${gruntFilePath}/lib/plugins.json`)); componentData.pluginTypes = components.plugintypes; const standardPlugins = Object.entries(pluginData.standard).map( ([pluginType, pluginNames]) => { return pluginNames.map(pluginName => `${pluginType}_${pluginName}`); } ).reduce((acc, val) => acc.concat(val), []); // Build the list of moodle subsystems. componentData.subsystems.lib = 'core'; componentData.pathList.push(process.cwd() + path.sep + 'lib'); for (const [component, thisPath] of Object.entries(components.subsystems)) { if (thisPath) { // Prefix "core_" to the front of the subsystems. componentData.subsystems[thisPath] = `core_${component}`; componentData.pathList.push(process.cwd() + path.sep + thisPath); } } // The list of components includes the list of subsystems. componentData.components = {...componentData.subsystems}; const subpluginAdder = (subpluginType, subpluginTypePath) => { glob.sync(`${subpluginTypePath}/*/version.php`).forEach(versionPath => { const componentPath = fs.realpathSync(path.dirname(versionPath)); const componentName = path.basename(componentPath); const frankenstyleName = `${subpluginType}_${componentName}`; componentData.components[`${subpluginTypePath}/${componentName}`] = frankenstyleName; componentData.pathList.push(componentPath); }); }; // Go through each of the plugintypes. Object.entries(components.plugintypes).forEach(([pluginType, pluginTypePath]) => { // We don't allow any code in this place..? glob.sync(`${pluginTypePath}/*/version.php`).forEach(versionPath => { const componentPath = fs.realpathSync(path.dirname(versionPath)); const componentName = path.basename(componentPath); const frankenstyleName = `${pluginType}_${componentName}`; componentData.components[`${pluginTypePath}/${componentName}`] = frankenstyleName; componentData.pathList.push(componentPath); // Look for any subplugins. const subPluginConfigurationFile = `${componentPath}/db/subplugins.json`; if (fs.existsSync(subPluginConfigurationFile)) { const subpluginList = JSON.parse(fs.readFileSync(fs.realpathSync(subPluginConfigurationFile))); if (subpluginList.subplugintypes) { Object.entries(subpluginList.subplugintypes).forEach(([subpluginType, subpluginTypePath]) => { subpluginAdder( subpluginType, `${pluginTypePath}/${componentName}/${subpluginTypePath}` ); }); } else if (subpluginList.plugintypes) { Object.entries(subpluginList.plugintypes).forEach(([subpluginType, subpluginTypePath]) => { subpluginAdder(subpluginType, subpluginTypePath); }); } } }); }); // Create a list of the standard subsystem and plugins. componentData.standardComponents = Object.fromEntries( Object.entries(componentData.components).filter(([, name]) => { if (name === 'core' || name.startsWith('core_')) { return true; } return standardPlugins.indexOf(name) !== -1; }) ); componentData.componentMapping = Object.fromEntries( Object.entries(componentData.components).map(([path, name]) => [name, path]) ); } 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. * * @returns {Array} */ const getAmdSrcGlobList = () => { const globList = []; fetchComponentData().pathList.forEach(componentPath => { globList.push(`${componentPath}/amd/src/*.js`); globList.push(`${componentPath}/amd/src/**/*.js`); }); return globList; }; /** * Get the list of paths to build YUI sources. * * @param {String} relativeTo * @returns {Array} */ const getYuiSrcGlobList = relativeTo => { const globList = []; fetchComponentData().pathList.forEach(componentPath => { const relativeComponentPath = componentPath.replace(relativeTo, ''); globList.push(`${relativeComponentPath}/yui/src/**/*.js`); }); return globList; }; /** * Get the list of paths to thirdpartylibs.xml. * * @param {String} relativeTo * @returns {Array} */ const getThirdPartyLibsList = relativeTo => { const fs = require('fs'); const path = require('path'); return fetchComponentData().pathList .map(componentPath => path.relative(relativeTo, componentPath) + '/thirdpartylibs.xml') .map(componentPath => componentPath.replace(/\\/g, '/')) .filter(path => fs.existsSync(path)) .sort(); }; /** * Get the list of thirdparty library paths. * * @returns {array} */ const getThirdPartyPaths = () => { const DOMParser = require('@xmldom/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. * * @param {String} path * @returns {String|null} Name of matching component. */ const getComponentFromPath = path => { const componentList = fetchComponentData().components; if (componentList.hasOwnProperty(path)) { return componentList[path]; } return null; }; /** * Check whether the supplied path, relative to the Gruntfile.js, is in a known component. * * @param {String} checkPath The path to check. This can be with either Windows, or Linux directory separators. * @returns {String|null} */ const getOwningComponentDirectory = checkPath => { const path = require('path'); // Fetch all components into a reverse sorted array. // This ensures that components which are within the directory of another component match first. const pathList = Object.keys(fetchComponentData().components).sort().reverse(); for (const componentPath of pathList) { // If the componentPath is the directory being checked, it will be empty. // If the componentPath is a parent of the directory being checked, the relative directory will not start with .. if (!path.relative(componentPath, checkPath).startsWith('..')) { return componentPath; } } return null; }; /** * Get the latest tag in a remote GitHub repository. * * @param {string} url The remote repository. * @returns {Array} */ const getRepositoryTags = async(url) => { const gtr = require('git-tags-remote'); try { const tags = await gtr.get(url); if (tags !== undefined) { return tags; } } catch (error) { return []; } return []; }; /** * Get the list of thirdparty libraries that could be upgraded. * * @returns {Array} */ const getThirdPartyLibsUpgradable = async() => { const libraries = getThirdPartyLibsData().filter((library) => !!library.repository); const upgradableLibraries = []; const versionCompare = (a, b) => { if (a === b) { return 0; } const aParts = a.split('.'); const bParts = b.split('.'); for (let i = 0; i < Math.min(aParts.length, bParts.length); i++) { const aPart = parseInt(aParts[i], 10); const bPart = parseInt(bParts[i], 10); if (aPart > bPart) { // 1.1.0 > 1.0.9 return 1; } else if (aPart < bPart) { // 1.0.9 < 1.1.0 return -1; } else { // Same version. continue; } } if (aParts.length > bParts.length) { // 1.0.1 > 1.0 return 1; } // 1.0 < 1.0.1 return -1; }; for (let library of libraries) { upgradableLibraries.push( getRepositoryTags(library.repository).then((tagMap) => { library.version = library.version.replace(/^v/, ''); const currentVersion = library.version.replace(/moodle-/, ''); const currentMajorVersion = library.version.split('.')[0]; const tags = [...tagMap] .map((tagData) => tagData[0]) .filter((tag) => !tag.match(/(alpha|beta|rc)/)) .map((tag) => tag.replace(/^v/, '')) .sort((a, b) => versionCompare(b, a)); if (!tags.length) { library.warning = "Unable to find any comparable tags."; return library; } library.latestVersion = tags[0]; tags.some((tag) => { if (!tag) { return false; } // See if the version part matches. const majorVersion = tag.split('.')[0]; if (majorVersion === currentMajorVersion) { library.latestSameMajorVersion = tag; return true; } return false; }); if (versionCompare(currentVersion, library.latestVersion) > 0) { // Moodle somehow has a newer version than the latest version. library.warning = `Newer version found: ${currentVersion} > ${library.latestVersion} for ${library.name}`; return library; } if (library.version !== library.latestVersion) { // Delete version and add it again at the end of the array. That way, current and new will stay closer. delete library.version; library.version = currentVersion; return library; } return null; }) ); } return (await Promise.all(upgradableLibraries)).filter((library) => !!library); }; /** * Get the list of thirdparty libraries. * * @returns {Array} */ const getThirdPartyLibsData = () => { const DOMParser = require('@xmldom/xmldom').DOMParser; const fs = require('fs'); const xpath = require('xpath'); const path = require('path'); const libraryList = []; const libraryFields = [ 'location', 'name', 'version', 'repository', ]; const thirdpartyfiles = getThirdPartyLibsList(fs.realpathSync('./')); thirdpartyfiles.forEach(function(libraryPath) { const xmlContent = fs.readFileSync(libraryPath, 'utf8'); const doc = new DOMParser().parseFromString(xmlContent); const libraries = xpath.select("/libraries/library", doc); for (const library of libraries) { const libraryData = []; for (const field of libraryFields) { libraryData[field] = xpath.select(`${field}/text()`, library)?.toString(); } libraryData.location = path.join(path.dirname(libraryPath), libraryData.location); libraryList.push(libraryData); } }); return libraryList.sort((a, b) => a.location.localeCompare(b.location)); }; module.exports = { fetchComponentData, getAmdSrcGlobList, getComponentFromPath, getComponentPaths, getOwningComponentDirectory, getYuiSrcGlobList, getThirdPartyLibsList, getThirdPartyPaths, getThirdPartyLibsUpgradable, }; babel-plugin-add-module-to-define.js000064400000020063152265256020013332 0ustar00// 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 . /** * This is a babel plugin to add the Moodle module names to the AMD modules * as part of the transpiling process. * * In addition it will also add a return statement for the default export if the * module is using default exports. This is a highly specific Moodle thing because * we're transpiling to AMD and none of the existing Babel 7 plugins work correctly. * * This will fix the issue where an ES6 module using "export default Foo" will be * transpiled into an AMD module that returns {default: Foo}; Instead it will now * just simply return Foo. * * Note: This means all other named exports in that module are ignored and won't be * exported. * * @copyright 2018 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ "use strict"; /* eslint-env node */ module.exports = ({template, types}) => { const fs = require('fs'); const path = require('path'); const cwd = process.cwd(); const ComponentList = require(path.join(process.cwd(), '.grunt', 'components.js')); /** * Search the list of components that match the given file name * and return the Moodle component for that file, if found. * * Throw an exception if no matching component is found. * * @throws {Error} * @param {string} searchFileName The file name to look for. * @return {string} Moodle component */ function getModuleNameFromFileName(searchFileName) { searchFileName = fs.realpathSync(searchFileName); const relativeFileName = searchFileName.replace(`${cwd}${path.sep}`, '').replace(/\\/g, '/'); const [componentPath, file] = relativeFileName.split('/amd/src/'); const fileName = file.replace('.js', ''); // Check subsystems first which require an exact match. const componentName = ComponentList.getComponentFromPath(componentPath); if (componentName) { return `${componentName}/${fileName}`; } // This matches the previous PHP behaviour that would throw an exception // if it couldn't parse an AMD file. throw new Error(`Unable to find module name for ${searchFileName} (${componentPath}::${file}}`); } /** * This is heavily inspired by the babel-plugin-add-module-exports plugin. * See: https://github.com/59naga/babel-plugin-add-module-exports * * This is used when we detect a module using "export default Foo;" to make * sure the transpiled code just returns Foo directly rather than an object * with the default property (i.e. {default: Foo}). * * Note: This means that we can't support modules that combine named exports * with a default export. * * @param {String} path * @param {String} exportObjectName */ function addModuleExportsDefaults(path, exportObjectName) { const rootPath = path.findParent(path => { return path.key === 'body' || !path.parentPath; }); // HACK: `path.node.body.push` instead of path.pushContainer(due doesn't work in Plugin.post). // This is hardcoded to work specifically with AMD. rootPath.node.body.push(template(`return ${exportObjectName}.default`)()); } return { pre() { this.seenDefine = false; this.addedReturnForDefaultExport = false; }, visitor: { // Plugin ordering is only respected if we visit the "Program" node. // See: https://babeljs.io/docs/en/plugins.html#plugin-preset-ordering // // We require this to run after the other AMD module transformation so // let's visit the "Program" node. Program: { exit(path) { path.traverse({ CallExpression(path) { // If we find a "define" function call. if (!this.seenDefine && path.get('callee').isIdentifier({name: 'define'})) { // We only want to modify the first instance of define that we find. this.seenDefine = true; // Get the Moodle component for the file being processed. var moduleName = getModuleNameFromFileName(this.file.opts.filename); // The function signature of `define()` is: // define = function (name, deps, callback) {...} // Ensure that if the moduel supplied its own name that it is replaced. if (path.node.arguments.length > 0) { // Ensure that there is only one name. if (path.node.arguments[0].type === 'StringLiteral') { // eslint-disable-next-line console.log(`Replacing module name '${path.node.arguments[0].extra.rawValue}' with ${moduleName}`); path.node.arguments.shift(); } } // Add the module name as the first argument to the define function. path.node.arguments.unshift(types.stringLiteral(moduleName)); // Add a space after the define function in the built file so that previous versions // of Moodle will not try to add the module name to the file when it's being served // by PHP. This forces the regex in PHP to not match for this file. path.node.callee.name = 'define '; } // Check for any Object.defineProperty('exports', 'default') calls. if (!this.addedReturnForDefaultExport && path.get('callee').matchesPattern('Object.defineProperty')) { const [identifier, prop] = path.get('arguments'); const objectName = identifier.get('name').node; const propertyName = prop.get('value').node; if ((objectName === 'exports' || objectName === '_exports') && propertyName === 'default') { addModuleExportsDefaults(path, objectName); this.addedReturnForDefaultExport = true; } } }, AssignmentExpression(path) { // Check for an exports.default assignments. if ( !this.addedReturnForDefaultExport && ( path.get('left').matchesPattern('exports.default') || path.get('left').matchesPattern('_exports.default') ) ) { const objectName = path.get('left.object.name').node; addModuleExportsDefaults(path, objectName); this.addedReturnForDefaultExport = true; } } }, this); } } } }; }; tasks/eslint.js000064400000005070152265256020007534 0ustar00// 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 . /* 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(); }); }; tasks/style.js000064400000002241152265256020007373 0ustar00// 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 . /* 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 ignorefiles tasks. require('./ignorefiles')(grunt); // Load the Style Lint tasks. require('./stylelint')(grunt); // Load the SASS tasks. require('./sass')(grunt); // Add the 'css' task as a startup task. grunt.moodleEnv.startupTasks.push('css'); }; tasks/startup.js000064400000003157152265256020007744 0ustar00// 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 . /* 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 all of the requested startup tasks. grunt.moodleEnv.startupTasks.forEach(taskName => grunt.task.run(taskName)); } }; // Register the startup task. grunt.registerTask('startup', 'Run the correct tasks for the current directory', handler); return handler; }; tasks/ignorefiles.js000064400000012147152265256020010547 0ustar00// 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 . /* 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 the PHPCS configuration. * * @param {Object} thirdPartyPaths */ const phpcsIgnore = (thirdPartyPaths) => { const {toXML} = require('jstoxml'); const config = { _name: 'ruleset', _attrs: { name: "MoodleCore", }, _content: [ { rule: { _attrs: { ref: './phpcs.xml.dist', }, }, }, ], }; thirdPartyPaths.forEach(library => { config._content.push({ 'exclude-pattern': library, }); }); grunt.file.write('phpcs.xml', toXML(config, { header: true, indent: ' ', }) + "\n"); }; /** * Extracts ignore entries from a local ignore file. * * @param {string} componentPath the file path to the component, relative to the code base directory * @param {string} ignoreFilePath the path to the ignore file * @return {array} array of ignore paths to be included in the global ignore files */ const getEntriesFromLocalIgnoreFile = (componentPath, ignoreFilePath) => { const ignorePaths = []; if (grunt.file.exists(ignoreFilePath)) { const ignoreFile = grunt.file.read(ignoreFilePath); const entries = ignoreFile.split('\n'); entries.forEach(entry => { entry = entry.trim(); if (entry.length > 0 && !entry.startsWith('#') && !entry.startsWith('!')) { while (entry.startsWith('/')) { entry = entry.substring(1); } ignorePaths.push(componentPath + '/' + entry); } }); } return ignorePaths; }; /** * 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(); const localStylelintIgnorePaths = []; const localEslintIgnorePaths = []; ComponentList.getComponentPaths(process.cwd() + '/').forEach(componentPath => { const localEslintIgnorePath = process.cwd() + '/' + componentPath + '/.eslintignore'; const localEslintIgnoreEntries = getEntriesFromLocalIgnoreFile(componentPath, localEslintIgnorePath); if (localEslintIgnoreEntries.length > 0) { localEslintIgnorePaths.push(...localEslintIgnoreEntries); } const localStylelintIgnorePath = process.cwd() + '/' + componentPath + '/.stylelintignore'; const localStylelintIgnoreEntries = getEntriesFromLocalIgnoreFile(componentPath, localStylelintIgnorePath); if (localStylelintIgnoreEntries.length > 0) { localStylelintIgnorePaths.push(...localStylelintIgnoreEntries); } }); // 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).concat(localEslintIgnorePaths); grunt.file.write('.eslintignore', eslintIgnores.join('\n') + '\n'); // Generate .stylelintignore. const stylelintIgnores = [ '# Generated by "grunt ignorefiles"', '**/yui/build/*', 'theme/boost/style/moodle.css', 'theme/classic/style/moodle.css', 'jsdoc/styles/*.css', 'admin/tool/componentlibrary/hugo/dist/css/docs.css', ].concat(thirdPartyPaths).concat(localStylelintIgnorePaths); grunt.file.write('.stylelintignore', stylelintIgnores.join('\n') + '\n'); phpcsIgnore(thirdPartyPaths); }; grunt.registerTask('ignorefiles', 'Generate ignore files for linters', handler); return handler; }; tasks/jsdoc.js000064400000003341152265256020007337 0ustar00// 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 . /* 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 path = require('path'); grunt.registerTask('jsdoc', 'Generate JavaScript documentation using jsdoc', function() { const done = this.async(); const configuration = path.resolve('.grunt/jsdoc/jsdoc.conf.js'); grunt.util.spawn({ cmd: 'jsdoc', args: [ '--configure', configuration, ] }, function(error, result, code) { if (result.stdout) { grunt.log.write(result.stdout); } if (result.stderr) { grunt.log.error(result.stderr); } if (error) { grunt.fail.fatal(`JSDoc failed with error code ${code}`); } else { grunt.log.write('JSDoc completed successfully'.green); } done(); }); }); }; tasks/javascript.js000064400000016552152265256020010413 0ustar00// 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 . /* 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 minification 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(`amd/src`, `amd/build`); destPath = destPath.replace(/\.js$/, '.min.js'); return destPath; }; module.exports = grunt => { // Load the Ignorefiles tasks. require('./ignorefiles')(grunt); // Load the Shifter tasks. require('./shifter')(grunt); // Load ESLint. require('./eslint')(grunt); // Load jsconfig. require('./jsconfig')(grunt); // Load JSDoc. require('./jsdoc')(grunt); const path = require('path'); // Register JS tasks. grunt.registerTask('yui', ['eslint:yui', 'shifter']); grunt.registerTask('amd', ['ignorefiles', 'eslint:amd', 'rollup']); grunt.registerTask('js', ['amd', 'yui']); // Register NPM tasks. grunt.loadNpmTasks('grunt-contrib-watch'); grunt.loadNpmTasks('grunt-rollup'); const babelTransform = require('@babel/core').transform; const babel = (options = {}) => { return { name: 'babel', transform: (code, id) => { grunt.log.debug(`Transforming ${id}`); options.filename = id; const transformed = babelTransform(code, options); return { code: transformed.code, map: transformed.map }; } }; }; // Note: We have to use a rate limit plugin here because rollup runs all tasks asynchronously and in parallel. // When we kick off a full run, if we kick off a rollup of every file this will fork-bomb the machine. // To work around this we use a concurrent Promise queue based on the number of available processors. const rateLimit = () => { const queue = []; let queueRunner; const startQueue = () => { if (queueRunner) { return; } queueRunner = setTimeout(() => { const limit = Math.max(1, require('os').cpus().length / 2); grunt.log.debug(`Starting rollup with queue size of ${limit}`); runQueue(limit); }, 100); }; // The queue runner will run the next `size` items in the queue. const runQueue = (size = 1) => { queue.splice(0, size).forEach(resolve => { grunt.log.debug(`Item resolved. Kicking off next one.`); resolve(); }); }; return { name: 'ratelimit', // The options hook is run in parallel. // We can return an unresolved Promise which is queued for later resolution. options: async(options) => { return new Promise(resolve => { queue.push(resolve); startQueue(); return options; }); }, // When an item in the queue completes, start the next item in the queue. generateBundle: (options, bundle) => { grunt.log.debug(`Finished output phase for ${Object.keys(bundle).join(', ')}`); runQueue(); }, }; }; const terser = require('rollup-plugin-terser').terser; grunt.config.merge({ rollup: { options: { format: 'esm', dir: 'output', sourcemap: true, treeshake: false, context: 'window', plugins: [ rateLimit(), babel({ sourceMaps: true, comments: false, compact: 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') ], presets: [ ['@babel/preset-env', { modules: false, useBuiltIns: false }] ] }), terser({ // Do not mangle variables. // Makes debugging easier. mangle: 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'] }, }, }); // Add the 'js' task as a startup task. grunt.moodleEnv.startupTasks.push('js'); // 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('rollup.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, }; }; tasks/gherkinlint.js000064400000006163152265256020010560 0ustar00// 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 . /* 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'], }, }, }); // Add the 'gherkinlint' task as a startup task. grunt.moodleEnv.startupTasks.push('gherkinlint'); return handler; }; tasks/upgradablelibs.js000064400000003142152265256020011214 0ustar00// 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 . /* jshint node: true, browser: false */ /* eslint-env node */ /** * @copyright 2023 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ module.exports = grunt => { /** * Generate upgradable third-party libraries (utilising thirdpartylibs.xml data) */ grunt.registerTask('upgradablelibs', 'Generate upgradable third-party libraries', async function() { const done = this.async(); const path = require('path'); const ComponentList = require(path.join(process.cwd(), '.grunt', 'components.js')); // An array of third party libraries that have a newer version in their repositories. const thirdPartyLibs = await ComponentList.getThirdPartyLibsUpgradable({progress: true}); for (let library of thirdPartyLibs) { grunt.log.ok(JSON.stringify(Object.assign({}, library), null, 4)); } done(); }); }; tasks/jsconfig.js000064400000003560152265256020010042 0ustar00// 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 . /* jshint node: true, browser: false */ /* eslint-env node */ /** * @copyright 2022 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ const configuration = { compilerOptions: { baseUrl: ".", paths: { }, target: "es2015", allowSyntheticDefaultImports: false, }, exclude: [ "node_modules", ], include: [], }; module.exports = (grunt) => { const handler = () => { const jsconfigData = Object.assign({}, configuration); const path = require('path'); const {fetchComponentData} = require(path.join(process.cwd(), '.grunt', 'components.js')); const componentData = fetchComponentData().components; for (const [thisPath, component] of Object.entries(componentData)) { jsconfigData.compilerOptions.paths[`${component}/*`] = [`${thisPath}/amd/src/*`]; jsconfigData.include.push(`${thisPath}/amd/src/**/*`); } grunt.file.write('jsconfig.json', JSON.stringify(jsconfigData, null, " ") + "\n"); }; grunt.registerTask('jsconfig', 'Generate a jsconfig configuration compatible with the LSP', handler); }; tasks/componentlibrary.js000064400000010264152265256020011626 0ustar00// 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 . /* jshint node: true, browser: false */ /* eslint-env node */ /** * Component Library build tasks. * * @copyright 2021 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ module.exports = grunt => { /** * Get a child path of the component library. * * @param {string} path * @returns {string} */ const getCLPath = path => `admin/tool/componentlibrary/${path}`; /** * Get a spawn handler. * * This is a generic function to write the spawn output, and then to exit if required and mark the async task as * complete. * * @param {Promise} done * @returns {function} */ const getSpawnHandler = done => (error, result, code) => { grunt.log.write(result); if (error) { grunt.log.error(result.stdout); process.exit(code); } done(); }; /** * Spawn a function against Node with the provided args. * * @param {array} args * @returns {object} */ const spawnNodeCall = (args) => grunt.util.spawn({ cmd: 'node', args, }, getSpawnHandler(grunt.task.current.async())); /** * Build the docs using Hugo. * * @returns {Object} Reference to the spawned task */ const docsBuild = () => spawnNodeCall([ 'node_modules/hugo-bin/cli.js', '--config', getCLPath('config.yml'), '--cleanDestinationDir', ]); /** * Build the docs index using the hugo-lunr-indexer. * * @returns {Object} Reference to the spawned task */ const indexBuild = () => spawnNodeCall([ 'node_modules/hugo-lunr-indexer/bin/hli.js', '-i', getCLPath('content/**'), '-o', getCLPath('hugo/site/data/my-index.json'), '-l', 'yaml', '-d', '---', ]); /** * Build the hugo CSS. * * @returns {Object} Reference to the spawned task */ const cssBuild = () => spawnNodeCall([ 'node_modules/sass/sass.js', '--style', 'expanded', '--source-map', '--embed-sources', '--precision', 6, '--load-path', process.cwd(), getCLPath('hugo/scss/docs.scss'), getCLPath('hugo/dist/css/docs.css'), ]); // Register the various component library tasks. grunt.registerTask('componentlibrary:docsBuild', 'Build the component library', docsBuild); grunt.registerTask('componentlibrary:cssBuild', 'Build the component library', cssBuild); grunt.registerTask('componentlibrary:indexBuild', 'Build the component library', indexBuild); grunt.registerTask('componentlibrary', 'Build the component library', [ 'componentlibrary:docsBuild', 'componentlibrary:cssBuild', 'componentlibrary:indexBuild', ]); grunt.config.merge({ watch: { componentLibraryDocs: { files: [ getCLPath('content/**/*.md'), getCLPath('hugo'), ], tasks: ['componentlibrary:docsBuild', 'componentlibrary:indexBuild'], }, componentLibraryCSS: { files: [ getCLPath('hugo/scss/**/*.scss'), 'hugo', ], tasks: ['componentlibrary:cssBuild'], }, }, }); // Add the 'componentlibrary' task as a startup task. grunt.moodleEnv.startupTasks.push('componentlibrary'); return { docsBuild, cssBuild, indexBuild, }; }; tasks/shifter.js000064400000013442152265256020007704 0ustar00// 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 . /* 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; }; tasks/sass.js000064400000002607152265256020007212 0ustar00// 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 . /* 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('sass'), includePaths: ["theme/boost/scss/", "theme/classic/scss/"] } }, }); }; tasks/watch.js000064400000025353152265256020007352 0ustar00/** * 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; }; tasks/stylelint.js000064400000014303152265256020010264 0ustar00// 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 . /* 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: { quietDeprecationWarnings: true, 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: { quietDeprecationWarnings: true, customSyntax: 'postcss-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'); } scssTasks.unshift('ignorefiles'); const cssTasks = ['ignorefiles']; 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', 'jsdoc/styles/*.css', ], tasks: ['rawcss'] }, scss: { files: getCoreThemeMatches(), tasks: ['scss'] }, }, }); }; notes/src/create.mjs000064400000005341152265256020010451 0ustar00#!/usr/bin/env node // 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 . import inquirer from 'inquirer'; import chalk from 'chalk'; import { createNote } from './note.mjs'; import { getInitialValues } from './helpers.mjs'; import * as Prompts from './prompts.mjs'; import logger from './logger.mjs'; export default async (options) => { // Processs the initial values. const initialValues = getInitialValues(options); // Fetch information. const messages = []; const { issueNumber } = await inquirer.prompt([ Prompts.getIssuePrompt(), ], initialValues); let selection = {}; let notePath; do { selection = {}; selection = await inquirer.prompt([ Prompts.getComponentsPrompt(), Prompts.getTypePrompt(), Prompts.getMessagePromptInput(), ], initialValues); if (selection.message === '') { selection = Object.assign( selection, await inquirer.prompt([ Prompts.getMessagePromptEditor(), ]), ); } logger.info(` Creating upgrade note with the following options: - Issue: ${chalk.bold(issueNumber)} - Component: ${chalk.bold(selection.components)} - Type: ${chalk.bold(selection.type)} - Message: ${chalk.bold(selection.message)} `); messages.push({ components: [selection.components], type: selection.type, message: selection.message, }); // Save the note so far. if (notePath) { await createNote(issueNumber, messages, notePath); logger.info(`Updated note at: ${chalk.underline(chalk.bold(notePath))}`); } else { notePath = await createNote(issueNumber, messages); logger.info(`Note created at: ${chalk.underline(chalk.bold(notePath))}`); } selection = Object.assign( selection, await inquirer.prompt([ Prompts.getAddAnotherPrompt(), ], initialValues), ); } while (selection.addAnother); }; notes/src/note.mjs000064400000013703152265256020010154 0ustar00// 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 . import yaml from 'js-yaml'; import path from 'path'; import { writeFile, mkdir, readdir, readFile, unlink } from 'fs/promises'; import { isValidNoteName, sortNoteTypes } from './noteTypes.mjs'; import { sortComponents } from './components.mjs'; const unreleasedPath = path.resolve('.upgradenotes'); /** * Get the filename for the note. * * @param {string} issueNumnber The issue number * @returns {string} */ const getFilename = (issueNumber) => { const dateTimeFormat = new Intl.DateTimeFormat(undefined, { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 2, timeZone: 'UTC', }); const date = Object.fromEntries( dateTimeFormat.formatToParts(new Date()) .filter((p) => p.type !== 'literal') .map((p) => ([p.type, p.value])) ); const dateString = [ date.year, date.month, date.day, date.hour, date.minute, date.second, date.fractionalSecond, ].join(''); return `${issueNumber}-${dateString}.yml`; }; /** * Create a new note. * * @param {string} issueNumber * @param {[type: string]: {message: string}} messages * @returns {string} The path to the note on disk */ export const createNote = async ( issueNumber, messages, notePath, ) => { const note = { issueNumber, notes: {}, }; messages.forEach(({components, type, message}) => { note.notes[components] = note.notes[components] || []; note.notes[components].push({message, type}); }); if (!notePath) { notePath = path.resolve(unreleasedPath, getFilename(issueNumber)); } const noteContent = yaml.dump(note); await mkdir(unreleasedPath, {recursive: true}); await writeFile(notePath, noteContent); return notePath; }; /** * Get all unreleased notes. * * @returns {Promise<{issueNumber: string, components: string[], types: {[type: string]: {message: string}[]}}[]> */ export const getAllNotes = async () => { const files = await readdir(unreleasedPath); const notes = files .filter((file) => file.endsWith('.yml')) .map(async (file) => { const filePath = path.resolve(unreleasedPath, file); const fileContent = await readFile(filePath, 'utf8'); return { ...yaml.load(fileContent), filePath, }; }); return Promise.all(notes); }; /** * Get the list of notes, grouped by note type, then component. * * @returns {Promise<{[type: string]: {[components: string]: {message: string, issueNumber: string}[]}}>} */ export const getCombinedNotes = async () => { const notes = await getAllNotes(); const combinedNotes = {}; notes.forEach((note) => { Object.entries(note.notes).forEach(([components, data]) => { data.forEach((entry) => { if (!isValidNoteName(entry.type)) { throw new Error(`Invalid note type: "${entry.type}" in file ${note.filePath}`); } combinedNotes[entry.type] = combinedNotes[entry.type] || {}; combinedNotes[entry.type][components] = combinedNotes[entry.type][components] || []; combinedNotes[entry.type][components].push({message: entry.message, issueNumber: note.issueNumber}); }); }); }); return combinedNotes; }; /** * Get the list of notes, grouped by component, then by note type. * * @returns {Promise<{[component: string]: {[type: string]: {message: string, issueNumber: string}[]}}>} */ export const getCombinedNotesByComponent = async () => { const notes = await getAllNotes(); const combinedNotes = {}; notes.forEach((note) => { Object.entries(note.notes).forEach(([component, data]) => { combinedNotes[component] = combinedNotes[component] || {}; data.forEach((entry) => { if (!isValidNoteName(entry.type)) { throw new Error(`Invalid note type: "${entry.type}" in file ${note.filePath}`); } combinedNotes[component][entry.type] = combinedNotes[component][entry.type] || []; combinedNotes[component][entry.type].push({ component, message: entry.message, issueNumber: note.issueNumber, type: entry.type, }); }); }); }); // Sort notes by note type. Object.entries(combinedNotes).forEach(([component, types]) => { combinedNotes[component] = Object.fromEntries( Object.entries(types).sort(([a], [b]) => sortNoteTypes(a, b)) ); }); // Sort components. return Object.fromEntries( Object.entries(combinedNotes).sort(([a], [b]) => sortComponents(a, b)) ); }; /** * Delete all unreleased notes. * * @returns {Promise} */ export const deleteAllNotes = async () => { const files = await readdir(unreleasedPath); return Promise.all( files .filter((item, index) => files.indexOf(item) === index) .filter((file) => file.endsWith('.yml')) .map((file) => unlink(`${unreleasedPath}/${file}`)) ); }; notes/src/generate.mjs000064400000026024152265256020011001 0ustar00#!/usr/bin/env node // 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 . import chalk from 'chalk'; import { getAllComponents } from './components.mjs'; import { getCombinedNotesByComponent, deleteAllNotes } from './note.mjs'; import { getNoteName } from './noteTypes.mjs'; import { writeFile, readFile, unlink } from 'fs/promises'; import { join as joinPath } from 'path'; import logger from './logger.mjs'; import { getCurrentVersion } from './helpers.mjs'; /** * Helper to fetch the current notes from a file. * * @param {string} file * @returns {Promise} */ const getCurrentNotes = async (file) => { try { return await readFile(file, 'utf8'); } catch (error) { return null; } } /** * Update the UPGRADING.md file. * * @param {string} upgradeNotes * @param {Object} options * @param {boolean} options.deleteNotes * @returns {Promise} */ const updateUpgradeNotes = async (upgradeNotes, options) => { const fileName = 'UPGRADING.md'; // Write the notes to a file. logger.info(`Writing notes to ${chalk.underline(chalk.bold(fileName))}`); // Prepend to the existing file. const existingContent = await getCurrentNotes(fileName); if (existingContent) { await writeFile(fileName, getUpdatedNotes(existingContent, upgradeNotes)); } else { // This should not normally happen. await writeFile(fileName, upgradeNotes); } if (options.deleteNotes) { logger.warn(`>>> Deleting all notes <<<`) // Delete the notes. deleteAllNotes(); } }; /** * Create the current summary notes. * * @param {string} upgradeNotes * @returns {Promise} */ const createCurrentSummary = async (upgradeNotes) => { const fileName = 'UPGRADING-CURRENT.md'; const notes = `# Moodle upgrade notes\n\n${upgradeNotes}`; await writeFile(fileName, notes); logger.info(`Running upgrade notes written to ${chalk.underline(chalk.bold(fileName))}`); }; /** * Get the indexes of the lines that contain the version headings. * * @param {array} lines * @returns {array} */ const getVersionLineIndexes = (lines) => { const h2Indexes = []; lines.forEach((line, index) => { const matches = line.match(/^##\s(?.*)$/); if (matches) { h2Indexes.push({ index, line, version: matches.groups.version, }); } }); return h2Indexes; }; /** * Find the index of the Unreleased heading. * * @param {array} versionHeadings * @returns {number} */ const findUnreleasedHeadingIndex = (versionHeadings) => versionHeadings.findIndex((heading) => { if (heading.version === 'Unreleased') { // Used if version cannot be guessed. return true; } if (heading.version.endsWith('+')) { // Weekly release for a stable branch. return true; } if (heading.version.match(/beta|rc\d/)) { // Beta and RC rolls are treated as weeklies. return true; } if (heading.version.endsWith('dev')) { // Development version. return true; } return false; }); /** * Get the before and after content, to facilitate replacing any existing Unreleased notes. * * @param {array} lines * @returns {Object} {beforeContent: string, afterContent: string} */ const getBeforeAndAfterContent = (lines) => { const existingLines = lines.split('\n'); const versionHeadings = getVersionLineIndexes(existingLines); if (versionHeadings.length > 0) { const unreleasedHeadingIndex = findUnreleasedHeadingIndex(versionHeadings); if (unreleasedHeadingIndex !== -1) { const beforeContent = existingLines.slice(0, versionHeadings[unreleasedHeadingIndex].index).join('\n'); if (versionHeadings.length > unreleasedHeadingIndex + 1) { const afterContent = existingLines.slice(versionHeadings[unreleasedHeadingIndex + 1].index).join('\n'); return { beforeContent, afterContent, }; } return { beforeContent, afterContent: '', }; } return { beforeContent: existingLines.slice(0, versionHeadings[0].index).join('\n'), afterContent: existingLines.slice(versionHeadings[0].index).join('\n'), }; } return { beforeContent: existingLines.join('\n'), afterContent: '', } }; /** * Get the notes for the component. * * @param {string} types * @param {Number} headingLevel * @returns {string} */ const getNotesForComponent = (types, headingLevel) => { let upgradeNotes = ''; Object.entries(types).forEach(([type, notes]) => { upgradeNotes += '#'.repeat(headingLevel); upgradeNotes += ` ${getNoteName(type)}\n\n`; notes.forEach(({ message, issueNumber }) => { // Split the message into lines, removing empty lines. const messageLines = message .split('\n') // Remove empty lines between tables, and list entries, but not after lists. .filter((line, index, lines) => { if (line.trim().length === 0) { // This line is empty. // If it's the first line in the file, remove it. if (index === 0) { return false; } // This is the last line in the file, remove it. if (index === lines.length - 1) { return false; } // If the previous line relates to a table, remove this line. if (lines[index - 1].match(/^\s*\|/)) { return false; } // If the next line is also empty, do not remove this line. if (lines[index + 1].trim().length === 0) { return true; } // Do not remove the line if the previous line was a list item. if (lines[index - 1].match(/^\s*[-*]\s/)) { return true; } if (lines[index - 1].match(/^\s*\d+\.\s/)) { return true; } // Preserve all other empty lines by default. return true; } // Keep any line which has content. return true; }); const firstLine = messageLines.shift().trim(); upgradeNotes += `- ${firstLine}\n`; messageLines .forEach((line) => { upgradeNotes += ` ${line}`.trimEnd() + `\n`; }); upgradeNotes += `\n For more information see [${issueNumber}](https://tracker.moodle.org/browse/${issueNumber})\n`; }); upgradeNotes += '\n'; }); return upgradeNotes; }; /** * Get the updated notes mixed with existing content. * * @param {string} existingContent * @param {string} upgradeNotes */ const getUpdatedNotes = (existingContent, upgradeNotes) => { const { beforeContent, afterContent } = getBeforeAndAfterContent(existingContent); const newContent = `${beforeContent}\n${upgradeNotes}\n${afterContent}` .split('\n') .filter((line, index, lines) => { if (line === '' && lines[index - 1] === '') { // Remove multiple consecutive empty lines. return false; } return true; }) .join('\n'); return newContent; }; /** * Update the notes for each component. */ const updateComponentNotes = ( notes, version, notesFileName = 'UPGRADING.md', removeEmpty = false, ) => { return getAllComponents().map(async (component) => { logger.verbose(`Updating notes for ${component.name} into ${component.path}`); const fileName = joinPath(component.path, notesFileName); const existingContent = await getCurrentNotes(fileName); if (!existingContent) { if (!notes[component.value]) { // No existing notes, and no new notes to add. return; } } else { if (!notes[component.value]) { // There is existing content, but nothing to add. if (removeEmpty) { logger.verbose(`Removing empty notes file ${fileName}`); await unlink(fileName); } return; } } const componentNotes = notes[component.value]; let upgradeNotes = `## ${version}\n\n`; upgradeNotes += getNotesForComponent(componentNotes, 3); if (existingContent) { await writeFile(fileName, getUpdatedNotes(existingContent, upgradeNotes)); } else { await writeFile( fileName, `# ${component.name} Upgrade notes\n\n${upgradeNotes}`, ); } }); } /** * Generate the upgrade notes for a new release. * * @param {string|undefined} version * @param {Object} options * @param {boolean} options.generateUpgradeNotes * @param {boolean} options.deleteNotes * @returns {Promise} */ export default async (version, options = {}) => { const notes = await getCombinedNotesByComponent(); if (Object.keys(notes).length === 0) { logger.warn('No notes to generate'); return; } if (!version) { version = await getCurrentVersion(); } // Generate the upgrade notes for this release. // We have // - a title with the release name // - the change types // - which contain the components // - which document each change let upgradeNotes = `## ${version}\n\n`; Object.entries(notes).forEach(([component, types]) => { upgradeNotes += `### ${component}\n\n`; upgradeNotes += getNotesForComponent(types, 4); }); await Promise.all([ createCurrentSummary(upgradeNotes), ...updateComponentNotes(notes, version, 'UPGRADING-CURRENT.md', true), ]); if (options.generateUpgradeNotes) { await Promise.all(updateComponentNotes(notes, version)); await updateUpgradeNotes(upgradeNotes, options); } }; notes/src/helpers.mjs000064400000013425152265256020010652 0ustar00// 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 . import chalk from 'chalk'; import { isStandardComponent, isCommunityComponent, rewritePlugintypeAsSubsystem } from './components.mjs'; import { isValidNoteName } from './noteTypes.mjs'; import logger from './logger.mjs'; import { readFile } from 'fs/promises'; /** * Validate an issue number input * * @param {string} input * @returns {string|boolean} */ export const validateIssueNumber = (input) => { if (!input) { return 'You must provide a tracker issue number'; } if (input.match(/^[a-zA-Z]*-\d+$/)) { return true; } if (input.match(/^\d+$/)) { return true; } return 'The issue number was not recognised as a valid issue number'; }; /** * Format an issue number input. * * @param {string} input * @returns {string} */ export const formatIssueNumber = (input) => { if (input.match(/^[a-zA-Z]*-\d+$/)) { return input; } if (input.match(/^\d+$/)) { return `MDL-${input}`; } return input; }; /** * Validate a component. * * @param {string} input * @returns {string|boolean} */ export const validateComponent = (input) => { if (isStandardComponent(input)) { return true; } if (isCommunityComponent(input)) { return 'Currently only core plugins are supported.'; } return 'The component was not recognised as a standard component'; }; export const formatComponent = (input) => { if (rewritePlugintypeAsSubsystem(input)) { return `core_${input}`; } return input; } /** * Get the initial values from the options. * * @param {object} options * @returns {object} */ export const getInitialValues = (options) => { const initialValues = {}; const type = getInitialTypeValue(options); if (type) { initialValues.type = type; } const issueNumber = getInitialIssueValue(options); if (issueNumber) { initialValues.issueNumber = issueNumber; } const component = getInitialComponentValue(options); if (component) { initialValues.components = component; } const message = getInitialMessageValue(options); if (message) { initialValues.message = message initialValues.addAnother = false; } return initialValues; }; /** * Get the initial type value. * * @param {Object} options * @returns {string|undefined} */ const getInitialTypeValue = (options) => { if (!options.type) { return; } options.type = options.type.trim().toLowerCase(); if (isValidNoteName(options.type)) { return options.type; } logger.warn(`Note type "${chalk.underline(chalk.red(options.type))}" is not valid.`); }; /** * Get the initial issue number value. * * @param {Object} options * @returns {string|undefined} */ const getInitialIssueValue = (options) => { if (!options.issue) { return; } options.issue = options.issue.trim().toUpperCase(); const issueNumberValidated = validateIssueNumber(options.issue); if (issueNumberValidated === true) { const issueNumber = formatIssueNumber(options.issue); if (issueNumber !== options.issue) { logger.warn( `Issue number "${chalk.underline(chalk.red(options.issue))}" was updated to ` + `"${chalk.underline(chalk.green(issueNumber))}"` ); } return issueNumber; } else { logger.warn(`Issue number "${chalk.underline(chalk.red(options.issue))}" is not valid: ${issueNumberValidated}`); } }; /** * Get the initial component value. * * @param {Object} options * @returns {string|undefined} */ const getInitialComponentValue = (options) => { if (!options.component) { return; } options.component = options.component.trim().toLowerCase(); const componentValidated = validateComponent(options.component); if (componentValidated === true) { const component = formatComponent(options.component); if (component !== options.component) { logger.warn( `Component "${chalk.underline(chalk.red(options.component))}" was updated to ` + `"${chalk.underline(chalk.green(component))}"` ); } return component; } else { logger.warn(`Component "${chalk.underline(chalk.red(options.component))}" is not valid: ${componentValidated}`); } }; /** * Get the initial message value. * * @param {Object} options * @returns {string|undefined} */ const getInitialMessageValue = (options) => { if (!options.message) { return; } return options.message.trim(); }; /** * Get the current version from the project /version.php file. * * @returns {Promise} */ export const getCurrentVersion = async () => { const versionRegex = new RegExp(/^ *\$release *= *['\"](?[^ \+]+\+?) *\(Build:.*/m); try { const versionFile = await readFile('version.php', 'utf8'); const match = versionFile.match(versionRegex); if (match) { return match.groups.release; } } catch(error) { logger.error('Unable to read the version file'); } return "Unreleased"; } notes/src/components.mjs000064400000007515152265256020011400 0ustar00// 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 . import * as Components from '../../components.js'; const componentData = Components.fetchComponentData(); /** * The standard components shipped with core Moodle. * * @type {Object} */ export const standardComponents = componentData.standardComponents; /** * All components of the current Moodle instance. * * @type {Object} */ export const allComponents = componentData.components; /** * Get all components of the current Moodle instance. * * @returns {Array} */ export const getAllComponents = () => { let components = new Map(Object.entries(componentData.pluginTypes).map(([value, path]) => ([path,{ path, value, name: `${value} (plugin type)`, }]))); Object .entries(componentData.components) .filter(([path, value]) => Object.values(componentData.standardComponents).includes(value)) .forEach(([path, value]) => { const entry = { path, value, name: value, }; if (Object.values(componentData.subsystems).includes(value)) { if (components.has(path)) { entry.name = `${value} (subsystem / plugintype)`; } else { entry.name = `${value} (subsystem)`; } } components.set(path, entry); }); return Array.from(components.values()); }; /** * Whether the specified component is a standard component shipped with core Moodle. * * @param {string} componentName * @returns {boolean} */ export const isStandardComponent = (componentName) => { if (Object.values(componentData.standardComponents).includes(componentName)) { return true; } if (Object.keys(componentData.pluginTypes).includes(componentName)) { return true; } return false; }; export const rewritePlugintypeAsSubsystem = (componentName) => { if (Object.keys(componentData.pluginTypes).includes(componentName)) { const pluginTypePath = componentData.pluginTypes[componentName]; if (Object.keys(componentData.subsystems).includes(pluginTypePath)) { return true; } } return false; }; /** * Whether the specified component is a community component. * * @param {string} componentName * @returns {boolean} */ export const isCommunityComponent = (componentName) => { if (isStandardComponent(componentName)) { return false; } return Object.values(componentData.components).indexOf(componentName) !== -1; }; /** * Sort method for components. * * This method sorts components putting `core` first, followed by core subsystems, then everything else. * * @param {String} a * @param {String} b * @returns {Number} */ export const sortComponents = (a, b) => { // Always put 'core' first. if (a === 'core') { return -1; } else if (b === 'core') { return 1; } // Put core subsystems next. if (a.startsWith('core_') && !b.startsWith('core_')) { return -1; } else if (b.startsWith('core_') && !a.startsWith('core_')) { return 1; } // Sort alphabetically for everything else. return a.localeCompare(b); }; notes/src/noteTypes.mjs000064400000003556152265256020011206 0ustar00// 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 . /** * The note types. * * Matches the suggested order in https://keepachangelog.com/en/1.1.0/. * @type {Object} */ const noteTypes = { 'improved': 'Added', 'changed': 'Changed', 'deprecated': 'Deprecated', 'removed': 'Removed', 'fixed': 'Fixed', }; /** * The preferred order of note types. * * @type {string[]} */ const preferredOrder = Object.keys(noteTypes); /** * Comparison method to sort note types. * * @param {String} a * @param {String} b * @returns {Number} */ export const sortNoteTypes = (a, b) => { const aIndex = preferredOrder.indexOf(a); const bIndex = preferredOrder.indexOf(b); if (aIndex === -1) { return 1; } if (bIndex === -1) { return -1; } return aIndex - bIndex; }; /** * Get the note names. * * @returns {string[]} */ export const getNoteNames = () => Object.keys(noteTypes); /** * Get the human-readable note name. * * @param {string} type * @returns {string} */ export const getNoteName = (type) => noteTypes[type]; /** * Whether the note name is valid. * * @param {string} type * @returns {boolean} */ export const isValidNoteName = (type) => noteTypes[type] !== undefined; notes/src/logger.mjs000064400000001746152265256020010472 0ustar00#!/usr/bin/env node // 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 . import winston from 'winston'; const logger = winston.createLogger({ level: 'info', format: winston.format.combine( winston.format.colorize(), winston.format.simple() ), transports: [ new winston.transports.Console(), ], }); export default logger; notes/src/prompts.mjs000064400000007557152265256020010725 0ustar00// 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 . import inquirer from 'inquirer'; import SearchList from 'inquirer-search-list'; import { getNoteNames } from './noteTypes.mjs'; import { getAllComponents } from './components.mjs'; import { formatComponent, formatIssueNumber, validateComponent, validateIssueNumber, } from './helpers.mjs'; /** * A Search List which accepts an initial value. */ class SearchListWithInitialValue extends SearchList { constructor(options, ...args) { super(options, ...args); if (options.default) { const pointer = this.filterList.findIndex((item) => { return item.value === options.default; }); if (pointer > -1) { this.pointer = pointer; } } } } inquirer.registerPrompt('search-list', SearchListWithInitialValue); /** * Get the issue type prompt. * * @param {string} defaultData The initially selected value * @returns {Object} */ export const getTypePrompt = (defaultData) => ({ default: defaultData, type: 'search-list', message: 'Type of change', name: 'type', choices: getNoteNames(), validate: (selection) => { if (selection.length < 1) { return 'You must select at least one type of change'; } return true; }, }); /** * Get the component prompt. * * @param {string} [defaultValue='core'] The initally selected value. * @returns */ export const getComponentsPrompt = (defaultValue) => { if (!defaultValue ) { defaultValue = 'core'; } return { choices: getAllComponents(), default: defaultValue, type: 'search-list', message: 'Component', name: 'components', validate: validateComponent, filter: formatComponent, }; }; /** * Get the issue number prompt as an inline input. * * @param {string} defaultData * @returns {object} */ export const getIssuePrompt = (defaultData) => ({ default: defaultData, type: 'input', message: 'Tracker issue number', name: 'issueNumber', validate: validateIssueNumber, filter: formatIssueNumber, }); /** * Get a message prompt. * * @param {string} defaultData * @returns */ export const getMessagePromptEditor = (defaultData) => ({ default: defaultData, type: process.stdin.isTTY ? 'editor' : 'input', postfix: '.md', message: 'Message', name: 'message', waitUserInput: false, validate: (input) => { if (!input) { return 'You must provide a message'; } return true; }, // Remove any trailing whitespace. filter: (input) => input.split('\n').map((line) => line.trimEnd()).join('\n'), }); /** * Get a message prompt. * * @param {string} defaultData * @returns */ export const getMessagePromptInput = (defaultData) => ({ default: defaultData, type: 'input', message: 'Message (leave empty to use editor)', name: 'message', filter: (input) => input.trim(), }); /** * Get a prompt to ask the user if they wish to add another entry. * * @returns {Object} */ export const getAddAnotherPrompt = () => ({ type: 'confirm', message: 'Do you want to add another note?', default: false, name: 'addAnother', }); upgradenotes.mjs000064400000007136152265256020007773 0ustar00#!/usr/bin/env node // 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 . import { Argument, Option, program } from 'commander'; import chalk from 'chalk'; import { getNoteNames } from './notes/src/noteTypes.mjs'; import createAction from './notes/src/create.mjs'; import generateAction from './notes/src/generate.mjs'; import logger from './notes/src/logger.mjs'; console.log(` ${chalk.bold(chalk.underline(chalk.green('Moodle Upgrade Notes Generator')))} This tool is used to generate the upgrade notes for changes you make in Moodle. Please remember that the intended audience of these changes is ${chalk.italic('plugin developers')} who need to know how to update their plugins for a new Moodle version. Upgrade notes should not be used to document changes for site administrators, or for internal API changes which are not expected to be used outside of the relevant component. `) program.configureHelp({ helpWidth: 100, }); program.on('option:verbose', () => { logger.level = 'verbose'; }); program.addOption( new Option( '-v, --verbose', 'Output more information during the generation process', ) .default(false) ); // Define the command line options. program .command('create') .summary('Generate a new upgrade note') .addOption( new Option('-t, --type ', `The type of change to document. Valid types are: ${getNoteNames().join(', ')}`) ) .addOption(new Option('-i, --issue ', 'The tracker issue number')) .addOption(new Option('-c, --component ', 'The component to write a note for')) .addOption(new Option( '-m, --message ', 'The message to use for the upgrade note', )) .action((options) => createAction(options)); program .command('summary') .summary('Generate a local copy of the upgrade notes summary') .addArgument( new Argument('[version]', 'The Moodle version to create the summary notes for') ) .action((version) => generateAction(version)); program .command('release') .summary('Generate the markdown copies of the upgrade notes for a Moodle release') .addArgument( new Argument('[version]', 'The Moodle version to create the release notes for') ) .addOption( new Option( '--generate-upgrade-notes', 'Generate the UPGRADING.md notes for the release. ' + 'Note: This option is intended for use by the release manager when generating the upgrade notes.', ) .default(true) ) .addOption( new Option( '-d, --delete-notes', 'Delete the notes after generating the UPGRADING.md notes for the release. ' + 'Note: This option is intended for use by the release manager when generating the upgrade notes.' + 'This option has no effect unless --generate-upgrade-notes is also set.' ) .default(false) ) .action((version, options) => generateAction(version, options)); program.parse(); jsdoc/README.md000064400000001154152265256020007133 0ustar00# Moodle JavaScript Documentation ``` .-..-. _____ | || | /____/-.---_ .---. .---. .-.| || | .---. | | _ _ |/ _ \/ _ \/ _ || |/ __ \ * | | | | | || |_| || |_| || |_| || || |___/ |_| |_| |_|\_____/\_____/\_____||_|\_____) Moodle - the world's open source learning platform ``` ## About This generated documentation includes API documentation for JavaScript written in the AMD and ES2015 module formats within Moodle. ## Related information See [https://moodledev.io](https://moodledev.io) for other related Developer Documentation. jsdoc/jsdoc.conf.js000064400000006523152265256020010245 0ustar00// 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 . /** * Helper functions for working with Moodle component names, directories, and sources. * * @copyright 2019 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ "use strict"; /* eslint-env node */ // Do not include any plugins as stanard. const plugins = []; plugins.push('plugins/markdown'); /** * Get the source configuration. * * @return {Object} */ const getSource = () => { const glob = require('glob'); const path = require('path'); const ComponentList = require(path.resolve('.grunt/components.js')); const thirdPartyPaths = ComponentList.getThirdPartyPaths(); const source = { include: [], includePattern: ".+\\.js$", }; let includeList = []; ComponentList.getAmdSrcGlobList().forEach(async pattern => { includeList.push(...glob.sync(pattern)); }); const cwdLength = process.cwd().length + 1; includeList.forEach(path => { if (source.include.indexOf(path) !== -1) { // Ensure no duplicates. return; } const relPath = path.substring(cwdLength); if (thirdPartyPaths.indexOf(relPath) !== -1) { return; } source.include.push(path); }); source.include.push('.grunt/jsdoc/README.md'); return source; }; const tags = { // Allow the use of unknown tags. // We have a lot of legacy uses of these. allowUnknownTags: true, // We make use of jsdoc and closure dictionaries as standard. dictionaries: [ 'jsdoc', 'closure', ], }; // Template configuraiton. const templates = { cleverLinks: false, monospaceLinks: false, }; module.exports = { opts: { destination: "./jsdoc/", template: "node_modules/docdash", }, plugins, recurseDepth: 10, source: getSource(), sourceType: 'module', tags, templates, docdash: { collapse: true, search: true, sort: true, sectionOrder: [ "Namespaces", "Modules", "Events", "Classes", "Externals", "Mixins", "Tutorials", "Interfaces" ], "menu": { "Developer Docs": { href: "https://moodledev.io", target: "_blank", "class": "menu-item", id: "devdocs" }, "MDN Docs": { href: "https://developer.mozilla.org/en-US/docs/Web/JavaScript", target: "_blank", "class": "menu-item", id: "mdndocs", }, }, typedefs: true, }, };