Moodle plugins and 3rd party AMD / UMD

Guy Thomas
5 min readSep 22, 2018

What’s the best way to get vendor AMD / UMD modules into your plugin? Hopefully this article will suggest the best technique, which I have discovered after years of adding vendor source to my AMD module folder.

The problem

What’s wrong with adding vendor source to my AMD module folder? Not only have I done this in the past but I’ve seen other developers do it too — so don’t feel bad if you’ve done this or are thinking of doing it now! The problems with this approach in my opinion are as follows:

a) Your bespoke plugin code is now mixed up in the same folder as your vendor code.

b) You are not taking advantage of CDNs which can drastically speed up delivery of code.

c) You are having to put vendor src into your AMD folder and you are relying on the ‘amd’ grunt task to package it up in the same way as the vendor’s pre built module. Granted, it’s probably going to be the same and even if it isn’t exactly the same it’s more than likely going to work. However, why are we wasting time minifying code that‘s already available ?

The solution

It’s entirely possible with require.js to load up AMD / UMD modules from anywhere at runtime. My approach for doing this with Moodle is to create a ‘main.js’ file which acts as a bootstrapper for my AMD code. In this example we are going to load vendor code for Vue.js :

main.js

require.config({
enforceDefine: false,
paths: {
"vue_2_5_16": "https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue"
}
});
define(['vue_2_5_16'], function(Vue) {

return {
vue: null,

init: function(opts) {
this.vue = new Vue(opts).$mount('#app');
return this.vue;
}
};

});

Notice how we are loading a fixed version of vue from our cdn — 2.5.16. One thing we really don’t want to do is load up a version of vue and then alias it as ‘vue’. If we do this it’s likely to cause problems for other people (and ourselves) if they require a different version of vue and also alias it as ‘vue’! Therefore, we alias vue with it’s version number — ‘vue_2_5_16’.

By doing this we can be sure that our code get’s the correct version of Vue and, so long as we use the same approach in other plugins, if two or more plugins reference the same alias, it will only be loaded once!

There’s one small snag with this approach — what if I want this to work in an environment where the internet connection is unreliable, or even completely unavailable? In this case the browser will be unable to reach the CDN and you won’t get your libraries. However, there is another solution to this — CDN fall backs!

CDN fall backs

A CDN fall back allows a local copy of a library to be loaded in cases where the CDN can not be reached. In order to do this (and avoid putting AMD code in our amd folder) we need some PHP code too. Let’s take a look at the JS code first:

require.config({
enforceDefine: false,
paths: {
"vue_2_5_16": [ "https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue",
// CDN fall back, whoop, whoop!
M.cfg.wwwroot + "/pluginfile.php/" + M.cfg.contextid + "/tool_behatdump/vendorjs/vue"
]
}
});
define(['vue_2_5_16'], function(Vue) {

return {
vue: null,

init: function(opts) {
this.vue = new Vue(opts).$mount('#app');
return this.vue;
}
};

});

Notice how the path we define for “vue_2_5_16” is now an array and not a string, and that it has 2 components:

    "vue_2_5_16": [ "https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue",// CDN fall back, whoop, whoop!
M.cfg.wwwroot + "/pluginfile.php/" + M.cfg.contextid + "/tool_behatdump/vendorjs/vue"
]

The first component is the original CDN link, the second component is a locally served version of the plugin.

Note that ‘/tool_behatdump/’ in the second component is the name of the plugin from where I am hosting the CDN fall back code.

Also note that it is served via “pluginfile.php”. I initially tried serving this up using a URL that pointed directly at the JS within my plugin but nginx config wouldn’t allow it to be served. Also, it’s common practice within moodle to load up files via pluginfile.php and so it makes sense to do the same here.

Now we need some code in our plugin to make the pluginfile.php link work. We put this in the lib.php file for our plugin — here is an example:

<?php

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

defined('MOODLE_INTERNAL') || die;

/**
* Serves 3rd party js files.
*
*
@param stdClass $course
*
@param stdClass $cm
*
@param context $context
*
@param string $filearea
*
@param array $args
*
@param bool $forcedownload
*
@param array $options
*
@return bool
*/
function tool_behatdump_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options = array()) {
global $CFG;

$pluginpath = $CFG->dirroot.'/admin/tool/behatdump/';

if ($filearea === 'vendorjs') {
// Typically CDN fall backs would go in vendorjs.
$path = $pluginpath.'vendorjs/'.implode('/', $args);
echo file_get_contents($path);
die;
} else if ($filearea === 'vue') {
// Vue components.
$jsfile = array_pop ($args);
$compdir = basename($jsfile, '.js');
$umdfile = $compdir.'.umd.js';
$args[] = $compdir;
$args[] = 'dist';
$args[] = $umdfile;
$path = $pluginpath.'vue/'.implode('/', $args);
echo file_get_contents($path);
die;
} else {
die('unsupported file area');
}
die;
}

To make this work in your plugin, simply add the above to your lib.php file and change “tool_behatdump” to your plugin frankenstlye name — e.g. for the forum module this would be “mod_forum”. Then change the $pluginpath variable to point to your plugin (make sure you include a fwd slash on the end) — e.g:

$pluginpath = $CFG->dirroot.'/mod/forum/';

Next we need to create a folder within your plugin to house your CDN fall backs. I’ve gone with ‘vendorjs’ as the name for this folder . This needs to go in the root of your plugin — e.g. mod/forum/vendorjs

Now all you need to do is copy the code from your CDN into a file within your vendorjs folder — e.g: mod/forum/vendorjs/vue.js. Note that the code should be minified.

That’s all there is to it — now if the CDN can’t be reached the local version from within your plugin will be loaded instead. How neat is that!!

A working example of this technique can be found here:

https://github.com/gthomas2/moodle_tool_behatdump

--

--