diff --git a/.grunt/components.js b/.grunt/components.js index 9f1651e94ab..a520198c075 100644 --- a/.grunt/components.js +++ b/.grunt/components.js @@ -232,6 +232,155 @@ const getOwningComponentDirectory = checkPath => { 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').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, @@ -241,4 +390,5 @@ module.exports = { getYuiSrcGlobList, getThirdPartyLibsList, getThirdPartyPaths, + getThirdPartyLibsUpgradable, }; diff --git a/.grunt/tasks/upgradablelibs.js b/.grunt/tasks/upgradablelibs.js new file mode 100644 index 00000000000..be1f19860ff --- /dev/null +++ b/.grunt/tasks/upgradablelibs.js @@ -0,0 +1,42 @@ +// 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(); + }); + +}; diff --git a/Gruntfile.js b/Gruntfile.js index b600f6db6ee..7cc31e33a39 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -260,6 +260,8 @@ module.exports = function(grunt) { addTask('watch', grunt); addTask('startup', grunt); + addTask('upgradablelibs', grunt); + // Register the default task. grunt.registerTask('default', ['startup']); }; diff --git a/lib/upgrade.txt b/lib/upgrade.txt index 5d85b1306e4..d9b8f2fa1a4 100644 --- a/lib/upgrade.txt +++ b/lib/upgrade.txt @@ -79,6 +79,7 @@ information provided here is intended especially for developers. Course formats using components will be allowed to use one level indentation only. * The method `flexible_table::set_columnsattributes` now can be used with 'class' key to add custom classes to the DOM. * The editor_tinymce plugin has been removed from core. +* A new grunt task, upgradablelibs, has been added to get the list of libraries that have a newer version in their repositories. === 4.1 === diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 3c53398cf18..fdae64ae012 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -29,6 +29,7 @@ "eslint-plugin-promise": "6.0.0", "fb-watchman": "2.0.1", "gherkin-lint": "^4.2.2", + "git-tags-remote": "^1.0.5", "glob": "7.2.0", "grunt": "^1.4.1", "grunt-contrib-uglify": "5.0.1", @@ -6372,6 +6373,15 @@ "node": ">= 6" } }, + "node_modules/git-tags-remote": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/git-tags-remote/-/git-tags-remote-1.0.5.tgz", + "integrity": "sha512-BMPL7t5XWDTD1AAyc+0rtq5zAE6e6QPE8KYu1nLPI0+JZztmCWhUNzNLF3P8vPSvJ1YupCL9NYiM6OQevJYY1g==", + "dev": true, + "dependencies": { + "semver": "^7.3.2" + } + }, "node_modules/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", @@ -18989,6 +18999,15 @@ } } }, + "git-tags-remote": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/git-tags-remote/-/git-tags-remote-1.0.5.tgz", + "integrity": "sha512-BMPL7t5XWDTD1AAyc+0rtq5zAE6e6QPE8KYu1nLPI0+JZztmCWhUNzNLF3P8vPSvJ1YupCL9NYiM6OQevJYY1g==", + "dev": true, + "requires": { + "semver": "^7.3.2" + } + }, "glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", diff --git a/package.json b/package.json index 5baef68aab9..80d38a53073 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "eslint-plugin-promise": "6.0.0", "fb-watchman": "2.0.1", "gherkin-lint": "^4.2.2", + "git-tags-remote": "^1.0.5", "glob": "7.2.0", "grunt": "^1.4.1", "grunt-contrib-uglify": "5.0.1",