Jenkins JS Builder
JIRA
Table of Contents:
Overview
NPM utility for building CommonJS module bundles (and optionally making them js-modules compatible).
See js-modules.
The following diagram illustrates the basic flow (and components used) in the process of building a CommonJS module bundle.
It uses a number of popular JavaScript and maven tools (CommonJS/node.js, Browserify, Gulp, frontend-maven-plugin and more).
The responsibilities of the components in the above diagram can be summarized as follows:
- CommonJS: JavaScript module system (i.e. the expected format of JavaScript modules). This module system works with the nice/clean synchronous
require
syntax synonymous with node.js (for module loading) e.g. var mathUtil = require('../util/mathUtil');
. This allows us to tap into the huge NPM JavaScript ecosystem. - Browserify: A build time utility (NPM package - executed as a Gulp "task") for "bundling" a graph of CommonJS style modules together, producing a single JavaScript file (bundle) that can be loaded (from a single request) in a browser. Browserify ensures that the
require
calls (see above) resolve properly to the correct module within the bundle. - Gulp: A JavaScript build system (NPM package), analogous to what Maven is for Java i.e. executes "tasks" that eventually produce build artifacts. In this case, a JavaScript bundle is produced via Gulps execution of a Browserify "task".
- frontend-maven-plugin: A Maven plugin that allows us to hook a Gulp "build" into a maven build e.g. for a Jenkins plugin. See Maven Integration below.
Features
js-builder
does a number of things:
- Runs Jasmine tests/specs and produce a JUnit report that can be picked up by a top level Maven build.
- Uses Browserify to produce a CommonJS module bundle file from a "main" CommonJS module (see the
bundle
task below). The bundle file is typically placed somewhere on the filesystem that allows a higher level Maven build to pick it up and include it in e.g. a Jenkins plugin HPI file (so it can be loaded by the browser at runtime). - Pre-process Handlebars files (
.hbs
) and include them in the bundle file (see 2 above). - Optionally pre-process a LESS fileset to a
.css
file that can be picked up by the top level Maven build and included in the e.g. a Jenkins plugin HPI file. See the bundle
task below. - Optionally perform module transformations (using a Browserify Transform) that "link" in Framework libs (
import
- see js-modules), making the bundle a lot lighter by allowing it to use a shared instance of the Framework lib Vs it being included in the bundle. This can easily reduce the size of a bundle from e.g. 1Mb to 50Kb or less, as Framework libs are often the most weighty components. See the bundle
task below. - Optionally
export
(see js-modules) the bundles "main" CommonJS module (see 2 above) so as to allow other bundles import
it i.e. effectively making the bundle a Framework lib (see 5 above). See the bundle
task below.
Install
npm install --save-dev @jenkins-cd/js-builder
This assumes you have node.js v4.0.0 (minimum) installed on your local development environment.
Note this is only required if you intend developing js-modules compatible module bundles. Plugins using this should automatically handle all build aspects via maven (see later) i.e. simple building of a plugin should require no machine level setup.
General Usage
Add a gulpfile.js
(see Gulp) in the same folder as the package.json
. Then use js-builder
as follows:
var builder = require('@jenkins-cd/js-builder');
builder.defineTasks(['test', 'bundle', 'rebundle']);
builder.bundle('./index.js', 'myappbundle.js').inAdjunctPackage('com.acme');
Notes:
- See the "
defineTasks
" section for details of the available tasks. - See the "
bundle
" section for details of the bundle
command.
defineTasks
js-builder
makes it possible to easily define a number of tasks. No tasks are turned on by default,
so you can also just define your own tasks. To use the tasks defined in js-builder
, simply call
the defineTasks
function:
builder.defineTasks(['test', 'bundle', 'rebundle']);
See next section.
Predefined Gulp Tasks
The following sections describe the available predefined Gulp tasks. The bundle
and test
tasks are
auto-installed as the default tasks.
'test' Task
Run all Jasmine style tests. The default location for tests is the spec
folder. The file names need to match the
pattern "*-spec.js". The default location can be overridden by calling builder.tests(<new-path>)
.
See jenkins-js-test for more on testing.
'bundle' Task
Run the 'bundle' task. See detail on this in the dedicated section titled "Bundling" (below).
'rebundle' Task
Watch module source files (index.js
, ./lib/**/*.js
and ./lib/**/*.hbs
) for change, auto-running the
bundle
task whenever changes are detected.
Note that this task will not be run by default, so you need to specify it explicitly on the gulp command in
order to run it e.g.
gulp rebundle
Bundling
As stated in the "Features" section above, much of the usefulness of js-builder
lies in how it
helps with the bundling of the different JavaScript and CSS components:
- Bundling CommonJS modules to produce a JavaScript bundle.
- Bundling LESS resource to produce a
.css
file. - Bundling Handlebars templates (
hbs
) into the JavaScript bundle.
It also helps with js-modules compatibility i.e. handling import
s and export
s so as to allow
slimming down of your "app" bundle.
Step 1: Create Bundle Spec
Most of the bundling options are configured on the "Bundle Spec", which is an object returned from
a call to the bundle
function on the builder
:
var bundleSpec = builder.bundle('<path-to-main-module>', '<bundle-name>');
path-to-main-module
: The path to the "main" CommonJS module, from which Browserify will start the bundling process (see Browserify for more details). E.g. 'js/bootstrap3.js'
.bundle-name
(Optional): The name of the bundle to be generated. If not specified, the "main" module name will be used.
Step 2: Specify Bundle Output Location
js-builder
lets you configure where the generated bundle is output to. There are 3 possible
options for this.
Option 1: Bundle as a js-modules "resource", which means it will be placed in the
./src/main/webapp/jsmodules
folder, from where it can be import
ed at runtime. This option
should be used in conjunction with bundleSpec.export()
(see below).
bundleSpec.asJenkinsModuleResource();
Option 2: Bundle in a specified directory/folder.
bundleSpec.inDir('<path-to-dir>');
Option 3: Bundle as an "adjunct", which means the bundle is put into a package
in ./target/generated-adjuncts/
. If using this option, make sure the project's
pom.xml
has the appropriate build <resource>
configuration (see below).
Of course, you can also just use the bundleSpec.inDir
option (num 2 above) if you'd prefer to handle
adjuncts differently i.e. use bundleSpec.inDir
to generate the bundle into a dir that gets picked up by your
maven build, placing the bundle in the correct place on the Java classpath.
bundleSpec.inAdjunctPackage('com.acme');
An example of how to configure the build <resource>
in your pom.xml
file (if using inAdjunctPackage
), allowing the adjunct to be referenced from a Jelly file.
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
<resource>
<directory>target/generated-adjuncts</directory>
</resource>
</resources>
</build>
Step 3 (Optional): Specify LESS Processing
Specify a LESS file for pre-processing to CSS:
bundleSpec.less('js/bootstrap3/style.less');
The output location for the generated .css
file depends on the output location chosen for the bundle. See Step 2 above.
Step 4 (Optional): Specify "external" Module Mappings (imports)
Some of the NPM packages used by your "app" bundle will be common Framework libs that, for performance reasons,
you do not want bundled in every "app" bundle. Instead, you would prefer all "app" bundles to share an instance of
these common Framework libs.
That said, you would generally prefer to code your application's CommonJS modules as normal, using the more
simple/intuitive CommonJS style require
syntax (synch), and forget about performance optimizations until
later (build time). When doing it this way, your CommonJS module code should just require
the NPM packages it
needs and just use them as normal e.g.
var moment = require('moment');
moment().format('MMMM Do YYYY, h:mm:ss a');
The above code will work fine as is (without performing any mappings), but the downside is that your app bundle will be more bloated as it will
include the moment
NPM module. To lighten your bundle for the browser (by using a shared instance of the moment
NPM module), we tell the builder
(via the bundleSpec
) to "map" (transform) all synchronous require
calls for moment
to async
import
s of the momentjs:momentjs2
Framework lib bundle
(see the momentjs framwork lib bundle).
bundleSpec.withExternalModuleMapping('moment', 'momentjs:momentjs2');
Of course your "app" bundle may depend on a number of weighty Framework libs that you would prefer not to
include in your bundle. If so, simply call withExternalModuleMapping
for each.
Note that you can apply global mappings by calling withExternalModuleMapping
on the top level builder
instance. This is useful if you are creating multiple bundles, many/all of which are using the same external module mappings.
Step 4.1 (Optional): Generating a "no_imports" bundle
Externalizing commons Framework libs (see Step 4)
is important in terms of producing a JavaScript bundle that can be used in production (is lighter etc), but can make
things a bit trickier when it comes to Integration Testing your bundle because your test (and test environment) will now need to
accommodate the fact that your bundle no longer contains all the Framework libs it depends on.
For that reason, js-builder
supports the generateNoImportsBundle
option, which tells the builder to also generate
a bundle that includes all of it's dependency Framework libs i.e. a bundle which does not apply imports (hence "no_imports").
bundleSpec.generateNoImportsBundle();
Note that this is an additional bundle i.e. not instead of the "main" bundle (in which "imports" are applied).
With this option set, the "no_imports" bundle is generated into a sub-folder named "no_imports", inside the same
folder in which the "main" bundle is generated.
For an example of how to use the generateNoImportsBundle
option, see the "step-08-zombie-tests" Integration Test sample plugin.
Step 5 (Optional): Export
Exporting the "main" module (allowing other bundle modules to import
it) from the bundle is easy:
bundleSpec.export();
The builder
will use the plugin's artifactId
from the pom.xml
(which becomes the plugin ID), as well as the
bundle name (normalised from the bundle name specified during Step 1) to determine the export
bundle ID for
the module.
For example, if the plugin's artifactId
is "acmeplugin" and the bundle name specified is "acme.js", then the
module would be exported as acmeplugin:acme
. The package associated with the "acme.js" module should also be
"published" to NPM so as to allow "app" bundles that might use it to add a dev
dependency on it (so tests
etc can run).
So how would an "app" bundle in another plugin use this later?
It would need to:
- Add a normal HPI dependency on "acmeplugin" (to make sure it gets loaded by Jenkins so it can serve the bundle).
- Add a
dev
dependency on the package associated with the "acme.js" module i.e. npm install --save-dev acme
. This allows the next step will work (and tests to run etc). - In the "app" bundle modules, simply
require
and use the acme
module e.g. var acme = require('acme');
. - In the "app" bundle's
gulpfile.js
, add a withExternalModuleMapping
e.g. bundleSpec.withExternalModuleMapping('acme', 'acmeplugin:acme');
.
See Step 4 above.
Step 6 (Optional): Minify bundle JavaScript
This can be done by calling minify
on js-builder
:
bundleSpec.minify();
Or, by passing --minify
on the command line. This will result in the minification of all generated bundles.
$ gulp --minify
onPreBundle listeners
There are times when you will need access to the underlying Browserify bundler
just before the
bundling process is executed (e.g. for adding transforms etc).
To do this, you call the onPreBundle
function. This function takes a listener
function as an argument.
This listener
function, when called, receives the bundle
as this
and the bundler
as the only argument to
the supplied listener
.
var builder = require('@jenkins-cd/js-builder');
builder.onPreBundle(function(bundler) {
var bundle = this;
console.log('Adding the funky transform to bundler for bundle: ' + bundle.as);
bundler.transform(myFunkyTransform);
});
Setting 'src' and 'test' (spec) paths
The default paths depend on whether or not running in a maven project.
For a maven project, the default source and test/spec paths are:
- src:
./src/main/js
and ./src/main/less
(used primarily by the rebundle
task, watching these folders for source changes) - test:
./src/test/js
(used by the test
task)
Otherwise, they are:
- src:
./js
and ./less
(used primarily by the rebundle
task, watching these folders for source changes) - test:
./spec
(used by the test
task)
Changing these defaults is done through the builder
instance e.g.:
var builder = require('@jenkins-cd/js-builder');
builder.src('src/main/js');
builder.tests('src/test/js');
You can also specify an array of src
folders e.g.
builder.src(['src/main/js', 'src/main/less']);
Command line options
A number of js-builder
options can be specified on the command line.
--minify
Passing --minify
on the command line will result in the minification of all generated bundles.
$ gulp --minify
--test
Run a single test.
$ gulp --test configeditor
The above example would run test specs matching the **/configeditor*-spec.js
pattern (in the test source directory).
Maven Integration
Hooking a Gulp based build into a Maven build involves adding a few Maven <profile>
s to the
Maven project's pom.xml
.
We have extracted these into a sample_extract_pom.xml
from which they can be copied.
NOTE: We hope to put these <profile>
definitions into one of the top level Jenkins parent POMs. Once that's
done and your project has that parent POM as a parent, then none of this will be required.
With these <profiles>
s installed, Maven will run Gulp as part of the build.
- runs
npm install
during the initialize
phase, - runs
gulp bundle
during the generate-sources
phase and - runs
gulp test
during the test
phase).
You can also execute:
mvn clean -DcleanNode
: Cleans out the local node and NPM artifacts and resource (including the node_modules
folder).