MDL-67449 grunt: Refactor path calculation for AMD
authorAndrew Nicols <andrew@nicols.co.uk>
Thu, 19 Dec 2019 00:22:41 +0000 (08:22 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Mon, 6 Jan 2020 01:29:11 +0000 (09:29 +0800)
Gruntfile.js
GruntfileComponents.js [new file with mode: 0644]
babel-plugin-add-module-to-define.js

index f05bb19..a1095e3 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+/* eslint-env node */
+
 /**
- * Grunt configuration
+ * Calculate the cwd, taking into consideration the `root` option (for Windows).
+ *
+ * @param {Object} grunt
+ * @returns {String} The current directory as best we can determine
  */
+const getCwd = grunt => {
+    const fs = require('fs');
+    const path = require('path');
 
-/* eslint-env node */
-module.exports = function(grunt) {
-    var path = require('path'),
-        tasks = {},
-        cwd = process.env.PWD || process.cwd(),
-        async = require('async'),
-        DOMParser = require('xmldom').DOMParser,
-        xpath = require('xpath'),
-        semver = require('semver'),
-        watchman = require('fb-watchman'),
-        watchmanClient = new watchman.Client(),
-        gruntFilePath = process.cwd();
-
-    // Verify the node version is new enough.
-    var expected = semver.validRange(grunt.file.readJSON('package.json').engines.node);
-    var actual = semver.valid(process.version);
-    if (!semver.satisfies(actual, expected)) {
-        grunt.fail.fatal('Node version not satisfied. Require ' + expected + ', version installed: ' + actual);
-    }
+    let cwd = fs.realpathSync(process.env.PWD || process.cwd());
 
     // Windows users can't run grunt in a subdirectory, so allow them to set
     // the root by passing --root=path/to/dir.
     if (grunt.option('root')) {
-        var root = grunt.option('root');
+        const root = grunt.option('root');
         if (grunt.file.exists(__dirname, root)) {
-            cwd = path.join(__dirname, root);
+            cwd = fs.realpathSync(path.join(__dirname, root));
             grunt.log.ok('Setting root to ' + cwd);
         } else {
             grunt.fail.fatal('Setting root to ' + root + ' failed - path does not exist');
         }
     }
 
+    return cwd;
+};
+
+/**
+ * Grunt configuration.
+ *
+ * @param {Object} grunt
+ */
+module.exports = function(grunt) {
+    const path = require('path');
+    const tasks = {};
+    const async = require('async');
+    const DOMParser = require('xmldom').DOMParser;
+    const xpath = require('xpath');
+    const semver = require('semver');
+    const watchman = require('fb-watchman');
+    const watchmanClient = new watchman.Client();
+    const fs = require('fs');
+    const ComponentList = require(path.resolve('GruntfileComponents.js'));
+
+    // Verify the node version is new enough.
+    var expected = semver.validRange(grunt.file.readJSON('package.json').engines.node);
+    var actual = semver.valid(process.version);
+    if (!semver.satisfies(actual, expected)) {
+        grunt.fail.fatal('Node version not satisfied. Require ' + expected + ', version installed: ' + actual);
+    }
+
+    // Detect directories:
+    // * gruntFilePath          The real path on disk to this Gruntfile.js
+    // * cwd                    The current working directory, which can be overridden by the `root` option
+    // * relativeCwd            The cwd, relative to the Gruntfile.js
+    // * componentDirectory     The root directory of the component if the cwd is in a valid component
+    // * inComponent            Whether the cwd is in a valid component
+    // * runDir                 The componentDirectory or cwd if not in a component, relative to Gruntfile.js
+    // * fullRunDir             The full path to the runDir
+    const gruntFilePath = fs.realpathSync(process.cwd());
+    const cwd = getCwd(grunt);
+    const relativeCwd = cwd.replace(new RegExp(`${gruntFilePath}/?`), '');
+    const componentDirectory = ComponentList.getOwningComponentDirectory(relativeCwd);
+    const inComponent = !!componentDirectory;
+    const runDir = inComponent ? componentDirectory : relativeCwd;
+    const fullRunDir = fs.realpathSync(gruntFilePath + path.sep + runDir);
+    grunt.log.debug(`The cwd was detected as ${cwd} with a fullRunDir of ${fullRunDir}`);
+
+    if (inComponent) {
+        grunt.log.ok(`Running tasks for component directory ${componentDirectory}`);
+    }
+
     var files = null;
     if (grunt.option('files')) {
         // Accept a comma separated list of files to process.
@@ -198,7 +236,9 @@ module.exports = function(grunt) {
                 nospawn: true // We need not to spawn so config can be changed dynamically.
             },
             amd: {
-                files: ['**/amd/src/**/*.js'],
+                files: inComponent
+                    ? ['amd/src/*.js', 'amd/src/**/*.js']
+                    : ['**/amd/src/**/*.js'],
                 tasks: ['amd']
             },
             boost: {
diff --git a/GruntfileComponents.js b/GruntfileComponents.js
new file mode 100644 (file)
index 0000000..26e7ed4
--- /dev/null
@@ -0,0 +1,152 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Helper functions for working with Moodle component names, directories, and sources.
+ *
+ * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @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 = [];
+
+        // Fetch the component definiitions from the distributed JSON file.
+        const components = JSON.parse(fs.readFileSync(`${gruntFilePath}/lib/components.json`));
+
+        // 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 incldues the list of subsystems.
+        componentData.components = componentData.subsystems;
+
+        // 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)));
+
+                    Object.entries(subpluginList.plugintypes).forEach(([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);
+                        });
+                    });
+                }
+            });
+        });
+
+    }
+
+    return componentData;
+};
+
+/**
+ * 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;
+};
+
+/**
+ * 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
+ * @returns {String|null}
+ */
+const getOwningComponentDirectory = checkPath => {
+    const path = require('path');
+
+    const pathList = fetchComponentData().components;
+    for (const componentPath of Object.keys(pathList)) {
+        if (checkPath === componentPath) {
+            return componentPath;
+        }
+        if (checkPath.startsWith(componentPath + path.sep)) {
+            return componentPath;
+        }
+    }
+
+    return null;
+};
+
+module.exports = {
+    getAmdSrcGlobList,
+    getComponentFromPath,
+    getOwningComponentDirectory,
+};
index cdbd8a7..dfe68c6 100644 (file)
 module.exports = ({template, types}) => {
     const fs = require('fs');
     const path = require('path');
-    const glob = require('glob');
     const cwd = process.cwd();
-
-    // Static variable to hold the modules.
-    let moodleSubsystems = null;
-    let moodlePlugins = null;
-
-    /**
-     * Parse Moodle's JSON files containing the lists of components.
-     *
-     * The values are stored in the static variables because we
-     * only need to load them once per transpiling run.
-     */
-    function loadMoodleModules() {
-        moodleSubsystems = {'lib': 'core'};
-        moodlePlugins = {};
-        let components = fs.readFileSync('lib/components.json');
-        components = JSON.parse(components);
-
-        for (const [component, path] of Object.entries(components.subsystems)) {
-            if (path) {
-                // Prefix "core_" to the front of the subsystems.
-                moodleSubsystems[path] = `core_${component}`;
-            }
-        }
-
-        for (const [component, path] of Object.entries(components.plugintypes)) {
-            if (path) {
-                moodlePlugins[path] = component;
-            }
-        }
-
-        for (const file of glob.sync('**/db/subplugins.json')) {
-            var rawContents = fs.readFileSync(file);
-            var subplugins = JSON.parse(rawContents);
-
-            for (const [component, path] of Object.entries(subplugins.plugintypes)) {
-                if (path) {
-                    moodlePlugins[path] = component;
-                }
-            }
-        }
-    }
+    const ComponentList = require(path.resolve('GruntfileComponents.js'));
 
     /**
      * Search the list of components that match the given file name
@@ -99,26 +58,14 @@ module.exports = ({template, types}) => {
         const fileName = file.replace('.js', '');
 
         // Check subsystems first which require an exact match.
-        if (moodleSubsystems.hasOwnProperty(componentPath)) {
-            return `${moodleSubsystems[componentPath]}/${fileName}`;
-        }
-
-        // It's not a subsystem so it must be a plugin. Moodle defines root folders
-        // where plugins can be installed so our path with be <plugin_root>/<plugin_name>.
-        // Let's separate the two.
-        let pathParts = componentPath.split('/');
-        const pluginName = pathParts.pop();
-        const pluginPath = pathParts.join('/');
-
-        // The plugin path mutch match exactly because some plugins are subplugins of
-        // other plugins which means their paths would partially match.
-        if (moodlePlugins.hasOwnProperty(pluginPath)) {
-            return `${moodlePlugins[pluginPath]}_${pluginName}/${fileName}`;
+        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);
+        throw new Error(`Unable to find module name for ${searchFileName} (${componentPath}::${file}}`);
     }
 
     /**
@@ -149,10 +96,6 @@ module.exports = ({template, types}) => {
         pre() {
             this.seenDefine = false;
             this.addedReturnForDefaultExport = false;
-
-            if (moodleSubsystems === null) {
-                loadMoodleModules();
-            }
         },
         visitor: {
             // Plugin ordering is only respected if we visit the "Program" node.