jenkins-modules
v1.1.1
Published
Jenkins CI JavaScript modules
Downloads
8
Readme
Jenkins CI JavaScript "module bundle" loader i.e. a loader for loading more than one module in one request (i.e. a "bundle").
Install Package:
npm install --save jenkins-modules
Background
JavaScript modularization has been a bit of a black spot for Jenkins UI for a while. Jenkins UI rendering has really been a server side process. Client side JavaScript is something that has been shoehorned in after-the-fact using adjuncts etc.
Something that we (at CloudBees) started experimenting with was the idea of modularizing JavaScript using nodejs/CommonJS style modules. This also allowed us to tap into a huge ecosystem of mature and modern JavaScript libraries.
On the browser side, the options for loading these modules seemed mainly to be RequireJS and Browserify. We went with Browserify because it allowed two things:
- Loading of all "application" modules in a single request as a "bundle". This might seem like premature optimization but we had heard stories re how this became a problem when using AMD/RequireJS i.e. performance issues with apps having lots of modules and the modules being loaded one at a time. Just google and you'll find plenty of them.
- It allowed us to have a really nice/clean synchronous nodejs style
require
semantics wrt to loading modules. Loading all modules via an asynchronousdefine
(ala RequireJS) is not so nice.
Browserify is a really nice solution for modularising CommonJS style JavaScript code for running in the browser. The problems for Jenkins are:
- There are many Jenkins components potentially building JavaScript modules, all requiring access to common JavaScript framework libraries (jQuery, Bootstrap, jQuery UI etc). We don't want each module loading their own copy of jQuery etc.
- Jenkins is not a website built by one team of developers. There are 1000+ plugins now, all contributing to the UI and all developed in different "eras"
which, among other things, means they will be developed to work against different versions of JavaScript libraries. We need to make it possible for Jenkins "apps" to
get the right version of a given framework library at runtime. The idea of a single "global" instance of these framework library shared across all apps is broken.
- jQuery is a big concern here, where the version issue is exacerbated by the jQuery extensions issue i.e. Jenkins may have a single jQuery instance with a varying version + any number of unknown jQuery extensions "glom'd" onto it, all potentially conflicting with each other. So in effect, this is basically replicating the global
window
namespace issues of old into the$
namespace. This is not sustainable for Jenkins and is guaranteed to lead to all sorts of strange UI errors. See "jquery-detached"
- jQuery is a big concern here, where the version issue is exacerbated by the jQuery extensions issue i.e. Jenkins may have a single jQuery instance with a varying version + any number of unknown jQuery extensions "glom'd" onto it, all potentially conflicting with each other. So in effect, this is basically replicating the global
- It is expect that Jenkins plugins will be building shareable JavaScript components e.g. a plugin that has a REST API could expose that rest API to other plugin UI components via a JavaScript module.
Browserify can be used to build a single, self contained JavaScript bundle. The modules in that bundle can require other modules in the bundle, but cannot require modules from other bundles i.e. "external" modules (this is not strictly true as Browserify does provide a way to link in external modules from other bundles, but not in a way that works for a "non website" type app such as Jenkins, where the external dependencies are not so easy to pre-resolve such that they can all be "there" before being require
d).
That means each bundle needs to include everything it needs, including jQuery and other framework libraries. This is not sustainable for Jenkins and is the problem that this module is targeted at i.e. to allow plugins
(or Jenkins core) that are building self contained CommonJS style JavaScript modules (using Browserify if they want)
to "export" one or more of those modules in the browser, allowing those modules to be "required" across bundle boundaries.
So, this module is all about loading module "bundles" (apps) and providing a means for them to load their dependencies (jQuery etc) and wiring them together. The idea is that
the "local" app modules are all loaded cleanly through the nodejs style require
semantics (because they are all loaded in a single bundle), while "external" dependencies (jQuery etc)
are loaded asynchronously (allowing them to be loaded on demand etc). The assumption here is that the number of external module dependencies should be relatively small in comparison
to the number of modules in the app itself. In fact, this module lets us async load the external modules upfront in the app's "main" module and then sync require those modules
from down in the app sub-modules (see later section on sync loading).
export
JavaScript modules
A Jenkins Plugin can "export" a JavaScript module (CommonJS style module) by calling
require('jenkins-modules').export
, allowing other plugin bundles to import
that module
(see next section).
exports.add = function(lhs, rhs {
return lhs + hrs;
}
// export the CommonJS module
require('jenkins-modules').export('pluginA', 'mathUtils', module);
We assume that the plugin bundle JavaScript is bundled using Browserify, and can be
loaded from <jenkins>/plugin/<pluginName>/jsmodules/<moduleName>.js
e.g. /jenkins/plugin/pluginA/jsmodules/mathUtils.js
.
Asynchronously import
JavaScript modules
A JavaScript module in one plugin ("pluginB") can "require" a module from another plugin ("pluginA" see above)
by calling require('jenkins-modules').import
. We call these "external" modules here.
var mathUtil; // initialise once the module is loaded and registered
// The require is async (returning a Promise) because the 'pluginA:mathUtils' is loaded async.
require('jenkins-modules').import('pluginA:mathUtils')
.onFulfilled(function(mathUtils) {
// Module loaded ok
mathUtil = module;
})
.onRejected(function(error) {
// Module didn't load for some reason e.g. a timeout
alert(error.detail);
});
exports.magicFunc = function() {
// might want to assert mathUtil is initialised
// do stuff ...
}
If require('jenkins-modules').import
is called for a module that is not yet loaded,
require('jenkins-modules').import
will trigger the loading of that module from the plugin, hence the
async/promise nature i.e. you can't synchronously get
a module.
You can also perform an import
operation if you require loading of multiple modules. So if you require
2 modules (e.g. "bootstrap3" and "jqueryui1") before proceeding, you can do the following:
// Again, the require is async (returning a Promise). The promise will not be fulfilled until both
// "bootstrap3" and "jqueryui1" are loaded.
require('jenkins-modules').import('jenkins-jslib:bootstrap3', 'jenkins-jslib:jqueryui1')
.onFulfilled(function(bootstrap3, jqueryui1) {
// Note how the loaded modules are passed as args in the
// same order as they are specified in the call to import.
});
}
You might call import
wth multiple module names in your "top level" script if you want to make sure your "application"
only runs after all required external modules are loaded.
require('jenkins-modules').import('jenkins-jslib:bootstrap3', 'jenkins-jslib:jqueryui1')
.onFulfilled(function() {
// Now it's safe for my "application" to run...
});
}
Synchronously require
JavaScript modules
Asynchronously requiring external modules in an application can be a bit "ugly", requiring wrapping of code in
async callbacks etc. For that reason, jenkins-modules
supports a require
function that can be used
to synchronously require
these external modules ala how CommonJS require
can be used to require
a
local module (local to the application bundle).
As stated earlier, one assumption is that the application has a top level "main" JavaScript file from where all other modules in the application are loaded, either directly or indirectly e.g.
-- main-mod.js
\- sub-mod-1.js
\- sub-mod-1.1.js
\- sub-mod-1.2.js
\- sub-mod-2.js
\- sub-mod-2.1.js
\- sub-mod-2.2.js
Another assumption was that "apps" will typically have more internal modules than they have dependencies on external modules (which would typically be "framework" type modules such as jQuery, Bootstrap etc).
Using Browserify, we can assemble all app modules (e.g. above) into a single bundle that can be loaded
in a single request. So lets assume that sub modules (sub-mod-1.1.js
etc) depend on jQuery, Bootstrap etc. Instead of defining async import
s
in all modules, a cleaner approach is to import
all external modules from the top level module (main-mod.js
) e.g.
main-mod.js
// load all external deps before we "start" the app
require('jenkins-modules')
.import('jquery-detached:jquery2', 'bootstrap:bootstrap3')
.onFulfilled(function() {
// run the app
// and some time later....
var subModule = require('sub-mod-1.1);
});
So, it's perfectly safe for sub-mod-1.1.js
to synchronously require
it's external modules e.g. jenkins.require('jquery-detached:jquery2')
:
sub-mod-1.1.js
var jenkins = require('jenkins-modules');
var jquery = jenkins.require('jquery-detached:jquery2');
// etc ...
Remember the assumption that there are "relatively" few external module dependencies Vs the number of internal app modules. Based on that and the
fact that all internal modules can be loaded synchronously using require
because they are are all in a single bundle (thanks to Browserify), we
should be able to keep the top level async loading (in main-mod.js
) relatively clean because we're able to limit it to the framework modules
(again, thanks to Browserify).