Skip to content

Commit

Permalink
MDL-67449 grunt: Refactor path calculation for AMD
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewnicols committed Jan 6, 2020
1 parent c554024 commit a8109e7
Show file tree
Hide file tree
Showing 3 changed files with 220 additions and 85 deletions.
86 changes: 63 additions & 23 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,42 +20,80 @@
* @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.
Expand Down Expand Up @@ -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: {
Expand Down
152 changes: 152 additions & 0 deletions GruntfileComponents.js
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>
* @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,
};
67 changes: 5 additions & 62 deletions babel-plugin-add-module-to-define.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,49 +38,8 @@
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
Expand All @@ -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}}`);
}

/**
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit a8109e7

Please sign in to comment.