MDL-67449 grunt: Refactor path calculation for AMD

This commit is contained in:
Andrew Nicols 2019-12-19 08:22:41 +08:00
parent c55402486b
commit a8109e759d
3 changed files with 222 additions and 87 deletions

View File

@ -20,22 +20,51 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Grunt configuration
*/
/* eslint-env node */
/**
* 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');
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')) {
const root = grunt.option('root');
if (grunt.file.exists(__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) {
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();
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);
@ -44,16 +73,25 @@ module.exports = function(grunt) {
grunt.fail.fatal('Node version not satisfied. Require ' + expected + ', version installed: ' + actual);
}
// 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');
if (grunt.file.exists(__dirname, root)) {
cwd = path.join(__dirname, root);
grunt.log.ok('Setting root to ' + cwd);
} else {
grunt.fail.fatal('Setting root to ' + root + ' failed - path does not exist');
}
// 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;
@ -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: {

152
GruntfileComponents.js Normal file
View File

@ -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,
};

View File

@ -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
@ -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.